ruby_wasm 2.5.0-arm64-darwin

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) 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.0/ruby_wasm.bundle +0 -0
  23. data/lib/ruby_wasm/3.1/ruby_wasm.bundle +0 -0
  24. data/lib/ruby_wasm/3.2/ruby_wasm.bundle +0 -0
  25. data/lib/ruby_wasm/build/build_params.rb +3 -0
  26. data/lib/ruby_wasm/build/downloader.rb +18 -0
  27. data/lib/ruby_wasm/build/executor.rb +187 -0
  28. data/lib/ruby_wasm/build/product/baseruby.rb +37 -0
  29. data/lib/ruby_wasm/build/product/crossruby.rb +330 -0
  30. data/lib/ruby_wasm/build/product/libyaml.rb +68 -0
  31. data/lib/ruby_wasm/build/product/openssl.rb +88 -0
  32. data/lib/ruby_wasm/build/product/product.rb +39 -0
  33. data/lib/ruby_wasm/build/product/ruby_source.rb +103 -0
  34. data/lib/ruby_wasm/build/product/wasi_vfs.rb +45 -0
  35. data/lib/ruby_wasm/build/product/zlib.rb +68 -0
  36. data/lib/ruby_wasm/build/product.rb +8 -0
  37. data/lib/ruby_wasm/build/toolchain/wit_bindgen.rb +31 -0
  38. data/lib/ruby_wasm/build/toolchain.rb +193 -0
  39. data/lib/ruby_wasm/build.rb +88 -0
  40. data/lib/ruby_wasm/cli.rb +217 -0
  41. data/lib/ruby_wasm/packager/core.rb +156 -0
  42. data/lib/ruby_wasm/packager/file_system.rb +158 -0
  43. data/lib/ruby_wasm/packager.rb +159 -0
  44. data/lib/ruby_wasm/rake_task.rb +59 -0
  45. data/lib/ruby_wasm/util.rb +15 -0
  46. data/lib/ruby_wasm/version.rb +3 -0
  47. data/lib/ruby_wasm.rb +33 -0
  48. data/package-lock.json +9500 -0
  49. data/package.json +12 -0
  50. data/rakelib/check.rake +37 -0
  51. data/rakelib/ci.rake +152 -0
  52. data/rakelib/doc.rake +29 -0
  53. data/rakelib/format.rake +35 -0
  54. data/rakelib/gem.rake +22 -0
  55. data/rakelib/packaging.rake +151 -0
  56. data/rakelib/version.rake +40 -0
  57. data/sig/open_uri.rbs +4 -0
  58. data/sig/ruby_wasm/build.rbs +318 -0
  59. data/sig/ruby_wasm/cli.rbs +27 -0
  60. data/sig/ruby_wasm/ext.rbs +13 -0
  61. data/sig/ruby_wasm/packager.rbs +91 -0
  62. data/sig/ruby_wasm/util.rbs +5 -0
  63. data/tools/clang-format-diff.sh +18 -0
  64. data/tools/exe/rbminify +12 -0
  65. data/tools/lib/syntax_tree/minify_ruby.rb +63 -0
  66. metadata +114 -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