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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49ff0856e24d9a62e6dda014115652431074d4a45e1cb4270c92beeae2baa7ea
4
- data.tar.gz: adaa86c18d254bf07704997050133db592dd60dda5ca9aed9f4c2bc34edfd7a9
3
+ metadata.gz: eeae38802618342fade685584872134cd905229adfe493619130bf008c649390
4
+ data.tar.gz: ff35b13a1ff26f2aa42063a91ebfea888efd03ffd4fabb32911baf70557a3cdb
5
5
  SHA512:
6
- metadata.gz: 599c397e9710bb7dfe528e3ec49b64cfbf61f0d73089dc3476d85587317c894367b09f8b47e062d5cc98646575512421f28860aadeceac62f05588a80830726b
7
- data.tar.gz: 60b9896f92cf41389933c80bb21e5c4e899e54648575ca778dcea2ba881876ef3f2539bdc46ab1c22b3f234f1237019454f8726957f5858c83ba59a2065e406f
6
+ metadata.gz: 43b28e5a1739a80ebe77064612fbf11024d9b6f7a9e3fd4357a0d3c6d99f90322953b34b484d7b8c010e40336fa1ba2d64bb4c59ec59bd0cef2ebaaea1fefd65
7
+ data.tar.gz: 70a9068038a5bc107ab6beb398491dd04cec70c2bf402dda35c80e70282033c2a1e7da482a15a0f378676835daee3eb220af66d02ab98a4d2be7991fb18d818d
data/.dockerignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ crystalruby-*.gem
10
+ /crystalruby/
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
- primes = 0
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
- primes += 1
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
- primes = 0
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
- primes += 1
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`. 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.
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
- You can define shards inside `./crystalruby/src/shard.yml`
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 installed shards after installing them. E.g. inside `./crystalruby/src/main.cr`
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 teardown operations.
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 by default uses only a single thread, while utilising 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.
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 expose this behaviour, `crystalruby` implements a Reactor, which multiplexes all Ruby calls to Crystal across a single thread. This way you can safely use `crystalruby` in a multi-threaded Ruby environment.
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 use the `async` option on crystalized ruby methods. This allows several Ruby threads to invoke Crystal code simultaneously.
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 around 10 nanoseconds per call.
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 you Ruby program is already single-threaded this is not a problem.
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
- The logic to detect when to JIT recompile is not robust and can end up in an inconsistent state. To remedy this it is useful to clear out all generated assets and build from scratch.
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/src"
574
- crystal_lib_dir: "./crystalruby/lib"
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/src"
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/src"
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.chdir("#{CrystalRuby.config.crystal_src_dir}") do
29
- if system("shards update")
30
- puts "Shards installed successfully."
31
- else
32
- puts "Error installing shards."
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
- # This is a stub for the clear command
40
- FileUtils.rm_rf("#{CrystalRuby.config.crystal_src_dir}/generated")
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
- # This is a stub for the build command
45
+ # TODO: Iterate through all generated libs and build
45
46
  puts "Build command is not implemented yet."
46
47
  end
47
48
 
@@ -1,104 +1,132 @@
1
1
  module CrystalRuby
2
2
  module Adapter
3
- # Define a method to set the @crystalize proc if it doesn't already exist
4
- def crystalize(raw: false, async: false, **options, &block)
5
- (args,), returns = options.first
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 = { raw: raw, async: async, args: args, returns: returns, block: block }
10
- end
11
-
12
- def crystal(raw: false, &block)
13
- inline_crystal_body = Template::InlineChunk.render(
14
- {
15
- module_name: name,
16
- body: block.source.lines[
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
- MethodSource.instance_variable_get(:@lines_for_file).delete(method.source_location[0])
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
- result = Reactor.schedule_work!(self, lib_fname, *args, function[:ffi_ret_type], async: async)
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
- if function[:retval_map]
97
- function[:retval_map][result]
98
- else
99
- result
100
- end
101
- end
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: CrystalRuby.config.crystal_src_dir_abs / CrystalRuby.config.crystal_main_file,
9
- lib: CrystalRuby.config.crystal_lib_dir_abs / CrystalRuby.config.crystal_lib_name,
9
+ src:,
10
+ lib:,
10
11
  verbose: CrystalRuby.config.verbose,
11
12
  debug: CrystalRuby.config.debug
12
13
  )
13
- Dir.chdir(CrystalRuby.config.crystal_src_dir_abs) do
14
- compile_command = compile_command!(verbose: verbose, debug: debug, lib: lib, src: src)
15
- link_command = link_cmd!(verbose: verbose, lib: lib, src: src)
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.compile_command!(verbose:, debug:, lib:, src:)
35
- @compile_command ||= begin
36
- verbose_flag = verbose ? "--verbose" : ""
37
- debug_flag = debug ? "" : "--release --no-debug"
38
- redirect_output = " > /dev/null " unless verbose
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
@@ -3,20 +3,19 @@ require "yaml"
3
3
  require "logger"
4
4
 
5
5
  module CrystalRuby
6
- def self.config
7
- Config.instance
8
- end
9
-
10
- %w[debug info warn error].each do |level|
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
- # Define a nested Config class
19
- class Config
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/src")
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[crystal_main_file crystal_lib_name crystal_project_root].each do |method_name|
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 crystal_lib_dir].each do |method_name|
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 = {}