ruby_wasm 2.5.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.clang-format +8 -0
  3. data/CONTRIBUTING.md +124 -0
  4. data/Cargo.lock +2452 -0
  5. data/Cargo.toml +7 -0
  6. data/Gemfile +17 -0
  7. data/LICENSE +21 -0
  8. data/NOTICE +1293 -0
  9. data/README.md +161 -0
  10. data/Rakefile +164 -0
  11. data/Steepfile +24 -0
  12. data/benchmarks/vm_deep_call.rb +55 -0
  13. data/builders/wasm32-unknown-emscripten/Dockerfile +43 -0
  14. data/builders/wasm32-unknown-emscripten/entrypoint.sh +7 -0
  15. data/builders/wasm32-unknown-wasi/Dockerfile +47 -0
  16. data/builders/wasm32-unknown-wasi/entrypoint.sh +7 -0
  17. data/docs/api.md +2 -0
  18. data/docs/cheat_sheet.md +195 -0
  19. data/docs/faq.md +25 -0
  20. data/exe/rbwasm +7 -0
  21. data/ext/.gitignore +2 -0
  22. data/ext/README.md +11 -0
  23. data/ext/extinit.c.erb +32 -0
  24. data/ext/ruby_wasm/Cargo.toml +17 -0
  25. data/ext/ruby_wasm/extconf.rb +6 -0
  26. data/ext/ruby_wasm/src/lib.rs +69 -0
  27. data/lib/ruby_wasm/build/build_params.rb +3 -0
  28. data/lib/ruby_wasm/build/downloader.rb +18 -0
  29. data/lib/ruby_wasm/build/executor.rb +187 -0
  30. data/lib/ruby_wasm/build/product/baseruby.rb +37 -0
  31. data/lib/ruby_wasm/build/product/crossruby.rb +326 -0
  32. data/lib/ruby_wasm/build/product/libyaml.rb +68 -0
  33. data/lib/ruby_wasm/build/product/openssl.rb +88 -0
  34. data/lib/ruby_wasm/build/product/product.rb +39 -0
  35. data/lib/ruby_wasm/build/product/ruby_source.rb +103 -0
  36. data/lib/ruby_wasm/build/product/wasi_vfs.rb +83 -0
  37. data/lib/ruby_wasm/build/product/zlib.rb +68 -0
  38. data/lib/ruby_wasm/build/product.rb +8 -0
  39. data/lib/ruby_wasm/build/toolchain/wit_bindgen.rb +31 -0
  40. data/lib/ruby_wasm/build/toolchain.rb +193 -0
  41. data/lib/ruby_wasm/build.rb +88 -0
  42. data/lib/ruby_wasm/cli.rb +195 -0
  43. data/lib/ruby_wasm/packager/core.rb +156 -0
  44. data/lib/ruby_wasm/packager/file_system.rb +157 -0
  45. data/lib/ruby_wasm/packager.rb +159 -0
  46. data/lib/ruby_wasm/rake_task.rb +58 -0
  47. data/lib/ruby_wasm/util.rb +15 -0
  48. data/lib/ruby_wasm/version.rb +3 -0
  49. data/lib/ruby_wasm.rb +33 -0
  50. data/package-lock.json +9500 -0
  51. data/package.json +12 -0
  52. data/ruby_wasm.gemspec +32 -0
  53. data/sig/open_uri.rbs +4 -0
  54. data/sig/ruby_wasm/build.rbs +322 -0
  55. data/sig/ruby_wasm/cli.rbs +24 -0
  56. data/sig/ruby_wasm/ext.rbs +11 -0
  57. data/sig/ruby_wasm/packager.rbs +91 -0
  58. data/sig/ruby_wasm/util.rbs +5 -0
  59. data/tasks/check.rake +37 -0
  60. data/tasks/ci.rake +152 -0
  61. data/tasks/doc.rake +24 -0
  62. data/tasks/format.rake +34 -0
  63. data/tasks/gem.rake +19 -0
  64. data/tasks/packaging.rake +148 -0
  65. data/tasks/version.rake +38 -0
  66. data/tools/clang-format-diff.sh +18 -0
  67. data/tools/exe/rbminify +12 -0
  68. data/tools/lib/syntax_tree/minify_ruby.rb +63 -0
  69. metadata +115 -0
