bootsnap 0.2.15 → 0.3.0.pre

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
  SHA1:
3
- metadata.gz: b0de1ff4a8bdd4478f422a07fba3981778828604
4
- data.tar.gz: d9b7d5cc09e58be7bc31382be47d72870c60bf7f
3
+ metadata.gz: '082f8dd8be53486b79ff5248ad426cc28263778b'
4
+ data.tar.gz: 5bc6a79f2054aa8b869704cdb9aeab685eabb400
5
5
  SHA512:
6
- metadata.gz: f153d3e9591eb2fb8d2efecb958cbe21f61d93040f891ef63809285418cf76b3883d5490b3aa60d656bd066f96d7489317b13833bdf465058f7a339547177f6c
7
- data.tar.gz: be2f217b2481c5d3eb44226d9861d2890dc7901143ab056e0d1f2fbd5e4f152ef497fb1cd67481ae44c4723dc5b0c93a6f6f6faebd705f4ce321aa0e5949bd2f
6
+ metadata.gz: ad062964bbc28d997901a1f6016289a4c6671071f9e1c4640820fe9545427136e1268f6588b350e7d058d85641604b0004225a744792fcce9b24bfcb9e5a3172
7
+ data.tar.gz: 33c77f2e085ba965e1a9fccb65b5afd42a4db0485dbfca7aa5d3e3920497ff41d0a3ca0ca4b6706df55814d29f015056e6d862568fe8b48ad5194f6eb80e26dc
data/README.md CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  **Beta-quality. See [the last section of this README](#trustworthiness).**
4
4
 
5
- **`compile_cache_*` features are only supported on MacOS (extremely likely to error on Linux;
6
- won't even compile on other platforms).**
7
-
8
5
  Bootsnap is a library that plugs into a number of Ruby and (optionally) `ActiveSupport` and `YAML`
9
6
  methods to optimize and cache expensive computations. See [the How Does This Work section](#how-does-this-work) for more information.
10
7
 
@@ -120,7 +117,8 @@ result too, raising a `LoadError` without touching the filesystem at all.
120
117
 
121
118
  ### Compilation Caching
122
119
 
123
- *(A simpler implementation of this concept can be found in [yomikomu](https://github.com/ko1/yomikomu)).*
120
+ *(A more readable implementation of this concept can be found in
121
+ [yomikomu](https://github.com/ko1/yomikomu)).*
124
122
 
125
123
  Ruby has complex grammar and parsing it is not a particularly cheap operation. Since 1.9, Ruby has
126
124
  translated ruby source to an internal bytecode format, which is then executed by the Ruby VM. Since
@@ -134,9 +132,8 @@ implementation. We use the same strategy of compilation caching for YAML documen
134
132
  equivalent of Ruby's "bytecode" format being a MessagePack document (or, in the case of YAML
135
133
  documents with types unsupported by MessagePack, a Marshal stream).
136
134
 
137
- These compilation results are stored using `xattr`s on the source files. This is likely to change in
138
- the future, as it has some limitations (notably precluding Linux support except where the user feels
139
- like changing mount flags). However, this is a very performant implementation.
135
+ These compilation results are stored in a cache directory, with filenames generated by taking a hash
136
+ of the full expanded path of the input file (FNV1a-64).
140
137
 
141
138
  Whereas before, the sequence of syscalls generated to `require` a file would look like:
142
139
 
@@ -158,32 +155,36 @@ With bootsnap, we get:
158
155
  ```
159
156
  open /c/foo.rb -> n
160
157
  fstat64 n
161
- fgetxattr n
162
- fgetxattr n
158
+ close n
159
+ open /c/foo.rb -> n
160
+ fstat64 n
161
+ open (cache) -> m
162
+ read m
163
+ read m
164
+ close m
163
165
  close n
164
166
  ```
165
167
 
166
- Bootsnap writes two `xattrs` attached to each file read:
168
+ This may look worse at a glance, but underlies a large performance difference.
167
169
 
168
- * `user.aotcc.value`, the binary compilation result; and
169
- * `user.aotcc.key`, a cache key to determine whether `user.aotcc.value` is still valid.
170
+ *(The first three syscalls in both listings -- `open`, `fstat64`, `close` -- are not inherently
171
+ useful. [This ruby patch](https://bugs.ruby-lang.org/issues/13378) optimizes them out when coupled
172
+ with bootsnap.)*
170
173
 
171
- The key includes several fields:
174
+ Bootsnap writes a cache file containing a 64 byte header followed by the cache contents. The header
175
+ is a cache key including several fields:
172
176
 
173
177
  * `version`, hardcoded in bootsnap. Essentially a schema version;
178
+ * `os_version`, A hash of the current kernel version (on macOS, BSD) or glibc version (on Linux);
174
179
  * `compile_option`, which changes with `RubyVM::InstructionSequence.compile_option` does;
175
- * `data_size`, the number of bytes in `user.aotcc.value`, which we need to read it into a buffer
176
- using `fgetxattr(2)`;
177
- * `ruby_revision`, the version of Ruby this was compiled with; and
178
- * `mtime`, the last-modification timestamp of the source file when it was compiled.
180
+ * `ruby_revision`, the version of Ruby this was compiled with;
181
+ * `size`, the size of the source file;
182
+ * `mtime`, the last-modification timestamp of the source file when it was compiled; and
183
+ * `data_size`, the number of bytes following the header, which we need to read it into a buffer.
179
184
 
180
185
  If the key is valid, the result is loaded from the value. Otherwise, it is regenerated and clobbers
181
186
  the current cache.
182
187
 
183
- This diagram may help illustrate how it works:
184
-
185
- ![Compilation Caching](https://burkelibbey.s3.amazonaws.com/bootsnap-compile-cache.png)
186
-
187
188
  ### Putting it all together
188
189
 
189
190
  Imagine we have this file structure:
@@ -227,8 +228,13 @@ With bootsnap, we get:
227
228
  ```
228
229
  open /c/foo.rb -> n
229
230
  fstat64 n
230
- fgetxattr n
231
- fgetxattr n
231
+ close n
232
+ open /c/foo.rb -> n
233
+ fstat64 n
234
+ open (cache) -> m
235
+ read m
236
+ read m
237
+ close m
232
238
  close n
233
239
  ```
234
240
 
@@ -253,8 +259,8 @@ open /c/nope.bundle -> -1
253
259
 
254
260
  We use the `*_path_cache` features in production and haven't experienced any issues in a long time.
255
261
 
256
- The `compile_cache_*` features work well for us in development on macOS, but probably don't work on
257
- Linux at all.
262
+ The `compile_cache_*` features work well for us in development on macOS. It should work on Linux,
263
+ and we intend to deploy it in production, but haven't we haven't yet.
258
264
 
259
265
  `disable_trace` should be completely safe, but we don't really use it because some people like to
260
266
  use tools that make use of `trace` instructions.
@@ -264,5 +270,5 @@ use tools that make use of `trace` instructions.
264
270
  | `load_path_cache` | everywhere |
265
271
  | `autoload_path_cache` | everywhere |
266
272
  | `disable_trace` | nowhere, but it's safe unless you need tracing |
267
- | `compile_cache_iseq` | development, unlikely to work on Linux |
268
- | `compile_cache_yaml` | development, unlikely to work on Linux |
273
+ | `compile_cache_iseq` | development, but probably safe to use everywhere |
274
+ | `compile_cache_yaml` | development, but probably safe to use everywhere |
@@ -1,60 +1,78 @@
1
1
  #include "bootsnap.h"
2
+ #include "ruby.h"
3
+ #include <stdint.h>
2
4
  #include <sys/types.h>
3
- #include <sys/xattr.h>
4
- #include <sys/stat.h>
5
5
  #include <errno.h>
6
- #include <unistd.h>
7
6
  #include <fcntl.h>
8
- #include <stdbool.h>
9
- #include <utime.h>
7
+ #include <sys/stat.h>
10
8
 
11
- #ifdef __APPLE__
12
- // Used for the OS Directives to define the os_version constant
13
- #include <Availability.h>
14
- #define _ENOATTR ENOATTR
9
+ #ifdef __linux__
10
+ #include <gnu/libc-version.h>
15
11
  #else
16
- #define _ENOATTR ENODATA
12
+ #include <sys/sysctl.h>
17
13
  #endif
18
14
 
19
- /*
20
- * TODO:
21
- * - test on linux or reject on non-darwin
22
- * - source files over 4GB will likely break things (meh)
23
- */
15
+ #define MAX_CACHEPATH_SIZE 1000
16
+ #define MAX_CACHEDIR_SIZE 981
17
+
18
+ #define KEY_SIZE 64
19
+
20
+ struct bs_cache_key {
21
+ uint32_t version;
22
+ uint32_t os_version;
23
+ uint32_t compile_option;
24
+ uint32_t ruby_revision;
25
+ uint64_t size;
26
+ uint64_t mtime;
27
+ uint64_t data_size; /* not used for equality */
28
+ uint8_t pad[24];
29
+ } __attribute__((packed));
30
+
31
+ #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__)
32
+ #define STATIC_ASSERT2(X,L) STATIC_ASSERT3(X,L)
33
+ #define STATIC_ASSERT3(X,L) STATIC_ASSERT_MSG(X,at_line_##L)
34
+ #define STATIC_ASSERT_MSG(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
35
+
36
+ STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
37
+
38
+ static const uint32_t current_version = 2;
39
+
40
+ static uint32_t current_os_version;
41
+ static uint32_t current_ruby_revision;
42
+ static uint32_t current_compile_option_crc32 = 0;
24
43
 
25
44
  static VALUE rb_mBootsnap;
26
45
  static VALUE rb_mBootsnap_CompileCache;
27
46
  static VALUE rb_mBootsnap_CompileCache_Native;
28
47
  static VALUE rb_eBootsnap_CompileCache_Uncompilable;
29
- static uint32_t current_ruby_revision;
30
- static uint32_t current_compile_option_crc32 = 0;
31
48
  static ID uncompilable;
32
49
 
33
- struct stats {
34
- uint64_t hit;
35
- uint64_t unwritable;
36
- uint64_t uncompilable;
37
- uint64_t miss;
38
- uint64_t fail;
39
- uint64_t retry;
40
- };
41
- static struct stats stats = {
42
- .hit = 0,
43
- .unwritable = 0,
44
- .uncompilable = 0,
45
- .miss = 0,
46
- .fail = 0,
47
- .retry = 0,
48
- };
50
+ /* Real API */
51
+ static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
52
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
53
+
54
+ /* Helpers */
55
+ static uint64_t fnv_64a(const char *str);
56
+ static void bs_cache_path(const char * cachedir, const char * path, char ** cache_path);
57
+ static int bs_read_key(int fd, struct bs_cache_key * key);
58
+ static int cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2);
59
+ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler);
60
+ static int open_current_file(char * path, struct bs_cache_key * key);
61
+ static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag);
62
+ static VALUE prot_exception_for_errno(VALUE err);
63
+ static uint32_t get_os_version(void);
49
64
 
50
- struct xattr_key {
51
- uint8_t version;
52
- uint8_t os_version;
53
- uint32_t compile_option;
54
- uint32_t data_size;
55
- uint32_t ruby_revision;
56
- uint64_t mtime;
57
- } __attribute__((packed));
65
+ static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data);
66
+ static VALUE prot_storage_to_output(VALUE arg);
67
+ static VALUE prot_input_to_output(VALUE arg);
68
+ static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag);
69
+ static VALUE prot_input_to_storage(VALUE arg);
70
+ static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data);
71
+
72
+ struct s2o_data {
73
+ VALUE handler;
74
+ VALUE storage_data;
75
+ };
58
76
 
59
77
  struct i2o_data {
60
78
  VALUE handler;
@@ -67,59 +85,6 @@ struct i2s_data {
67
85
  VALUE pathval;
68
86
  };
69
87
 
70
- struct s2o_data {
71
- VALUE handler;
72
- VALUE storage_data;
73
- };
74
-
75
- static const uint8_t current_version = 11;
76
- static const char * xattr_key_name = "user.aotcc.key";
77
- static const char * xattr_data_name = "user.aotcc.value";
78
- static const size_t xattr_key_size = sizeof (struct xattr_key);
79
-
80
- #ifdef __MAC_10_15 // Mac OS 10.15 (future)
81
- static const int os_version = 15;
82
- #elif __MAC_10_14 // Mac OS 10.14 (future)
83
- static const int os_version = 14;
84
- #elif __MAC_10_13 // Mac OS 10.13 (future)
85
- static const int os_version = 13;
86
- #elif __MAC_10_12 // Mac OS X Sierra
87
- static const int os_version = 12;
88
- #elif __MAC_10_11 // Mac OS X El Capitan
89
- static const int os_version = 11;
90
- # else
91
- static const int os_version = 0;
92
- #endif
93
-
94
- #ifdef __APPLE__
95
- #define GETXATTR_TRAILER ,0,0
96
- #define SETXATTR_TRAILER ,0
97
- #define REMOVEXATTR_TRAILER ,0
98
- #else
99
- #define GETXATTR_TRAILER
100
- #define SETXATTR_TRAILER
101
- #define REMOVEXATTR_TRAILER
102
- #endif
103
-
104
- /* forward declarations */
105
- static int bs_fetch_data(int fd, size_t size, VALUE handler, VALUE * storage_data, int * exception_tag);
106
- static int bs_update_key(int fd, uint32_t data_size, uint64_t current_mtime);
107
- static int bs_open(const char * path, bool * writable);
108
- static int bs_get_cache(int fd, struct xattr_key * key);
109
- static size_t bs_read_contents(int fd, size_t size, char ** contents);
110
- static int bs_close_and_unclobber_times(int * fd, const char * path, time_t atime, time_t mtime);
111
- static VALUE bs_fetch(VALUE self, VALUE pathval, VALUE handler);
112
- static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32val);
113
- static VALUE prot_exception_for_errno(VALUE err);
114
- static VALUE prot_input_to_output(VALUE arg);
115
- static void bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * exception_tag);
116
- static VALUE prot_input_to_storage(VALUE arg);
117
- static int bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * storage_data);
118
- static VALUE prot_storage_to_output(VALUE arg);
119
- static int bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data);
120
- static int logging_enabled();
121
- static VALUE bs_stats(VALUE self);
122
-
123
88
  void
