ruby_wasm 2.5.0-x64-mingw-ucrt

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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.clang-format +8 -0
  3. data/CONTRIBUTING.md +126 -0
  4. data/Gemfile +17 -0
  5. data/LICENSE +21 -0
  6. data/NOTICE +1293 -0
  7. data/README.md +153 -0
  8. data/Rakefile +163 -0
  9. data/Steepfile +24 -0
  10. data/benchmarks/vm_deep_call.rb +55 -0
  11. data/builders/wasm32-unknown-emscripten/Dockerfile +43 -0
  12. data/builders/wasm32-unknown-emscripten/entrypoint.sh +7 -0
  13. data/builders/wasm32-unknown-wasi/Dockerfile +47 -0
  14. data/builders/wasm32-unknown-wasi/entrypoint.sh +7 -0
  15. data/docs/api.md +2 -0
  16. data/docs/cheat_sheet.md +195 -0
  17. data/docs/faq.md +25 -0
  18. data/exe/rbwasm +7 -0
  19. data/ext/.gitignore +2 -0
  20. data/ext/README.md +11 -0
  21. data/ext/extinit.c.erb +32 -0
  22. data/lib/ruby_wasm/3.1/ruby_wasm.so +0 -0
  23. data/lib/ruby_wasm/3.2/ruby_wasm.so +0 -0
  24. data/lib/ruby_wasm/build/build_params.rb +3 -0
  25. data/lib/ruby_wasm/build/downloader.rb +18 -0
  26. data/lib/ruby_wasm/build/executor.rb +187 -0
  27. data/lib/ruby_wasm/build/product/baseruby.rb +37 -0
  28. data/lib/ruby_wasm/build/product/crossruby.rb +330 -0
  29. data/lib/ruby_wasm/build/product/libyaml.rb +68 -0
  30. data/lib/ruby_wasm/build/product/openssl.rb +88 -0
  31. data/lib/ruby_wasm/build/product/product.rb +39 -0
  32. data/lib/ruby_wasm/build/product/ruby_source.rb +103 -0
  33. data/lib/ruby_wasm/build/product/wasi_vfs.rb +45 -0
  34. data/lib/ruby_wasm/build/product/zlib.rb +68 -0
  35. data/lib/ruby_wasm/build/product.rb +8 -0
  36. data/lib/ruby_wasm/build/toolchain/wit_bindgen.rb +31 -0
  37. data/lib/ruby_wasm/build/toolchain.rb +193 -0
  38. data/lib/ruby_wasm/build.rb +88 -0
  39. data/lib/ruby_wasm/cli.rb +217 -0
  40. data/lib/ruby_wasm/packager/core.rb +156 -0
  41. data/lib/ruby_wasm/packager/file_system.rb +158 -0
  42. data/lib/ruby_wasm/packager.rb +159 -0
  43. data/lib/ruby_wasm/rake_task.rb +59 -0
  44. data/lib/ruby_wasm/util.rb +15 -0
  45. data/lib/ruby_wasm/version.rb +3 -0
  46. data/lib/ruby_wasm.rb +33 -0
  47. data/package-lock.json +9500 -0
  48. data/package.json +12 -0
  49. data/rakelib/check.rake +37 -0
  50. data/rakelib/ci.rake +152 -0
  51. data/rakelib/doc.rake +29 -0
  52. data/rakelib/format.rake +35 -0
  53. data/rakelib/gem.rake +22 -0
  54. data/rakelib/packaging.rake +151 -0
  55. data/rakelib/version.rake +40 -0
  56. data/sig/open_uri.rbs +4 -0
  57. data/sig/ruby_wasm/build.rbs +318 -0
  58. data/sig/ruby_wasm/cli.rbs +27 -0
  59. data/sig/ruby_wasm/ext.rbs +13 -0
  60. data/sig/ruby_wasm/packager.rbs +91 -0
  61. data/sig/ruby_wasm/util.rbs +5 -0
  62. data/tools/clang-format-diff.sh +18 -0
  63. data/tools/exe/rbminify +12 -0
  64. data/tools/lib/syntax_tree/minify_ruby.rb +63 -0
  65. metadata +113 -0
