Files
brolang/main.odin
T
2026-07-05 00:09:39 +02:00

263 lines
6.9 KiB
Odin

package main
import "./compiler"
import "./compiler/cimport"
import "./compiler/linker"
import "./compiler/target"
import "./compiler/translatec"
import "core:fmt"
import "core:os"
import "core:os/os2"
import "core:path/filepath"
import "core:strings"
Cli_Options :: struct {
input_path: string,
output_path: string,
link_arguments: []linker.Argument,
c_options: cimport.Options,
target: target.Target,
}
parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_Options, bool) {
if len(args) < 4 {
return {}, false
}
options := Cli_Options{input_path=args[1], target=target.DEFAULT}
link_arguments: [dynamic]linker.Argument
link_arguments.allocator = allocator
include_paths: [dynamic]string
include_paths.allocator = allocator
defines: [dynamic]string
defines.allocator = allocator
success := false
defer {
if !success {
delete(link_arguments)
delete(include_paths)
delete(defines)
}
}
output_set := false
cursor := 2
for cursor < len(args) {
option := args[cursor]
cursor += 1
if cursor >= len(args) {
return {}, false
}
value := args[cursor]
cursor += 1
switch option {
case "-o":
if output_set {
return {}, false
}
output_set = true
options.output_path = value
case "--c-link":
append(&link_arguments, linker.Argument{kind=.Input, value=value})
case "--c-library-path":
append(&link_arguments, linker.Argument{kind=.Library_Path, value=value})
case "--c-library":
append(&link_arguments, linker.Argument{kind=.Library, value=value})
case "--c-include-path":
append(&include_paths, value)
case "--c-define":
append(&defines, value)
case "--target":
selected, ok := target.parse(value)
if !ok {
return {}, false
}
options.target = selected
case:
return {}, false
}
}
if !output_set || len(options.output_path) == 0 {
return {}, false
}
options.link_arguments = link_arguments[:]
options.c_options.include_paths = include_paths[:]
options.c_options.defines = defines[:]
success = true
return options, true
}
print_usage :: proc() {
fmt.eprintln(
"usage: brolang <package-directory> -o <executable> [--target aarch64-macos] [--c-link <path> | --c-library-path <dir> | --c-library <name> | --c-include-path <dir> | --c-define <name[=value]>]...",
)
fmt.eprintln(
" brolang translate-c|--translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...",
)
fmt.eprintln(
" brolang build [root] (reads root/build.bro; root defaults to the current directory)",
)
}
is_translate_c_command :: proc(arg: string) -> bool {
return arg == "translate-c" || arg == "--translate-c"
}
is_build_command :: proc(arg: string) -> bool {
return arg == "build"
}
zig_lib_dir_from_env_output :: proc(text: string, allocator := context.allocator) -> (string, bool) {
rest := text
prefix := ".lib_dir = \""
for line in strings.split_lines_iterator(&rest) {
trimmed := strings.trim_space(line)
if !strings.has_prefix(trimmed, prefix) {
continue
}
value := trimmed[len(prefix):]
end := strings.index_byte(value, '"')
if end < 0 {
return "", false
}
return strings.clone(value[:end], allocator), true
}
return "", false
}
zig_lib_dir :: proc(allocator := context.allocator) -> (string, bool) {
state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=[]string{"/usr/bin/env", "zig", "env"}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
if err != nil || state.exit_code != 0 {
return "", false
}
return zig_lib_dir_from_env_output(string(stdout), allocator)
}
append_owned_include_path :: proc(paths, owned: ^[dynamic]string, parts: []string) {
path, err := filepath.join(parts)
if err != nil {
return
}
append(paths, path)
append(owned, path)
}
append_zig_libc_include_paths :: proc(paths, owned: ^[dynamic]string, selected: target.Target) -> bool {
lib_dir, ok := zig_lib_dir()
if !ok {
return false
}
defer delete(lib_dir)
append_owned_include_path(paths, owned, {lib_dir, "include"})
switch selected.kind {
case .Aarch64_Macos:
append_owned_include_path(paths, owned, {lib_dir, "libc", "include", "any-darwin-any"})
}
return true
}
resolve_translate_c_header :: proc(header: string, include_paths: []string, allocator := context.allocator) -> (string, bool) {
if os.exists(header) || filepath.is_abs(header) {
return strings.clone(header, allocator), os.exists(header)
}
for path in include_paths {
candidate, err := filepath.join({path, header}, allocator)
if err != nil {
continue
}
if os.exists(candidate) {
return candidate, true
}
delete(candidate, allocator)
}
return strings.clone(header, allocator), false
}
// run_translate_c emits native brolang bindings for a C header to stdout, the
// offline counterpart of `native :: import "x.h"`.
run_translate_c :: proc(args: []string) -> int {
if len(args) < 3 {
print_usage()
return 2
}
header := args[2]
selected := target.DEFAULT
include_paths: [dynamic]string
owned_include_paths: [dynamic]string
defines: [dynamic]string
defer delete(include_paths)
defer {
for path in owned_include_paths {
delete(path)
}
delete(owned_include_paths)
}
defer delete(defines)
cursor := 3
for cursor < len(args) {
option := args[cursor]
cursor += 1
if cursor >= len(args) {
fmt.eprintfln("missing value for %s", option)
return 2
}
value := args[cursor]
cursor += 1
switch option {
case "--c-include-path":
append(&include_paths, value)
case "--c-define":
append(&defines, value)
case "--target":
parsed, ok := target.parse(value)
if !ok {
fmt.eprintfln("unknown target '%s'", value)
return 2
}
selected = parsed
case:
fmt.eprintfln("unknown option '%s'", option)
return 2
}
}
_ = append_zig_libc_include_paths(&include_paths, &owned_include_paths, selected)
import_header, _ := resolve_translate_c_header(header, include_paths[:])
defer delete(import_header)
c_options := cimport.Options{include_paths=include_paths[:], defines=defines[:]}
result := cimport.import_header(c_options, import_header, selected)
defer cimport.destroy_result(&result)
if !result.available {
message := result.error_message if len(result.error_message) > 0 else "failed to import header"
fmt.eprintln(message)
return 1
}
fmt.print(translatec.emit(&result, header))
return 0
}
main :: proc() {
if len(os2.args) >= 2 && is_build_command(os2.args[1]) {
root := os2.args[2] if len(os2.args) >= 3 else "."
os2.exit(compiler.run_build(root))
}
if len(os2.args) >= 2 && is_translate_c_command(os2.args[1]) {
os2.exit(run_translate_c(os2.args))
}
options, valid := parse_cli_args(os2.args)
if !valid {
print_usage()
os2.exit(2)
}
defer delete(options.link_arguments)
defer delete(options.c_options.include_paths)
defer delete(options.c_options.defines)
status := compiler.compile_package(options.input_path, options.output_path, options.link_arguments, options.target, options.c_options)
if status != 0 {
os2.exit(status)
}
}