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 -o [--root ] [--target aarch64-macos] [--c-link | --c-library-path | --c-library | --c-include-path | --c-define ]...", ) fmt.eprintln( " brolang translate-c|--translate-c ... [--output-dir ] [--target aarch64-macos] [--c-include-path | --c-define ]...", ) fmt.eprintln( " brolang build [root] (reads exactly one of root/build.bro or root/build.hon; writes root/build/name)", ) fmt.eprintln( " brolang test [root] (reads exactly one of root/build.bro or root/build.hon; writes and runs root/build/name-test)", ) fmt.eprintln( " brolang new ", ) 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_bro, bro_error := filepath.join({root, "std", "build", "build.bro"}) if bro_error != nil { return false } defer delete(std_build_bro) std_build_hon, hon_error := filepath.join({root, "std", "build", "build.hon"}) if hon_error != nil { return false } defer delete(std_build_hon) 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_bro) || os.exists(std_build_hon)) && 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}\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) } }