@@ -0,0 +1,156 @@
1
+ require "forwardable"
2
+
3
+ class RubyWasm::Packager::Core
4
+ def initialize(packager)
5
+ @packager = packager
6
+ end
7
+
8
+ def build(executor, options)
9
+ strategy = build_strategy
10
+ strategy.build(executor, options)
11
+ end
12
+
13
+ extend Forwardable
14
+
15
+ def_delegators :build_strategy, :cache_key, :artifact
16
+
17
+ private
18
+
19
+ def build_strategy
20
+ @build_strategy ||=
21
+ begin
22
+ has_exts = @packager.specs.any? { |spec| spec.extensions.any? }
23
+ if @packager.support_dynamic_linking?
24
+ DynamicLinking.new(@packager)
25
+ else
26
+ StaticLinking.new(@packager)
27
+ end
28
+ end
29
+ end
30
+
31
+ class BuildStrategy
32
+ def initialize(packager)
33
+ @packager = packager
34
+ end
35
+
36
+ def build(executor, options)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Array of paths to extconf.rb files.
41
+ def specs_with_extensions
42
+ @packager.specs.filter_map do |spec|
43
+ exts =
44
+ spec.extensions.select do |ext|
45
+ # Filter out extensions of default gems (e.g. json, openssl)
46
+ # for the exactly same gem version.
47
+ File.exist?(File.join(spec.full_gem_path, ext))
48
+ end
49
+ next nil if exts.empty?
50
+ [spec, exts]
51
+ end
52
+ end
53
+
54
+ def cache_key(digest)
55
+ raise NotImplementedError
56
+ end
57
+
58
+ def artifact
59
+ raise NotImplementedError
60
+ end
61
+ end
62
+
63
+ class DynamicLinking < BuildStrategy
64
+ end
65
+
66
+ class StaticLinking < BuildStrategy
67
+ def build(executor, options)
68
+ build = derive_build
69
+ force_rebuild =
70
+ options[:remake] || options[:clean] || options[:reconfigure]
71
+ if File.exist?(build.crossruby.artifact) && !force_rebuild
72
+ return build.crossruby.artifact
73
+ end
74
+ build.crossruby.clean(executor) if options[:clean]
75
+
76
+ do_build =
77
+ proc do
78
+ build.crossruby.build(
79
+ executor,
80
+ remake: options[:remake],
81
+ reconfigure: options[:reconfigure]
82
+ )
83
+ end
84
+
85
+ __skip__ =
86
+ if defined?(Bundler)
87
+ Bundler.with_unbundled_env(&do_build)
88
+ else
89
+ do_build.call
90
+ end
91
+ build.crossruby.artifact
92
+ end
93
+
94
+ def cache_key(digest)
95
+ derive_build.cache_key(digest)
96
+ end
97
+
98
+ def artifact
99
+ derive_build.crossruby.artifact
100
+ end
101
+
102
+ def derive_build
103
+ return @build if @build
104
+ __skip__ =
105
+ build ||= RubyWasm::Build.new(name, **@packager.full_build_options)
106
+ build.crossruby.user_exts = user_exts(build)
107
+ # Emscripten uses --global-base=1024 by default, but it conflicts with
108
+ # --stack-first and -z stack-size since global-base 1024 is smaller than
109
+ # the large stack size.
110
+ # Also -g produces some warnings on Emscripten and it confuses the configure
111
+ # script of Ruby.
112
+ if @packager.full_build_options[:target] != "wasm32-unknown-emscripten"
113
+ build.crossruby.debugflags = %w[-g]
114
+ build.crossruby.wasmoptflags = %w[-O3 -g]
115
+ build.crossruby.ldflags = %w[
116
+ -Xlinker
117
+ --stack-first
118
+ -Xlinker
119
+ -z
120
+ -Xlinker
121
+ stack-size=16777216
122
+ ]
123
+ end
124
+ @build = build
125
+ build
126
+ end
127
+
128
+ def user_exts(build)
129
+ @user_exts ||=
130
+ specs_with_extensions.flat_map do |spec, exts|
131
+ exts.map do |ext|
132
+ ext_feature = File.dirname(ext) # e.g. "ext/cgi/escape"
133
+ ext_srcdir = File.join(spec.full_gem_path, ext_feature)
134
+ ext_relative_path = File.join(spec.full_name, ext_feature)
135
+ RubyWasm::CrossRubyExtProduct.new(
136
+ ext_srcdir,
137
+ build.toolchain,
138
+ ext_relative_path: ext_relative_path
139
+ )
140
+ end
141
+ end
142
+ end
143
+
144
+ def name
145
+ require "digest"
146
+ options = @packager.full_build_options
147
+ src_channel = options[:src][:name]
148
+ target_triplet = options[:target]
149
+ base = "ruby-#{src_channel}-#{target_triplet}#{options[:suffix]}"
150
+ exts = specs_with_extensions.sort
151
+ hash = ::Digest::MD5.new
152
+ specs_with_extensions.each { |spec, _| hash << spec.full_name }
153
+ exts.empty? ? base : "#{base}-#{hash.hexdigest}"
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,158 @@
1
+ # Package Ruby code into a mountable directory.
2
+ class RubyWasm::Packager::FileSystem
3
+ def initialize(dest_dir, packager)
4
+ @dest_dir = dest_dir
5
+ @packager = packager
6
+ @ruby_root = File.join(@dest_dir, "usr", "local")
7
+ end
8
+
9
+ def package_ruby_root(tarball, executor)
10
+ usr_dir = File.dirname(@ruby_root)
11
+ executor.mkdir_p usr_dir
12
+ executor.system(
13
+ "tar",
14
+ "-C",
15
+ usr_dir,
16
+ "-xzf",
17
+ tarball,
18
+ "--strip-components=2"
19
+ )
20
+ end
21
+
22
+ def remove_stdlib(executor)
23
+ # Include only rbconfig.rb
24
+ rbconfig =
25
+ File.join(
26
+ @ruby_root,
27
+ "lib",
28
+ "ruby",
29
+ ruby_version,
30
+ "wasm32-wasi",
31
+ "rbconfig.rb"
32
+ )
33
+ # Remove all files except rbconfig.rb
34
+ RubyWasm.logger.info "Removing stdlib (except rbconfig.rb: #{rbconfig})"
35
+ rbconfig_contents = File.read(rbconfig)
36
+ executor.rm_rf @ruby_root
37
+ executor.mkdir_p File.dirname(rbconfig)
38
+ File.write(rbconfig, rbconfig_contents)
39
+ end
40
+
41
+ def package_gems
42
+ @packager.specs.each do |spec|
43
+ RubyWasm.logger.info "Packaging gem: #{spec.full_name}"
44
+ end
45
+ self.each_gem_content_path do |relative, source|
46
+ RubyWasm.logger.debug "Packaging gem file: #{relative}"
47
+ dest = File.join(@dest_dir, relative)
48
+ FileUtils.mkdir_p File.dirname(dest)
49
+ FileUtils.cp_r source, dest
50
+ end
51
+
52
+ setup_rb_path = File.join(bundle_relative_path, "setup.rb")
53
+ RubyWasm.logger.info "Packaging setup.rb: #{setup_rb_path}"
54
+ full_setup_rb_path = File.join(@dest_dir, setup_rb_path)
55
+ FileUtils.mkdir_p File.dirname(full_setup_rb_path)
56
+ File.write(full_setup_rb_path, setup_rb_content)
57
+ end
58
+
59
+ def setup_rb_content
60
+ content = ""
61
+ self.each_gem_require_path do |relative, _|
62
+ content << %Q[$:.unshift File.expand_path("#{File.join("/", relative)}")\n]
63
+ end
64
+ content
65
+ end
66
+
67
+ def remove_non_runtime_files(executor)
68
+ %w[
69
+ **/*.so
70
+ usr/local/lib/libruby-static.a
71
+ usr/local/bin/ruby
72
+ usr/local/include
73
+ ].each do |pattern|
74
+ Dir
75
+ .glob(File.join(@dest_dir, pattern))
76
+ .each do |entry|
77
+ RubyWasm.logger.debug do
78
+ relative_entry = Pathname.new(entry).relative_path_from(@dest_dir)
79
+ "Removing non-runtime file: #{relative_entry}"
80
+ end
81
+ executor.rm_rf entry
82
+ end
83
+ end
84
+ end
85
+
86
+ def bundle_dir
87
+ File.join(@dest_dir, bundle_relative_path)
88
+ end
89
+
90
+ def ruby_root
91
+ @ruby_root
92
+ end
93
+
94
+ private
95
+
96
+ # Iterates over each gem's require path and extension path.
97
+ # Yields the installation relative path and the source path.
98
+ def each_gem_require_path(&block)
99
+ each_gem_extension_path(&block)
100
+ @packager.specs.each do |spec|
101
+ # Use raw_require_paths to exclude extensions
102
+ spec.raw_require_paths.each do |require_path|
103
+ source = File.expand_path(require_path, spec.full_gem_path)
104
+ next unless File.exist?(source)
105
+ relative =
106
+ File.join(bundle_relative_path, "gems", spec.full_name, require_path)
107
+ yield relative, source
108
+ end
109
+ end
110
+ end
111
+
112
+ def each_gem_content_path(&block)
113
+ each_gem_extension_path(&block)
114
+
115
+ @packager.specs.each do |spec|
116
+ next unless File.exist?(spec.full_gem_path)
117
+
118
+ # spec.files is only valid before the gem is packaged.
119
+ if spec.source.path?
120
+ relative_paths = spec.files
121
+ else
122
+ # All files in .gem are required.
123
+ relative_paths = Dir.children(spec.full_gem_path)
124
+ end
125
+ relative_paths.each do |require_path|
126
+ source = File.expand_path(require_path, spec.full_gem_path)
127
+ next unless File.exist?(source)
128
+ relative =
129
+ File.join(bundle_relative_path, "gems", spec.full_name, require_path)
130
+ yield relative, source
131
+ end
132
+ end
133
+ end
134
+
135
+ def each_gem_extension_path
136
+ @packager.specs.each do |spec|
137
+ if !spec.extensions.empty? && File.exist?(spec.extension_dir)
138
+ relative = File.join(bundle_relative_path, "extensions", spec.full_name)
139
+ yield relative, spec.extension_dir
140
+ end
141
+ end
142
+ end
143
+
144
+ def bundle_relative_path
145
+ "bundle"
146
+ end
147
+
148
+ def ruby_version
149
+ rubyarchdir = self.rubyarchdir
150
+ File.basename(File.dirname(rubyarchdir))
151
+ end
152
+
153
+ def rubyarchdir
154
+ maybe =
155
+ Dir.glob(File.join(@ruby_root, "lib", "ruby", "*", "wasm32-wasi")).first
156
+ maybe || raise("Cannot find rubyarchdir")
157
+ end
158
+ end
@@ -0,0 +1,159 @@
1
+ # A class responsible for packaging whole Ruby project
2
+ class RubyWasm::Packager
3
+ # Initializes a new instance of the RubyWasm::Packager class.
4
+ #
5
+ # @param config [Hash] The build config used for building Ruby.
6
+ # @param definition [Bundler::Definition] The Bundler definition.
7
+ def initialize(config = nil, definition = nil)
8
+ @definition = definition
9
+ @config = config
10
+ end
11
+
12
+ # Packages the Ruby code into a Wasm binary. (including extensions)
13
+ #
14
+ # @param executor [RubyWasm::BuildExecutor] The executor for building the Wasm binary.
15
+ # @param dest_dir [String] The destination used to construct the filesystem.
16
+ # @param options [Hash] The packaging options.
17
+ # @return [Array<Integer>] The bytes of the packaged Wasm binary.
18
+ def package(executor, dest_dir, options)
19
+ ruby_core = self.ruby_core_build()
20
+ tarball = ruby_core.build(executor, options)
21
+
22
+ fs = RubyWasm::Packager::FileSystem.new(dest_dir, self)
23
+ fs.package_ruby_root(tarball, executor)
24
+
25
+ ruby_wasm_bin = File.expand_path("bin/ruby", fs.ruby_root)
26
+ wasm_bytes = File.binread(ruby_wasm_bin).bytes
27
+
28
+ fs.package_gems
29
+ fs.remove_non_runtime_files(executor)
30
+ fs.remove_stdlib(executor) unless options[:stdlib]
31
+
32
+ if full_build_options[:target] == "wasm32-unknown-wasi"
33
+ # wasi-vfs supports only WASI target
34
+ wasi_vfs = RubyWasmExt::WasiVfs.new
35
+ wasi_vfs.map_dir("/bundle", fs.bundle_dir)
36
+ wasi_vfs.map_dir("/usr", File.dirname(fs.ruby_root))
37
+
38
+ wasm_bytes = wasi_vfs.pack(wasm_bytes)
39
+ end
40
+
41
+ wasm_bytes = RubyWasmExt.preinitialize(wasm_bytes) if options[:optimize]
42
+ wasm_bytes
43
+ end
44
+
45
+ def ruby_core_build
46
+ @ruby_core_build ||= RubyWasm::Packager::Core.new(self)
47
+ end
48
+
49
+ # The list of excluded gems from the Bundler definition.
50
+ EXCLUDED_GEMS = %w[ruby_wasm bundler]
51
+
52
+ # Retrieves the specs from the Bundler definition, excluding the excluded gems.
53
+ def specs
54
+ return [] unless @definition
55
+ @definition.specs.reject { |spec| EXCLUDED_GEMS.include?(spec.name) }
56
+ end
57
+
58
+ # Checks if dynamic linking is supported.
59
+ def support_dynamic_linking?
60
+ @ruby_channel == "head"
61
+ end
62
+
63
+ # Retrieves the root directory of the Ruby project.
64
+ # The root directory contains the following stuff:
65
+ # * patches/*.patch
66
+ # * build_manifest.json
67
+ # * rubies
68
+ # * build
69
+ def root
70
+ __skip__ =
71
+ @root ||=
72
+ begin
73
+ if explicit = ENV["RUBY_WASM_ROOT"]
74
+ File.expand_path(explicit)
75
+ elsif defined?(Bundler)
76
+ Bundler.root
77
+ else
78
+ Dir.pwd
79
+ end
80
+ rescue Bundler::GemfileNotFound
81
+ Dir.pwd
82
+ end
83
+ end
84
+
85
+ # Retrieves the alias definitions for the Ruby sources.
86
+ def self.build_source_aliases(root)
87
+ patches = Dir[File.join(root, "patches", "*.patch")]
88
+ sources = {
89
+ "head" => {
90
+ type: "github",
91
+ repo: "ruby/ruby",
92
+ rev: "master",
93
+ patches: patches.map { |p| File.expand_path(p) }
94
+ },
95
+ "3.3" => {
96
+ type: "tarball",
97
+ url: "https://cache.ruby-lang.org/pub/ruby/3.3/ruby-3.3.0.tar.gz"
98
+ },
99
+ "3.2" => {
100
+ type: "tarball",
101
+ url: "https://cache.ruby-lang.org/pub/ruby/3.2/ruby-3.2.3.tar.gz"
102
+ }
103
+ }
104
+ sources.each { |name, source| source[:name] = name }
105
+
106
+ build_manifest = File.join(root, "build_manifest.json")
107
+ if File.exist?(build_manifest)
108
+ begin
109
+ manifest = JSON.parse(File.read(build_manifest))
110
+ manifest["ruby_revisions"].each do |name, rev|
111
+ sources[name][:rev] = rev
112
+ end
113
+ rescue StandardError => e
114
+ RubyWasm.logger.warn "Failed to load build_manifest.json: #{e}"
115
+ end
116
+ end
117
+ sources
118
+ end
119
+
120
+ ALL_DEFAULT_EXTS =
121
+ "bigdecimal,cgi/escape,continuation,coverage,date,dbm,digest/bubblebabble,digest,digest/md5,digest/rmd160,digest/sha1,digest/sha2,etc,fcntl,fiber,gdbm,json,json/generator,json/parser,nkf,objspace,pathname,psych,racc/cparse,rbconfig/sizeof,ripper,stringio,strscan,monitor,zlib,openssl"
122
+
123
+ # Retrieves the build options used for building Ruby itself.
124
+ def build_options
125
+ default = {
126
+ target: "wasm32-unknown-wasi",
127
+ src: "3.3",
128
+ default_exts: ALL_DEFAULT_EXTS
129
+ }
130
+ override = @config || {}
131
+ # Merge the default options with the config options
132
+ default.merge(override)
133
+ end
134
+
135
+ # Retrieves the resolved build options
136
+ def full_build_options
137
+ options = build_options
138
+ build_dir = File.join(root, "build")
139
+ rubies_dir = File.join(root, "rubies")
140
+ toolchain = RubyWasm::Toolchain.get(options[:target], build_dir)
141
+ src =
142
+ if options[:src].is_a?(Hash)
143
+ options[:src]
144
+ else
145
+ src_name = options[:src]
146
+ aliases = self.class.build_source_aliases(root)
147
+ aliases[src_name] ||
148
+ raise(
149
+ "Unknown Ruby source: #{src_name} (available: #{aliases.keys.join(", ")})"
150
+ )
151
+ end
152
+ options.merge(
153
+ toolchain: toolchain,
154
+ build_dir: build_dir,
155
+ rubies_dir: rubies_dir,
156
+ src: src
157
+ )
158
+ end
159
+ end
@@ -0,0 +1,59 @@
1
+ require "rake/tasklib"
2
+ require_relative "./util"
3
+ require_relative "./build"
4
+
5
+ class RubyWasm::BuildTask < ::Rake::TaskLib
6
+ # Name of the task.
7
+ attr_accessor :name
8
+
9
+ def initialize(
10
+ name,
11
+ target:,
12
+ src:,
13
+ toolchain: nil,
14
+ build_dir: nil,
15
+ rubies_dir: nil,
16
+ **options,
17
+ &block
18
+ )
19
+ @build =
20
+ RubyWasm::Build.new(
21
+ name,
22
+ target: target,
23
+ src: src,
24
+ toolchain: toolchain,
25
+ build_dir: build_dir || File.join(Dir.pwd, "build"),
26
+ rubies_dir: rubies_dir || File.join(Dir.pwd, "rubies"),
27
+ **options,
28
+ &block
29
+ )
30
+ yield @build if block_given?
31
+ @crossruby = @build.crossruby
32
+ # Rake.verbose can be Object.new by default, so compare with true explicitly.
33
+ executor = RubyWasm::BuildExecutor.new(verbose: Rake.verbose == true)
34
+
35
+ desc "Cross-build Ruby for #{@target}"
36
+ task name do
37
+ next if File.exist? @crossruby.artifact
38
+ @crossruby.build(executor)
39
+ end
40
+ namespace name do
41
+ task :remake do
42
+ @crossruby.build(executor, remake: true)
43
+ end
44
+ task :reconfigure do
45
+ @crossruby.build(executor, reconfigure: true)
46
+ end
47
+ task :clean do
48
+ @crossruby.clean(executor)
49
+ end
50
+ end
51
+ end
52
+
53
+ def hexdigest
54
+ require "digest"
55
+ digest = Digest::SHA256.new
56
+ @build.cache_key(digest)
57
+ digest.hexdigest
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ module RubyWasm
2
+ module SizeFormatter
3
+ def format(size)
4
+ units = %w[B KB MB GB TB]
5
+ unit = 0
6
+ while size > 1024 and unit < units.size - 1
7
+ size /= 1024.0
8
+ unit += 1
9
+ end
10
+ "%s #{units[unit]}" % size.round(2)
11
+ end
12
+
13
+ module_function :format
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module RubyWasm
2
+ VERSION = "2.5.0"
3
+ end
data/lib/ruby_wasm.rb ADDED
@@ -0,0 +1,33 @@
1
+ require "logger"
2
+
3
+ require_relative "ruby_wasm/version"
4
+ require_relative "ruby_wasm/util"
5
+ require_relative "ruby_wasm/build"
6
+ require_relative "ruby_wasm/packager"
7
+ require_relative "ruby_wasm/packager/file_system"
8
+ require_relative "ruby_wasm/packager/core"
9
+
10
+ module RubyWasm
11
+ class << self
12
+ attr_accessor :log_level
13
+
14
+ def logger
15
+ @logger ||=
16
+ begin
17
+ logger =
18
+ Logger.new(
19
+ $stderr,
20
+ level: @log_level || Logger::INFO,
21
+ progname: "rbwasm"
22
+ )
23
+ logger.formatter =
24
+ proc { |severity, datetime, progname, msg| "#{severity}: #{msg}\n" }
25
+ logger
26
+ end
27
+ end
28
+
29
+ def logger=(logger)
30
+ @logger = logger
31
+ end
32
+ end
33
+ end