Skip to content

Commit 18e4358

Browse files
Merge pull request #447 from ruby/katei/dynamic-component
Dynamic Component
2 parents b2bf55a + cff9095 commit 18e4358

File tree

12 files changed

+119
-40
lines changed

12 files changed

+119
-40
lines changed

lib/ruby_wasm/build/product/crossruby.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def make_args(crossruby)
4242
make_args
4343
end
4444

45-
def build(executor, crossruby)
45+
def build(executor, crossruby, extra_mkargs = [])
4646
objdir = product_build_dir crossruby
4747
executor.mkdir_p objdir
4848
do_extconf executor, crossruby
@@ -54,7 +54,8 @@ def build(executor, crossruby)
5454
"-C",
5555
"#{objdir}",
5656
*make_args(crossruby),
57-
build_target
57+
build_target,
58+
*extra_mkargs
5859
# A ext can provide link args by link.filelist. It contains only built archive file by default.
5960
unless File.exist?(linklist(crossruby))
6061
executor.write(
@@ -187,7 +188,7 @@ def need_extinit_obj?
187188
def build_exts(executor)
188189
@user_exts.each do |prod|
189190
executor.begin_section prod.class, prod.name, "Building"
190-
prod.build(executor, self)
191+
prod.build(executor, self, [])
191192
executor.end_section prod.class, prod.name
192193
end
193194
end

lib/ruby_wasm/build/toolchain/wit_bindgen.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class WitBindgen
44

55
def initialize(
66
build_dir:,
7-
revision: "v0.24.0"
7+
revision: "2e8fb8ede8242288d4cc682cd9dff3057ef09a57"
88
)
99
@build_dir = build_dir
1010
@tool_dir = File.join(@build_dir, "toolchain", "wit-bindgen-#{revision}")

lib/ruby_wasm/cli.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def build(args)
5353
target_triplet: "wasm32-unknown-wasip1",
5454
profile: "full",
5555
stdlib: true,
56+
without_stdlib_components: [],
57+
dest_dir: nil,
5658
disable_gems: false,
5759
gemfile: nil,
5860
patches: [],
@@ -104,10 +106,18 @@ def build(args)
104106
options[:stdlib] = stdlib
105107
end
106108

109+
opts.on("--without-stdlib COMPONENT", "Exclude stdlib component") do |component|
110+
options[:without_stdlib_components] << component
111+
end
112+
107113
opts.on("--disable-gems", "Disable gems") do
108114
options[:disable_gems] = true
109115
end
110116

117+
opts.on("--dest-dir PATH", "(Experimental) Destination directory") do |path|
118+
options[:dest_dir] = path
119+
end
120+
111121
opts.on("-p", "--patch PATCH", "Apply a patch") do |patch|
112122
options[:patches] << patch
113123
end
@@ -149,7 +159,9 @@ def do_build_with_force_ruby_platform(options)
149159

150160
require "tmpdir"
151161

152-
if options[:save_temps]
162+
if dest_dir = options[:dest_dir]
163+
self.do_build(executor, dest_dir, packager, options)
164+
elsif options[:save_temps]
153165
tmpdir = Dir.mktmpdir
154166
self.do_build(executor, tmpdir, packager, options)
155167
@stderr.puts "Temporary files are saved to #{tmpdir}"

lib/ruby_wasm/packager.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,27 @@ def package(executor, dest_dir, options)
3232

3333
wasm_bytes = File.binread(File.join(fs.ruby_root, "bin", "ruby"))
3434

35+
ruby_core.build_gem_exts(executor, fs.bundle_dir)
36+
3537
fs.package_gems
3638
fs.remove_non_runtime_files(executor)
37-
fs.remove_stdlib(executor) unless options[:stdlib]
39+
if options[:stdlib]
40+
options[:without_stdlib_components].each do |component|
41+
fs.remove_stdlib_component(executor, component)
42+
end
43+
else
44+
fs.remove_stdlib(executor)
45+
end
3846

39-
if full_build_options[:target] == "wasm32-unknown-wasip1"
47+
if full_build_options[:target] == "wasm32-unknown-wasip1" && !features.support_dynamic_linking?
4048
# wasi-vfs supports only WASI target
4149
wasi_vfs = RubyWasmExt::WasiVfs.new
4250
wasi_vfs.map_dir("/bundle", fs.bundle_dir)
4351
wasi_vfs.map_dir("/usr", File.dirname(fs.ruby_root))
4452

4553
wasm_bytes = wasi_vfs.pack(wasm_bytes)
4654
end
47-
wasm_bytes = ruby_core.build_and_link_exts(executor, wasm_bytes)
55+
wasm_bytes = ruby_core.link_gem_exts(executor, fs.ruby_root, fs.bundle_dir, wasm_bytes)
4856

4957
wasm_bytes = RubyWasmExt.preinitialize(wasm_bytes) if options[:optimize]
5058
wasm_bytes

lib/ruby_wasm/packager/core.rb

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def build(executor, options)
1212

1313
extend Forwardable
1414

15-
def_delegators :build_strategy, :cache_key, :artifact, :build_and_link_exts
15+
def_delegators :build_strategy, :cache_key, :artifact, :build_gem_exts, :link_gem_exts
1616

1717
private
1818

@@ -37,7 +37,11 @@ def build(executor, options)
3737
raise NotImplementedError
3838
end
3939

40-
def build_and_link_exts(executor, module_bytes)
40+
def build_gem_exts(executor, gem_home)
41+
raise NotImplementedError
42+
end
43+
44+
def link_gem_exts(executor, ruby_root, gem_home, module_bytes)
4145
raise NotImplementedError
4246
end
4347

@@ -55,6 +59,14 @@ def specs_with_extensions
5559
end
5660
end
5761

62+
def wasi_exec_model
63+
# TODO: Detect WASI exec-model from binary exports (_start or _initialize)
64+
use_js_gem = @packager.specs.any? do |spec|
65+
spec.name == "js"
66+
end
67+
use_js_gem ? "reactor" : "command"
68+
end
69+
5870
def cache_key(digest)
5971
raise NotImplementedError
6072
end
@@ -93,20 +105,24 @@ def build(executor, options)
93105
build.crossruby.artifact
94106
end
95107

96-
def build_and_link_exts(executor, module_bytes)
108+
def build_gem_exts(executor, gem_home)
97109
build = derive_build
98-
self.build_exts(executor, build)
99-
self.link_exts(executor, build)
110+
self._build_gem_exts(executor, build, gem_home)
100111
end
101112

102-
def link_exts(executor, build)
103-
ruby_root = build.crossruby.dest_dir
113+
def link_gem_exts(executor, ruby_root, gem_home, module_bytes)
114+
build = derive_build
115+
self._link_gem_exts(executor, build, ruby_root, gem_home, module_bytes)
116+
end
104117

105-
libraries = [File.join(ruby_root, "usr", "local", "bin", "ruby")]
118+
def _link_gem_exts(executor, build, ruby_root, gem_home, module_bytes)
119+
libraries = []
106120

107121
# TODO: Should be computed from dyinfo of ruby binary
108122
wasi_libc_shared_libs = [
109123
"libc.so",
124+
"libc++.so",
125+
"libc++abi.so",
110126
"libwasi-emulated-getpid.so",
111127
"libwasi-emulated-mman.so",
112128
"libwasi-emulated-process-clocks.so",
@@ -119,13 +135,18 @@ def link_exts(executor, build)
119135
wasi_sdk_path = toolchain.wasi_sdk_path
120136
libraries << File.join(wasi_sdk_path, "share/wasi-sysroot/lib/wasm32-wasi", lib)
121137
end
122-
wasi_adapter = RubyWasm::Packager::ComponentAdapter.wasi_snapshot_preview1("command")
138+
wasi_adapter = RubyWasm::Packager::ComponentAdapter.wasi_snapshot_preview1(wasi_exec_model)
123139
adapters = [wasi_adapter]
124-
dl_openable_libs = Dir.glob(File.join(ruby_root, "usr", "local", "lib", "ruby", "**", "*.so"))
140+
dl_openable_libs = []
141+
dl_openable_libs << [File.dirname(ruby_root), Dir.glob(File.join(ruby_root, "lib", "ruby", "**", "*.so"))]
142+
dl_openable_libs << [gem_home, Dir.glob(File.join(gem_home, "**", "*.so"))]
143+
125144
linker = RubyWasmExt::ComponentLink.new
126145
linker.use_built_in_libdl(true)
127146
linker.stub_missing_functions(false)
128-
linker.validate(true)
147+
linker.validate(ENV["RUBYWASM_SKIP_LINKER_VALIDATION"] != "1")
148+
149+
linker.library("ruby", module_bytes, false)
129150

130151
libraries.each do |lib|
131152
# Non-DL openable libraries should be referenced as base name
@@ -135,42 +156,51 @@ def link_exts(executor, build)
135156
linker.library(lib_name, module_bytes, false)
136157
end
137158

138-
dl_openable_libs.each do |lib|
139-
# DL openable lib_name should be a relative path from ruby_root
140-
lib_name = "/" + Pathname.new(lib).relative_path_from(Pathname.new(ruby_root)).to_s
141-
module_bytes = File.binread(lib)
142-
RubyWasm.logger.info "Linking #{lib_name} (#{module_bytes.size} bytes)"
143-
linker.library(lib_name, module_bytes, true)
159+
dl_openable_libs.each do |root, libs|
160+
libs.each do |lib|
161+
# DL openable lib_name should be a relative path from ruby_root
162+
lib_name = "/" + Pathname.new(lib).relative_path_from(Pathname.new(File.dirname(root))).to_s
163+
module_bytes = File.binread(lib)
164+
RubyWasm.logger.info "Linking #{lib_name} (#{module_bytes.size} bytes)"
165+
linker.library(lib_name, module_bytes, true)
166+
end
144167
end
145168

146169
adapters.each do |adapter|
147170
adapter_name = File.basename(adapter)
148171
# e.g. wasi_snapshot_preview1.command.wasm -> wasi_snapshot_preview1
149172
adapter_name = adapter_name.split(".")[0]
150173
module_bytes = File.binread(adapter)
174+
RubyWasm.logger.info "Linking adapter #{adapter_name}=#{adapter} (#{module_bytes.size} bytes)"
151175
linker.adapter(adapter_name, module_bytes)
152176
end
153177
return linker.encode()
154178
end
155179

156-
def build_exts(executor, build)
180+
def _build_gem_exts(executor, build, gem_home)
157181
exts = specs_with_extensions.flat_map do |spec, exts|
158182
exts.map do |ext|
159183
ext_feature = File.dirname(ext) # e.g. "ext/cgi/escape"
160184
ext_srcdir = File.join(spec.full_gem_path, ext_feature)
161185
ext_relative_path = File.join(spec.full_name, ext_feature)
162-
RubyWasm::CrossRubyExtProduct.new(
186+
prod = RubyWasm::CrossRubyExtProduct.new(
163187
ext_srcdir,
164188
build.toolchain,
165189
features: @packager.features,
166190
ext_relative_path: ext_relative_path
167191
)
192+
[prod, spec]
168193
end
169194
end
170195

171-
exts.each do |prod|
196+
exts.each do |prod, spec|
197+
libdir = File.join(gem_home, "gems", spec.full_name, spec.raw_require_paths.first)
198+
extra_mkargs = [
199+
"sitearchdir=#{libdir}",
200+
"sitelibdir=#{libdir}",
201+
]
172202
executor.begin_section prod.class, prod.name, "Building"
173-
prod.build(executor, build.crossruby)
203+
prod.build(executor, build.crossruby, extra_mkargs)
174204
executor.end_section prod.class, prod.name
175205
end
176206
end
@@ -301,15 +331,19 @@ def derive_build
301331
build
302332
end
303333

304-
def build_and_link_exts(executor, module_bytes)
334+
def build_gem_exts(executor, gem_home)
335+
# No-op because we already built extensions as part of the Ruby build
336+
end
337+
338+
def link_gem_exts(executor, ruby_root, gem_home, module_bytes)
305339
return module_bytes unless @packager.features.support_component_model?
306340

307341
linker = RubyWasmExt::ComponentEncode.new
308-
linker.validate(true)
342+
linker.validate(ENV["RUBYWASM_SKIP_LINKER_VALIDATION"] != "1")
309343
linker.module(module_bytes)
310344
linker.adapter(
311345
"wasi_snapshot_preview1",
312-
File.binread(RubyWasm::Packager::ComponentAdapter.wasi_snapshot_preview1("reactor"))
346+
File.binread(RubyWasm::Packager::ComponentAdapter.wasi_snapshot_preview1(wasi_exec_model))
313347
)
314348

315349
linker.encode()

lib/ruby_wasm/packager/file_system.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ def remove_stdlib(executor)
3838
File.write(rbconfig, rbconfig_contents)
3939
end
4040

41+
def remove_stdlib_component(executor, component)
42+
RubyWasm.logger.info "Removing stdlib component: #{component}"
43+
case component
44+
when "enc"
45+
# Remove all encodings except for encdb.so and transdb.so
46+
enc_dir = File.join(@ruby_root, "lib", "ruby", ruby_version, "wasm32-wasi", "enc")
47+
puts File.join(enc_dir, "**/*.so")
48+
Dir.glob(File.join(enc_dir, "**/*.so")).each do |entry|
49+
next if entry.end_with?("encdb.so", "transdb.so")
50+
RubyWasm.logger.debug "Removing stdlib encoding: #{entry}"
51+
executor.rm_rf entry
52+
end
53+
else
54+
raise "Unknown stdlib component: #{component}"
55+
end
56+
end
57+
4158
def package_gems
4259
@packager.specs.each do |spec|
4360
RubyWasm.logger.info "Packaging gem: #{spec.full_name}"

packages/gems/js/ext/js/bindgen/ext.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ void ruby_js_js_runtime_list_borrow_js_abi_value_free(ruby_js_js_runtime_list_bo
150150
}
151151
}
152152

153-
__attribute__((__import_module__("ruby:js/ruby-runtime"), __import_name__("[resource-drop]rb-iseq")))
153+
__attribute__((__import_module__("[export]ruby:js/ruby-runtime"), __import_name__("[resource-drop]rb-iseq")))
154154
extern void __wasm_import_exports_ruby_js_ruby_runtime_rb_iseq_drop(int32_t handle);
155155

156156
void exports_ruby_js_ruby_runtime_rb_iseq_drop_own(exports_ruby_js_ruby_runtime_own_rb_iseq_t handle) {
@@ -176,7 +176,7 @@ void __wasm_export_exports_ruby_js_ruby_runtime_rb_iseq_dtor(exports_ruby_js_rub
176176
exports_ruby_js_ruby_runtime_rb_iseq_destructor(arg);
177177
}
178178

179-
__attribute__((__import_module__("ruby:js/ruby-runtime"), __import_name__("[resource-drop]rb-abi-value")))
179+
__attribute__((__import_module__("[export]ruby:js/ruby-runtime"), __import_name__("[resource-drop]rb-abi-value")))
180180
extern void __wasm_import_exports_ruby_js_ruby_runtime_rb_abi_value_drop(int32_t handle);
181181

182182
void exports_ruby_js_ruby_runtime_rb_abi_value_drop_own(exports_ruby_js_ruby_runtime_own_rb_abi_value_t handle) {
Binary file not shown.

sig/ruby_wasm/build.rbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ module RubyWasm
168168
def feature_name: (CrossRubyProduct crossruby) -> String
169169

170170
def make_args: (CrossRubyProduct crossruby) -> Array[String]
171-
def build: (BuildExecutor executor, CrossRubyProduct crossruby) -> void
171+
def build: (BuildExecutor executor, CrossRubyProduct crossruby, Array[String] extra_mkargs) -> void
172172
def do_extconf: (BuildExecutor executor, CrossRubyProduct crossruby) -> void
173173
def do_install_rb: (BuildExecutor executor, CrossRubyProduct crossruby) -> void
174174

sig/ruby_wasm/cli.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ module RubyWasm
1414
target_triplet: String,
1515
profile: String,
1616
stdlib: bool,
17+
without_stdlib_components: Array[String],
1718
disable_gems: bool,
1819
gemfile: String?,
1920
patches: Array[String],
2021
format: String,
22+
dest_dir: String,
2123
}
2224

2325
DEFAULT_RUBIES_DIR: string

0 commit comments

Comments
 (0)