Skip to content

Instantly share code, notes, and snippets.

@skinnyjames
Last active November 1, 2025 05:35
Show Gist options
  • Select an option

  • Save skinnyjames/643842fed49766814810a181e72f8ca8 to your computer and use it in GitHub Desktop.

Select an option

Save skinnyjames/643842fed49766814810a181e72f8ca8 to your computer and use it in GitHub Desktop.
MRBuild gemspecs
gemspec("counter-app") do |config|
gem path: "../mruby"
gem path: "../hokusai-pocket"
end
gemspec("hokusaipocket") do |config|
task "setup" do |r|
def build
command("rm -Rf vendor")
command("mkdir -p vendor")
end
end
task "raylib" do |raylib|
dependency "setup"
def build
command("rm -rf raylib")
command("git clone https://github.com/raysan5/raylib.git vendor/raylib")
command("make clean", chdir: "vendor/raylib/src")
command("make PLATFORM=PLATFORM_DESKTOP", chdir: "vendor/raylib/src")
config.cc.objs << "#{path}/vendor/raylib/src/libraylib.a"
config.cc.includes << File.join(path, "vendor", "raylib", "src")
end
end
task "tree-sitter" do |ts|
dependency "setup"
def build
command("rm -rf vendor/tree-sitter")
command("git clone https://github.com/tree-sitter/tree-sitter.git vendor/tree-sitter")
command("mkdir -p vendor/tree-sitter/build")
command("make all install PREFIX=build", chdir: "vendor/tree-sitter", env: env)
config.cc.objs << "#{path}/vendor/tree-sitter/build/lib/libtree-sitter.a"
config.cc.includes << File.join(path, "vendor", "tree-sitter", "build", "include")
end
def build_dir
"vendor/tree-sitter/build"
end
def env
{
"CC" => config.cc.gcc,
"PREFIX" => build_dir
}
end
end
task "hokusai-build" do |hb|
def build
config.cc.includes += %w[include hp/grammar/tree_sitter].map { |i| File.join(path, i) }
config.cc.objs += %w[
src/ast/log.c
src/ast/hml.c
src/ast/style.c
src/ast/hashmap.c
src/ast/ast.c
grammar/src/parser.c
grammar/src/scanner.c
src/hp/ast/event.c
src/hp/ast/func.c
src/hp/ast/loop.c
src/hp/ast/prop.c
src/hp/ast.c
src/hp/error.c
src/hp/font.c
src/hp/style.c
src/hp/monotonic_timer.c
src/hp/backend.c
].map { |p| File.join(path, p) }
end
end
end
gemspec("mruby") do |config|
gem path: "#{path}/mrbgems/mruby-io"
module GemHelpers
def gemspecs
@specs ||= config.gems.select { |k, spec| spec.is_a?(MRBuild::GemSpec) && k != "mruby" }.values
end
def mruby_build_gems
gemspecs.each do |spec|
mruby_build_gem(spec)
end
end
def gemspecs_presym_srcs
gemspecs.reduce([]) do |paths, spec|
srcs = Dir.glob(File.join(spec.path, "src", "**", "*.c"))
paths.concat(srcs)
paths
end
end
def gemspecs_includes
gemspecs.reduce([]) do |paths, spec|
paths.push(File.join(spec.path, "include")) if File.directory?(File.join(spec.path, "include"))
paths
end
end
def mruby_build_gem(spec, presym_enabled = false)
export_include_paths = []
export_include_paths << File.join(spec.path, "include") #if File.directory? File.joiin#{spec.path}/include"
rbfiles = Dir.glob(File.join(spec.path, "mrblib", "**", "*.rb"))
srcs = Dir.glob(File.join(spec.path, "src", "**", "*.c"))
generate_functions = !(rbfiles.empty? && srcs.empty?)
build_dir = File.join(mruby_build_dir, "mrbgems", spec.name)
init = ""
if generate_functions
mruby_generate_gem_init(spec, srcs, rbfiles, presym_enabled)
# config.cc.objs += File.join(build_dir, "gem_init.c")
init = "gem_init.c"
end
incs = include_str(files_from_build_directory(["include", *export_include_paths]))
csrcs = files_from_build_directory(srcs).join(" ")
mrblib = files_from_build_directory([File.join(mruby_build_dir, "mrbc", "lib", "libmruby_core.a")]).join(" ")
command("#{config.cc.gcc} #{incs} -c #{init} #{csrcs}", chdir: build_dir)
end
def mruby_generate_gem_init(spec, srcs, rbfiles, presym_enabled)
funcname = spec.name.tr("-", "_")
build_path = File.join(mruby_build_dir, "mrbgems")
mkdir(File.join(build_path, spec.name))
gem_init_path = File.join(build_path, spec.name, "gem_init.c")
ruby do
File.open(gem_init_path, "w") do |io|
# print headers
if rbfiles.empty?
io.puts "#include <mruby.h>"
else
io.puts "#include <mruby.h>"
io.puts "#include <mruby/irep.h>"
io.puts "#include <stdlib.h>"
# handle presym cdump
end
unless rbfiles.empty?
# TODO: handle presym cdump
# print irep
# gets the output of the mrbc compile of the gem ruby files
command("#{mrbc_bin} -Bgem_mrblib_irep_#{funcname} -s -o- #{rbfiles.join(" ")}")
.forward_output { |out| io.puts out unless out =~ /running command/ }
.forward_error { |err| puts err }
.execute
end
# TODO: use actual templates for this
# begin file
io.puts <<~EOF
void mrb_#{funcname}_gem_init(mrb_state* mrb);
void mrb_#{funcname}_gem_final(mrb_state* mrb);
EOF
# start tmp init
io.puts <<~EOF
void GENERATED_TMP_mrb_#{funcname}_gem_init(mrb_state* mrb)
{
EOF
io.puts "mrb_#{funcname}_gem_init(mrb);" if srcs != [File.join(spec.path, "src", "gem_init.c")]
unless rbfiles.empty?
# TODO handle presym cdump
io.puts <<~EOF
mrb_load_irep(mrb, gem_mrblib_irep_#{funcname});
EOF
end
# end tmp init
io.puts <<~EOF
}
EOF
# start gem final
io.puts "void GENERATED_TMP_mrb_#{funcname}_gem_final(mrb_state* mrb){"
io.puts " mrb_#{funcname}_gem_final(mrb);" if srcs != [File.join(spec.path, "src", "gem_init.c")]
io.puts "}"
end
end
end
end
module Helpers
def mruby_core_sources
Dir.glob(File.join(path, "src", "*.c"))
end
def mruby_compiler_sources
Dir.glob(File.join(path, "mrbgems", "mruby-compiler", "**", "*.c"))
end
def mruby_gem_files(gem_name, type = :c)
case type
when :c
ext = "c"
folder = "src"
when :ruby
ext = "rb"
folder = "mrblib"
else
raise StandardError.new("Unsupported type for #{gem_name} -> #{type}")
end
Dir.glob(File.join(path, "mrbgems", gem_name, folder, "**", "*.#{ext}"))
end
def mruby_gem_tool_bin_sources(gem_name, bin_name)
Dir.glob(File.join(path, "mrbgems", gem_name, "tools", bin_name, "**", "*.c"))
end
def files_from_build_directory(files)
files.map do |file|
File.join("..", "..", "..", "..", file)
end
end
def mruby_build_dir(target = "host")
File.join(path, "build", target)
end
def mruby_build_mrbc_dir(target = "host")
File.join(mruby_build_dir(target), "mrbc", "src")
end
def include_str(files)
files.map { |file| "-I#{file}" }.join(" ")
end
end
# Task: presym
#
# Scans all source files for preallocated symbols
# and generates a mapping in /include/presym/<id.h>|<table.h>
task "presym" do |resolver|
include Helpers
include GemHelpers
def build
# clean build
puts "clean build"
command("rm -Rf #{mruby_build_dir}")
puts "build preprocess"
build_preprocess
ruby do
puts "scanning pi sources #{pi_sources.join(" ")}"
presyms = scan(pi_sources)
raise "Something went wrong: presymbols are empty" if presyms.empty?
puts "write presym headers"
write_id_header(presyms)
write_table_header(presyms)
end
end
# generates .pi files from core source c files and gem c files
def build_preprocess
# todo: add other gems and cc.objs
files_from_build_directory(mruby_core_sources + mruby_compiler_sources + mruby_gem_tool_bin_sources("mruby-bin-mruby", "mruby") + gemspecs_presym_srcs).each do |path|
build_preprocess_file(path)
end
end
def pi_sources
(mruby_core_sources + mruby_compiler_sources + mruby_gem_tool_bin_sources("mruby-bin-mruby", "mruby") + gemspecs_presym_srcs).map do |file|
File.join(mruby_build_mrbc_dir, File.basename(file).gsub(/\..*$/, ".pi"))
end
end
# processes a single file for presym scanning
def build_preprocess_file(file)
out = File.basename(file).gsub(/\..*$/, ".pi")
includes = files_from_build_directory(["include"].concat(config.cc.includes).concat(gemspecs_includes))
command("mkdir -p #{mruby_build_mrbc_dir}")
command("#{config.cc.gcc} -DMRB_PRESYM_SCANNING #{include_str(includes)} -E -P #{file} -o #{out}", chdir: mruby_build_mrbc_dir)
end
# scans the sources for pre allocated symbols
def scan(sources, &cb)
hash = {}
sources.each do |path|
read_presym(hash, path)
end
hash.keys.sort_by {|sym| [sym.size, sym] }
end
def presym_path
File.join(path, "include", "mruby", "presym")
end
def id_header_path
File.join(presym_path,"id.h")
end
def table_header_path
File.join(presym_path, "table.h")
end
def write_table_header(presyms)
File.open(table_header_path, "wb") do |f|
f.puts "static const uint16_t presym_length_table[] = {"
presyms.each{|sym| f.puts " #{sym.bytesize},\t/* #{sym} */"}
f.puts "};"
f.puts
f.puts "static const char * const presym_name_table[] = {"
presyms.each do |sym|
sym = sym.gsub(/([\x01-\x1f\x7f-\xff])|("|\\)/n) {
case
when $1
e = ESCAPE_SEQUENCE_MAP[$1]
e ? "\\#{e}" : '\\x%02x""' % $1.ord
when $2
"\\#$2"
end
}
f.puts %| "#{sym}",|
end
f.puts "};"
end
end
def write_id_header(presyms)
prefix_re = SYMBOL_TO_MACRO.keys.map(&:first).uniq.map{|a| Regexp.escape(a) }.join("|")
suffix_re = SYMBOL_TO_MACRO.keys.map(&:last).uniq.map{|a| Regexp.escape(a) }.join("|")
sym_re = /^(#{prefix_re})?([\w&&\D]\w*)(#{suffix_re})?$/o
File.open(id_header_path, "wb") do |f|
f.puts "enum mruby_presym {"
presyms.each.with_index(1) do |sym, num|
if name = OPERATORS[sym]
f.puts " MRB_OPSYM__#{name} = #{num},"
elsif sym_re =~ sym && (affixes = SYMBOL_TO_MACRO[[$1, $3]])
f.puts " MRB_#{affixes * 'SYM'}__#{$2} = #{num}," unless $2 == "?"
end
end
f.puts "};"
f.puts
f.puts "#define MRB_PRESYM_MAX #{presyms.size}"
end
end
def read_presym(hash, path)
File.open(path, "rb").readlines.join("\n").scan(/<@! (.*?) !@>/) do |(part,_)|
literals = part.scan(C_STR_LITERAL_RE)
unless literals.empty?
literals = literals.map { |literal| literal[1..-2] }
literals.each do |e|
e.gsub!(/\\x([0-9A-Fa-f]{1,2})|\\(0[0-7]{,3})|\\([abefnrtv])|\\(.)/) do
case
when $1; $1.hex.chr(Encoding::BINARY)
when $2; $2.oct.chr(Encoding::BINARY)
when $3; ESCAPE_SEQUENCE_MAP[$3]
when $4; $4
end
end
end
hash[literals.join] = true
end
end
end
OPERATORS = {
"!" => "not",
"%" => "mod",
"&" => "and",
"*" => "mul",
"+" => "add",
"-" => "sub",
"/" => "div",
"<" => "lt",
">" => "gt",
"^" => "xor",
"`" => "tick",
"|" => "or",
"~" => "neg",
"!=" => "neq",
"!~" => "nmatch",
"&&" => "andand",
"**" => "pow",
"+@" => "plus",
"-@" => "minus",
"<<" => "lshift",
"<=" => "le",
"==" => "eq",
"=~" => "match",
">=" => "ge",
">>" => "rshift",
"[]" => "aref",
"||" => "oror",
"<=>" => "cmp",
"===" => "eqq",
"[]=" => "aset",
}.freeze
SYMBOL_TO_MACRO = {
# Symbol => Macro
# [prefix, suffix] => [prefix, suffix]
["$" , "" ] => ["GV" , "" ],
["@@" , "" ] => ["CV" , "" ],
["@" , "" ] => ["IV" , "" ],
["" , "!" ] => ["" , "_B" ],
["" , "?" ] => ["" , "_Q" ],
["" , "=" ] => ["" , "_E" ],
["" , "" ] => ["" , "" ],
}.freeze
C_STR_LITERAL_RE = /"(?:[^\\\"]|\\.)*"/
ESCAPE_SEQUENCE_MAP = {
"a" => "\a",
"b" => "\b",
"e" => "\e",
"f" => "\f",
"n" => "\n",
"r" => "\r",
"t" => "\t",
"v" => "\v",
}
ESCAPE_SEQUENCE_MAP.keys.each { |k| ESCAPE_SEQUENCE_MAP[ESCAPE_SEQUENCE_MAP[k]] = k }
end
# Task mrbc
#
# Generates a minimal mruby compiler
# that will be used to process ruby gems/code
task "mrbc" do |resolver|
include Helpers
dependency "presym"
def build
create_libmruby_archive
# compile mrbc
out = File.join(mruby_build_dir, "mrbc", "bin", "mrbc")
libmruby_a = File.join(mruby_build_dir, "mrbc", "lib", "libmruby_core.a")
command("#{config.cc.gcc} #{include_str(["include"])} mrbgems/mruby-bin-mrbc/tools/mrbc/mrbc.c #{libmruby_a} -o #{out}")
end
# Now that presym scanning is complete
# and presym/<id|table.h> is generated
# we can compile the sources
def create_libmruby_archive
# TODO: add gem sources / includes
includes = include_str(files_from_build_directory(["include"] + config.cc.includes))
sources = files_from_build_directory(mruby_core_sources + mruby_compiler_sources + config.cc.objs)
command("#{config.cc.gcc} #{includes} -c #{sources.join(" ")}", chdir: mruby_build_mrbc_dir)
command("mkdir -p #{File.join(mruby_build_dir, "mrbc", "bin")} && mkdir -p #{File.join(mruby_build_dir, "mrbc", "lib")}")
# create a static archive for libmruby_core
ruby do
libdir = File.join(mruby_build_dir, "mrbc", "lib")
mrbc_sources = Dir.glob(File.join(mruby_build_mrbc_dir, "**", "*.o"))
command("#{config.cc.ar} r libmruby_core.a #{mrbc_sources.join(" ")} && mv libmruby_core.a #{libdir}")
.forward_output(&on_output)
.forward_error(&on_error)
.execute
end
end
def mrb_compiler_sources
%w[codegen.c y.tab.c].map { |file| "mrbgems/mruby-compiler/core/#{file}" }
end
end
# Compile mruby proper
task "ruby-compile" do |resolver|
include Helpers
include GemHelpers
dependency "mrbc"
def build
mrblib_dir = File.join(mruby_build_dir, "mrblib")
include_dir = File.join("..", "..", "..", "include")
mkdir(mrblib_dir)
# generates mrblib.c from ruby source code
# TODO: use templates to do this sort of thing.
generate_mrblib_template
# compile the resulting mrblib.c file
command("#{config.cc.gcc} #{include_str([include_dir])} -c mrblib.c -o mrblib.o", chdir: mrblib_dir)
# build the sources for all gems
mruby_build_gems
# make the build/host/lib && build/host/bin directory
mkdir(File.join(path, "build", "host", "lib"))
mkdir(File.join(path, "build", "host", "bin"))
# we will now compile all the mrbgem source codes into a single `gem_init.c` file.
compile_mrbgems
# package libmruby_a with all the c source code and the ruby source code
# we need to run in a ruby wrapper because these files will only exist when the previous commands are invoked.
mrbc_sources = []
mrbgems_sources = []
ruby do
mrbc_sources = Dir.glob File.join(mruby_build_mrbc_dir, "**", "*.o")
mrbgems_sources = Dir.glob File.join(mruby_build_dir, "mrbgems", "**", "*.o")
mrblib_o = File.join(mruby_build_dir, "mrblib", "mrblib.o")
# archive libmruby.a and move it to build/host/lib
command("#{config.cc.ar} r libmruby.a #{mrbc_sources.join(" ")} #{mrbgems_sources.join(" ")} #{mrblib_o} && mv libmruby.a #{File.join(mruby_build_dir, "lib", "libmruby.a")}")
.forward_output(&on_output)
.forward_error(&on_error)
.execute
end
# compile mruby binary
compile_mruby_bin
end
# TODO: compile mruby with all the gem sources <need to compile mrbgems with all gems>
def compile_mruby_bin
out = File.join("build", "host", "bin", "mruby")
mruby_bin_mruby_sources = mruby_gem_tool_bin_sources("mruby-bin-mruby", "mruby").join(" ")
gem_init = File.join("build", "mrbgems", "gem_init.c")
libmruby_a = File.join(mruby_build_dir, "lib", "libmruby.a")
libmruby_core_a = File.join(mruby_build_dir, "mrbc", "lib", "libmruby_core.a")
includes = include_str(["include"])
command("#{config.cc.gcc} #{includes} -o #{out} #{mruby_bin_mruby_sources} #{gem_init} #{libmruby_a} #{libmruby_core_a}")
end
def compile_mrbgems
mkdir(File.join(path, "build", "mrbgems"))
ruby do
# have all the gemspecs, so we can build sources and what not...
decls = []
funcs = []
gemspecs.each do |spec|
funcname = spec.name.tr("-", "_")
init = "GENERATED_TMP_mrb_#{funcname}_gem_init"
final = "GENERATED_TMP_mrb_#{funcname}_gem_final"
decls << "void #{init}(mrb_state*);\n" \
"void #{final}(mrb_state*);\n"
funcs << "{ #{init}, #{final} },\n"
end
# shell for mrbgems
File.open("#{path}/build/mrbgems/gem_init.c","w") do |io|
io.puts <<-EOF
#include <mruby.h>
#include <mruby/error.h>
#include <mruby/proc.h>
EOF
unless funcs.empty?
io.write decls.join(" ")
io.puts <<-EOF
static const struct {
void (*init)(mrb_state*);
void (*final)(mrb_state*);
} gem_funcs[] = {#{funcs.join(" ")}};
EOF
io.puts %Q[]
io.puts %Q[#define NUM_GEMS ((int)(sizeof(gem_funcs) / sizeof(gem_funcs[0])))]
io.puts %Q[]
io.puts %Q[struct final_mrbgems {]
io.puts %Q[ int i;]
io.puts %Q[ int ai;]
io.puts %Q[};]
io.puts %Q[]
io.puts %Q[static mrb_value]
io.puts %Q[final_mrbgems_body(mrb_state *mrb, void *ud) {]
io.puts %Q[ struct final_mrbgems *p = (struct final_mrbgems*)ud;]
io.puts %Q[ for (; p->i >= 0; p->i--) {]
io.puts %Q[ gem_funcs[p->i].final(mrb);]
io.puts %Q[ mrb_gc_arena_restore(mrb, p->ai);]
io.puts %Q[ }]
io.puts %Q[ return mrb_nil_value();]
io.puts %Q[}]
io.puts %Q[]
io.puts %Q[static void]
io.puts %Q[mrb_final_mrbgems(mrb_state *mrb) {]
io.puts %Q[ struct final_mrbgems a = { NUM_GEMS - 1, mrb_gc_arena_save(mrb) };]
io.puts %Q[ for (; a.i >= 0; a.i--) {]
io.puts %Q[ mrb_protect_error(mrb, final_mrbgems_body, &a, NULL);]
io.puts %Q[ mrb_gc_arena_restore(mrb, a.ai);]
io.puts %Q[ }]
io.puts %Q[}]
io.puts %Q[]
end
io.puts %Q[void]
io.puts %Q[mrb_init_mrbgems(mrb_state *mrb) {]
unless funcs.empty?
io.puts %Q[ int ai = mrb_gc_arena_save(mrb);]
io.puts %Q[ for (int i = 0; i < NUM_GEMS; i++) {]
io.puts %Q[ gem_funcs[i].init(mrb);]
io.puts %Q[ mrb_gc_arena_restore(mrb, ai);]
io.puts %Q[ mrb_vm_ci_env_clear(mrb, mrb->c->cibase);]
io.puts %Q[ if (mrb->exc) {]
io.puts %Q[ mrb_exc_raise(mrb, mrb_obj_value(mrb->exc));]
io.puts %Q[ }]
io.puts %Q[ }]
io.puts %Q[ mrb_state_atexit(mrb, mrb_final_mrbgems);]
end
io.puts %Q[}]
end
end
end
def mruby_mrblib_files
Dir.glob(File.join(path, "mrblib", "**", "*.rb"))
end
def mrbc_bin
File.join(mruby_build_dir, "mrbc", "bin", "mrbc")
end
def generate_mrblib_template(presym_enabled = false)
suffix = presym_enabled ? "proc" : "irep"
mrblib_path = File.join(mruby_build_dir, "mrblib", "mrblib.c")
ruby do
File.open(mrblib_path, "w") do |io|
io.puts %Q[#include <mruby.h>]
io.puts %Q[#include <mruby/irep.h>]
# gets the output of the mrbc compile of the core ruby_file
command("#{mrbc_bin} -Bmrblib_#{suffix} -s -o- #{mruby_mrblib_files.join(" ")}")
.forward_output { |out| io.puts out unless out =~ /running command/ }
.forward_error { |err| puts err }
.execute
io.puts <<~EOF
void mrb_init_mrblib(mrb_state* mrb)
{
mrb_load_irep(mrb, mrblib_irep);
}
EOF
end
end
end
end
end
gemspec("mruby-io") do |config|
config.cc.defines << "HAVE_MRUBY_IO_GEM"
task "default" do |resolver|
def build
config.cc.includes << "#{path}/include"
config.cc.objs << "#{path}/../hal-posix-io/src/io_hal.c"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment