Start implementing a proper build system

This commit is contained in:
sergeypdev 2025-05-23 14:50:21 +04:00
parent 6e2ad6a3b0
commit 7cda6a3f7d
11 changed files with 570 additions and 37 deletions

3
build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash

193
builder/builder.odin Normal file
View File

@ -0,0 +1,193 @@
package builder
import "core:flags"
import "core:fmt"
import "core:log"
import os "core:os/os2"
import "core:slice"
Build_Variant :: enum {
Hot_Reload,
Desktop,
Web,
}
Options :: struct {
variant: Build_Variant `usage:"Variant of the build"`,
optimize: bool `args:"name=opt",usage:"Enable compiler optimizations"`,
debug: bool `usage:"Enable debug symbols"`,
rebuild_deps: bool `usage:"When enabled dependencies will be cleaned and rebuilt"`,
tracy: bool `usage:"Enable tracy profiler"`,
}
Error :: union #shared_nil {
Run_Error,
Copy_Error,
os.Error,
}
build_deps :: proc(opts: Options) {
log.infof("build_deps")
force := opts.rebuild_deps
shared := opts.variant == .Hot_Reload
// Raylib
{
cwd := "./libs/raylib"
out_dir := shared ? "zig-out-shared" : "zig-out-static"
if force {
remove_all(fmt.tprintf("./libs/raylib/%s", out_dir))
}
target := opts.variant == .Web ? "wasm32-emscripten" : "native"
handle_error(
run_cmd(
{
"zig",
"build",
"-p",
out_dir,
fmt.tprintf("-Dshared=%v", shared),
fmt.tprintf("-Dtarget=%s", target),
},
cwd,
),
)
}
// Physfs
{
cwd := "./libs/physfs"
file_name := shared ? "libphysfs.so" : "libphysfs.a"
is_built := os.is_file(fmt.tprintf("./libs/physfs/%s", file_name))
if is_built && force {
handle_error(run_cmd({"make", "clean"}, cwd))
}
if !is_built || force {
handle_error(run_cmd({"make", file_name}, cwd))
}
}
// Tracy
if opts.tracy {
cwd := "./libs/tracy"
when ODIN_OS == .Windows {
TRACY_NAME_SHARED :: "tracydll.lib"
TRACY_NAME_STATIC :: "tracy.lib"
} else when ODIN_OS == .Linux {
TRACY_NAME_SHARED :: "tracy.so"
TRACY_NAME_STATIC :: "tracy.a"
} else when ODIN_OS == .Darwin {
TRACY_NAME_SHARED :: "tracy.dynlib"
TRACY_NAME_STATIC :: "tracy.a"
}
file_path := fmt.tprintf("./libs/tracy/%s", shared ? TRACY_NAME_SHARED : TRACY_NAME_STATIC)
is_built := os.is_file(file_path)
if is_built && force {
remove_file(file_path)
}
if !is_built || force {
handle_error(
run_cmd(
slice.concatenate(
[][]string {
{
"zig",
"c++",
"-std=c++11",
"-DTRACY_ENABLE",
"-O2",
"vendor/tracy/public/TracyClient.cpp",
"-fPIC",
},
shared ? {"-shared", "-o", TRACY_NAME_SHARED} : {"-c", "-o", "tracy.o"},
},
context.temp_allocator,
),
cwd,
),
)
if !shared {
handle_error(run_cmd({"zig", "ar", "rc", TRACY_NAME_STATIC, "tracy.o"}, cwd))
}
}
}
}
COMMON_FLAGS :: []string {
"-collection:libs=./libs",
"-collection:common=./common",
"-collection:game=./game",
"-strict-style",
"-vet",
}
main :: proc() {
context.logger = log.create_console_logger()
opts := Options {
tracy = true,
debug = true,
}
flags.parse_or_exit(&opts, os.args, .Unix, context.temp_allocator)
if opts.variant == .Web {
log.warnf("tracy is not supported on Web")
opts.tracy = false
}
tracy_flag: []string = opts.tracy ? {"-define:TRACY_ENABLE=true"} : {}
debug_flag: []string = opts.debug ? {"-debug"} : {}
optimize_flag: []string = opts.optimize ? {"-o:speed"} : {}
build_deps(opts)
#partial switch opts.variant {
case .Hot_Reload:
cmd := slice.concatenate(
[][]string {
[]string {
"odin",
"build",
"game",
"-define:RAYLIB_SHARED=true",
"-define:PHYSFS_SHARED=true",
"-build-mode:dll",
"-out:game_tmp.so",
},
tracy_flag,
debug_flag,
optimize_flag,
COMMON_FLAGS,
},
context.temp_allocator,
)
handle_error(run_cmd(cmd, "."))
handle_error(os.rename("game_tmp.so", "game.so"))
case .Desktop:
cmd := slice.concatenate(
[][]string {
[]string{"odin", "build", "main_release", "-out:game.bin"},
tracy_flag,
debug_flag,
optimize_flag,
COMMON_FLAGS,
},
context.temp_allocator,
)
handle_error(run_cmd(cmd, "."))
case:
cmd := slice.concatenate(
[][]string {
[]string{"odin", "build", "main_web", "-build-mode:obj", "-out:game_web/game"},
COMMON_FLAGS,
debug_flag,
optimize_flag,
},
)
handle_error(run_cmd(cmd, "."))
}
}

80
builder/helpers.odin Normal file
View File

@ -0,0 +1,80 @@
package builder
import "base:intrinsics"
import "core:fmt"
import "core:log"
import os "core:os/os2"
import "core:path/filepath"
import "core:strings"
Process_Error :: enum {
OK,
Invalid_Exit_Code,
Crash,
}
Run_Error :: union #shared_nil {
Process_Error,
os.Error,
}
@(require_results)
run_cmd :: proc(cmd: []string, cwd: string, loc := #caller_location) -> Run_Error {
log.infof(
"running [%s]: %s",
cwd,
strings.join(cmd, " ", context.temp_allocator),
location = loc,
)
desc := os.Process_Desc {
command = cmd,
working_dir = cwd,
stderr = os.stderr,
stdout = os.stdout,
}
process := os.process_start(desc) or_return
state := os.process_wait(process) or_return
if !state.success {
return .Crash
}
if state.exit_code != 0 {
return .Invalid_Exit_Code
}
return nil
}
Copy_Error :: union #shared_nil {
os.Error,
filepath.Match_Error,
}
handle_error :: proc(
err: $E,
msg: string = "",
expr := #caller_expression,
loc := #caller_location,
) where intrinsics.type_is_enum(E) ||
intrinsics.type_is_union(E) {
if err != nil {
log.panicf("%v %s error: %v", expr, msg, err, location = loc)
}
}
remove_file :: proc(path: string, expr := #caller_expression, loc := #caller_location) {
log.infof("remove(%s)", path, location = loc)
err := os.remove(path)
if err != .Not_Exist {
handle_error(err, fmt.tprintf("failed to remove %s", path), expr = expr, loc = loc)
}
}
remove_all :: proc(path: string, expr := #caller_expression, loc := #caller_location) {
log.infof("remove_all(%s)", path, location = loc)
err := os.remove_all(path)
if err != .Not_Exist {
handle_error(err, fmt.tprintf("failed to remove %s", path), expr = expr, loc = loc)
}
}

View File

@ -6,3 +6,5 @@ libphysfs.a: build_physfs
cp ./build/libphysfs.a . cp ./build/libphysfs.a .
libphysfs.so: build_physfs libphysfs.so: build_physfs
cp ./build/libphysfs.so* . cp ./build/libphysfs.so* .
clean:
rm -r build; rm libphysfs.*

View File

@ -113,3 +113,6 @@ docgen_tmp/
# Parser stuff # Parser stuff
parser/raylib_parser parser/raylib_parser
zig-out-static
zig-out-shared

View File

@ -103,13 +103,13 @@ RAYLIB_SHARED :: #config(RAYLIB_SHARED, false)
when ODIN_OS == .Windows { when ODIN_OS == .Windows {
@(extra_linker_flags = "/NODEFAULTLIB:" + ("msvcrt" when RAYLIB_SHARED else "libcmt")) @(extra_linker_flags = "/NODEFAULTLIB:" + ("msvcrt" when RAYLIB_SHARED else "libcmt"))
foreign import lib {"windows/raylibdll.lib" when RAYLIB_SHARED else "windows/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"} foreign import lib {"zig-out-shared/lib/raylib.lib" when RAYLIB_SHARED else "zig-out-static/lib/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"}
} else when ODIN_OS == .Linux { } else when ODIN_OS == .Linux {
foreign import lib {// Note(bumbread): I'm not sure why in `linux/` folder there are foreign import lib {"zig-out-shared/lib/libraylib.so" when RAYLIB_SHARED else "zig-out-static/lib/libraylib.a", "system:dl", "system:pthread"} // Note(bumbread): I'm not sure why in `linux/` folder there are// multiple copies of raylib.so, but since these bindings are for// particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4)
// multiple copies of raylib.so, but since these bindings are for
"src/libraylib.so.550" when RAYLIB_SHARED else "src/libraylib.a", "system:dl", "system:pthread"} // particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4)
} else when ODIN_OS == .Darwin { } else when ODIN_OS == .Darwin {
foreign import lib {"src/libraylib.550.dylib" when RAYLIB_SHARED else "src/libraylib.a", "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"} foreign import lib {"zig-out-shared/lib/libraylib.dylib" when RAYLIB_SHARED else "zig-out-static/lib/libraylib.a", "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"}
} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
foreign import lib "zig-out-static/lib/libraylib.a"
} else { } else {
foreign import lib "system:raylib" foreign import lib "system:raylib"
} }

View File

@ -121,14 +121,11 @@ RAYLIB_SHARED :: #config(RAYLIB_SHARED, false)
when ODIN_OS == .Windows { when ODIN_OS == .Windows {
@(extra_linker_flags = "/NODEFAULTLIB:" + ("msvcrt" when RAYLIB_SHARED else "libcmt")) @(extra_linker_flags = "/NODEFAULTLIB:" + ("msvcrt" when RAYLIB_SHARED else "libcmt"))
foreign import lib {"../windows/raylibdll.lib" when RAYLIB_SHARED else "../windows/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"} foreign import lib {"../zig-out-shared/lib/raylib.lib" when RAYLIB_SHARED else "../zig-out-static/lib/raylib.lib", "system:Winmm.lib", "system:Gdi32.lib", "system:User32.lib", "system:Shell32.lib"}
} else when ODIN_OS == .Linux { } else when ODIN_OS == .Linux {
foreign import lib {// Note(bumbread): I'm not sure why in `linux/` folder there are foreign import lib {"../zig-out-shared/lib/libraylib.so" when RAYLIB_SHARED else "../zig-out-static/lib/libraylib.a", "system:dl", "system:pthread"} // Note(bumbread): I'm not sure why in `linux/` folder there are// multiple copies of raylib.so, but since these bindings are for// particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4)
// multiple copies of raylib.so, but since these bindings are for
"../src/libraylib.so.550" when RAYLIB_SHARED else "../src/libraylib.a", "system:dl", "system:pthread"} // particular version of the library, I better specify it. Ideally,// though, it's best specified in terms of major (.so.4)
} else when ODIN_OS == .Darwin { } else when ODIN_OS == .Darwin {
foreign import lib {"../macos" + ("-arm64" when ODIN_ARCH == foreign import lib {"../zig-out-shared/lib/libraylib.dylib" when RAYLIB_SHARED else "../zig-out-static/lib/libraylib.a", "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"}
.arm64 else "") + "/libraylib" + (".500.dylib" when RAYLIB_SHARED else ".a"), "system:Cocoa.framework", "system:OpenGL.framework", "system:IOKit.framework"}
} else { } else {
foreign import lib "system:raylib" foreign import lib "system:raylib"
} }

5
libs/tracy/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
tracy.o
tracy.so
tracy.a
tracy.lib
tracy.dll

View File

