bootsnap 1.4.8 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdadfdd9d316f3362a593314fc596293395f5cfdb2f4ea0985f8fc77c54a4c0e
4
- data.tar.gz: 88e75f2440f3f7fdd3785437e9e3e4494e5bbe4c9e95437f81fde4ea4845b884
3
+ metadata.gz: '029d63ba428f470d2cd2ef194d857e20689e7c8e377e2eaaaab83570f366034a'
4
+ data.tar.gz: 36a55f9d12dddef6ea3c4739f586c9236d7f4bc677a8b6747dfd7465d46eeca2
5
5
  SHA512:
6
- metadata.gz: 31d24bdddce9bdb204f7fa346c7089bccac4b11081b8f172515883fc75fc77c1a2becab479753d7ebda52050ee3bd2790c54b3e32fd6100b07d09fa849d07134
7
- data.tar.gz: 180abf489969e8629dec58d631553287f8f6546efcadb87f9e5caaf286bfc2e9d5c64f90f617e2626640862d5a31f347e1b69d9a225f9b3fdcc279bdb9cf7d66
6
+ metadata.gz: 131ec17c4e4912f387c18778250e689467ebfcc80e91eb884237307af1a57a3c89d4fb20c772fbc0330123a796d631861a9cf145bd32c54183077bbc97d7fa6f
7
+ data.tar.gz: d0c92454c8a5d8b16a25908fd0ae134fc817c5977958bde8424baca2bb6c96e60eb648c9f770bf7c7f58a3dd98871d4e7b0e3a0950c269100fded45ca9fddfa3
@@ -1,3 +1,26 @@
1
+ # Unreleased
2
+
3
+ # 1.6.0
4
+
5
+ * Fix a Ruby 2.7/3.0 issue with `YAML.load_file` keyword arguments. (#342)
6
+ * `bootsnap precompile` CLI use multiple processes to complete faster. (#341)
7
+ * `bootsnap precompile` CLI also precompile YAML files. (#340)
8
+ * 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)
9
+ * Changed the compile cache directory from `$BOOTSNAP_CACHE_DIR/bootsnap-compile-cache` to `$BOOTSNAP_CACHE_DIR/bootsnap/compile-cache` for ease of use. (#334)
10
+
11
+ # 1.5.1
12
+
13
+ * Workaround a Ruby bug in InstructionSequence.compile_file. (#332)
14
+
15
+ # 1.5.0
16
+
17
+ * Add a command line to statically precompile the ISeq cache. (#326)
18
+
19
+ # 1.4.9
20
+
21
+ * [Windows support](https://github.com/Shopify/bootsnap/pull/319)
22
+ * [Fix potential crash](https://github.com/Shopify/bootsnap/pull/322)
23
+
1
24
  # 1.4.8
2
25
 
3
26
  * [Prevent FallbackScan from polluting exception cause](https://github.com/Shopify/bootsnap/pull/314)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Bootsnap [![Build Status](https://travis-ci.org/Shopify/bootsnap.svg?branch=master)](https://travis-ci.org/Shopify/bootsnap)
1
+ # Bootsnap [![Actions Status](https://github.com/Shopify/bootsnap/workflows/ci/badge.svg)](https://github.com/Shopify/bootsnap/actions)
2
2
 
3
3
  Bootsnap is a library that plugs into Ruby, with optional support for `ActiveSupport` and `YAML`,
4
4
  to optimize and cache expensive computations. See [How Does This Work](#how-does-this-work).
@@ -29,7 +29,8 @@ If you are using Rails, add this to `config/boot.rb` immediately after `require
29
29
  require 'bootsnap/setup'
30
30
  ```
31
31
 
32
- Note that bootsnap writes to `tmp/cache`, and that directory *must* be writable. Rails will fail to
32
+ Note that bootsnap writes to `tmp/cache` (or the path specified by `ENV['BOOTSNAP_CACHE_DIR']`),
33
+ and that directory *must* be writable. Rails will fail to
33
34
  boot if it is not. If this is unacceptable (e.g. you are running in a read-only container and
34
35
  unwilling to mount in a writable tmpdir), you should remove this line or wrap it in a conditional.
35
36
 
@@ -294,6 +295,19 @@ open /c/nope.bundle -> -1
294
295
  # (nothing!)
295
296
  ```
296
297
 
298
+ ## Precompilation
299
+
300
+ In development environments the bootsnap compilation cache is generated on the fly when source files are loaded.
301
+ But in production environments, such as docker images, you might need to precompile the cache.
302
+
303
+ To do so you can use the `bootsnap precompile` command.
304
+
305
+ Example:
306
+
307
+ ```bash
308
+ $ bundle exec bootsnap precompile --gemfile app/ lib/
309
+ ```
310
+
297
311
  ## When not to use Bootsnap
298
312
 
299
313
  *Alternative engines*: Bootsnap is pretty reliant on MRI features, and parts are disabled entirely on alternative ruby
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bootsnap/cli'
5
+ exit Bootsnap::CLI.new(ARGV).run
@@ -70,7 +70,7 @@ struct bs_cache_key {
70
70
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
71
71
 
72
72
  /* Effectively a schema version. Bumping invalidates all previous caches */
73
- static const uint32_t current_version = 2;
73
+ static const uint32_t current_version = 3;
74
74
 
75
75
  /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
76
76
  * new OS ABI, etc. */
@@ -91,16 +91,18 @@ static ID uncompilable;
91
91
 
92
92
  /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
93
93
  static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
94
- static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
94
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
95
+ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
95
96
 
96
97
  /* Helpers */
97
98
  static uint64_t fnv1a_64(const char *str);
98
99
  static void bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
99
100
  static int bs_read_key(int fd, struct bs_cache_key * key);
100
101
  static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
101
- static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
102
+ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args);
103
+ static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
102
104
  static int open_current_file(char * path, struct bs_cache_key * key, const char ** errno_provenance);
103
- static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
105
+ 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);
104
106
  static uint32_t get_ruby_revision(void);
105
107
  static uint32_t get_ruby_platform(void);
106
108
 
@@ -108,12 +110,12 @@ static uint32_t get_ruby_platform(void);
108
110
  * Helper functions to call ruby methods on handler object without crashing on
109
111
  * exception.
110
112
  */
111
- static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data);
113
+ static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data);
112
114
  static VALUE prot_storage_to_output(VALUE arg);
113
115
  static VALUE prot_input_to_output(VALUE arg);
114
- static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag);
116
+ static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag);
115
117
  static VALUE prot_input_to_storage(VALUE arg);
116
- static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data);
118
+ static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data);
117
119
  struct s2o_data;
118
120
  struct i2o_data;
119
121
  struct i2s_data;
@@ -148,7 +150,8 @@ Init_bootsnap(void)
148
150
  uncompilable = rb_intern("__bootsnap_uncompilable__");
149
151
 
150
152
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
151
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 3);
153
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
154
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
152
155
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
153
156
 
154
157
  current_umask = umask(0777);
@@ -267,7 +270,6 @@ static void
267
270
  bs_cache_path(const char * cachedir, const char * path, char (* cache_path)[MAX_CACHEPATH_SIZE])
268
271
  {
269
272
  uint64_t hash = fnv1a_64(path);
270
-
271
273
  uint8_t first_byte = (hash >> (64 - 8));
272
274
  uint64_t remainder = hash & 0x00ffffffffffffff;
273
275
 
@@ -301,7 +303,7 @@ cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
301
303
  * conversions on the ruby VALUE arguments before passing them along.
302
304
  */
303
305
  static VALUE
304
- bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
306
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args)
305
307
  {
306
308
  FilePathValue(path_v);
307
309
 
@@ -319,9 +321,35 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
319
321
  /* generate cache path to cache_path */
320
322
  bs_cache_path(cachedir, path, &cache_path);
321
323
 
322
- return bs_fetch(path, path_v, cache_path, handler);
324
+ return bs_fetch(path, path_v, cache_path, handler, args);
323
325
  }
324
326
 
327
+ /*
328
+ * Entrypoint for Bootsnap::CompileCache::Native.precompile.
329
+ * Similar to fetch, but it only generate the cache if missing
330
+ * and doesn't return the content.
331
+ */
332
+ static VALUE
333
+ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
334
+ {
335
+ FilePathValue(path_v);
336
+
337
+ Check_Type(cachedir_v, T_STRING);
338
+ Check_Type(path_v, T_STRING);
339
+
340
+ if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
341
+ rb_raise(rb_eArgError, "cachedir too long");
342
+ }
343
+
344
+ char * cachedir = RSTRING_PTR(cachedir_v);
345
+ char * path = RSTRING_PTR(path_v);
346
+ char cache_path[MAX_CACHEPATH_SIZE];
347
+
348
+ /* generate cache path to cache_path */
349
+ bs_cache_path(cachedir, path, &cache_path);
350
+
351
+ return bs_precompile(path, path_v, cache_path, handler);
352
+ }
325
353
  /*
326
354
  * Open the file we want to load/cache and generate a cache key for it if it
327
355
  * was loaded.
@@ -428,7 +456,7 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
428
456
  * or exception, will be the final data returnable to the user.
429
457
  */
430
458
  static int
431
- fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
459
+ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance)
432
460
  {
433
461
  char * data = NULL;
434
462
  ssize_t nread;
@@ -454,9 +482,9 @@ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data,
454
482
  goto done;
455
483
  }
456
484
 
457
- storage_data = rb_str_new_static(data, data_size);
485
+ storage_data = rb_str_new(data, data_size);
458
486
 
459
- *exception_tag = bs_storage_to_output(handler, storage_data, output_data);
487
+ *exception_tag = bs_storage_to_output(handler, args, storage_data, output_data);
460
488
  ret = 0;
461
489
  done:
462
490
  if (data != NULL) xfree(data);
@@ -624,7 +652,7 @@ bs_read_contents(int fd, size_t size, char ** contents, const char ** errno_prov
624
652
  * - Return storage_to_output(storage_data)
625
653
  */
626
654
  static VALUE
627
- bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
655
+ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args)
628
656
  {
629
657
  struct bs_cache_key cached_key, current_key;
630
658
  char * contents = NULL;
@@ -657,7 +685,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
657
685
  if (valid_cache) {
658
686
  /* Fetch the cache data and return it if we're able to load it successfully */
659
687
  res = fetch_cached_data(
660
- cache_fd, (ssize_t)cached_key.data_size, handler,
688
+ cache_fd, (ssize_t)cached_key.data_size, handler, args,
661
689
  &output_data, &exception_tag, &errno_provenance
662
690
  );
663
691
  if (exception_tag != 0) goto raise;
@@ -671,15 +699,15 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
671
699
 
672
700
  /* Read the contents of the source file into a buffer */
673
701
  if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail_errno;
674
- input_data = rb_str_new_static(contents, current_key.size);
702
+ input_data = rb_str_new(contents, current_key.size);
675
703
 
676
704
  /* Try to compile the input_data using input_to_storage(input_data) */
677
- exception_tag = bs_input_to_storage(handler, input_data, path_v, &storage_data);
705
+ exception_tag = bs_input_to_storage(handler, args, input_data, path_v, &storage_data);
678
706
  if (exception_tag != 0) goto raise;
679
707
  /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
680
708
  * to cache anything; just return input_to_output(input_data) */
681
709
  if (storage_data == uncompilable) {
682
- bs_input_to_output(handler, input_data, &output_data, &exception_tag);
710
+ bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
683
711
  if (exception_tag != 0) goto raise;
684
712
  goto succeed;
685
713
  }
@@ -691,7 +719,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
691
719
  if (res < 0) goto fail_errno;
692
720
 
693
721
  /* Having written the cache, now convert storage_data to output_data */
694
- exception_tag = bs_storage_to_output(handler, storage_data, &output_data);
722
+ exception_tag = bs_storage_to_output(handler, args, storage_data, &output_data);
695
723
  if (exception_tag != 0) goto raise;
696
724
 
697
725
  /* If output_data is nil, delete the cache entry and generate the output
@@ -701,7 +729,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
701
729
  errno_provenance = "bs_fetch:unlink";
702
730
  goto fail_errno;
703
731
  }
704
- bs_input_to_output(handler, input_data, &output_data, &exception_tag);
732
+ bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
705
733
  if (exception_tag != 0) goto raise;
706
734
  }
707
735
 
@@ -732,6 +760,79 @@ invalid_type_storage_data:
732
760
  #undef CLEANUP
733
761
  }
734
762
 
763
+ static VALUE
764
+ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
765
+ {
766
+ struct bs_cache_key cached_key, current_key;
767
+ char * contents = NULL;
768
+ int cache_fd = -1, current_fd = -1;
769
+ int res, valid_cache = 0, exception_tag = 0;
770
+ const char * errno_provenance = NULL;
771
+
772
+ VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
773
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
774
+
775
+ /* Open the source file and generate a cache key for it */
776
+ current_fd = open_current_file(path, &current_key, &errno_provenance);
777
+ if (current_fd < 0) goto fail;
778
+
779
+ /* Open the cache key if it exists, and read its cache key in */
780
+ cache_fd = open_cache_file(cache_path, &cached_key, &errno_provenance);
781
+ if (cache_fd == CACHE_MISSING_OR_INVALID) {
782
+ /* This is ok: valid_cache remains false, we re-populate it. */
783
+ } else if (cache_fd < 0) {
784
+ goto fail;
785
+ } else {
786
+ /* True if the cache existed and no invalidating changes have occurred since
787
+ * it was generated. */
788
+ valid_cache = cache_key_equal(&current_key, &cached_key);
789
+ }
790
+
791
+ if (valid_cache) {
792
+ goto succeed;
793
+ }
794
+
795
+ close(cache_fd);
796
+ cache_fd = -1;
797
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
798
+
799
+ /* Read the contents of the source file into a buffer */
800
+ if (bs_read_contents(current_fd, current_key.size, &contents, &errno_provenance) < 0) goto fail;
801
+ input_data = rb_str_new(contents, current_key.size);
802
+
803
+ /* Try to compile the input_data using input_to_storage(input_data) */
804
+ exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
805
+ if (exception_tag != 0) goto fail;
806
+
807
+ /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
808
+ * to cache anything; just return false */
809
+ if (storage_data == uncompilable) {
810
+ goto fail;
811
+ }
812
+ /* If storage_data isn't a string, we can't cache it */
813
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto fail;
814
+
815
+ /* Write the cache key and storage_data to the cache directory */
816
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data, &errno_provenance);
817
+ if (res < 0) goto fail;
818
+
819
+ goto succeed;
820
+
821
+ #define CLEANUP \
822
+ if (contents != NULL) xfree(contents); \
823
+ if (current_fd >= 0) close(current_fd); \
824
+ if (cache_fd >= 0) close(cache_fd);
825
+
826
+ succeed:
827
+ CLEANUP;
828
+ return Qtrue;
829
+ fail:
830
+ CLEANUP;
831
+ return Qfalse;
832
+ #undef CLEANUP
833
+ }
834
+
835
+
735
836
  /*****************************************************************************/
736
837
  /********************* Handler Wrappers **************************************/
737
838
  /*****************************************************************************
@@ -751,11 +852,13 @@ invalid_type_storage_data:
751
852
 
752
853
  struct s2o_data {
753
854
  VALUE handler;
855
+ VALUE args;
754
856
  VALUE storage_data;
755
857
  };
756
858
 
757
859
  struct i2o_data {
758
860
  VALUE handler;
861
+ VALUE args;
759
862
  VALUE input_data;
760
863
  };
761
864
 
@@ -769,15 +872,16 @@ static VALUE
769
872
  prot_storage_to_output(VALUE arg)
770
873
  {
771
874
  struct s2o_data * data = (struct s2o_data *)arg;
772
- return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data);
875
+ return rb_funcall(data->handler, rb_intern("storage_to_output"), 2, data->storage_data, data->args);
773
876
  }
774
877
 
775
878
  static int
776
- bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
879
+ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data)
777
880
  {
778
881
  int state;
779
882
  struct s2o_data s2o_data = {
780
883
  .handler = handler,
884
+ .args = args,
781
885
  .storage_data = storage_data,
782
886
  };
783
887
  *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
@@ -785,10 +889,11 @@ bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
785
889
  }
786
890
 
787
891
  static void
788
- bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag)
892
+ bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag)
789
893
  {
790
894
  struct i2o_data i2o_data = {
791
895
  .handler = handler,
896
+ .args = args,
792
897
  .input_data = input_data,
793
898
  };
794
899
  *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
@@ -798,7 +903,7 @@ static VALUE
798
903
  prot_input_to_output(VALUE arg)
799
904
  {
800
905
  struct i2o_data * data = (struct i2o_data *)arg;
801
- return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data);
906
+ return rb_funcall(data->handler, rb_intern("input_to_output"), 2, data->input_data, data->args);
802
907
  }
803
908
 
804
909
  static VALUE
@@ -825,7 +930,7 @@ prot_input_to_storage(VALUE arg)
825
930
  }
826
931
 
827
932
  static int
828
- bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data)
933
+ bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data)
829
934
  {
830
935
  int state;
831
936
  struct i2s_data i2s_data = {
@@ -24,13 +24,13 @@ module Bootsnap
24
24
  setup_disable_trace if disable_trace
25
25
 
26
26
  Bootsnap::LoadPathCache.setup(
27
- cache_path: cache_dir + '/bootsnap-load-path-cache',
27
+ cache_path: cache_dir + '/bootsnap/load-path-cache',
28
28
  development_mode: development_mode,
29
29
  active_support: autoload_paths_cache
30
30
  ) if load_path_cache
31
31
 
32
32
  Bootsnap::CompileCache.setup(
33
- cache_dir: cache_dir + '/bootsnap-compile-cache',
33
+ cache_dir: cache_dir + '/bootsnap/compile-cache',
34
34
  iseq: compile_cache_iseq,
35
35
  yaml: compile_cache_yaml
36
36
  )
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bootsnap'
4
+ require 'bootsnap/cli/worker_pool'
5
+ require 'optparse'
6
+ require 'fileutils'
7
+ require 'etc'
8
+
9
+ module Bootsnap
10
+ class CLI
11
+ unless Regexp.method_defined?(:match?)
12
+ module RegexpMatchBackport
13
+ refine Regexp do
14
+ def match?(string)
15
+ !!match(string)
16
+ end
17
+ end
18
+ end
19
+ using RegexpMatchBackport
20
+ end
21
+
22
+ attr_reader :cache_dir, :argv
23
+
24
+ attr_accessor :compile_gemfile, :exclude, :verbose, :iseq, :yaml, :jobs
25
+
26
+ def initialize(argv)
27
+ @argv = argv
28
+ self.cache_dir = ENV.fetch('BOOTSNAP_CACHE_DIR', 'tmp/cache')
29
+ self.compile_gemfile = false
30
+ self.exclude = nil
31
+ self.verbose = false
32
+ self.jobs = Etc.nprocessors
33
+ self.iseq = true
34
+ self.yaml = true
35
+ end
36
+
37
+ def precompile_command(*sources)
38
+ require 'bootsnap/compile_cache/iseq'
39
+ require 'bootsnap/compile_cache/yaml'
40
+
41
+ fix_default_encoding do
42
+ Bootsnap::CompileCache::ISeq.cache_dir = self.cache_dir
43
+ Bootsnap::CompileCache::YAML.init!
44
+ Bootsnap::CompileCache::YAML.cache_dir = self.cache_dir
45
+
46
+ @work_pool = WorkerPool.create(size: jobs, jobs: {
47
+ ruby: method(:precompile_ruby),
48
+ yaml: method(:precompile_yaml),
49
+ })
50
+ @work_pool.spawn
51
+
52
+ main_sources = sources.map { |d| File.expand_path(d) }
53
+ precompile_ruby_files(main_sources)
54
+ precompile_yaml_files(main_sources)
55
+
56
+ if compile_gemfile
57
+ # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
58
+ gem_exclude = Regexp.union([exclude, '/spec/', '/test/'].compact)
59
+ precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
60
+
61
+ # Gems that include YAML files usually don't put them in `lib/`.
62
+ # So we look at the gem root.
63
+ gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gem\/[^/]+}
64
+ gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
65
+ precompile_yaml_files(gem_paths, exclude: gem_exclude)
66
+ end
67
+
68
+ if exitstatus = @work_pool.shutdown
69
+ exit(exitstatus)
70
+ end
71
+ end
72
+ 0
73
+ end
74
+
75
+ dir_sort = begin
76
+ Dir[__FILE__, sort: false]
77
+ true
78
+ rescue ArgumentError, TypeError
79
+ false
80
+ end
81
+
82
+ if dir_sort
83
+ def list_files(path, pattern)
84
+ if File.directory?(path)
85
+ Dir[File.join(path, pattern), sort: false]
86
+ elsif File.exist?(path)
87
+ [path]
88
+ else
89
+ []
90
+ end
91
+ end
92
+ else
93
+ def list_files(path, pattern)
94
+ if File.directory?(path)
95
+ Dir[File.join(path, pattern)]
96
+ elsif File.exist?(path)
97
+ [path]
98
+ else
99
+ []
100
+ end
101
+ end
102
+ end
103
+
104
+ def run
105
+ parser.parse!(argv)
106
+ command = argv.shift
107
+ method = "#{command}_command"
108
+ if respond_to?(method)
109
+ public_send(method, *argv)
110
+ else
111
+ invalid_usage!("Unknown command: #{command}")
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def precompile_yaml_files(load_paths, exclude: self.exclude)
118
+ return unless yaml
119
+
120
+ load_paths.each do |path|
121
+ if !exclude || !exclude.match?(path)
122
+ list_files(path, '**/*.{yml,yaml}').each do |yaml_file|
123
+ # We ignore hidden files to not match the various .ci.yml files
124
+ if !yaml_file.include?('/.') && (!exclude || !exclude.match?(yaml_file))
125
+ @work_pool.push(:yaml, yaml_file)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def precompile_yaml(*yaml_files)
133
+ Array(yaml_files).each do |yaml_file|
134
+ if CompileCache::YAML.precompile(yaml_file, cache_dir: cache_dir)
135
+ STDERR.puts(yaml_file) if verbose
136
+ end
137
+ end
138
+ end
139
+
140
+ def precompile_ruby_files(load_paths, exclude: self.exclude)
141
+ return unless iseq
142
+
143
+ load_paths.each do |path|
144
+ if !exclude || !exclude.match?(path)
145
+ list_files(path, '**/*.rb').each do |ruby_file|
146
+ if !exclude || !exclude.match?(ruby_file)
147
+ @work_pool.push(:ruby, ruby_file)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def precompile_ruby(*ruby_files)
155
+ Array(ruby_files).each do |ruby_file|
156
+ if CompileCache::ISeq.precompile(ruby_file, cache_dir: cache_dir)
157
+ STDERR.puts(ruby_file) if verbose
158
+ end
159
+ end
160
+ end
161
+
162
+ def fix_default_encoding
163
+ if Encoding.default_external == Encoding::US_ASCII
164
+ Encoding.default_external = Encoding::UTF_8
165
+ begin
166
+ yield
167
+ ensure
168
+ Encoding.default_external = Encoding::US_ASCII
169
+ end
170
+ else
171
+ yield
172
+ end
173
+ end
174
+
175
+ def invalid_usage!(message)
176
+ STDERR.puts message
177
+ STDERR.puts
178
+ STDERR.puts parser
179
+ 1
180
+ end
181
+
182
+ def cache_dir=(dir)
183
+ @cache_dir = File.expand_path(File.join(dir, 'bootsnap/compile-cache'))
184
+ end
185
+
186
+ def exclude_pattern(pattern)
187
+ (@exclude_patterns ||= []) << Regexp.new(pattern)
188
+ self.exclude = Regexp.union(@exclude_patterns)
189
+ end
190
+
191
+ def parser
192
+ @parser ||= OptionParser.new do |opts|
193
+ opts.banner = "Usage: bootsnap COMMAND [ARGS]"
194
+ opts.separator ""
195
+ opts.separator "GLOBAL OPTIONS"
196
+ opts.separator ""
197
+
198
+ help = <<~EOS
199
+ Path to the bootsnap cache directory. Defaults to tmp/cache
200
+ EOS
201
+ opts.on('--cache-dir DIR', help.strip) do |dir|
202
+ self.cache_dir = dir
203
+ end
204
+
205
+ help = <<~EOS
206
+ Print precompiled paths.
207
+ EOS
208
+ opts.on('--verbose', '-v', help.strip) do
209
+ self.verbose = true
210
+ end
211
+
212
+ help = <<~EOS
213
+ Number of workers to use. Default to number of processors, set to 0 to disable multi-processing.
214
+ EOS
215
+ opts.on('--jobs JOBS', '-j', help.strip) do |jobs|
216
+ self.jobs = Integer(jobs)
217
+ end
218
+
219
+ opts.separator ""
220
+ opts.separator "COMMANDS"
221
+ opts.separator ""
222
+ opts.separator " precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories"
223
+
224
+ help = <<~EOS
225
+ Precompile the gems in Gemfile
226
+ EOS
227
+ opts.on('--gemfile', help) { self.compile_gemfile = true }
228
+
229
+ help = <<~EOS
230
+ Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api'
231
+ EOS
232
+ opts.on('--exclude PATTERN', help) { |pattern| exclude_pattern(pattern) }
233
+
234
+ help = <<~EOS
235
+ Disable ISeq (.rb) precompilation.
236
+ EOS
237
+ opts.on('--no-iseq', help) { self.iseq = false }
238
+
239
+ help = <<~EOS
240
+ Disable YAML precompilation.
241
+ EOS
242
+ opts.on('--no-yaml', help) { self.yaml = false }
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootsnap
4
+ class CLI
5
+ class WorkerPool
6
+ class << self
7
+ def create(size:, jobs:)
8
+ if size > 0 && Process.respond_to?(:fork)
9
+ new(size: size, jobs: jobs)
10
+ else
11
+ Inline.new(jobs: jobs)
12
+ end
13
+ end
14
+ end
15
+
16
+ class Inline
17
+ def initialize(jobs: {})
18
+ @jobs = jobs
19
+ end
20
+
21
+ def push(job, *args)
22
+ @jobs.fetch(job).call(*args)
23
+ nil
24
+ end
25
+
26
+ def spawn
27
+ # noop
28
+ end
29
+
30
+ def shutdown
31
+ # noop
32
+ end
33
+ end
34
+
35
+ class Worker
36
+ attr_reader :to_io, :pid
37
+
38
+ def initialize(jobs)
39
+ @jobs = jobs
40
+ @pipe_out, @to_io = IO.pipe
41
+ @pid = nil
42
+ end
43
+
44
+ def write(message, block: true)
45
+ payload = Marshal.dump(message)
46
+ if block
47
+ to_io.write(payload)
48
+ true
49
+ else
50
+ to_io.write_nonblock(payload, exception: false) != :wait_writable
51
+ end
52
+ end
53
+
54
+ def close
55
+ to_io.close
56
+ end
57
+
58
+ def work_loop
59
+ loop do
60
+ job, *args = Marshal.load(@pipe_out)
61
+ return if job == :exit
62
+ @jobs.fetch(job).call(*args)
63
+ end
64
+ rescue IOError
65
+ nil
66
+ end
67
+
68
+ def spawn
69
+ @pid = Process.fork do
70
+ to_io.close
71
+ work_loop
72
+ exit!(0)
73
+ end
74
+ @pipe_out.close
75
+ true
76
+ end
77
+ end
78
+
79
+ def initialize(size:, jobs: {})
80
+ @size = size
81
+ @jobs = jobs
82
+ @queue = Queue.new
83
+ @pids = []
84
+ end
85
+
86
+ def spawn
87
+ @workers = @size.times.map { Worker.new(@jobs) }
88
+ @workers.each(&:spawn)
89
+ @dispatcher_thread = Thread.new { dispatch_loop }
90
+ @dispatcher_thread.abort_on_exception = true
91
+ true
92
+ end
93
+
94
+ def dispatch_loop
95
+ loop do
96
+ case job = @queue.pop
97
+ when nil
98
+ @workers.each do |worker|
99
+ worker.write([:exit])
100
+ worker.close
101
+ end
102
+ return true
103
+ else
104
+ unless @workers.sample.write(job, block: false)
105
+ free_worker.write(job)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def free_worker
112
+ IO.select(nil, @workers)[1].sample
113
+ end
114
+
115
+ def push(*args)
116
+ @queue.push(args)
117
+ nil
118
+ end
119
+
120
+ def shutdown
121
+ @queue.close
122
+ @dispatcher_thread.join
123
+ @workers.each do |worker|
124
+ _pid, status = Process.wait2(worker.pid)
125
+ return status.exitstatus unless status.success?
126
+ end
127
+ nil
128
+ end
129
+ end
130
+ end
131
+ end
@@ -34,9 +34,9 @@ module Bootsnap
34
34
  end
35
35
 
36
36
  def self.supported?
37
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
37
+ # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
38
38
  RUBY_ENGINE == 'ruby' &&
39
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
39
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
40
40
  Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
41
41
  end
42
42
  end
@@ -15,7 +15,7 @@ module Bootsnap
15
15
  raise(Uncompilable, 'syntax error')
16
16
  end
17
17
 
18
- def self.storage_to_output(binary)
18
+ def self.storage_to_output(binary, _args)
19
19
  RubyVM::InstructionSequence.load_from_binary(binary)
20
20
  rescue RuntimeError => e
21
21
  if e.message == 'broken binary format'
@@ -26,7 +26,24 @@ module Bootsnap
26
26
  end
27
27
  end
28
28
 
29
- def self.input_to_output(_)
29
+ def self.fetch(path, cache_dir: ISeq.cache_dir)
30
+ Bootsnap::CompileCache::Native.fetch(
31
+ cache_dir,
32
+ path.to_s,
33
+ Bootsnap::CompileCache::ISeq,
34
+ nil,
35
+ )
36
+ end
37
+
38
+ def self.precompile(path, cache_dir: ISeq.cache_dir)
39
+ Bootsnap::CompileCache::Native.precompile(
40
+ cache_dir,
41
+ path.to_s,
42
+ Bootsnap::CompileCache::ISeq,
43
+ )
44
+ end
45
+
46
+ def self.input_to_output(_data, _kwargs)
30
47
  nil # ruby handles this
31
48
  end
32
49
 
@@ -35,11 +52,7 @@ module Bootsnap
35
52
  # Having coverage enabled prevents iseq dumping/loading.
36
53
  return nil if defined?(Coverage) && Bootsnap::CompileCache::Native.coverage_running?
37
54
 
38
- Bootsnap::CompileCache::Native.fetch(
39
- Bootsnap::CompileCache::ISeq.cache_dir,
40
- path.to_s,
41
- Bootsnap::CompileCache::ISeq
42
- )
55
+ Bootsnap::CompileCache::ISeq.fetch(path.to_s)
43
56
  rescue Errno::EACCES
44
57
  Bootsnap::CompileCache.permission_error(path)
45
58
  rescue RuntimeError => e
@@ -60,6 +73,7 @@ module Bootsnap
60
73
  crc = Zlib.crc32(option.inspect)
61
74
  Bootsnap::CompileCache::Native.compile_option_crc32 = crc
62
75
  end
76
+ compile_option_updated
63
77
 
64
78
  def self.install!(cache_dir)
65
79
  Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
@@ -5,58 +5,107 @@ module Bootsnap
5
5
  module CompileCache
6
6
  module YAML
7
7
  class << self
8
- attr_accessor(:msgpack_factory)
9
- end
8
+ attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
10
9
 
11
- def self.input_to_storage(contents, _)
12
- raise(Uncompilable) if contents.index("!ruby/object")
13
- obj = ::YAML.load(contents)
14
- msgpack_factory.packer.write(obj).to_s
15
- rescue NoMethodError, RangeError
16
- # if the object included things that we can't serialize, fall back to
17
- # Marshal. It's a bit slower, but can encode anything yaml can.
18
- # NoMethodError is unexpected types; RangeError is Bignums
19
- Marshal.dump(obj)
20
- end
10
+ def input_to_storage(contents, _)
11
+ raise(Uncompilable) if contents.index("!ruby/object")
12
+ obj = ::YAML.load(contents)
13
+ msgpack_factory.dump(obj)
14
+ rescue NoMethodError, RangeError
15
+ # The object included things that we can't serialize
16
+ raise(Uncompilable)
17
+ end
21
18
 
22
- def self.storage_to_output(data)
23
- # This could have a meaning in messagepack, and we're being a little lazy
24
- # about it. -- but a leading 0x04 would indicate the contents of the YAML
25
- # is a positive integer, which is rare, to say the least.
26
- if data[0] == 0x04.chr && data[1] == 0x08.chr
27
- Marshal.load(data)
28
- else
29
- msgpack_factory.unpacker.feed(data).read
19
+ def storage_to_output(data, kwargs)
20
+ if kwargs && kwargs.key?(:symbolize_names)
21
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
22
+ end
23
+ msgpack_factory.load(data, kwargs)
30
24
  end
31
- end
32
25
 
33
- def self.input_to_output(data)
34
- ::YAML.load(data)
35
- end
26
+ def input_to_output(data, kwargs)
27
+ ::YAML.load(data, **(kwargs || {}))
28
+ end
29
+
30
+ def precompile(path, cache_dir: YAML.cache_dir)
31
+ Bootsnap::CompileCache::Native.precompile(
32
+ cache_dir,
33
+ path.to_s,
34
+ Bootsnap::CompileCache::YAML,
35
+ )
36
+ end
37
+
38
+ def install!(cache_dir)
39
+ self.cache_dir = cache_dir
40
+ init!
41
+ ::YAML.singleton_class.prepend(Patch)
42
+ end
36
43
 
37
- def self.install!(cache_dir)
38
- require('yaml')
39
- require('msgpack')
44
+ def init!
45
+ require('yaml')
46
+ require('msgpack')
47
+ require('date')
40
48
 
41
- # MessagePack serializes symbols as strings by default.
42
- # We want them to roundtrip cleanly, so we use a custom factory.
43
- # see: https://github.com/msgpack/msgpack-ruby/pull/122
44
- factory = MessagePack::Factory.new
45
- factory.register_type(0x00, Symbol)
46
- Bootsnap::CompileCache::YAML.msgpack_factory = factory
49
+ # MessagePack serializes symbols as strings by default.
50
+ # We want them to roundtrip cleanly, so we use a custom factory.
51
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
52
+ factory = MessagePack::Factory.new
53
+ factory.register_type(0x00, Symbol)
54
+ factory.register_type(
55
+ MessagePack::Timestamp::TYPE, # or just -1
56
+ Time,
57
+ packer: MessagePack::Time::Packer,
58
+ unpacker: MessagePack::Time::Unpacker
59
+ )
60
+
61
+ marshal_fallback = {
62
+ packer: ->(value) { Marshal.dump(value) },
63
+ unpacker: ->(payload) { Marshal.load(payload) },
64
+ }
65
+ {
66
+ Date => 0x01,
67
+ Regexp => 0x02,
68
+ }.each do |type, code|
69
+ factory.register_type(code, type, marshal_fallback)
70
+ end
71
+
72
+ self.msgpack_factory = factory
73
+
74
+ self.supported_options = []
75
+ params = ::YAML.method(:load).parameters
76
+ if params.include?([:key, :symbolize_names])
77
+ self.supported_options << :symbolize_names
78
+ end
79
+ if params.include?([:key, :freeze])
80
+ if factory.load(factory.dump('yaml'), freeze: true).frozen?
81
+ self.supported_options << :freeze
82
+ end
83
+ end
84
+ self.supported_options.freeze
85
+ end
86
+ end
87
+
88
+ module Patch
89
+ def load_file(path, *args)
90
+ return super if args.size > 1
91
+ if kwargs = args.first
92
+ return super unless kwargs.is_a?(Hash)
93
+ return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
94
+ end
47
95
 
48
- klass = class << ::YAML; self; end
49
- klass.send(:define_method, :load_file) do |path|
50
96
  begin
51
- Bootsnap::CompileCache::Native.fetch(
52
- cache_dir,
97
+ ::Bootsnap::CompileCache::Native.fetch(
98
+ Bootsnap::CompileCache::YAML.cache_dir,
53
99
  path,
54
- Bootsnap::CompileCache::YAML
100
+ ::Bootsnap::CompileCache::YAML,
101
+ kwargs,
55
102
  )
56
103
  rescue Errno::EACCES
57
- Bootsnap::CompileCache.permission_error(path)
104
+ ::Bootsnap::CompileCache.permission_error(path)
58
105
  end
59
106
  end
107
+
108
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
60
109
  end
61
110
  end
62
111
  end
@@ -61,7 +61,7 @@ module Bootsnap
61
61
 
62
62
  def supported?
63
63
  RUBY_ENGINE == 'ruby' &&
64
- RUBY_PLATFORM =~ /darwin|linux|bsd/
64
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/
65
65
  end
66
66
  end
67
67
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Bootsnap
3
- VERSION = "1.4.8"
3
+ VERSION = "1.6.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.8
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-11 00:00:00.000000000 Z
11
+ date: 2021-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -97,7 +97,8 @@ dependencies:
97
97
  description: Boot large ruby/rails apps faster
98
98
  email:
99
99
  - burke.libbey@shopify.com
100
- executables: []
100
+ executables:
101
+ - bootsnap
101
102
  extensions:
102
103
  - ext/bootsnap/extconf.rb
103
104
  extra_rdoc_files: []
@@ -105,11 +106,14 @@ files:
105
106
  - CHANGELOG.md
106
107
  - LICENSE.txt
107
108
  - README.md
109
+ - exe/bootsnap
108
110
  - ext/bootsnap/bootsnap.c
109
111
  - ext/bootsnap/bootsnap.h
110
112
  - ext/bootsnap/extconf.rb
111
113
  - lib/bootsnap.rb
112
114
  - lib/bootsnap/bundler.rb
115
+ - lib/bootsnap/cli.rb
116
+ - lib/bootsnap/cli/worker_pool.rb
113
117
  - lib/bootsnap/compile_cache.rb
114
118
  - lib/bootsnap/compile_cache/iseq.rb
115
119
  - lib/bootsnap/compile_cache/yaml.rb
@@ -134,6 +138,7 @@ metadata:
134
138
  bug_tracker_uri: https://github.com/Shopify/bootsnap/issues
135
139
  changelog_uri: https://github.com/Shopify/bootsnap/blob/master/CHANGELOG.md
136
140
  source_code_uri: https://github.com/Shopify/bootsnap
141
+ allowed_push_host: https://rubygems.org
137
142
  post_install_message:
138
143
  rdoc_options: []
139
144
  require_paths:
@@ -149,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
154
  - !ruby/object:Gem::Version
150
155
  version: '0'
151
156
  requirements: []
152
- rubygems_version: 3.0.2
157
+ rubygems_version: 3.0.3
153
158
  signing_key:
154
159
  specification_version: 4
155
160
  summary: Boot large ruby/rails apps faster