crystalruby 0.1.13 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.dockerignore +10 -0
- data/README.md +49 -26
- data/exe/crystalruby +15 -14
- data/lib/crystalruby/adapter.rb +98 -70
- data/lib/crystalruby/compilation.rb +13 -83
- data/lib/crystalruby/config.rb +14 -17
- data/lib/crystalruby/function.rb +234 -0
- data/lib/crystalruby/library.rb +191 -0
- data/lib/crystalruby/reactor.rb +30 -62
- data/lib/crystalruby/template.rb +0 -1
- data/lib/crystalruby/templates/index.cr +33 -12
- data/lib/crystalruby/typebuilder.rb +2 -0
- data/lib/crystalruby/typemaps.rb +87 -1
- data/lib/crystalruby/types/array.rb +4 -3
- data/lib/crystalruby/types/hash.rb +5 -3
- data/lib/crystalruby/types/named_tuple.rb +6 -3
- data/lib/crystalruby/types/time.rb +3 -1
- data/lib/crystalruby/types/tuple.rb +3 -1
- data/lib/crystalruby/types/type_serializer/json.rb +3 -2
- data/lib/crystalruby/types/typedef.rb +3 -1
- data/lib/crystalruby/types/union_type.rb +8 -6
- data/lib/crystalruby/version.rb +1 -1
- data/lib/crystalruby.rb +24 -253
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eeae38802618342fade685584872134cd905229adfe493619130bf008c649390
|
4
|
+
data.tar.gz: ff35b13a1ff26f2aa42063a91ebfea888efd03ffd4fabb32911baf70557a3cdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43b28e5a1739a80ebe77064612fbf11024d9b6f7a9e3fd4357a0d3c6d99f90322953b34b484d7b8c010e40336fa1ba2d64bb4c59ec59bd0cef2ebaaea1fefd65
|
7
|
+
data.tar.gz: 70a9068038a5bc107ab6beb398491dd04cec70c2bf402dda35c80e70282033c2a1e7da482a15a0f378676835daee3eb220af66d02ab98a4d2be7991fb18d818d
|
data/.dockerignore
ADDED
data/README.md
CHANGED
@@ -46,8 +46,7 @@ require 'benchmark'
|
|
46
46
|
module PrimeCounter
|
47
47
|
crystalize [n: :int32] => :int32
|
48
48
|
def count_primes_upto_cr(n)
|
49
|
-
|
50
|
-
(2..n).each do |i|
|
49
|
+
(2..n).each.count do |i|
|
51
50
|
is_prime = true
|
52
51
|
(2..Math.sqrt(i).to_i).each do |j|
|
53
52
|
if i % j == 0
|
@@ -55,16 +54,14 @@ module PrimeCounter
|
|
55
54
|
break
|
56
55
|
end
|
57
56
|
end
|
58
|
-
|
57
|
+
is_prime
|
59
58
|
end
|
60
|
-
primes
|
61
59
|
end
|
62
60
|
|
63
61
|
module_function
|
64
62
|
|
65
63
|
def count_primes_upto_rb(n)
|
66
|
-
|
67
|
-
(2..n).each do |i|
|
64
|
+
(2..n).each.count do |i|
|
68
65
|
is_prime = true
|
69
66
|
(2..Math.sqrt(i).to_i).each do |j|
|
70
67
|
if i % j == 0
|
@@ -72,9 +69,8 @@ module PrimeCounter
|
|
72
69
|
break
|
73
70
|
end
|
74
71
|
end
|
75
|
-
|
72
|
+
is_prime
|
76
73
|
end
|
77
|
-
primes
|
78
74
|
end
|
79
75
|
end
|
80
76
|
|
@@ -262,7 +258,7 @@ E.g.
|
|
262
258
|
|
263
259
|
IntArrOrBoolArr = crtype{ Array(Bool) | Array(Int32) }
|
264
260
|
|
265
|
-
crystalize [a: IntArrOrBoolArr] => json{ IntArrOrBoolArr }
|
261
|
+
crystalize [a: json{ IntArrOrBoolArr }] => json{ IntArrOrBoolArr }
|
266
262
|
def method_with_named_types(a)
|
267
263
|
return a
|
268
264
|
end
|
@@ -277,16 +273,19 @@ Exceptions thrown in Crystal code can be caught in Ruby.
|
|
277
273
|
You can use any Crystal shards and write ordinary, stand-alone Crystal code.
|
278
274
|
|
279
275
|
The default entry point for the crystal shared library generated by the gem is
|
280
|
-
inside `./crystalruby/src/main.cr`.
|
276
|
+
inside `./crystalruby/{library_name}/src/main.cr`.
|
277
|
+
`{library_name}` defaults to `crystalruby` if you haven't explicitly specific a different library target.
|
281
278
|
|
282
|
-
|
279
|
+
This file is not automatically overridden by the gem, and is safe for you to define and require new files relative to this location to write additional stand-alone Crystal code.
|
280
|
+
|
281
|
+
You can define shard dependencies inside `./crystalruby/{library_name}/src/shard.yml`
|
283
282
|
Run the below to install new shards
|
284
283
|
|
285
284
|
```bash
|
286
285
|
bundle exec crystalruby install
|
287
286
|
```
|
288
287
|
|
289
|
-
Remember to require these
|
288
|
+
Remember to also require these dependencies after installing them to make them available to `crystalruby` code. E.g. inside `./crystalruby/{libraryname}/src/main.cr`
|
290
289
|
|
291
290
|
You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
|
292
291
|
|
@@ -313,7 +312,7 @@ MyModule.add("1", "2")
|
|
313
312
|
|
314
313
|
## Inline Chunks
|
315
314
|
|
316
|
-
`crystalruby` also allows you to write inline Crystal code that does not require binding to Ruby. This can be useful for e.g. performing setup or
|
315
|
+
`crystalruby` also allows you to write inline Crystal code that does not require binding to Ruby. This can be useful for e.g. performing setup operations or initializations.
|
317
316
|
|
318
317
|
Follow these steps for a toy example of how we can use crystalized ruby and inline chunks to expose the [crystal-redis](https://github.com/stefanwille/crystal-redis) library to Ruby.
|
319
318
|
|
@@ -471,13 +470,13 @@ Then you can run this file as part of your build step, to ensure all Crystal cod
|
|
471
470
|
|
472
471
|
## Concurrency
|
473
472
|
|
474
|
-
While Ruby programs allow multi-threading, Crystal
|
473
|
+
While Ruby programs allow multi-threading, Crystal (if not using experimental multi-thread support) uses only a single thread and utilises Fiber based cooperative-multitasking to allow for concurrent execution. This means that by default, Crystal libraries can not safely be invoked in parallel across multiple Ruby threads.
|
475
474
|
|
476
|
-
To safely
|
475
|
+
To safely utilise `crystalruby` in a multithreaded environment, `crystalruby` implements a Reactor, which multiplexes all Ruby calls to Crystal across a single thread.
|
477
476
|
|
478
|
-
By default `crystalruby` methods are blocking/synchronous, this means that for blocking operations, a single crystalruby call can block the entire reactor.
|
477
|
+
By default `crystalruby` methods are blocking/synchronous, this means that for blocking operations, a single crystalruby call can block the entire reactor across _all_ threads.
|
479
478
|
|
480
|
-
To allow you to benefit from Crystal's fiber based concurrency, you can use
|
479
|
+
To allow you to benefit from Crystal's fiber based concurrency, you can use the `async: true` option on crystalized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
|
481
480
|
|
482
481
|
E.g.
|
483
482
|
|
@@ -502,11 +501,11 @@ end
|
|
502
501
|
|
503
502
|
### Reactor performance
|
504
503
|
|
505
|
-
There is a small amount of overhead to multiplexing calls across a single thread. Ad-hoc testing amounts this to be
|
504
|
+
There is a small amount of synchronization overhead to multiplexing calls across a single thread. Ad-hoc testing on a fast machine amounts this to be within the order of 10 nanoseconds per call.
|
506
505
|
For most use-cases this overhead is negligible, especially if the bulk of your CPU heavy task occurs exclusively in Crystal code. However, if you are invoking very fast Crystal code from Ruby in a tight loop (e.g. a simple 1 + 2)
|
507
506
|
then the overhead of the reactor can become significant.
|
508
507
|
|
509
|
-
In this case you can use the `crystalruby` in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that _all_ calls to Crystal must occur from a single thread. If
|
508
|
+
In this case you can use the `crystalruby` in a single-threaded mode to avoid the reactor overhead and greatly increase performance, with the caveat that _all_ calls to Crystal must occur from a single thread. If your Ruby program is already single-threaded this is not a problem.
|
510
509
|
|
511
510
|
```ruby
|
512
511
|
CrystalRuby.configure do |config|
|
@@ -514,9 +513,36 @@ CrystalRuby.configure do |config|
|
|
514
513
|
end
|
515
514
|
```
|
516
515
|
|
516
|
+
## Live Reloading
|
517
|
+
|
518
|
+
`crystalruby` supports live reloading of Crystal code. It will intelligently
|
519
|
+
recompile Crystal code only when it detects changes to the embedded function or block bodies. This allows you to iterate quickly on your Crystal code without having to restart your Ruby process in live-reloading environments like Rails.
|
520
|
+
|
521
|
+
## Multi-library support
|
522
|
+
|
523
|
+
Large Crystal projects are known to have long compile times. To mitigate this, `crystalruby` supports splitting your Crystal code into multiple libraries. This allows you to only recompile any libraries that have changed, rather than all crystalcode within the entire project.
|
524
|
+
To indicate which library a piece of embedded Crystal code belongs to, you can use the `lib` option in the `crystalize` and `crystal` methods.
|
525
|
+
If the `lib` option is not provided, the code will be compiled into the default library (simply named `crystalruby`).
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
module Foo
|
529
|
+
crystalize lib: "foo"
|
530
|
+
def bar
|
531
|
+
puts "Hello from Foo"
|
532
|
+
end
|
533
|
+
|
534
|
+
crystal lib: "foo" do
|
535
|
+
REDIS = Redis.new
|
536
|
+
end
|
537
|
+
end
|
538
|
+
```
|
539
|
+
|
540
|
+
Naturally, Crystal methods must reside in the same library to natively interact.
|
541
|
+
Cross library interaction can be facilitated via Ruby code.
|
542
|
+
|
517
543
|
## Troubleshooting
|
518
544
|
|
519
|
-
|
545
|
+
In cases where compiled assets are in left an invalid state, it can be useful to clear out generated assets and rebuild from scratch.
|
520
546
|
|
521
547
|
To do this execute:
|
522
548
|
|
@@ -570,8 +596,8 @@ crystalruby init
|
|
570
596
|
```
|
571
597
|
|
572
598
|
```yaml
|
573
|
-
crystal_src_dir: "./crystalruby
|
574
|
-
|
599
|
+
crystal_src_dir: "./crystalruby"
|
600
|
+
crystal_codegen_dir: "generated"
|
575
601
|
crystal_main_file: "main.cr"
|
576
602
|
crystal_lib_name: "crlib"
|
577
603
|
crystal_codegen_dir: "generated"
|
@@ -582,10 +608,7 @@ Alternatively, these can be set programmatically, e.g:
|
|
582
608
|
|
583
609
|
```ruby
|
584
610
|
CrystalRuby.configure do |config|
|
585
|
-
config.crystal_src_dir = "./crystalruby
|
586
|
-
config.crystal_lib_dir = "./crystalruby/lib"
|
587
|
-
config.crystal_main_file = "main.cr"
|
588
|
-
config.crystal_lib_name = "crlib"
|
611
|
+
config.crystal_src_dir = "./crystalruby"
|
589
612
|
config.crystal_codegen_dir = "generated"
|
590
613
|
config.debug = true
|
591
614
|
config.verbose = false
|
data/exe/crystalruby
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require "bundler/setup"
|
4
3
|
require "crystalruby"
|
5
4
|
require "fileutils"
|
6
5
|
|
@@ -9,10 +8,7 @@ def init
|
|
9
8
|
# Define some dummy content for the YAML file
|
10
9
|
yaml_content = <<~YAML
|
11
10
|
# crystalruby configuration file
|
12
|
-
crystal_src_dir: "./crystalruby
|
13
|
-
crystal_lib_dir: "./crystalruby/lib"
|
14
|
-
crystal_main_file: "main.cr"
|
15
|
-
crystal_lib_name: "crlib"
|
11
|
+
crystal_src_dir: "./crystalruby"
|
16
12
|
crystal_codegen_dir: "generated"
|
17
13
|
log_level: "info"
|
18
14
|
single_thread_mode: false
|
@@ -25,23 +21,28 @@ def init
|
|
25
21
|
end
|
26
22
|
|
27
23
|
def install
|
28
|
-
Dir
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
24
|
+
Dir["#{CrystalRuby.config.crystal_src_dir}/**/src"].each do |src_dir|
|
25
|
+
Dir.chdir(src_dir) do
|
26
|
+
if system("shards check") || system("shards update")
|
27
|
+
puts "Shards installed successfully."
|
28
|
+
else
|
29
|
+
puts "Error installing shards."
|
30
|
+
end
|
33
31
|
end
|
34
32
|
end
|
35
|
-
clean
|
36
33
|
end
|
37
34
|
|
38
35
|
def clean
|
39
|
-
#
|
40
|
-
|
36
|
+
Dir["#{CrystalRuby.config.crystal_src_dir}/**/src/generated"].each do |codegen_dir|
|
37
|
+
FileUtils.rm_rf(codegen_dir)
|
38
|
+
end
|
39
|
+
Dir["#{CrystalRuby.config.crystal_src_dir}/**/lib"].each do |lib_dir|
|
40
|
+
FileUtils.rm_rf(lib_dir)
|
41
|
+
end
|
41
42
|
end
|
42
43
|
|
43
44
|
def build
|
44
|
-
#
|
45
|
+
# TODO: Iterate through all generated libs and build
|
45
46
|
puts "Build command is not implemented yet."
|
46
47
|
end
|
47
48
|
|
data/lib/crystalruby/adapter.rb
CHANGED
@@ -1,104 +1,132 @@
|
|
1
1
|
module CrystalRuby
|
2
2
|
module Adapter
|
3
|
-
#
|
4
|
-
|
5
|
-
|
3
|
+
# Use this method to annotate a Ruby method that should be crystalized.
|
4
|
+
# Compilation and attachment of the method is done lazily.
|
5
|
+
# You can force compilation by calling `CrystalRuby.compile!`
|
6
|
+
# It's important that all code using crystalized methods is
|
7
|
+
# loaded before any manual calls to compile.
|
8
|
+
#
|
9
|
+
# E.g.
|
10
|
+
#
|
11
|
+
# crystalize [a: :int32, b: :int32] => :int32
|
12
|
+
# def add(a, b)
|
13
|
+
# a + b
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Pass `raw: true` to pass Raw crystal code to the compiler as a string instead.
|
17
|
+
# (Useful for cases where the Crystal method body is not valid Ruby)
|
18
|
+
# E.g.
|
19
|
+
# crystalize raw: true [a: :int32, b: :int32] => :int32
|
20
|
+
# def add(a, b)
|
21
|
+
# <<~CRYSTAL
|
22
|
+
# a + b
|
23
|
+
# CRYSTAL
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Pass `async: true` to make the method async.
|
27
|
+
# Crystal methods will always block the currently executing Ruby thread.
|
28
|
+
# With async: false, all other Crystal code will be blocked while this Crystal method is executing (similar to Ruby code with the GVL)
|
29
|
+
# With async: true, several Crystal methods can be executing concurrently.
|
30
|
+
#
|
31
|
+
# Pass lib: "name_of_lib" to compile Crystal code into several distinct libraries.
|
32
|
+
# This can help keep compilation times low, by packaging your Crystal code into separate shared objects.
|
33
|
+
def crystalize(raw: false, async: false, lib: "crystalruby", **options, &block)
|
34
|
+
(args,), returns = options.first || [[], :void]
|
6
35
|
args ||= {}
|
7
36
|
raise "Arguments should be of the form name: :type. Got #{args}" unless args.is_a?(Hash)
|
8
37
|
|
9
|
-
@crystalize_next = {
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
raw ? 2...-2 : 1...-1
|
18
|
-
].join("\n")
|
19
|
-
}
|
20
|
-
)
|
21
|
-
CrystalRuby.write_chunk(self, body: inline_crystal_body)
|
38
|
+
@crystalize_next = {
|
39
|
+
raw: raw,
|
40
|
+
async: async,
|
41
|
+
args: args,
|
42
|
+
returns: returns,
|
43
|
+
block: block,
|
44
|
+
lib: lib
|
45
|
+
}
|
22
46
|
end
|
23
47
|
|
48
|
+
# This method provides a useful DSL for defining Crystal types in pure Ruby.
|
49
|
+
# These types can not be passed directly to Ruby, and must be serialized as either:
|
50
|
+
# JSON or
|
51
|
+
# C-Structures (WIP)
|
52
|
+
#
|
53
|
+
# See #json for an example of how to define arguments or return types for complex objects.
|
54
|
+
# E.g.
|
55
|
+
#
|
56
|
+
# MyType = crtype{ Int32 | Hash(String, Array(Bool) | Float65 | Nil) }
|
24
57
|
def crtype(&block)
|
25
58
|
TypeBuilder.with_injected_type_dsl(self) do
|
26
59
|
TypeBuilder.build(&block)
|
27
60
|
end
|
28
61
|
end
|
29
62
|
|
63
|
+
# Use the json{} helper for defining complex method arguments or return types
|
64
|
+
# that should be serialized to and from Crystal using JSON. (This conversion is applied automatically)
|
65
|
+
#
|
66
|
+
# E.g.
|
67
|
+
# crystalize [a: json{ Int32 | Float64 | Nil } ] => NamedStruct(result: Int32 | Float64 | Nil)
|
30
68
|
def json(&block)
|
31
69
|
crtype(&block).serialize_as(:json)
|
32
70
|
end
|
33
71
|
|
72
|
+
# We trigger attaching of crystalized instance methods here.
|
73
|
+
# If a method is added after a crystalize annotation we assume it's the target of the crystalize annotation.
|
34
74
|
def method_added(method_name)
|
35
|
-
if @crystalize_next
|
36
|
-
define_crystalized_method(method_name, instance_method(method_name))
|
37
|
-
@crystalize_next = nil
|
38
|
-
end
|
75
|
+
define_crystalized_method(method_name, instance_method(method_name)) if @crystalize_next
|
39
76
|
super
|
40
77
|
end
|
41
78
|
|
79
|
+
# We trigger attaching of crystalized class methods here.
|
80
|
+
# If a method is added after a crystalize annotation we assume it's the target of the crystalize annotation.
|
42
81
|
def singleton_method_added(method_name)
|
43
|
-
if @crystalize_next
|
44
|
-
define_crystalized_method(method_name, singleton_method(method_name))
|
45
|
-
@crystalize_next = nil
|
46
|
-
end
|
82
|
+
define_crystalized_method(method_name, singleton_method(method_name)) if @crystalize_next
|
47
83
|
super
|
48
84
|
end
|
49
85
|
|
86
|
+
# Use this method to define inline Crystal code that does not need to be bound to a Ruby method.
|
87
|
+
# This is useful for defining classes, modules, performing set-up tasks etc.
|
88
|
+
# See: docs for .crystalize to understand the `raw` and `lib` parameters.
|
89
|
+
def crystal(raw: false, lib: "crystalruby", &block)
|
90
|
+
inline_crystal_body = Template::InlineChunk.render(
|
91
|
+
{
|
92
|
+
module_name: name, body: extract_source(block, raw: raw)
|
93
|
+
}
|
94
|
+
)
|
95
|
+
CrystalRuby::Library[lib].crystalize_chunk(
|
96
|
+
self,
|
97
|
+
Digest::MD5.hexdigest(inline_crystal_body),
|
98
|
+
inline_crystal_body
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
# We attach crystalized class methods here.
|
103
|
+
# This function is responsible for
|
104
|
+
# - Generating the Crystal source code
|
105
|
+
# - Overwriting the method and class methods by the same name in the caller.
|
106
|
+
# - Lazily triggering compilation and attachment of the Ruby method to the Crystal code.
|
107
|
+
# - We also optionally prepend a block (if given) to the owner, to allow Ruby code to wrap around Crystal code.
|
50
108
|
def define_crystalized_method(method_name, method)
|
51
109
|
CrystalRuby.log_debug("Defining crystalized method #{name}.#{method_name}")
|
52
|
-
CrystalRuby.instantiate_crystal_ruby! unless CrystalRuby.instantiated?
|
53
|
-
|
54
|
-
function_body = method.source.lines[
|
55
|
-
@crystalize_next[:raw] ? 2...-2 : 1...-1
|
56
|
-
].join("\n")
|
57
110
|
|
58
|
-
|
59
|
-
lib_fname = "#{name.downcase}_#{method_name}_#{Digest::MD5.hexdigest(function_body)}"
|
60
|
-
args, returns, block, async = @crystalize_next.values_at(:args, :returns, :block, :async)
|
61
|
-
args ||= {}
|
111
|
+
args, returns, block, async, lib, raw = @crystalize_next.values_at(:args, :returns, :block, :async, :lib, :raw)
|
62
112
|
@crystalize_next = nil
|
63
|
-
function = CrystalRuby.build_function(self, lib_fname, method_name, args, returns, function_body)
|
64
|
-
CrystalRuby.write_chunk(self, name: function[:name], body: function[:body]) do
|
65
|
-
CrystalRuby.log_debug("attaching #{lib_fname} to #{name}")
|
66
|
-
extend FFI::Library
|
67
|
-
ffi_lib CrystalRuby.config.crystal_lib_dir / CrystalRuby.config.crystal_lib_name
|
68
|
-
if async
|
69
|
-
attach_function lib_fname, "#{lib_fname}_async", function[:ffi_types] + %i[int pointer], :void,
|
70
|
-
blocking: true
|
71
|
-
else
|
72
|
-
attach_function lib_fname, function[:ffi_types], function[:ffi_ret_type], blocking: true
|
73
|
-
end
|
74
|
-
if block
|
75
|
-
[self, singleton_class].each do |receiver|
|
76
|
-
receiver.prepend(Module.new do
|
77
|
-
define_method(method_name, &block)
|
78
|
-
end)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
[self, singleton_class].each do |receiver|
|
84
|
-
receiver.define_method(method_name) do |*args|
|
85
|
-
CrystalRuby.build! unless CrystalRuby.compiled?
|
86
|
-
unless CrystalRuby.attached?
|
87
|
-
CrystalRuby.attach!
|
88
|
-
return send(method_name, *args) if block
|
89
|
-
end
|
90
|
-
args.each_with_index do |arg, i|
|
91
|
-
args[i] = function[:arg_maps][i][arg] if function[:arg_maps][i]
|
92
|
-
end
|
93
113
|
|
94
|
-
|
114
|
+
CrystalRuby::Library[lib].crystalize_method(
|
115
|
+
method,
|
116
|
+
args,
|
117
|
+
returns,
|
118
|
+
extract_source(method, raw: raw),
|
119
|
+
async,
|
120
|
+
&block
|
121
|
+
)
|
122
|
+
end
|
95
123
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
124
|
+
# Extract Ruby source to serve as Crystal code directly.
|
125
|
+
# If it's a raw method, we'll strip the string delimiters at either end of the definition.
|
126
|
+
# We need to clear the MethodSource cache here to allow for code reloading.
|
127
|
+
def extract_source(method_or_block, raw: false)
|
128
|
+
method_or_block.source.lines[raw ? 2...-2 : 1...-1].join("\n").tap do
|
129
|
+
MethodSource.instance_variable_get(:@lines_for_file).delete(method_or_block.source_location[0])
|
102
130
|
end
|
103
131
|
end
|
104
132
|
end
|
@@ -1,97 +1,27 @@
|
|
1
|
-
require "open3"
|
2
1
|
require "tmpdir"
|
3
2
|
require "shellwords"
|
4
3
|
|
5
4
|
module CrystalRuby
|
6
5
|
module Compilation
|
6
|
+
class CompilationFailedError < StandardError; end
|
7
|
+
|
7
8
|
def self.compile!(
|
8
|
-
src
|
9
|
-
lib
|
9
|
+
src:,
|
10
|
+
lib:,
|
10
11
|
verbose: CrystalRuby.config.verbose,
|
11
12
|
debug: CrystalRuby.config.debug
|
12
13
|
)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
CrystalRuby.log_debug "Compiling Crystal code: #{compile_command}"
|
18
|
-
unless system(compile_command)
|
19
|
-
CrystalRuby.log_error "Failed to build Crystal object file."
|
20
|
-
return false
|
21
|
-
end
|
22
|
-
|
23
|
-
CrystalRuby.log_debug "Linking Crystal code: #{link_command}"
|
24
|
-
unless system(link_command)
|
25
|
-
CrystalRuby.log_error "Failed to link Crystal library."
|
26
|
-
return false
|
27
|
-
end
|
28
|
-
CrystalRuby.log_info "Compilation successful"
|
29
|
-
end
|
30
|
-
|
31
|
-
true
|
14
|
+
compile_command = build_compile_command(verbose: verbose, debug: debug, lib: lib, src: src)
|
15
|
+
CrystalRuby.log_debug "Compiling Crystal code #{verbose ? ": #{compile_command}" : ""}"
|
16
|
+
raise CompilationFailedError, "Compilation failed" unless system(compile_command)
|
32
17
|
end
|
33
18
|
|
34
|
-
def self.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
src = Shellwords.escape(src)
|
41
|
-
lib = Shellwords.escape(lib)
|
42
|
-
|
43
|
-
%(crystal build #{verbose_flag} #{debug_flag} --cross-compile -o #{lib} #{src}#{redirect_output})
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# Here we misuse the crystal compiler to build a valid linking command
|
48
|
-
# with all of the platform specific flags that we need.
|
49
|
-
# We then use this command to link the object file that we compiled in the previous step.
|
50
|
-
# This is not robust and is likely to need revision in the future.
|
51
|
-
def self.link_cmd!(verbose:, lib:, src:)
|
52
|
-
@link_cmd ||= begin
|
53
|
-
result = nil
|
54
|
-
|
55
|
-
Dir.mktmpdir do |tmp|
|
56
|
-
CrystalRuby.log_debug "Building link command"
|
57
|
-
src = Shellwords.escape(src)
|
58
|
-
lib_dir = Shellwords.escape(CrystalRuby.config.crystal_src_dir_abs / "lib")
|
59
|
-
escaped_output_path = Shellwords.escape(Pathname.new(tmp) / "main")
|
60
|
-
|
61
|
-
command = "timeout -k 2s 2s bash -c \"export CRYSTAL_PATH=$(crystal env CRYSTAL_PATH):#{lib_dir} && crystal build --verbose #{src} -o #{escaped_output_path} \""
|
62
|
-
|
63
|
-
output = ""
|
64
|
-
pid = nil
|
65
|
-
|
66
|
-
CrystalRuby.log_debug "Running command: #{command}"
|
67
|
-
|
68
|
-
Open3.popen2e(command) do |_stdin, stdout_and_stderr, _wait_thr|
|
69
|
-
while line = stdout_and_stderr.gets
|
70
|
-
puts line if verbose
|
71
|
-
output += line # Capture the output
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
CrystalRuby.log_debug "Parsing link command"
|
76
|
-
|
77
|
-
# Parse the output to find the last invocation of the C compiler, which is likely the linking stage
|
78
|
-
# and strip off the targets that the crystal compiler added.
|
79
|
-
link_command_suffix = output.lines.select { |line| line.strip.start_with?("cc") }.last.strip[/.*(-o.*)/, 1]
|
80
|
-
|
81
|
-
# Replace the output file with the path to the object file we compiled
|
82
|
-
link_command_suffix.gsub!(
|
83
|
-
/-o.*main/,
|
84
|
-
"-o #{lib}"
|
85
|
-
)
|
86
|
-
result = %(cc #{lib}.o -shared #{link_command_suffix})
|
87
|
-
result << " > /dev/null 2>&1" unless verbose
|
88
|
-
result
|
89
|
-
end
|
90
|
-
|
91
|
-
result
|
92
|
-
end
|
93
|
-
rescue StandardError
|
94
|
-
""
|
19
|
+
def self.build_compile_command(verbose:, debug:, lib:, src:)
|
20
|
+
verbose_flag = verbose ? "--verbose --progress" : ""
|
21
|
+
debug_flag = debug ? "" : "--release --no-debug"
|
22
|
+
redirect_output = " &> /dev/null " unless verbose
|
23
|
+
lib, src, lib_dir = [lib, src, File.dirname(src)].map(&Shellwords.method(:escape))
|
24
|
+
%(cd #{lib_dir} && crystal build #{verbose_flag} #{debug_flag} --single-module --link-flags "-shared" -o #{lib} #{src}#{redirect_output})
|
95
25
|
end
|
96
26
|
end
|
97
27
|
end
|
data/lib/crystalruby/config.rb
CHANGED
@@ -3,20 +3,19 @@ require "yaml"
|
|
3
3
|
require "logger"
|
4
4
|
|
5
5
|
module CrystalRuby
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
define_singleton_method("log_#{level}") do |*msg|
|
12
|
-
prefix = config.colorize_log_output ? "\e[33mcrystalruby\e[0m\e[90m [#{Thread.current.object_id}]\e[0m" : "[crystalruby] #{Thread.current.object_id}"
|
13
|
-
|
14
|
-
config.logger.send(level, "#{prefix} #{msg.join(", ")}")
|
6
|
+
# Config mixin to easily access the configuration
|
7
|
+
# from anywhere in the code
|
8
|
+
module Config
|
9
|
+
def config
|
10
|
+
Configuration.instance
|
15
11
|
end
|
16
12
|
end
|
17
13
|
|
18
|
-
#
|
19
|
-
|
14
|
+
# Defines our configuration singleton
|
15
|
+
# Config can be specified through either:
|
16
|
+
# - crystalruby.yaml OR
|
17
|
+
# - CrystalRuby.configure block
|
18
|
+
class Configuration
|
20
19
|
include Singleton
|
21
20
|
attr_accessor :debug, :verbose, :logger, :colorize_log_output, :single_thread_mode
|
22
21
|
|
@@ -28,10 +27,7 @@ module CrystalRuby
|
|
28
27
|
rescue StandardError
|
29
28
|
nil
|
30
29
|
end || {}
|
31
|
-
@crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby
|
32
|
-
@crystal_lib_dir = config.fetch("crystal_lib_dir", "./crystalruby/lib")
|
33
|
-
@crystal_main_file = config.fetch("crystal_main_file", "main.cr")
|
34
|
-
@crystal_lib_name = config.fetch("crystal_lib_name", "crlib")
|
30
|
+
@crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby")
|
35
31
|
@crystal_codegen_dir = config.fetch("crystal_codegen_dir", "generated")
|
36
32
|
@crystal_project_root = config.fetch("crystal_project_root", Pathname.pwd)
|
37
33
|
@debug = config.fetch("debug", true)
|
@@ -43,7 +39,7 @@ module CrystalRuby
|
|
43
39
|
@logger.level = Logger.const_get(@log_level.to_s.upcase)
|
44
40
|
end
|
45
41
|
|
46
|
-
%w[
|
42
|
+
%w[crystal_project_root].each do |method_name|
|
47
43
|
define_method(method_name) do
|
48
44
|
@paths_cache[method_name] ||= Pathname.new(instance_variable_get(:"@#{method_name}"))
|
49
45
|
end
|
@@ -60,7 +56,7 @@ module CrystalRuby
|
|
60
56
|
end
|
61
57
|
end
|
62
58
|
|
63
|
-
%w[crystal_src_dir
|
59
|
+
%w[crystal_src_dir].each do |method_name|
|
64
60
|
abs_method_name = "#{method_name}_abs"
|
65
61
|
define_method(abs_method_name) do
|
66
62
|
@paths_cache[abs_method_name] ||= crystal_project_root / instance_variable_get(:"@#{method_name}")
|
@@ -77,6 +73,7 @@ module CrystalRuby
|
|
77
73
|
end
|
78
74
|
end
|
79
75
|
|
76
|
+
extend Config
|
80
77
|
def self.configure
|
81
78
|
yield(config)
|
82
79
|
@paths_cache = {}
|