@ -0,0 +1,144 @@
/*
This allocator uses the malloc, calloc, free and realloc procs that emscripten
exposes in order to allocate memory. Just like Odin's default heap allocator
this uses proper alignment, so that maps and simd works.
*/
package main_web
import "base:intrinsics"
import "core:c"
import "core:mem"
// This will create bindings to emscripten's implementation of libc
// memory allocation features.
@(default_calling_convention = "c")
foreign _ {
calloc :: proc(num, size: c.size_t) -> rawptr ---
free :: proc(ptr: rawptr) ---
malloc :: proc(size: c.size_t) -> rawptr ---
realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
}
emscripten_allocator :: proc "contextless" () -> mem.Allocator {
return mem.Allocator{emscripten_allocator_proc, nil}
}
emscripten_allocator_proc :: proc(
allocator_data: rawptr,
mode: mem.Allocator_Mode,
size, alignment: int,
old_memory: rawptr,
old_size: int,
location := #caller_location,
) -> (
data: []byte,
err: mem.Allocator_Error,
) {
// These aligned alloc procs are almost indentical those in
// `_heap_allocator_proc` in `core:os`. Without the proper alignment you
// cannot use maps and simd features.
aligned_alloc :: proc(
size, alignment: int,
zero_memory: bool,
old_ptr: rawptr = nil,
) -> (
[]byte,
mem.Allocator_Error,
) {
a := max(alignment, align_of(rawptr))
space := size + a - 1
allocated_mem: rawptr
if old_ptr != nil {
original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^
allocated_mem = realloc(original_old_ptr, c.size_t(space + size_of(rawptr)))
} else if zero_memory {
// calloc automatically zeros memory, but it takes a number + size
// instead of just size.
allocated_mem = calloc(c.size_t(space + size_of(rawptr)), 1)
} else {
allocated_mem = malloc(c.size_t(space + size_of(rawptr)))
}
aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr)))
ptr := uintptr(aligned_mem)
aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a)
diff := int(aligned_ptr - ptr)
if (size + diff) > space || allocated_mem == nil {
return nil, .Out_Of_Memory
}
aligned_mem = rawptr(aligned_ptr)
mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem
return mem.byte_slice(aligned_mem, size), nil
}
aligned_free :: proc(p: rawptr) {
if p != nil {
free(mem.ptr_offset((^rawptr)(p), -1)^)
}
}
aligned_resize :: proc(
p: rawptr,
old_size: int,
new_size: int,
new_alignment: int,
) -> (
[]byte,
mem.Allocator_Error,
) {
if p == nil {
return nil, nil
}
return aligned_alloc(new_size, new_alignment, true, p)
}
switch mode {
case .Alloc:
return aligned_alloc(size, alignment, true)
case .Alloc_Non_Zeroed:
return aligned_alloc(size, alignment, false)
case .Free:
aligned_free(old_memory)
return nil, nil
case .Resize:
if old_memory == nil {
return aligned_alloc(size, alignment, true)
}
bytes := aligned_resize(old_memory, old_size, size, alignment) or_return
// realloc doesn't zero the new bytes, so we do it manually.
if size > old_size {
new_region := raw_data(bytes[old_size:])
intrinsics.mem_zero(new_region, size - old_size)
}
return bytes, nil
case .Resize_Non_Zeroed:
if old_memory == nil {
return aligned_alloc(size, alignment, false)
}
return aligned_resize(old_memory, old_size, size, alignment)
case .Query_Features:
set := (^mem.Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Free, .Resize, .Query_Features}
}
return nil, nil
case .Free_All, .Query_Info:
return nil, .Mode_Not_Implemented
}
return nil, .Mode_Not_Implemented
}

View File