124
89
  Init_bootsnap(void)
125
90
  {
@@ -127,339 +92,356 @@ Init_bootsnap(void)
127
92
  rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
128
93
  rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
129
94
  rb_eBootsnap_CompileCache_Uncompilable = rb_define_class_under(rb_mBootsnap_CompileCache, "Uncompilable", rb_eStandardError);
95
+
130
96
  current_ruby_revision = FIX2INT(rb_const_get(rb_cObject, rb_intern("RUBY_REVISION")));
97
+ current_os_version = get_os_version();
131
98
 
132
99
  uncompilable = rb_intern("__bootsnap_uncompilable__");
133
100
 
134
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_fetch, 2);
135
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "stats", bs_stats, 0);
101
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 3);
136
102
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
137
103
  }
138
104
 
139
105
  static VALUE
140
- bs_stats(VALUE self)
106
+ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
141
107
  {
142
- VALUE ret = rb_hash_new();
143
- rb_hash_aset(ret, ID2SYM(rb_intern("hit")), INT2NUM(stats.hit));
144
- rb_hash_aset(ret, ID2SYM(rb_intern("miss")), INT2NUM(stats.miss));
145
- rb_hash_aset(ret, ID2SYM(rb_intern("unwritable")), INT2NUM(stats.unwritable));
146
- rb_hash_aset(ret, ID2SYM(rb_intern("uncompilable")), INT2NUM(stats.uncompilable));
147
- rb_hash_aset(ret, ID2SYM(rb_intern("fail")), INT2NUM(stats.fail));
148
- rb_hash_aset(ret, ID2SYM(rb_intern("retry")), INT2NUM(stats.retry));
149
- return ret;
108
+ Check_Type(crc32_v, T_FIXNUM);
109
+ current_compile_option_crc32 = FIX2UINT(crc32_v);
110
+ return Qnil;
150
111
  }
151
112
 
152
- static VALUE
153
- bs_compile_option_crc32_set(VALUE self, VALUE crc32val)
113
+ static uint64_t
114
+ fnv_64a(const char *str)
154
115
  {
155
- Check_Type(crc32val, T_FIXNUM);
156
- current_compile_option_crc32 = FIX2UINT(crc32val);
157
- return Qnil;
116
+ unsigned char *s = (unsigned char *)str;
117
+ uint64_t h = ((uint64_t)0xcbf29ce484222325ULL);
118
+
119
+ while (*s) {
120
+ h ^= (uint64_t)*s++;
121
+ h += (h << 1) + (h << 4) + (h << 5) + (h << 7) + (h << 8) + (h << 40);
122
+ }
123
+
124
+ return h;
158
125
  }
159
126
 
160
- #define CHECK_C(ret, func) \
161
- do { if ((int)(ret) == -1) FAIL((func), errno); } while(0);
127
+ /* On Darwin and FreeBSD, this is the kernel version, since that tends to vary
128
+ * with whole-system upgrades. What we probably care about more is the libc
129
+ * version, which is what we explicitly ask for on linux.
130
+ * (and KERN_OSRELEASE came back empty for me one one linux box, so...?)
131
+ */
132
+ static uint32_t
133
+ get_os_version(void)
134
+ {
135
+ size_t len;
136
+ uint64_t hash;
137
+ #ifdef __linux__
138
+ const char * version;
139
+ version = gnu_get_libc_version();
140
+ hash = fnv_64a(version);
141
+ #else
142
+ char * version;
143
+ int mib[2] = {CTL_KERN, KERN_OSRELEASE};
144
+ if (sysctl(mib, 2, NULL, &len, NULL, 0) < 0) return 0;
145
+ version = malloc(sizeof(char) * len);
146
+ if (sysctl(mib, 2, version, &len, NULL, 0) < 0) return 0;
147
+ hash = fnv_64a(version);
148
+ free(version);
149
+ #endif
150
+ return (uint32_t)(hash >> 32);
151
+ }
162
152
 