@@ -0,0 +1,68 @@
1
+ require_relative "./product"
2
+
3
+ module RubyWasm
4
+ class ZlibProduct < AutoconfProduct
5
+ attr_reader :target
6
+
7
+ ZLIB_VERSION = "1.3"
8
+
9
+ def initialize(build_dir, target, toolchain)
10
+ @build_dir = build_dir
11
+ @target = target
12
+ super(target, toolchain)
13
+ end
14
+
15
+ def product_build_dir
16
+ File.join(@build_dir, target, "zlib-#{ZLIB_VERSION}")
17
+ end
18
+
19
+ def destdir
20
+ File.join(product_build_dir, "opt")
21
+ end
22
+
23
+ def install_root
24
+ File.join(destdir, "usr", "local")
25
+ end
26
+
27
+ def name
28
+ "zlib-#{ZLIB_VERSION}-#{target}"
29
+ end
30
+
31
+ def configure_args
32
+ args = %w[CHOST=linux]
33
+
34
+ args + tools_args
35
+ end
36
+
37
+ def build(executor)
38
+ return if Dir.exist?(install_root)
39
+
40
+ executor.mkdir_p File.dirname(product_build_dir)
41
+ executor.rm_rf product_build_dir
42
+ executor.mkdir_p product_build_dir
43
+
44
+ tarball_path = File.join(product_build_dir, "zlib-#{ZLIB_VERSION}.tar.gz")
45
+ executor.system "curl",
46
+ "-o",
47
+ tarball_path,
48
+ "-L",
49
+ "https://zlib.net/zlib-#{ZLIB_VERSION}.tar.gz"
50
+ executor.system "tar",
51
+ "xzf",
52
+ tarball_path,
53
+ "-C",
54
+ product_build_dir,
55
+ "--strip-components=1"
56
+
57
+ executor.system "env",
58
+ *configure_args,
59
+ "./configure",
60
+ "--static",
61
+ chdir: product_build_dir
62
+ executor.system "make",
63
+ "install",
64
+ "DESTDIR=#{destdir}",
65
+ chdir: product_build_dir
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,8 @@
1
+ require_relative "product/product"
2
+ require_relative "product/ruby_source"
3
+ require_relative "product/baseruby"
4
+ require_relative "product/zlib"
5
+ require_relative "product/libyaml"
6
+ require_relative "product/openssl"
7
+ require_relative "product/wasi_vfs"
8
+ require_relative "product/crossruby"
@@ -0,0 +1,31 @@
1
+ module RubyWasm
2
+ class WitBindgen
3
+ attr_reader :bin_path
4
+
5
+ def initialize(
6
+ build_dir:,
7
+ revision: "251e84b89121751f79ac268629e9285082b2596d"
8
+ )
9
+ @build_dir = build_dir
10
+ @tool_dir = File.join(@build_dir, "toolchain", "wit-bindgen")
11
+ @bin_path = File.join(@tool_dir, "bin", "wit-bindgen")
12
+ @revision = revision
13
+ end
14
+
15
+ def install
16
+ return if File.exist?(@bin_path)
17
+ RubyWasm::Toolchain.check_executable("cargo")
18
+ Kernel.system(
19
+ "cargo",
20
+ "install",
21
+ "--git",
22
+ "https://github.com/bytecodealliance/wit-bindgen",
23
+ "--rev",
24
+ @revision,
25
+ "--root",
26
+ @tool_dir,
27
+ "wit-bindgen-cli"
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,193 @@
1
+ require_relative "./toolchain/wit_bindgen"
2
+
3
+ module RubyWasm
4
+ class Toolchain
5
+ attr_reader :name
6
+
7
+ def initialize
8
+ @tools = {}
9
+ end
10
+
11
+ def find_tool(name)
12
+ raise "not implemented"
13
+ end
14
+
15
+ def check_envvar(name)
16
+ raise "missing environment variable: #{name}" if ENV[name].nil?
17
+ end
18
+
19
+ def self.get(target, build_dir = nil)
20
+ case target
21
+ when "wasm32-unknown-wasi"
22
+ return RubyWasm::WASISDK.new(build_dir: build_dir)
23
+ when "wasm32-unknown-emscripten"
24
+ return RubyWasm::Emscripten.new
25
+ else
26
+ raise "unknown target: #{target}"
27
+ end
28
+ end
29
+
30
+ def self.find_path(command)
31
+ (ENV["PATH"] || "")
32
+ .split(File::PATH_SEPARATOR)
33
+ .each do |path_dir|
34
+ bin_path = File.join(path_dir, command)
35
+ return bin_path if File.executable?(bin_path)
36
+ end
37
+ nil
38
+ end
39
+
40
+ def self.check_executable(command)
41
+ tool = find_path(command)
42
+ raise "missing executable: #{command}" unless tool
43
+ tool
44
+ end
45
+
46
+ %i[cc ranlib ld ar].each do |name|
47
+ define_method(name) do
48
+ @tools_cache ||= {}
49
+ @tools_cache[name] ||= find_tool(name)
50
+ @tools_cache[name]
51
+ end
52
+ end
53
+ end
54
+
55
+ class WASISDK < Toolchain
56
+ def initialize(
57
+ wasi_sdk_path = ENV["WASI_SDK_PATH"],
58
+ build_dir: nil,
59
+ version_major: 20,
60
+ version_minor: 0,
61
+ binaryen_version: 108
62
+ )
63
+ @wasm_opt_path = Toolchain.find_path("wasm-opt")
64
+ @need_fetch_wasi_sdk = wasi_sdk_path.nil?
65
+ @need_fetch_binaryen = @wasm_opt_path.nil?
66
+
67
+ if @need_fetch_wasi_sdk
68
+ if build_dir.nil?
69
+ raise "build_dir is required when WASI_SDK_PATH is not set"
70
+ end
71
+ wasi_sdk_path = File.join(build_dir, "toolchain", "wasi-sdk")
72
+ @version_major = version_major
73
+ @version_minor = version_minor
74
+ end
75
+
76
+ if @need_fetch_binaryen
77
+ if build_dir.nil?
78
+ raise "build_dir is required when wasm-opt not installed in PATH"
79
+ end
80
+ @binaryen_path = File.join(build_dir, "toolchain", "binaryen")
81
+ @binaryen_version = binaryen_version
82
+ @wasm_opt_path = File.join(@binaryen_path, "bin", "wasm-opt")
83
+ end
84
+
85
+ @tools = {
86
+ cc: "#{wasi_sdk_path}/bin/clang",
87
+ ld: "#{wasi_sdk_path}/bin/clang",
88
+ ar: "#{wasi_sdk_path}/bin/llvm-ar",
89
+ ranlib: "#{wasi_sdk_path}/bin/llvm-ranlib"
90
+ }
91
+ @wasi_sdk_path = wasi_sdk_path
92
+ @name = "wasi-sdk"
93
+ end
94
+
95
+ def find_tool(name)
96
+ if !File.exist?(@tools[name]) && !ENV["WASI_SDK_PATH"].nil?
97
+ raise "missing tool '#{name}' at #{@tools[name]}"
98
+ end
99
+ @tools[name]
100
+ end
101
+
102
+ def wasm_opt
103
+ @wasm_opt_path
104
+ end
105
+
106
+ def wasi_sdk_path
107
+ @wasi_sdk_path
108
+ end
109
+
110
+ def download_url(version_major, version_minor)
111
+ version = "#{version_major}.#{version_minor}"
112
+ assets = [
113
+ [/x86_64-linux/, "wasi-sdk-#{version}-linux.tar.gz"],
114
+ [/(arm64e?|x86_64)-darwin/, "wasi-sdk-#{version}-macos.tar.gz"]
115
+ ]
116
+ asset = assets.find { |os, _| os =~ RUBY_PLATFORM }&.at(1)
117
+ if asset.nil?
118
+ raise "unsupported platform for fetching WASI SDK: #{RUBY_PLATFORM}"
119
+ end
120
+ "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-#{version_major}/#{asset}"
121
+ end
122
+
123
+ def binaryen_download_url(version)
124
+ assets = [
125
+ [
126
+ /x86_64-linux/,
127
+ "binaryen-version_#{@binaryen_version}-x86_64-linux.tar.gz"
128
+ ],
129
+ [
130
+ /x86_64-darwin/,
131
+ "binaryen-version_#{@binaryen_version}-x86_64-macos.tar.gz"
132
+ ],
133
+ [
134
+ /arm64e?-darwin/,
135
+ "binaryen-version_#{@binaryen_version}-arm64-macos.tar.gz"
136
+ ]
137
+ ]
138
+ asset = assets.find { |os, _| os =~ RUBY_PLATFORM }&.at(1)
139
+ if asset.nil?
140
+ raise "unsupported platform for fetching Binaryen: #{RUBY_PLATFORM}"
141
+ end
142
+ "https://github.com/WebAssembly/binaryen/releases/download/version_#{@binaryen_version}/#{asset}"
143
+ end
144
+
145
+ def install_wasi_sdk
146
+ return unless @need_fetch_wasi_sdk
147
+ wasi_sdk_tarball =
148
+ File.join(File.dirname(@wasi_sdk_path), "wasi-sdk.tar.gz")
149
+ unless File.exist? wasi_sdk_tarball
150
+ FileUtils.mkdir_p File.dirname(wasi_sdk_tarball)
151
+ system "curl -L -o #{wasi_sdk_tarball} #{self.download_url(@version_major, @version_minor)}"
152
+ end
153
+ unless File.exist? @wasi_sdk_path
154
+ FileUtils.mkdir_p @wasi_sdk_path
155
+ system "tar -C #{@wasi_sdk_path} --strip-component 1 -xzf #{wasi_sdk_tarball}"
156
+ end
157
+ end
158
+
159
+ def install_binaryen
160
+ return unless @need_fetch_binaryen
161
+ binaryen_tarball = File.expand_path("../binaryen.tar.gz", @binaryen_path)
162
+ unless File.exist? binaryen_tarball
163
+ FileUtils.mkdir_p File.dirname(binaryen_tarball)
164
+ system "curl -L -o #{binaryen_tarball} #{self.binaryen_download_url(@binaryen_version)}"
165
+ end
166
+
167
+ unless File.exist? @binaryen_path
168
+ FileUtils.mkdir_p @binaryen_path
169
+ system "tar -C #{@binaryen_path} --strip-component 1 -xzf #{binaryen_tarball}"
170
+ end
171
+ end
172
+
173
+ def install
174
+ install_wasi_sdk
175
+ install_binaryen
176
+ end
177
+ end
178
+
179
+ class Emscripten < Toolchain
180
+ def initialize
181
+ @tools = { cc: "emcc", ld: "emcc", ar: "emar", ranlib: "emranlib" }
182
+ @name = "emscripten"
183
+ end
184
+
185
+ def install
186
+ end
187
+
188
+ def find_tool(name)
189
+ Toolchain.check_executable(@tools[name])
190
+ @tools[name]
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,88 @@
1
+ require_relative "build/build_params"
2
+ require_relative "build/product"
3
+ require_relative "build/toolchain"
4
+ require_relative "build/executor"
5
+
6
+ class RubyWasm::Build
7
+ # Source to build from.
8
+ attr_reader :source
9
+
10
+ # Target to build for.
11
+ attr_reader :target
12
+
13
+ # Toolchain for the build.
14
+ # Defaults to the Toolchain.get for the target.
15
+ attr_reader :toolchain
16
+
17
+ # LibYAML product to build.
18
+ attr_reader :libyaml
19
+
20
+ # zlib product to build.
21
+ attr_reader :zlib
22
+
23
+ # wasi-vfs product used by the crossruby.
24
+ attr_reader :wasi_vfs
25
+
26
+ # BaseRuby product to build.
27
+ attr_reader :baseruby
28
+
29
+ # CrossRuby product to build.
30
+ attr_reader :crossruby
31
+
32
+ def initialize(
33
+ name,
34
+ target:,
35
+ src:,
36
+ toolchain:,
37
+ build_dir:,
38
+ rubies_dir:,
39
+ **options
40
+ )
41
+ @target = target
42
+ @build_dir = build_dir
43
+ @rubies_dir = rubies_dir
44
+ @toolchain = (toolchain || RubyWasm::Toolchain.get(target, @build_dir))
45
+
46
+ @libyaml = RubyWasm::LibYAMLProduct.new(@build_dir, @target, @toolchain)
47
+ @zlib = RubyWasm::ZlibProduct.new(@build_dir, @target, @toolchain)
48
+ @wasi_vfs = RubyWasm::WasiVfsProduct.new(@build_dir)
49
+ @source = RubyWasm::BuildSource.new(src, @build_dir)
50
+ @baseruby = RubyWasm::BaseRubyProduct.new(@build_dir, @source)
51
+ @openssl = RubyWasm::OpenSSLProduct.new(@build_dir, @target, @toolchain)
52
+
53
+ build_params =
54
+ RubyWasm::BuildParams.new(
55
+ name: name,
56
+ target: target,
57
+ default_exts: options[:default_exts]
58
+ )
59
+
60
+ @crossruby =
61
+ RubyWasm::CrossRubyProduct.new(
62
+ build_params,
63
+ @build_dir,
64
+ @rubies_dir,
65
+ @baseruby,
66
+ @source,
67
+ @toolchain
68
+ )
69
+
70
+ @crossruby.with_libyaml @libyaml
71
+ @crossruby.with_zlib @zlib
72
+ @crossruby.with_wasi_vfs @wasi_vfs
73
+ @crossruby.with_openssl @openssl
74
+ end
75
+
76
+ def cache_key(digest)
77
+ @source.cache_key(digest)
78
+ @crossruby.cache_key(digest)
79
+ digest << @build_dir
80
+ digest << @rubies_dir
81
+ digest << @target
82
+ digest << @toolchain.name
83
+ digest << @libyaml.name
84
+ digest << @zlib.name
85
+ digest << @openssl.name
86
+ digest << @wasi_vfs.name
87
+ end
88
+ end
@@ -0,0 +1,195 @@
1
+ require "optparse"
2
+ require "rbconfig"
3
+
4
+ module RubyWasm
5
+ class CLI
6
+ def initialize(stdout:, stderr:)
7
+ @stdout = stdout
8
+ @stderr = stderr
9
+ end
10
+
11
+ def run(args)
12
+ available_commands = %w[build]
13
+ parser =
14
+ OptionParser.new do |opts|
15
+ opts.banner = <<~USAGE
16
+ Usage: rbwasm [options...] [command]
17
+
18
+ Available commands: #{available_commands.join(", ")}
19
+ USAGE
20
+ opts.version = RubyWasm::VERSION
21
+ opts.on("-h", "--help", "Prints this help") do
22
+ @stderr.puts opts
23
+ exit
24
+ end
25
+ opts.on("--log-level LEVEL", "Log level") do |level|
26
+ RubyWasm.log_level = level.to_sym
27
+ end
28
+ end
29
+ parser.order!(args)
30
+
31
+ command = args.shift
32
+ case command
33
+ when "build"
34
+ build(args)
35
+ else
36
+ @stderr.puts parser
37
+ exit
38
+ end
39
+ end
40
+
41
+ def build(args)
42
+ # @type var options: Hash[Symbol, untyped]
43
+ options = {
44
+ save_temps: false,
45
+ optimize: false,
46
+ remake: false,
47
+ reconfigure: false,
48
+ clean: false,
49
+ ruby_version: "3.3",
50
+ target_triplet: "wasm32-unknown-wasi",
51
+ profile: "full",
52
+ stdlib: true,
53
+ disable_gems: false
54
+ }
55
+ OptionParser
56
+ .new do |opts|
57
+ opts.banner = "Usage: rbwasm componentize [options]"
58
+ opts.on("-h", "--help", "Prints this help") do
59
+ @stdout.puts opts
60
+ exit
61
+ end
62
+
63
+ opts.on("--save-temps", "Save temporary files") do
64
+ options[:save_temps] = true
65
+ end
66
+
67
+ opts.on("--ruby-version VERSION", "Ruby version") do |version|
68
+ options[:ruby_version] = version
69
+ end
70
+
71
+ opts.on("--target TRIPLET", "Target triplet") do |triplet|
72
+ options[:target_triplet] = triplet
73
+ end
74
+
75
+ opts.on(
76
+ "--build-profile PROFILE",
77
+ "Build profile. full or minimal"
78
+ ) { |profile| options[:profile] = profile }
79
+
80
+ opts.on("--optimize", "Optimize the output") do
81
+ options[:optimize] = true
82
+ end
83
+
84
+ opts.on("--remake", "Re-execute make for Ruby") do
85
+ options[:remake] = true
86
+ end
87
+
88
+ opts.on("--reconfigure", "Re-execute configure for Ruby") do
89
+ options[:reconfigure] = true
90
+ end
91
+
92
+ opts.on("--clean", "Clean build artifacts") { options[:clean] = true }
93
+
94
+ opts.on("-o", "--output FILE", "Output file") do |file|
95
+ options[:output] = file
96
+ end
97
+
98
+ opts.on("--[no-]stdlib", "Include stdlib") do |stdlib|
99
+ options[:stdlib] = stdlib
100
+ end
101
+
102
+ opts.on("--disable-gems", "Disable gems") do
103
+ options[:disable_gems] = true
104
+ end
105
+
106
+ opts.on("--format FORMAT", "Output format") do |format|
107
+ options[:format] = format
108
+ end
109
+
110
+ opts.on("--print-ruby-cache-key", "Print Ruby cache key") do
111
+ options[:print_ruby_cache_key] = true
112
+ end
113
+ end
114
+ .parse!(args)
115
+
116
+ verbose = RubyWasm.logger.level == :debug
117
+ executor = RubyWasm::BuildExecutor.new(verbose: verbose)
118
+ packager = self.derive_packager(options)
119
+
120
+ if options[:print_ruby_cache_key]
121
+ self.do_print_ruby_cache_key(packager)
122
+ exit
123
+ end
124
+
125
+ unless options[:output]
126
+ @stderr.puts "Output file is not specified"
127
+ exit 1
128
+ end
129
+
130
+ require "tmpdir"
131
+
132
+ if options[:save_temps]
133
+ tmpdir = Dir.mktmpdir
134
+ self.do_build(executor, tmpdir, packager, options)
135
+ @stderr.puts "Temporary files are saved to #{tmpdir}"
136
+ exit
137
+ else
138
+ Dir.mktmpdir do |tmpdir|
139
+ self.do_build(executor, tmpdir, packager, options)
140
+ end
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def build_config(options)
147
+ config = { target: options[:target_triplet], src: options[:ruby_version] }
148
+ case options[:profile]
149
+ when "full"
150
+ config[:default_exts] = RubyWasm::Packager::ALL_DEFAULT_EXTS
151
+ when "minimal"
152
+ config[:default_exts] = ""
153
+ else
154
+ RubyWasm.logger.error "Unknown profile: #{options[:profile]} (available: full, minimal)"
155
+ exit 1
156
+ end
157
+ config[:suffix] = "-#{options[:profile]}"
158
+ config
159
+ end
160
+
161
+ def derive_packager(options)
162
+ __skip__ =
163
+ if defined?(Bundler) && !options[:disable_gems]
164
+ definition = Bundler.definition
165
+ end
166
+ RubyWasm::Packager.new(build_config(options), definition)
167
+ end
168
+
169
+ def do_print_ruby_cache_key(packager)
170
+ ruby_core_build = packager.ruby_core_build
171
+ require "digest"
172
+ digest = Digest::SHA256.new
173
+ ruby_core_build.cache_key(digest)
174
+ hexdigest = digest.hexdigest
175
+ require "json"
176
+ @stdout.puts JSON.generate(
177
+ hexdigest: hexdigest,
178
+ artifact: ruby_core_build.artifact
179
+ )
180
+ end
181
+
182
+ def do_build(executor, tmpdir, packager, options)
183
+ require_relative "ruby_wasm.so"
184
+ wasm_bytes = packager.package(executor, tmpdir, options)
185
+ RubyWasm.logger.info "Size: #{SizeFormatter.format(wasm_bytes.size)}"
186
+ case options[:output]
187
+ when "-"
188
+ @stdout.write wasm_bytes.pack("C*")
189
+ else
190
+ File.binwrite(options[:output], wasm_bytes.pack("C*"))
191
+ RubyWasm.logger.debug "Wrote #{options[:output]}"
192
+ end
193
+ end
194
+ end
195
+ end