bootsnap 0.2.15 → 0.3.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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