163
- #define FAIL(func, err) \
164
- do { \
165
- int state; \
166
- exception = rb_protect(prot_exception_for_errno, INT2FIX(err), &state); \
167
- if (state) exception = rb_eStandardError; \
168
- goto fail; \
169
- } while(0);
153
+ static void
154
+ bs_cache_path(const char * cachedir, const char * path, char ** cache_path)
155
+ {
156
+ uint64_t hash = fnv_64a(path);
170
157
 
171
- #define CHECK_RB0() \
172
- do { if (exception_tag != 0) goto raise; } while (0);
158
+ uint8_t first_byte = (hash >> (64 - 8));
159
+ uint64_t remainder = hash & 0x00ffffffffffffff;
173
160
 
174
- #define CHECK_RB(body) \
175
- do { (body); CHECK_RB0(); } while (0);
161
+ sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder);
162
+ }
176
163
 
177
- #define SUCCEED(final) \
178
- do { \
179
- output_data = final; \
180
- goto cleanup; \
181
- } while(0);
164
+ static int
165
+ cache_key_equal(struct bs_cache_key * k1, struct bs_cache_key * k2)
166
+ {
167
+ return (
168
+ k1->version == k2->version &&
169
+ k1->os_version == k2->os_version &&
170
+ k1->compile_option == k2->compile_option &&
171
+ k1->ruby_revision == k2->ruby_revision &&
172
+ k1->size == k2->size &&
173
+ k1->mtime == k2->mtime
174
+ );
175
+ }
182
176
 
183
177
  static VALUE
184
- bs_fetch(VALUE self, VALUE pathval, VALUE handler)
178
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
185
179
  {
186
- const char * path;
187
-
188
- VALUE exception;
189
- int exception_tag;
190
-
191
- int fd, ret, retry;
192
- bool valid_cache;
193
- bool writable;
194
- uint32_t data_size;
195
- struct xattr_key cache_key;
196
- struct stat statbuf;
197
- char * contents;
180
+ Check_Type(cachedir_v, T_STRING);
181
+ Check_Type(path_v, T_STRING);
198
182
 
199
- VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
200
- VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
201
- VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
202
-
203
- /* don't leak memory */
204
- #define return error!
205
- #define rb_raise error!
206
-
207
- retry = 0;
208
- begin:
209
- output_data = Qnil;
210
- contents = 0;
211
-
212
- /* Blow up if we can't turn our argument into a char* */
213
- Check_Type(pathval, T_STRING);
214
- path = RSTRING_PTR(pathval);
215
-
216
- /* open the file, get its mtime and read the cache key xattr */
217
- CHECK_C(fd = bs_open(path, &writable), "open");
218
- CHECK_C( fstat(fd, &statbuf), "fstat");
219
- CHECK_C(valid_cache = bs_get_cache(fd, &cache_key), "fgetxattr");
220
-
221
- /* `valid_cache` is true if the cache key isn't trivially invalid, e.g. built
222
- * with a different RUBY_REVISION */
223
- if (valid_cache && cache_key.mtime == (uint64_t)statbuf.st_mtime) {
224
- /* if the mtimes match, assume the cache is valid. fetch the cached data. */
225
- ret = bs_fetch_data(fd, (size_t)cache_key.data_size, handler, &output_data, &exception_tag);
226
- if (ret == -1 && errno == _ENOATTR) {
227
- /* the key was present, but the data was missing. remove the key, and
228
- * start over */
229
- CHECK_C(fremovexattr(fd, xattr_key_name REMOVEXATTR_TRAILER), "fremovexattr");
230
- goto retry;
231
- }
232
- CHECK_RB0();
233
- CHECK_C(ret, "fgetxattr/fetch-data");
234
- if (!NIL_P(output_data)) {
235
- stats.hit++;
236
- SUCCEED(output_data); /* this is the fast-path to shoot for */
237
- }
238
- valid_cache = false; /* invalid cache; we'll want to regenerate it */
183
+ if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
184
+ rb_raise(rb_eArgError, "cachedir too long");
239
185
  }
240
186
 
241
- /* read the contents of the file and crc32 it to compare with the cache key */
242
- CHECK_C(bs_read_contents(fd, statbuf.st_size, &contents), "read") /* contents must be xfree'd */
243
-
244
- /* we need to pass this char* to ruby-land */
245
- input_data = rb_str_new_static(contents, statbuf.st_size);
187
+ char * cachedir = RSTRING_PTR(cachedir_v);
188
+ char * path = RSTRING_PTR(path_v);
189
+ char cache_path[MAX_CACHEPATH_SIZE];
246
190
 
247
- /* if we didn't have write permission to the file, bail now -- everything
248
- * that follows is about generating and writing the cache. Let's just convert
249
- * the input format to the output format and return */
250
- if (!writable) {
251
- stats.unwritable++;
252
- CHECK_RB(bs_input_to_output(handler, input_data, &output_data, &exception_tag));
253
- SUCCEED(output_data);
191
+ { /* generate cache path to cache_path */
192
+ char * tmp = (char *)&cache_path;
193
+ bs_cache_path(cachedir, path, &tmp);
254
194
  }
255
195
 
256
- /* Now, we know we have write permission, and can update the xattrs.
257
- * Additionally, we know the cache is currently missing or absent, and needs
258
- * to be updated. */
259
- stats.miss++;
196
+ return bs_fetch(path, path_v, cache_path, handler);
197
+ }
260
198
 
261
- /* First, convert the input format to the storage format by calling into the
262
- * handler. */
263
- CHECK_RB(exception_tag = bs_input_to_storage(handler, input_data, pathval, &storage_data));
264
- if (storage_data == uncompilable) {
265
- /* The handler can raise Bootsnap::CompileCache::Uncompilable. When it does this,
266
- * we just call the input_to_output handler method, bypassing the storage format. */
267
- CHECK_RB(bs_input_to_output(handler, input_data, &output_data, &exception_tag));
268
- stats.uncompilable++;
269
- SUCCEED(output_data);
270
- }
199
+ static int
200
+ open_current_file(char * path, struct bs_cache_key * key)
201
+ {
202
+ struct stat statbuf;
203
+ int fd;
271
204
 
272
- /* we can only really write strings to xattrs */
273
- if (!RB_TYPE_P(storage_data, T_STRING)) {
274
- goto invalid_type_storage_data;
275
- }
205
+ fd = open(path, O_RDONLY);
206
+ if (fd < 0) return fd;
276
207
 
277
- /* xattrs can't exceed 64MB */
278
- if (RB_TYPE_P(storage_data, T_STRING) && RSTRING_LEN(storage_data) > 64 * 1024 * 1024) {
279
- if (logging_enabled()) {
280
- fprintf(stderr, "[OPT_AOT_LOG] warning: compiled artifact is over 64MB, which is too large to store in an xattr.%s\n", path);
281
- }
282
- CHECK_RB(bs_input_to_output(handler, input_data, &output_data, &exception_tag));
283
- SUCCEED(output_data);
208
+ if (fstat(fd, &statbuf) < 0) {
209
+ close(fd);
210
+ return -1;
284
211
  }
285
212
 
286
- data_size = (uint32_t)RSTRING_LEN(storage_data);
287
-
288
- /* update the cache, but don't leave it in an invalid state even briefly: remove the key first. */
289
- fremovexattr(fd, xattr_key_name REMOVEXATTR_TRAILER);
290
- CHECK_C(fsetxattr(fd, xattr_data_name, RSTRING_PTR(storage_data), (size_t)data_size, 0 SETXATTR_TRAILER), "fsetxattr");
291
- CHECK_C(bs_update_key(fd, data_size, statbuf.st_mtime), "fsetxattr");
213
+ key->version = current_version;
214
+ key->os_version = current_os_version;
215
+ key->compile_option = current_compile_option_crc32;
216
+ key->ruby_revision = current_ruby_revision;
217
+ key->size = (uint64_t)statbuf.st_size;
218
+ key->mtime = (uint64_t)statbuf.st_mtime;
292
219
 
293
- /* updating xattrs bumps mtime, so we set them back after */
294
- CHECK_C(bs_close_and_unclobber_times(&fd, path, statbuf.st_atime, statbuf.st_mtime), "close/utime");
220
+ return fd;
221
+ }
295
222
 
296
- /* convert the data we just stored into the output format */
297
- CHECK_RB(exception_tag = bs_storage_to_output(handler, storage_data, &output_data));
223
+ #define ERROR_WITH_ERRNO -1
224
+ #define CACHE_MISSING_OR_INVALID -2
298
225
 
299
- /* if the storage data was broken, remove the cache and run input_to_output */
300
- if (output_data == Qnil) {
301
- /* deletion here is best effort; no need to fail if it does */
302
- fremovexattr(fd, xattr_key_name REMOVEXATTR_TRAILER);
303
- fremovexattr(fd, xattr_data_name REMOVEXATTR_TRAILER);
304
- CHECK_RB(bs_input_to_output(handler, input_data, &output_data, &exception_tag));
305
- }
226
+ /*
227
+ * @return CACHE_MISSING_OR_INVALID
228
+ * @return ERROR_WITH_ERRNO(-1) and errno
229
+ */
230
+ static int
231
+ bs_read_key(int fd, struct bs_cache_key * key)
232
+ {
233
+ ssize_t nread = read(fd, key, KEY_SIZE);
234
+ if (nread < 0) return ERROR_WITH_ERRNO;
235
+ if (nread < KEY_SIZE) return CACHE_MISSING_OR_INVALID;
236
+ return 0;
237
+ }
306
238
 
307
- SUCCEED(output_data);
239
+ /*
240
+ * @return CACHE_MISSING_OR_INVALID
241
+ * @return ERROR_WITH_ERRNO(-1) and errno
242
+ */
243
+ static int
244
+ open_cache_file(const char * path, struct bs_cache_key * key)
245
+ {
246
+ int fd, res;
308
247
 
309
- #undef return
310
- #undef rb_raise
311
- #define CLEANUP \
312
- if (contents != 0) xfree(contents); \
313
- if (fd > 0) close(fd);
248
+ fd = open(path, O_RDWR, 0644);
249
+ if (fd < 0) {
250
+ if (errno == ENOENT) return CACHE_MISSING_OR_INVALID;
251
+ return ERROR_WITH_ERRNO;
252
+ }
314
253
 
315
- __builtin_unreachable();
316
- cleanup:
317
- CLEANUP;
318
- return output_data;
319
- fail:
320
- CLEANUP;
321
- stats.fail++;
322
- rb_exc_raise(exception);
323
- __builtin_unreachable();
324
- invalid_type_storage_data:
325
- CLEANUP;
326
- stats.fail++;
327
- Check_Type(storage_data, T_STRING);
328
- __builtin_unreachable();
329
- retry:
330
- CLEANUP;
331
- stats.retry++;
332
- if (retry == 1) {
333
- rb_raise(rb_eRuntimeError, "internal error in bootsnap");
334
- __builtin_unreachable();
254
+ res = bs_read_key(fd, key);
255
+ if (res < 0) {
256
+ close(fd);
257
+ return res;
335
258
  }
336
- retry = 1;
337
- goto begin;
338
- raise:
339
- CLEANUP;
340
- stats.fail++;
341
- rb_jump_tag(exception_tag);
342
- __builtin_unreachable();
259
+
260
+ return fd;
343
261
  }
344
262
 
345
263
  static int
346
- bs_fetch_data(int fd, size_t size, VALUE handler, VALUE * output_data, int * exception_tag)
264
+ fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE * output_data, int * exception_tag)
347
265
  {
266
+ char * data;
267
+ ssize_t nread;
348
268
  int ret;
349
- ssize_t nbytes;
350
- void * xattr_data;
351
- VALUE storage_data;
352
269
 
353
- *output_data = Qnil;
354
- *exception_tag = 0;
270
+ VALUE storage_data;
355
271
 
356
- xattr_data = ALLOC_N(uint8_t, size);
357
- nbytes = fgetxattr(fd, xattr_data_name, xattr_data, size GETXATTR_TRAILER);
358
- if (nbytes == -1) {
359
- ret = -1;
360
- goto done;
272
+ if (data_size > 100000000000) {
273
+ errno = EINVAL; /* because wtf? */
274
+ return -1;
361
275
  }
362
- if (nbytes != (ssize_t)size) {
363
- errno = EIO; /* lies but whatever */
276
+ data = ALLOC_N(char, data_size);
277
+ nread = read(fd, data, data_size);
278
+ if (nread < 0) {
364
279
  ret = -1;
365
280
  goto done;
366
281
  }
367
- storage_data = rb_str_new_static(xattr_data, nbytes);
368
- ret = bs_storage_to_output(handler, storage_data, output_data);
369
- if (ret != 0) {
370
- *exception_tag = ret;
371
- errno = 0;
282
+ if (nread != data_size) {
283
+ ret = CACHE_MISSING_OR_INVALID;
284
+ goto done;
372
285
  }
286
+
287
+ storage_data = rb_str_new_static(data, data_size);
288
+
289
+ *exception_tag = bs_storage_to_output(handler, storage_data, output_data);
290
+ ret = 0;
373
291
  done:
374
- xfree(xattr_data);
292
+ xfree(data);
375
293
  return ret;
376
294
  }
377
295
 
378
- static int
379
- bs_update_key(int fd, uint32_t data_size, uint64_t current_mtime)
296
+ static ssize_t
297
+ bs_read_contents(int fd, size_t size, char ** contents)
380
298
  {
381
- struct xattr_key xattr_key;
382
-
383
- xattr_key = (struct xattr_key){
384
- .version = current_version,
385
- .os_version = os_version,
386
- .data_size = data_size,
387
- .compile_option = current_compile_option_crc32,
388
- .ruby_revision = current_ruby_revision,
389
- .mtime = current_mtime,
390
- };
391
-
392
- return fsetxattr(fd, xattr_key_name, &xattr_key, (size_t)xattr_key_size, 0 SETXATTR_TRAILER);
299
+ *contents = ALLOC_N(char, size);
300
+ return read(fd, *contents, size);
393
301
  }
394
302
 
395
- /*
396
- * Open the file O_RDWR if possible, or O_RDONLY if that throws EACCES.
397
- * Set +writable+ to indicate which mode was used.
398
- */
399
303
  static int
400
- bs_open(const char * path, bool * writable)
304
+ mkpath(char * file_path, mode_t mode)
401
305
  {
402
- int fd;
403
-
404
- *writable = true;
405
- fd = open(path, O_RDWR);
406
- if (fd == -1 && errno == EACCES) {
407
- *writable = false;
408
- if (logging_enabled()) {
409
- fprintf(stderr, "[OPT_AOT_LOG] warning: unable to cache because no write permission to %s\n", path);
306
+ /* It would likely be more efficient to count back until we
307
+ * find a component that *does* exist, but this will only run
308
+ * at most 256 times, so it seems not worthwhile to change. */
309
+ char * p;
310
+ for (p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) {
311
+ *p = '\0';
312
+ if (mkdir(file_path, mode) == -1) {
313
+ if (errno != EEXIST) {
314
+ *p = '/';
315
+ return -1;
316
+ }
410
317
  }
411
- fd = open(path, O_RDONLY);
318
+ *p = '/';
412
319
  }
413
- return fd;
320
+ return 0;
414
321
  }
415
322
 
416
- /*
417
- * Fetch the cache key from the relevant xattr into +key+.
418
- * Returns:
419
- * 0: invalid/no cache
420
- * 1: valid cache
421
- * -1: fgetxattr failed, errno is set
422
- */
423
323
  static int
424
- bs_get_cache(int fd, struct xattr_key * key)
324
+ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data)
425
325
  {
426
- ssize_t nbytes;
326
+ char template[MAX_CACHEPATH_SIZE + 20];
327
+ char * dest;
328
+ char * tmp_path;
329
+ int fd;
330
+ ssize_t nwrite;
427
331
 
428
- nbytes = fgetxattr(fd, xattr_key_name, (void *)key, xattr_key_size GETXATTR_TRAILER);
429
- if (nbytes == -1 && errno != _ENOATTR) {
332
+ dest = strncpy(template, path, MAX_CACHEPATH_SIZE);
333
+ strcat(dest, ".tmp.XXXXXX");
334
+
335
+ tmp_path = mktemp(template);
336
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
337
+ if (fd < 0) {
338
+ if (mkpath(path, 0755) < 0) return -1;
339
+ fd = open(tmp_path, O_WRONLY | O_CREAT, 0644);
340
+ if (fd < 0) return -1;
341
+ }
342
+
343
+ key->data_size = RSTRING_LEN(data);
344
+ nwrite = write(fd, key, KEY_SIZE);
345
+ if (nwrite < 0) return -1;
346
+ if (nwrite != KEY_SIZE) {
347
+ errno = EIO; /* Lies but whatever */
430
348
  return -1;
431
349
  }
432
350
 
433
- return (nbytes == (ssize_t)xattr_key_size && \
434
- key->version == current_version && \
435
- key->os_version == os_version && \
436
- key->compile_option == current_compile_option_crc32 && \
437
- key->ruby_revision == current_ruby_revision);
438
- }
351
+ nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
352
+ if (nwrite < 0) return -1;
353
+ if (nwrite != RSTRING_LEN(data)) {
354
+ errno = EIO; /* Lies but whatever */
355
+ return -1;
356
+ }
439
357
 
440
- /*
441
- * Read an entire file into a char*
442
- * contents must be freed with xfree() when done.
443
- */
444
- static size_t
445
- bs_read_contents(int fd, size_t size, char ** contents)
446
- {
447
- *contents = ALLOC_N(char, size);
448
- return read(fd, *contents, size);
358
+ close(fd);
359
+ return rename(tmp_path, path);
449
360
  }
450
361
 
451
- static int
452
- bs_close_and_unclobber_times(int * fd, const char * path, time_t atime, time_t mtime)
362
+ static VALUE
363
+ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler)
453
364
  {
454
- struct utimbuf times = {
455
- .actime = atime,
456
- .modtime = mtime,
457
- };
458
- if (close(*fd) == -1) {
459
- return -1;
365
+ struct bs_cache_key cached_key, current_key;
366
+ char * contents = NULL;
367
+ int cache_fd = -1, current_fd = -1;
368
+ int res, valid_cache, exception_tag = 0;
369
+
370
+ VALUE input_data; /* data read from source file, e.g. YAML or ruby source */
371
+ VALUE storage_data; /* compiled data, e.g. msgpack / binary iseq */
372
+ VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */
373
+
374
+ VALUE exception;
375
+
376
+ current_fd = open_current_file(path, &current_key);
377
+ if (current_fd < 0) goto fail_errno;
378
+
379
+ cache_fd = open_cache_file(cache_path, &cached_key);
380
+ if (cache_fd < 0 && cache_fd != CACHE_MISSING_OR_INVALID) goto fail_errno;
381
+
382
+ valid_cache = cache_key_equal(&current_key, &cached_key);
383
+
384
+ if (valid_cache) {
385
+ /* Fetch the cache data and return it if we're able to load it successfully */
386
+ res = fetch_cached_data(cache_fd, (ssize_t)cached_key.data_size, handler, &output_data, &exception_tag);
387
+ if (exception_tag != 0) goto raise;
388
+ else if (res == CACHE_MISSING_OR_INVALID) valid_cache = 0;
389
+ else if (res == ERROR_WITH_ERRNO) goto fail_errno;
390
+ else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */
391
+ }
392
+ close(cache_fd);
393
+ cache_fd = -1;
394
+ /* Cache is stale, invalid, or missing. Regenerate and write it out. */
395
+
396
+ if (bs_read_contents(current_fd, current_key.size, &contents) < 0) goto fail_errno;
397
+ input_data = rb_str_new_static(contents, current_key.size);
398
+
399
+ exception_tag = bs_input_to_storage(handler, input_data, path_v, &storage_data);
400
+ if (exception_tag != 0) goto raise;
401
+ if (storage_data == uncompilable) {
402
+ bs_input_to_output(handler, input_data, &output_data, &exception_tag);
403
+ if (exception_tag != 0) goto raise;
404
+ goto succeed;
460
405
  }
461
- *fd = 0;
462
- return utime(path, &times);
406
+ if (!RB_TYPE_P(storage_data, T_STRING)) goto invalid_type_storage_data;
407
+
408
+ res = atomic_write_cache_file(cache_path, &current_key, storage_data);
409
+ if (res < 0) goto fail_errno;
410
+
411
+ exception_tag = bs_storage_to_output(handler, storage_data, &output_data);
412
+ if (exception_tag != 0) goto raise;
413
+
414
+ if (NIL_P(output_data)) {
415
+ if (unlink(cache_path) < 0) goto fail_errno;
416
+ bs_input_to_output(handler, input_data, &output_data, &exception_tag);
417
+ if (exception_tag != 0) goto raise;
418
+ }
419
+ // output_data is now the correct return; cascade into succeed:
420
+
421
+ #define CLEANUP \
422
+ if (contents != NULL) xfree(contents); \
423
+ if (current_fd >= 0) close(current_fd); \
424
+ if (cache_fd >= 0) close(cache_fd);
425
+
426
+ succeed:
427
+ CLEANUP;
428
+ return output_data;
429
+ fail_errno:
430
+ CLEANUP;
431
+ exception = rb_protect(prot_exception_for_errno, INT2FIX(errno), &res);
432
+ if (res) exception = rb_eStandardError;
433
+ rb_exc_raise(exception);
434
+ __builtin_unreachable();
435
+ raise:
436
+ CLEANUP;
437
+ rb_jump_tag(exception_tag);
438
+ __builtin_unreachable();
439
+ invalid_type_storage_data:
440
+ CLEANUP;
441
+ Check_Type(storage_data, T_STRING);
442
+ __builtin_unreachable();
443
+
444
+ #undef CLEANUP
463
445
  }
464
446
 
465
447
  static VALUE
@@ -474,11 +456,28 @@ prot_exception_for_errno(VALUE err)
474
456
  return rb_eStandardError;
475
457
  }
476
458
 
459
+
460
+ /*****************************************************************************/
461
+ /********************* Handler Wrappers **************************************/
462
+ /*****************************************************************************/
463
+
477
464
  static VALUE
478
- prot_input_to_output(VALUE arg)
465
+ prot_storage_to_output(VALUE arg)
479
466
  {
480
- struct i2o_data * data = (struct i2o_data *)arg;
481
- return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data);
467
+ struct s2o_data * data = (struct s2o_data *)arg;
468
+ return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data);
469
+ }
470
+
471
+ static int
472
+ bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
473
+ {
474
+ int state;
475
+ struct s2o_data s2o_data = {
476
+ .handler = handler,
477
+ .storage_data = storage_data,
478
+ };
479
+ *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
480
+ return state;
482
481
  }
483
482
 
484
483
  static void
@@ -491,6 +490,13 @@ bs_input_to_output(VALUE handler, VALUE input_data, VALUE * output_data, int * e
491
490
  *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
492
491
  }
493
492
 
493
+ static VALUE
494
+ prot_input_to_output(VALUE arg)
495
+ {
496
+ struct i2o_data * data = (struct i2o_data *)arg;
497
+ return rb_funcall(data->handler, rb_intern("input_to_output"), 1, data->input_data);
498
+ }
499
+
494
500
  static VALUE
495
501
  try_input_to_storage(VALUE arg)
496
502
  {
@@ -526,36 +532,3 @@ bs_input_to_storage(VALUE handler, VALUE input_data, VALUE pathval, VALUE * stor
526
532
  *storage_data = rb_protect(prot_input_to_storage, (VALUE)&i2s_data, &state);
527
533
  return state;
528
534
  }
529
-
530
- static VALUE
531
- prot_storage_to_output(VALUE arg)
532
- {
533
- struct s2o_data * data = (struct s2o_data *)arg;
534
- return rb_funcall(data->handler, rb_intern("storage_to_output"), 1, data->storage_data);
535
- }
536
-
537
- static int
538
- bs_storage_to_output(VALUE handler, VALUE storage_data, VALUE * output_data)
539
- {
540
- int state;
541
- struct s2o_data s2o_data = {
542
- .handler = handler,
543
- .storage_data = storage_data,
544
- };
545
- *output_data = rb_protect(prot_storage_to_output, (VALUE)&s2o_data, &state);
546
- return state;
547
- }
548
-
549
- /* default no if empty, yes if present, no if "0" */
550
- static int
551
- logging_enabled()
552
- {
553
- char * log = getenv("OPT_AOT_LOG");
554
- if (log == 0) {
555
- return 0;
556
- } else if (log[0] == '0') {
557
- return 0;
558
- } else {
559
- return 1;
560
- }
561
- }
@@ -1,8 +1,6 @@
1
1
  #ifndef BOOTSNAP_H
2
2
  #define BOOTSNAP_H 1
3
3
 
4
- #include <stdint.h>
5
- #include <sys/types.h>
6
- #include "ruby.h"
4
+ /* doesn't expose anything */
7
5
 
8
6
  #endif /* BOOTSNAP_H */
@@ -27,6 +27,7 @@ module Bootsnap
27
27
  ) if load_path_cache
28
28
 
29
29
  Bootsnap::CompileCache.setup(
30
+ cache_dir: cache_dir + '/bootsnap-compile-cache',
30
31
  iseq: compile_cache_iseq,
31
32
  yaml: compile_cache_yaml
32
33
  )
@@ -3,13 +3,13 @@ require_relative 'compile_cache/yaml'
3
3
 
4
4
  module Bootsnap
5
5
  module CompileCache
6
- def self.setup(iseq:, yaml:)
6
+ def self.setup(cache_dir:, iseq:, yaml:)
7
7
  if iseq
8
- Bootsnap::CompileCache::ISeq.install!
8
+ Bootsnap::CompileCache::ISeq.install!(cache_dir)
9
9
  end
10
10
 
11
11
  if yaml
12
- Bootsnap::CompileCache::YAML.install!
12
+ Bootsnap::CompileCache::YAML.install!(cache_dir)
13
13
  end
14
14
  end
15
15
  end
@@ -4,6 +4,10 @@ require 'zlib'
4
4
  module Bootsnap
5
5
  module CompileCache
6
6
  module ISeq
7
+ class << self
8
+ attr_accessor :cache_dir
9
+ end
10
+
7
11
  def self.input_to_storage(_, path)
8
12
  RubyVM::InstructionSequence.compile_file(path).to_binary
9
13
  rescue SyntaxError
@@ -28,6 +32,7 @@ module Bootsnap
28
32
  module InstructionSequenceMixin
29
33
  def load_iseq(path)
30
34
  Bootsnap::CompileCache::Native.fetch(
35
+ Bootsnap::CompileCache::ISeq.cache_dir,
31
36
  path.to_s,
32
37
  Bootsnap::CompileCache::ISeq
33
38
  )
@@ -62,7 +67,8 @@ module Bootsnap
62
67
  Bootsnap::CompileCache::Native.compile_option_crc32 = crc
63
68
  end
64
69
 
65
- def self.install!
70
+ def self.install!(cache_dir)
71
+ Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
66
72
  Bootsnap::CompileCache::ISeq.compile_option_updated
67
73
  class << RubyVM::InstructionSequence
68
74
  prepend InstructionSequenceMixin
@@ -30,7 +30,7 @@ module Bootsnap
30
30
  ::YAML.load(data)
31
31
  end
32
32
 
33
- def self.install!
33
+ def self.install!(cache_dir)
34
34
  require 'yaml'
35
35
  require 'msgpack'
36
36
 
@@ -44,6 +44,7 @@ module Bootsnap
44
44
  klass = class << ::YAML; self; end
45
45
  klass.send(:define_method, :load_file) do |path|
46
46
  Bootsnap::CompileCache::Native.fetch(
47
+ cache_dir,
47
48
  path.to_s,
48
49
  Bootsnap::CompileCache::YAML
49
50
  )
@@ -1,3 +1,3 @@
1
1
  module Bootsnap
2
- VERSION = "0.2.15"
2
+ VERSION = "0.3.0.pre"
3
3
  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: 0.2.15
4
+ version: 0.3.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Burke Libbey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-23 00:00:00.000000000 Z
11
+ date: 2017-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -162,9 +162,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
162
162
  version: 2.3.0
163
163
  required_rubygems_version: !ruby/object:Gem::Requirement
164
164
  requirements:
165
- - - ">="
165
+ - - ">"
166
166
  - !ruby/object:Gem::Version
167
- version: '0'
167
+ version: 1.3.1
168
168
  requirements: []
169
169
  rubyforge_project:
170
170
  rubygems_version: 2.6.10