@ -0,0 +1,99 @@
/*
This logger is largely a copy of the console logger in `core:log`, but it uses
emscripten's `puts` proc to write into he console of the web browser.
This is more or less identical to the logger in Aronicu's repository:
https://github.com/Aronicu/Raylib-WASM/tree/main
*/
package main_web
import "core:c"
import "core:fmt"
import "core:log"
import "core:strings"
Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line}
create_emscripten_logger :: proc(
lowest := log.Level.Debug,
opt := Emscripten_Logger_Opts,
) -> log.Logger {
return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt}
}
// This create's a binding to `puts` which will be linked in as part of the
// emscripten runtime.
@(default_calling_convention = "c")
foreign _ {
puts :: proc(buffer: cstring) -> c.int ---
}
@(private = "file")
logger_proc :: proc(
logger_data: rawptr,
level: log.Level,
text: string,
options: log.Options,
location := #caller_location,
) {
b := strings.builder_make(context.temp_allocator)
strings.write_string(&b, Level_Headers[level])
do_location_header(options, &b, location)
fmt.sbprint(&b, text)
if bc, bc_err := strings.to_cstring(&b); bc_err == nil {
puts(bc)
}
}
@(private = "file")
Level_Headers := [?]string {
0 ..< 10 = "[DEBUG] --- ",
10 ..< 20 = "[INFO ] --- ",
20 ..< 30 = "[WARN ] --- ",
30 ..< 40 = "[ERROR] --- ",
40 ..< 50 = "[FATAL] --- ",
}
@(private = "file")
do_location_header :: proc(
opts: log.Options,
buf: ^strings.Builder,
location := #caller_location,
) {
if log.Location_Header_Opts & opts == nil {
return
}
fmt.sbprint(buf, "[")
file := location.file_path
if .Short_File_Path in opts {
last := 0
for r, i in location.file_path {
if r == '/' {
last = i + 1
}
}
file = location.file_path[last:]
}
if log.Location_File_Opts & opts != nil {
fmt.sbprint(buf, file)
}
if .Line in opts {
if log.Location_File_Opts & opts != nil {
fmt.sbprint(buf, ":")
}
fmt.sbprint(buf, location.line)
}
if .Procedure in opts {
if (log.Location_File_Opts | {.Line}) & opts != nil {
fmt.sbprint(buf, ":")
}
fmt.sbprintf(buf, "%s()", location.procedure)
}
fmt.sbprint(buf, "] ")
}

View File

@ -2,45 +2,52 @@
These procs are the ones that will be called from `main_wasm.c`. These procs are the ones that will be called from `main_wasm.c`.
*/ */
#+build wasm32, wasm64p32
package main_web package main_web
import game "../game"
import "base:runtime" import "base:runtime"
import "core:c" import "core:c"
import "core:mem" import "core:mem"
import rl "libs:raylib"
import "../game"
@(private = "file") @(private = "file")
wasm_context: runtime.Context web_context: runtime.Context
// I'm not sure @thread_local works with WASM. We'll see if anyone makes a @(export)
// multi-threaded WASM game! main_start :: proc "c" () {
@(private="file")
@thread_local temp_allocator: WASM_Temp_Allocator
@export
web_init :: proc "c" () {
context = runtime.default_context() context = runtime.default_context()
context.allocator = rl.MemAllocator()
wasm_temp_allocator_init(&temp_allocator, 1*mem.Megabyte) // The WASM allocator doesn't seem to work properly in combination with
context.temp_allocator = wasm_temp_allocator(&temp_allocator) // emscripten. There is some kind of conflict with how the manage memory.
context.logger = create_wasm_logger() // So this sets up an allocator that uses emscripten's malloc.
wasm_context = context context.allocator = emscripten_allocator()
runtime.init_global_temporary_allocator(1 * mem.Megabyte)
game.game_init_window() // Since we now use js_wasm32 we should be able to remove this and use
// context.logger = log.create_console_logger(). However, that one produces
// extra newlines on web. So it's a bug in that core lib.
context.logger = create_emscripten_logger()
web_context = context
game.game_init_window({})
game.game_init() game.game_init()
} }
@export @(export)
web_update :: proc "c" () { main_update :: proc "c" () -> bool {
context = wasm_context context = web_context
game.game_update() return game.game_update()
} }
@export @(export)
web_window_size_changed :: proc "c" (w: c.int, h: c.int) { main_end :: proc "c" () {
rl.SetWindowSize(w, h) context = web_context
game.game_shutdown()
game.game_shutdown_window()
}
@(export)
web_window_size_changed :: proc "c" (w: c.int, h: c.int) {
context = web_context
// game.game_parent_window_size_changed(int(w), int(h))
} }