///|
async fn emit_files(
files : Array[@resolver.GeneratedFile],
output_dir : @path.Path,
) -> Unit {
// Ensure output directory exists
let dir_exists = @fs.exists(output_dir.0)
if !dir_exists {
@fs.mkdir(output_dir.0, permission=0o755, recursive=true)
}
// Write output files
let c_files : Array[String] = []
let mbt_files : Array[String] = []
for file in files {
let path = output_dir.join(@path.Path(file.filename))
@fs.write_file(
path.0,
file.content,
create_mode=CreateOrTruncate,
permission=0o644,
)
@stdio.stdout.write(" " + path.0 + "\n")
let ext = path.extname()
if ext == ".c" || ext == ".h" {
c_files.push(path.0)
} else if ext == ".mbt" {
mbt_files.push(path.0)
}
}
// Format generated files
if !c_files.is_empty() {
let args : Array[String] = ["-i"]
args.append(c_files[:])
let exit_code = @process.run("clang-format", args[:])
if exit_code != 0 {
@stdio.stderr.write("Warning: clang-format failed\n")
}
}
for path in mbt_files {
let (exit_code, output) = @process.collect_stdout("moonfmt", [path][:])
if exit_code == 0 {
@fs.write_file(
path,
output,
create_mode=CreateOrTruncate,
permission=0o644,
)
} else {
@stdio.stderr.write("Warning: moonfmt failed for " + path + "\n")
}
}
}
///|
async fn main {
let cmd = @argparse.Command(
"gir",
about="Generate MoonBit FFI bindings from GIR XML",
positionals=[
@argparse.PositionArg("gir-name", about="GIR name, e.g. Gtk-4.0"),
],
options=[
@argparse.OptionArg(
"output",
short='o',
long="output",
about="Output directory",
default_values=["src/gtk"],
),
@argparse.OptionArg("pkg", short='p', long="pkg", about="Package name", default_values=[
"gtk",
]),
@argparse.OptionArg(
"classes",
short='c',
long="classes",
about="Comma-separated class whitelist (e.g. Widget,Button)",
),
@argparse.OptionArg(
"config",
long="config",
about="Path to gir.json config file",
),
@argparse.OptionArg(
"gir-dir",
long="gir-dir",
about="Directory containing GIR files",
default_values=["/usr/share/gir-1.0"],
),
],
)
let matches = cmd.parse() catch {
e => {
@stdio.stderr.write(e.to_string())
return
}
}
let output_dir = match matches.values.get("output") {
Some(vals) => @path.Path(vals[0])
None => @path.Path("src/gtk")
}
let gir_dir = match matches.values.get("gir-dir") {
Some(vals) => vals[0]
None => "/usr/share/gir-1.0"
}
// Config mode
match matches.values.get("config") {
Some(config_vals) => {
let config_path = config_vals[0]
let config_data = @fs.read_file(config_path)
let config_text = config_data.text() catch {
e => {
@stdio.stderr.write(
"Failed to read config file: " + e.to_string() + "\n",
)
return
}
}
let config : @config.GirConfig = @json.from_json(@json.parse(config_text)) catch {
e => {
@stdio.stderr.write("Failed to parse config: " + e.to_string() + "\n")
return
}
}
// Read primary GIR
let gir_path = @path.Path(gir_dir).join(@path.Path(config.gir + ".gir"))
let gir_exists = @fs.exists(gir_path.0)
if !gir_exists {
@stdio.stderr.write("GIR file not found: " + gir_path.0 + "\n")
return
}
let data = @fs.read_file(gir_path.0)
let content = data.text() catch {
e => {
@stdio.stderr.write(
"Failed to read GIR file: " + e.to_string() + "\n",
)
return
}
}
let pkg_name = match matches.values.get("pkg") {
Some(vals) => vals[0]
None => "gtk"
}
let ns = @parser.parse_gir(content) catch {
e => {
@stdio.stderr.write("Generation failed: " + e.to_string() + "\n")
return
}
}
let resolver = @resolver.BindingResolver::new(
pkg_name,
config.header,
version_macro=config.version_macro,
extra_c_includes=config.includes,
)
// Cairo is a built-in introspection namespace and commonly has no
// standalone cairo-1.0.gir. Register the value type needed by
// PangoCairo so signatures can still resolve to the Cairo package.
resolver.register_enum(
@model.GirEnum::new(
name="cairo.FontType",
c_type="cairo_font_type_t",
import_path="@cairo",
),
"cairo.FontType",
)
// Load dependencies first
for dep_name, dep_config in config.deps {
let dep_gir_path = @path.Path(gir_dir).join(
@path.Path(dep_name + ".gir"),
)
let dep_exists = @fs.exists(dep_gir_path.0)
if !dep_exists {
@stdio.stderr.write(
"Dependency GIR not found: " + dep_gir_path.0 + "\n",
)
continue
}
let dep_data = @fs.read_file(dep_gir_path.0)
let dep_content = dep_data.text() catch {
e => {
@stdio.stderr.write(
"Failed to read dep GIR: " + e.to_string() + "\n",
)
continue
}
}
let dep_ns = @parser.parse_gir(dep_content) catch {
e => {
@stdio.stderr.write(
"Failed to parse dep GIR: " + e.to_string() + "\n",
)
continue
}
}
resolver.register_dependency(dep_ns, dep_config, dep_ns.name())
resolver.register_callbacks(dep_ns.callbacks())
}
// Register primary namespace
resolver.register_namespace(ns, config)
resolver.register_callbacks(ns.callbacks())
// Register manual types not in GIR as classes
resolver.register_manual_type(
"GLib.Variant",
c_type="GVariant",
codegen_stem="variant",
import_path="@glib",
)
// Resolve declarations directly into package destinations
for name in config.enums {
match ns.get_enum(name) {
Some(e) => {
let qualified = ns.name() + "." + name
resolver.resolve_enum(e, qualified)
}
None => ()
}
}
resolver.resolve_constants(ns.constants(), overrides=config.constants)
resolver.resolve_alias_types(config, ns.name())
resolver.resolve_record_types(config, ns.name())
for name in config.records {
let qualified = ns.name() + "." + name
if ns.parse_record(name) is Some(rec) {
resolver.resolve_record(rec, qualified)
}
}
for name in config.interfaces {
match ns.parse_interface(name) {
Some(iface) => {
let qualified = ns.name() + "." + name
resolver.resolve_interface(iface, qualified)
}
None => ()
}
}
for name in config.classes {
match ns.parse_class(name) {
Some(cls) => {
let qualified = ns.name() + "." + name
resolver.resolve_class(cls, qualified)
}
None => ()
}
}
for name in config.functions {
match ns.get_function(name) {
Some(function_) => resolver.resolve_function(function_, name)
None => ()
}
}
let (files, diagnostics) = resolver.finish()
emit_files(files, output_dir)
for d in diagnostics {
@stdio.stderr.write(" " + d.to_string() + "\n")
}
@stdio.stdout.write("Done.\n")
}
None => {
// Legacy CLI mode
let gir_name = match matches.values.get("gir-name") {
Some(vals) => vals[0]
None => {
@stdio.stderr.write(cmd.render_help())
return
}
}
let pkg_name = match matches.values.get("pkg") {
Some(vals) => vals[0]
None => "gtk"
}
let class_whitelist : Array[String]? = match
matches.values.get("classes") {
Some(vals) => {
let result : Array[String] = []
for v in vals {
for part in v.split(",") {
let s = part.to_owned()
if !s.is_empty() {
result.push(s)
}
}
}
Some(result)
}
None => None
}
let gir_path = @path.Path(gir_dir).join(@path.Path(gir_name + ".gir"))
let gir_exists = @fs.exists(gir_path.0)
if !gir_exists {
@stdio.stderr.write("GIR file not found: " + gir_path.0 + "\n")
return
}
// Read GIR file
let data = @fs.read_file(gir_path.0)
let content = data.text() catch {
e => {
@stdio.stderr.write(
"Failed to read GIR file: " + e.to_string() + "\n",
)
return
}
}
let ns = @parser.parse_gir(content) catch {
e => {
@stdio.stderr.write("Generation failed: " + e.to_string() + "\n")
return
}
}
let header = pkg_name + "/" + pkg_name + ".h"
let resolver = @resolver.BindingResolver::new(pkg_name, header)
// Register all enums and parent chain
for name in ns.enum_names() {
match ns.get_enum(name) {
Some(e) => resolver.register_enum(e, e.name)
None => ()
}
}
for name in ns.class_names() {
let qualified = ns.name() + "." + name
match ns.parent_of(name) {
Some(parent) => resolver.store_parent(qualified, parent)
None => ()
}
}
// Register and resolve classes
let class_names : Array[String] = match class_whitelist {
Some(classes) => classes
None => ns.class_names().collect()
}
for name in class_names {
match ns.parse_class(name) {
Some(cls) => {
let qualified = ns.name() + "." + name
resolver.register_class(cls, qualified)
}
None => ()
}
}
for name in ns.enum_names() {
match ns.get_enum(name) {
Some(e) => {
let qualified = ns.name() + "." + name
resolver.resolve_enum(e, qualified)
}
None => ()
}
}
resolver.resolve_constants(ns.constants())
for name in class_names {
match ns.parse_class(name) {
Some(cls) => {
let qualified = ns.name() + "." + name
resolver.resolve_class(cls, qualified)
}
None => ()
}
}
let (files, diagnostics) = resolver.finish()
emit_files(files, output_dir)
for d in diagnostics {
@stdio.stderr.write(" " + d.to_string() + "\n")
}
@stdio.stdout.write("Done.\n")
}
}
}