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, 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 [--target aarch64-macos] [--c-include-path | --c-define ]...", ) fmt.eprintln( " brolang build [root] (reads root/build.bro; writes root/build/name)", ) fmt.eprintln( " brolang new ", ) fmt.eprintln( " brolang init", ) } 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_new_command :: proc(arg: string) -> bool { return arg == "new" } is_init_command :: proc(arg: string) -> bool { return arg == "init" } 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 } 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]) { 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_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) } }