Skip to content

Commit fc56a37

Browse files
Extensible build system (#42)
This commit introduces `lib/ruby_wasm` library, which provides a build system for cross-compiling CRuby. This will allow application-specific build configurations
1 parent 35dca5d commit fc56a37

File tree

15 files changed

+764
-489
lines changed

15 files changed

+764
-489
lines changed

Rakefile

Lines changed: 19 additions & 489 deletions
Large diffs are not rendered by default.

ext/js/extconf.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
require "mkmf"
22
$objs = ["js-core.o", "bindgen/rb-js-abi-host.o"]
3+
raise "missing executable: wit-bindgen" unless find_executable("wit-bindgen")
34
create_makefile("js")

ext/witapi/extconf.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
require "mkmf"
22
$objs = ["witapi-core.o", "bindgen/rb-abi-guest.o"]
3+
raise "missing executable: wit-bindgen" unless find_executable("wit-bindgen")
34
create_makefile("witapi")

lib/ruby_wasm/build_system.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "rake"
2+
require_relative "build_system/build_params"
3+
require_relative "build_system/product"
4+
require_relative "build_system/toolchain"
5+
6+
module RubyWasm
7+
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module RubyWasm
2+
BuildParams =
3+
Struct.new(
4+
:name,
5+
:src,
6+
:target,
7+
:debug,
8+
:default_exts,
9+
:user_exts,
10+
:profile,
11+
keyword_init: true
12+
)
13+
end

lib/ruby_wasm/build_system/product.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "rake"
2+
require_relative "product/product"
3+
require_relative "product/ruby_source"
4+
require_relative "product/baseruby"
5+
require_relative "product/zlib"
6+
require_relative "product/libyaml"
7+
require_relative "product/crossruby"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require "rake"
2+
require_relative "./product"
3+
4+
module RubyWasm
5+
class BaseRubyProduct < BuildProduct
6+
attr_reader :base_dir, :source, :install_task
7+
8+
def initialize(channel, base_dir, source)
9+
@channel = channel
10+
@base_dir = base_dir
11+
@source = source
12+
end
13+
14+
def install_dir
15+
File.join(
16+
base_dir,
17+
"/build/deps/#{RbConfig::CONFIG["host"]}/opt/baseruby-#{@channel}"
18+
)
19+
end
20+
21+
def name
22+
"baseruby-#{@channel}"
23+
end
24+
25+
def define_task
26+
baseruby_build_dir =
27+
File.join(
28+
base_dir,
29+
"/build/deps/#{RbConfig::CONFIG["host"]}/baseruby-#{@channel}"
30+
)
31+
32+
directory baseruby_build_dir
33+
34+
desc "build baseruby #{@channel}"
35+
@install_task =
36+
task name => [
37+
source.src_dir,
38+
source.configure_file,
39+
baseruby_build_dir
40+
] do
41+
next if Dir.exist?(install_dir)
42+
sh "#{source.configure_file} --prefix=#{install_dir} --disable-install-doc",
43+
chdir: baseruby_build_dir
44+
sh "make install", chdir: baseruby_build_dir
45+
end
46+
end
47+
end
48+
end
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
require "rake"
2+
require_relative "./product"
3+
4+
module RubyWasm
5+
class CrossRubyExtProduct < BuildProduct
6+
attr_reader :name, :toolchain
7+
def initialize(name, toolchain)
8+
@name, @toolchain = name, toolchain
9+
end
10+
11+
def define_task(crossruby)
12+
task "#{crossruby.name}-ext-#{@name}" => [crossruby.configure] do
13+
make_args = []
14+
make_args << "CC=#{toolchain.cc}"
15+
make_args << "RANLIB=#{toolchain.ranlib}"
16+
make_args << "LD=#{toolchain.ld}"
17+
make_args << "AR=#{toolchain.ar}"
18+
19+
lib = @name
20+
source = crossruby.source
21+
objdir = "#{crossruby.ext_build_dir}/#{lib}"
22+
FileUtils.mkdir_p objdir
23+
srcdir = "#{crossruby.base_dir}/ext/#{lib}"
24+
extconf_args = [
25+
"--disable=gems",
26+
# HACK: top_srcdir is required to find ruby headers
27+
"-e",
28+
%Q('$top_srcdir="#{source.src_dir}"'),
29+
# HACK: extout is required to find config.h
30+
"-e",
31+
%Q('$extout="#{crossruby.build_dir}/.ext"'),
32+
# HACK: force static ext build by imitating extmk
33+
"-e",
34+
"'$static = true; trace_var(:$static) {|v| $static = true }'",
35+
# HACK: $0 should be extconf.rb path due to mkmf source file detection
36+
# and we want to insert some hacks before it. But -e and $0 cannot be
37+
# used together, so we rewrite $0 in -e.
38+
"-e",
39+
%Q('$0="#{srcdir}/extconf.rb"'),
40+
"-e",
41+
%Q('require_relative "#{srcdir}/extconf.rb"'),
42+
"-I#{crossruby.build_dir}"
43+
]
44+
sh "#{crossruby.baseruby_path} #{extconf_args.join(" ")}", chdir: objdir
45+
make_cmd = %Q(make -C "#{objdir}" #{make_args.join(" ")} static)
46+
sh make_cmd
47+
# A ext can provide link args by link.filelist. It contains only built archive file by default.
48+
unless File.exist?("#{objdir}/link.filelist")
49+
File.write(
50+
"#{objdir}/link.filelist",
51+
Dir.glob("#{objdir}/*.a").join("\n")
52+
)
53+
end
54+
end
55+
end
56+
end
57+
58+
class CrossRubyProduct < BuildProduct
59+
attr_reader :base_dir, :source, :toolchain, :build, :configure
60+
61+
def initialize(params, base_dir, baseruby, source, toolchain)
62+
@params = params
63+
@base_dir = base_dir
64+
@baseruby = baseruby
65+
@source = source
66+
@toolchain = toolchain
67+
@dep_tasks = []
68+
end
69+
70+
def define_task
71+
directory dest_dir
72+
directory build_dir
73+
74+
@configure =
75+
task "#{name}-configure",
76+
[:reconfigure] =>
77+
[build_dir, source.src_dir, source.configure_file] +
78+
dep_tasks do |t, args|
79+
args.with_defaults(reconfigure: false)
80+
81+
if !File.exist?("#{build_dir}/Makefile") || args[:reconfigure]
82+
args = configure_args(RbConfig::CONFIG["host"], toolchain)
83+
sh "#{source.configure_file} #{args.join(" ")}", chdir: build_dir
84+
end
85+
# NOTE: we need rbconfig.rb at configuration time to build user given extensions with mkmf
86+
sh "make rbconfig.rb", chdir: build_dir
87+
end
88+
89+
user_ext_products = @params.user_exts
90+
user_ext_tasks = user_ext_products.map { |prod| prod.define_task(self) }
91+
user_ext_names = user_ext_products.map(&:name)
92+
user_exts =
93+
task "#{name}-libs" => [@configure] + user_ext_tasks do
94+
mkdir_p File.dirname(extinit_obj)
95+
sh %Q(ruby #{base_dir}/ext/extinit.c.erb #{user_ext_names.join(" ")} | #{toolchain.cc} -c -x c - -o #{extinit_obj})
96+
end
97+
98+
install =
99+
task "#{name}-install" => [@configure, user_exts, dest_dir] do
100+
next if File.exist?("#{dest_dir}-install")
101+
sh "make install DESTDIR=#{dest_dir}-install", chdir: build_dir
102+
end
103+
104+
desc "Build #{name}"
105+
task name => [@configure, install, dest_dir] do
106+
artifact = "rubies/ruby-#{name}.tar.gz"
107+
next if File.exist?(artifact)
108+
rm_rf dest_dir
109+
cp_r "#{dest_dir}-install", dest_dir
110+
ruby_api_version =
111+
`#{baseruby_path} -e 'print RbConfig::CONFIG["ruby_version"]'`
112+
# TODO: move copying logic to ext product
113+
user_ext_names.each do |lib|
114+
next unless File.exist?("ext/#{lib}/lib")
115+
cp_r(
116+
File.join(base_dir, "ext/#{lib}/lib/."),
117+
File.join(dest_dir, "usr/local/lib/ruby/#{ruby_api_version}")
118+
)
119+
end
120+
sh "tar cfz #{artifact} -C rubies #{name}"
121+
end
122+
end
123+
124+
def name
125+
@params.name
126+
end
127+
128+
def build_dir
129+
"#{@base_dir}/build/build/#{name}"
130+
end
131+
132+
def ext_build_dir
133+
"#{@base_dir}/build/ext-build/#{name}"
134+
end
135+
136+
def with_libyaml(libyaml)
137+
@libyaml = libyaml
138+
@dep_tasks << libyaml.install_task
139+
end
140+
141+
def with_zlib(zlib)
142+
@zlib = zlib
143+
@dep_tasks << zlib.install_task
144+
end
145+
146+
def dest_dir
147+
"#{@base_dir}/rubies/#{name}"
148+
end
149+
150+
def extinit_obj
151+
"#{ext_build_dir}/extinit.o"
152+
end
153+
154+
def baseruby_path
155+
File.join(@baseruby.install_dir, "bin/ruby")
156+
end
157+
158+
def dep_tasks
159+
[@baseruby.install_task] + @dep_tasks
160+
end
161+
162+
def configure_args(build_triple, toolchain)
163+
target = @params.target
164+
default_exts = @params.default_exts
165+
user_exts = @params.user_exts
166+
167+
ldflags =
168+
if @params.debug
169+
# use --stack-first to detect stack overflow easily
170+
%w[-Xlinker --stack-first -Xlinker -z -Xlinker stack-size=16777216]
171+
else
172+
%w[-Xlinker -zstack-size=16777216]
173+
end
174+
175+
xldflags = []
176+
177+
args = ["--host", target, "--build", build_triple]
178+
args << "--with-static-linked-ext"
179+
args << %Q(--with-ext="#{default_exts}")
180+
args << %Q(--with-libyaml-dir="#{@libyaml.install_root}")
181+
args << %Q(--with-zlib-dir="#{@zlib.install_root}")
182+
args << %Q(--with-baseruby="#{baseruby_path}")
183+
184+
case target
185+
when "wasm32-unknown-wasi"
186+
unless toolchain.lib_wasi_vfs_a.nil?
187+
xldflags << toolchain.lib_wasi_vfs_a
188+
end
189+
when "wasm32-unknown-emscripten"
190+
ldflags.concat(%w[-s MODULARIZE=1])
191+
args.concat(%w[CC=emcc LD=emcc AR=emar RANLIB=emranlib])
192+
else
193+
raise "unknown target: #{target}"
194+
end
195+
196+
(user_exts || []).each do |lib|
197+
xldflags << "@#{ext_build_dir}/#{lib.name}/link.filelist"
198+
end
199+
xldflags << extinit_obj
200+
201+
xcflags = []
202+
xcflags << "-DWASM_SETJMP_STACK_BUFFER_SIZE=24576"
203+
xcflags << "-DWASM_FIBER_STACK_BUFFER_SIZE=24576"
204+
xcflags << "-DWASM_SCAN_STACK_BUFFER_SIZE=24576"
205+
206+
args << %Q(LDFLAGS="#{ldflags.join(" ")}")
207+
args << %Q(XLDFLAGS="#{xldflags.join(" ")}")
208+
args << %Q(XCFLAGS="#{xcflags.join(" ")}")
209+
if @params.debug
210+
args << %Q(debugflags="-g")
211+
args << %Q(wasmoptflags="-O3 -g")
212+
else
213+
args << %Q(debugflags="-g0")
214+
end
215+
args << "--disable-install-doc"
216+
args
217+
end
218+
end
219+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
require "rake"
2+
require_relative "./product"
3+
4+
module RubyWasm
5+
class LibYAMLProduct < AutoconfProduct
6+
attr_reader :base_dir, :install_dir, :target, :install_task
7+
8+
def initialize(base_dir, install_dir, target, toolchain)
9+
@base_dir = base_dir
10+
@install_dir = install_dir
11+
@target = target
12+
super(target, toolchain)
13+
end
14+
15+
def install_root
16+
File.join(install_dir, "usr/local")
17+
end
18+
19+
def name
20+
"libyaml-#{target}"
21+
end
22+
23+
def define_task
24+
libyaml_version = "0.2.5"
25+
desc "build libyaml #{libyaml_version} for #{target}"
26+
@install_task =
27+
task(name) do
28+
next if Dir.exist?(install_root)
29+
30+
build_dir =
31+
File.join(base_dir, "/build/deps/#{target}/yaml-#{libyaml_version}")
32+
mkdir_p File.dirname(build_dir)
33+
rm_rf build_dir
34+
sh "curl -L https://github.com/yaml/libyaml/releases/download/#{libyaml_version}/yaml-#{libyaml_version}.tar.gz | tar xz",
35+
chdir: File.dirname(build_dir)
36+
37+
# obtain the latest config.guess and config.sub for Emscripten and WASI triple support
38+
sh "curl -o #{build_dir}/config/config.guess 'https://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD'"
39+
sh "curl -o #{build_dir}/config/config.sub 'https://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD'"
40+
41+
sh "./configure #{configure_args.join(" ")}", chdir: build_dir
42+
sh "make install DESTDIR=#{install_dir}", chdir: build_dir
43+
end
44+
end
45+
end
46+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
require "rake"
2+
3+
module RubyWasm
4+
class BuildProduct
5+
include Rake::DSL
6+
7+
def name
8+
raise NotImplementedError, "identifiable product name must be implemented"
9+
end
10+
end
11+
12+
class AutoconfProduct < BuildProduct
13+
def initialize(target, toolchain)
14+
@target = target
15+
@toolchain = toolchain
16+
end
17+
def system_triplet_args
18+
args = []
19+
case @target
20+
when "wasm32-unknown-wasi"
21+
args.concat(%W[--host wasm32-wasi])
22+
when "wasm32-unknown-emscripten"
23+
args.concat(%W[--host wasm32-emscripten])
24+
else
25+
raise "unknown target: #{@target}"
26+
end
27+
args
28+
end
29+
30+
def tools_args
31+
args = []
32+
args << "CC=#{@toolchain.cc}"
33+
args << "LD=#{@toolchain.ld}"
34+
args << "AR=#{@toolchain.ar}"
35+
args << "RANLIB=#{@toolchain.ranlib}"
36+
args
37+
end
38+
39+
def configure_args
40+
system_triplet_args + tools_args
41+
end
42+
end
43+
end

0 commit comments

Comments
 (0)