664 lines
16 KiB
Odin
664 lines
16 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"
|
|
|
|
BROLANG_VERSION :: "0.0.0-dev"
|
|
|
|
Cli_Options :: struct {
|
|
input_path: string,
|
|
output_path: string,
|
|
project_root: 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 "--root":
|
|
if len(options.project_root) > 0 {
|
|
return {}, false
|
|
}
|
|
options.project_root = value
|
|
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> [--root <project-root>] [--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>... [--output-dir <dir>] [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...",
|
|
)
|
|
fmt.eprintln(
|
|
" brolang build [root] (reads root/build.bro; writes root/build/name)",
|
|
)
|
|
fmt.eprintln(
|
|
" brolang test [root] (reads root/build.bro; writes and runs root/build/name-test)",
|
|
)
|
|
fmt.eprintln(
|
|
" brolang new <project-path>",
|
|
)
|
|
fmt.eprintln(
|
|
" brolang init",
|
|
)
|
|
fmt.eprintln(
|
|
" brolang version",
|
|
)
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
is_test_command :: proc(arg: string) -> bool {
|
|
return arg == "test"
|
|
}
|
|
|
|
is_new_command :: proc(arg: string) -> bool {
|
|
return arg == "new"
|
|
}
|
|
|
|
is_init_command :: proc(arg: string) -> bool {
|
|
return arg == "init"
|
|
}
|
|
|
|
is_version_command :: proc(arg: string) -> bool {
|
|
return arg == "version"
|
|
}
|
|
|
|
template_root_valid :: proc(root: string) -> bool {
|
|
std_build, std_error := filepath.join({root, "std", "build", "build.bro"})
|
|
if std_error != nil {
|
|
return false
|
|
}
|
|
defer delete(std_build)
|
|
ffi_stdio, ffi_error := filepath.join({root, "ffi", "c", "stdio.bro"})
|
|
if ffi_error != nil {
|
|
return false
|
|
}
|
|
defer delete(ffi_stdio)
|
|
return os.exists(std_build) && os.exists(ffi_stdio)
|
|
}
|
|
|
|
find_template_root :: proc(allocator := context.allocator) -> (string, bool) {
|
|
if exe, exe_error := os2.get_executable_path(allocator); exe_error == nil {
|
|
defer delete(exe, allocator)
|
|
exe_dir := filepath.dir(exe, allocator)
|
|
defer delete(exe_dir, allocator)
|
|
if template_root_valid(exe_dir) {
|
|
return strings.clone(exe_dir, allocator), true
|
|
}
|
|
parent := filepath.dir(exe_dir, allocator)
|
|
defer delete(parent, allocator)
|
|
if template_root_valid(parent) {
|
|
return strings.clone(parent, allocator), true
|
|
}
|
|
}
|
|
cwd := os.get_current_directory(allocator)
|
|
defer delete(cwd, allocator)
|
|
if template_root_valid(cwd) {
|
|
return strings.clone(cwd, allocator), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
ensure_directory :: proc(path: string) -> bool {
|
|
if os.exists(path) {
|
|
if os.is_dir(path) {
|
|
return true
|
|
}
|
|
fmt.eprintfln("path exists and is not a directory: %s", path)
|
|
return false
|
|
}
|
|
if err := os2.make_directory_all(path); err != nil {
|
|
fmt.eprintfln("failed to create directory '%s': %v", path, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
ensure_child_directory :: proc(root, name: string) -> bool {
|
|
path, err := filepath.join({root, name})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer delete(path)
|
|
return ensure_directory(path)
|
|
}
|
|
|
|
write_file_if_missing :: proc(path, text: string) -> bool {
|
|
if os.exists(path) {
|
|
if os.is_dir(path) {
|
|
fmt.eprintfln("path exists and is not a file: %s", path)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
if !os.write_entire_file(path, transmute([]byte)text) {
|
|
fmt.eprintfln("failed to write file '%s'", path)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
write_escaped_brolang_string :: proc(builder: ^strings.Builder, value: string) {
|
|
for b in transmute([]byte)value {
|
|
if b == '\\' || b == '"' {
|
|
strings.write_byte(builder, '\\')
|
|
}
|
|
strings.write_byte(builder, b)
|
|
}
|
|
}
|
|
|
|
default_build_bro :: proc(project_name: string, allocator := context.allocator) -> string {
|
|
name := project_name
|
|
if len(name) == 0 || name == "." || name == "/" {
|
|
name = "app"
|
|
}
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
strings.write_string(&builder, "b :: import \"@std/build\"\n\nconfig :: b.BuildConfig{\n\tname = \"")
|
|
write_escaped_brolang_string(&builder, name)
|
|
strings.write_string(&builder, "\",\n\tsource = \"source\",\n\tlibraries = &[],\n\tlib_paths = &[],\n\tincludes = &[],\n\tdefines = &[],\n\tlinks = &[],\n}\n")
|
|
return strings.clone(strings.to_string(builder), allocator)
|
|
}
|
|
|
|
ensure_default_sources :: proc(root: string) -> bool {
|
|
source_dir, source_error := filepath.join({root, "source"})
|
|
if source_error != nil {
|
|
return false
|
|
}
|
|
defer delete(source_dir)
|
|
if !ensure_directory(source_dir) {
|
|
return false
|
|
}
|
|
main_path, main_error := filepath.join({source_dir, "main.bro"})
|
|
if main_error != nil {
|
|
return false
|
|
}
|
|
defer delete(main_path)
|
|
return write_file_if_missing(main_path, "main func() i32 {\n\treturn 0\n}\n")
|
|
}
|
|
|
|
copy_tree_if_missing :: proc(root, template_root, name: string) -> bool {
|
|
dst, dst_error := filepath.join({root, name})
|
|
if dst_error != nil {
|
|
return false
|
|
}
|
|
defer delete(dst)
|
|
if os.exists(dst) {
|
|
if os.is_dir(dst) {
|
|
return true
|
|
}
|
|
fmt.eprintfln("path exists and is not a directory: %s", dst)
|
|
return false
|
|
}
|
|
src, src_error := filepath.join({template_root, name})
|
|
if src_error != nil {
|
|
return false
|
|
}
|
|
defer delete(src)
|
|
if err := os2.copy_directory_all(dst, src); err != nil {
|
|
fmt.eprintfln("failed to copy '%s' to '%s': %v", src, dst, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
init_project :: proc(root: string) -> bool {
|
|
if !ensure_directory(root) {
|
|
return false
|
|
}
|
|
if !ensure_default_sources(root) || !ensure_child_directory(root, "vendor") {
|
|
return false
|
|
}
|
|
|
|
needs_template := true
|
|
std_path, std_error := filepath.join({root, "std"})
|
|
ffi_path, ffi_error := filepath.join({root, "ffi"})
|
|
if std_error == nil && ffi_error == nil {
|
|
needs_template = !os.exists(std_path) || !os.exists(ffi_path)
|
|
}
|
|
if std_error == nil {
|
|
delete(std_path)
|
|
}
|
|
if ffi_error == nil {
|
|
delete(ffi_path)
|
|
}
|
|
|
|
template_root := ""
|
|
if needs_template {
|
|
ok: bool
|
|
template_root, ok = find_template_root()
|
|
if !ok {
|
|
fmt.eprintln("could not find bundled std/ffi sources")
|
|
return false
|
|
}
|
|
defer delete(template_root)
|
|
if !copy_tree_if_missing(root, template_root, "std") ||
|
|
!copy_tree_if_missing(root, template_root, "ffi") {
|
|
return false
|
|
}
|
|
}
|
|
|
|
build_path, build_error := filepath.join({root, "build.bro"})
|
|
if build_error != nil {
|
|
return false
|
|
}
|
|
defer delete(build_path)
|
|
project_name := filepath.base(root)
|
|
build_text := default_build_bro(project_name)
|
|
defer delete(build_text)
|
|
return write_file_if_missing(build_path, build_text)
|
|
}
|
|
|
|
new_project :: proc(path: string) -> bool {
|
|
if os.exists(path) {
|
|
fmt.eprintfln("project path already exists: %s", path)
|
|
return false
|
|
}
|
|
template_root, ok := find_template_root()
|
|
if !ok {
|
|
fmt.eprintln("could not find bundled std/ffi sources")
|
|
return false
|
|
}
|
|
defer delete(template_root)
|
|
if !ensure_directory(path) {
|
|
return false
|
|
}
|
|
if !init_project(path) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
run_init_project :: proc() -> int {
|
|
cwd := os.get_current_directory()
|
|
defer delete(cwd)
|
|
return 0 if init_project(cwd) else 2
|
|
}
|
|
|
|
run_new_project :: proc(path: string) -> int {
|
|
return 0 if new_project(path) else 2
|
|
}
|
|
|
|
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
|
|
}
|
|
selected := target.DEFAULT
|
|
headers: [dynamic]string
|
|
include_paths: [dynamic]string
|
|
owned_include_paths: [dynamic]string
|
|
defines: [dynamic]string
|
|
output_dir := ""
|
|
output_dir_set := false
|
|
defer delete(headers)
|
|
defer delete(include_paths)
|
|
defer {
|
|
for path in owned_include_paths {
|
|
delete(path)
|
|
}
|
|
delete(owned_include_paths)
|
|
}
|
|
defer delete(defines)
|
|
cursor := 2
|
|
for cursor < len(args) {
|
|
option := args[cursor]
|
|
cursor += 1
|
|
if !strings.has_prefix(option, "--") {
|
|
append(&headers, option)
|
|
continue
|
|
}
|
|
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 "--output-dir":
|
|
if output_dir_set || len(value) == 0 {
|
|
fmt.eprintln("--output-dir may be provided once with a non-empty value")
|
|
return 2
|
|
}
|
|
output_dir = value
|
|
output_dir_set = true
|
|
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
|
|
}
|
|
}
|
|
if len(headers) == 0 {
|
|
print_usage()
|
|
return 2
|
|
}
|
|
if len(headers) > 1 && !output_dir_set {
|
|
fmt.eprintln("multiple headers require --output-dir")
|
|
return 2
|
|
}
|
|
names := make([]string, len(headers))
|
|
defer {
|
|
for name in names {
|
|
delete(name)
|
|
}
|
|
delete(names)
|
|
}
|
|
if output_dir_set {
|
|
seen_names: map[string]bool
|
|
defer delete(seen_names)
|
|
for header, index in headers {
|
|
base := filepath.base(header)
|
|
extension := filepath.ext(base)
|
|
stem := base[:len(base)-len(extension)]
|
|
if len(stem) == 0 {
|
|
fmt.eprintfln("cannot derive an output name from header '%s'", header)
|
|
return 2
|
|
}
|
|
name := fmt.aprintf("%s.bro", stem)
|
|
if seen_names[name] {
|
|
fmt.eprintfln("duplicate generated output name '%s'", name)
|
|
delete(name)
|
|
return 2
|
|
}
|
|
seen_names[name] = true
|
|
names[index] = name
|
|
}
|
|
}
|
|
_ = append_zig_libc_include_paths(&include_paths, &owned_include_paths, selected)
|
|
c_options := cimport.Options{include_paths=include_paths[:], defines=defines[:]}
|
|
resolved_headers: [dynamic]string
|
|
results: [dynamic]cimport.Result
|
|
defer {
|
|
for header in resolved_headers {
|
|
delete(header)
|
|
}
|
|
delete(resolved_headers)
|
|
for &result in results {
|
|
cimport.destroy_result(&result)
|
|
}
|
|
delete(results)
|
|
}
|
|
for header in headers {
|
|
resolved, _ := resolve_translate_c_header(header, include_paths[:])
|
|
append(&resolved_headers, resolved)
|
|
result := cimport.import_header(c_options, resolved, selected)
|
|
append(&results, result)
|
|
if !result.available {
|
|
message := result.error_message if len(result.error_message) > 0 else "failed to import header"
|
|
fmt.eprintln(message)
|
|
return 1
|
|
}
|
|
}
|
|
if !output_dir_set {
|
|
fmt.print(translatec.emit(&results[0], headers[0]))
|
|
return 0
|
|
}
|
|
|
|
inputs := make([]translatec.Package_Input, len(headers))
|
|
defer delete(inputs)
|
|
for header, index in headers {
|
|
inputs[index] = translatec.Package_Input{
|
|
result=&results[index],
|
|
header=header,
|
|
name=names[index],
|
|
}
|
|
}
|
|
outputs, generation_error := translatec.emit_package(inputs[:])
|
|
defer translatec.destroy_package_outputs(outputs)
|
|
defer delete(generation_error)
|
|
if len(generation_error) > 0 {
|
|
fmt.eprintln(generation_error)
|
|
return 1
|
|
}
|
|
if !ensure_directory(output_dir) {
|
|
return 1
|
|
}
|
|
paths := make([]string, len(outputs))
|
|
defer {
|
|
for path in paths {
|
|
delete(path)
|
|
}
|
|
delete(paths)
|
|
}
|
|
for output, index in outputs {
|
|
path, join_error := filepath.join({output_dir, output.name})
|
|
if join_error != nil {
|
|
fmt.eprintfln("failed to form output path for '%s'", output.name)
|
|
return 1
|
|
}
|
|
paths[index] = path
|
|
}
|
|
for output, index in outputs {
|
|
if !os.write_entire_file(paths[index], transmute([]byte)output.source) {
|
|
fmt.eprintfln("failed to write generated bindings '%s'", paths[index])
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
main :: proc() {
|
|
if len(os2.args) >= 2 && is_version_command(os2.args[1]) {
|
|
if len(os2.args) != 2 {
|
|
print_usage()
|
|
os2.exit(2)
|
|
}
|
|
fmt.printfln("brolang %s", BROLANG_VERSION)
|
|
os2.exit(0)
|
|
}
|
|
if len(os2.args) >= 2 && is_build_command(os2.args[1]) {
|
|
if len(os2.args) > 3 {
|
|
print_usage()
|
|
os2.exit(2)
|
|
}
|
|
root := os2.args[2] if len(os2.args) == 3 else ""
|
|
os2.exit(compiler.run_build(root))
|
|
}
|
|
if len(os2.args) >= 2 && is_test_command(os2.args[1]) {
|
|
if len(os2.args) > 3 {
|
|
print_usage()
|
|
os2.exit(2)
|
|
}
|
|
root := os2.args[2] if len(os2.args) == 3 else ""
|
|
os2.exit(compiler.run_tests(root))
|
|
}
|
|
if len(os2.args) >= 2 && is_new_command(os2.args[1]) {
|
|
if len(os2.args) != 3 {
|
|
print_usage()
|
|
os2.exit(2)
|
|
}
|
|
os2.exit(run_new_project(os2.args[2]))
|
|
}
|
|
if len(os2.args) >= 2 && is_init_command(os2.args[1]) {
|
|
if len(os2.args) != 2 {
|
|
print_usage()
|
|
os2.exit(2)
|
|
}
|
|
os2.exit(run_init_project())
|
|
}
|
|
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,
|
|
options.project_root,
|
|
)
|
|
if status != 0 {
|
|
os2.exit(status)
|
|
}
|
|
}
|