-
-
Save Tomarty/ce00d310647a3337e1c31614164dfdd4 to your computer and use it in GitHub Desktop.
Custom C/C++ build/CI system in C (WIP)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * This file ("zb.c") was created by Thomas Martell (Tomarty) 2026-01. Public domain. | |
| * The file generators (e.g. ninja.build, compile_commands.json, vcxproj) were implemented with the assistance of LLMs. | |
| */ | |
| /* | |
| * Zol builder: Custom zero-allocation build system with automated testing | |
| * See `zb_generate_graph` for entry points | |
| * Requires ninja and C/C++ compiler installed | |
| * Docker Desktop currently needed for compiling e.g. arm or linux presets from windows (could probably refactor to use podman or cross compile directly) | |
| * | |
| * Motivation: | |
| * CMake was slowing down builds and spamming the output. | |
| * This makes it easy to flag source files as `ZB_SRCF_HOT` so they compile with optimization in debug builds. | |
| * | |
| * Linux (cd'd into project) | |
| cc zb.c -o zb | |
| ./zb help | |
| * | |
| * Windows (e.g. x64 Native Tools Command Prompt cd'd into project): | |
| cl zb.c | |
| zb help | |
| * | |
| * Visual Studio setup: | |
| zb vcxproj windev project_name | |
| * | |
| * Notes: | |
| * Flags are hardcoded for my current project. | |
| * Works on linux but it's not yet tested with docker (it can't spawn MSVC/clang-cl containers). | |
| * Should be relatively easy to plug into distributed CI/CD systems and add new features. | |
| * Currently missing per-source/target flag support. | |
| * How flags are generated for targets is currently hardcoded for each file generator. | |
| * compile_commands.json results are not yet tested. | |
| * You can get multiple include dirs by using interface libraries as dependencies. | |
| * Target names are limited to 4-15 chars (see `ZB_TGT_NAME_MIN` / `ZB_TGT_NAME_MAX`) | |
| * For dependencies it is recommended to encapsulate and unity build them, or implement support for copying prebuilt artifacts as a custom build step. | |
| * | |
| */ | |
| #include <stdio.h> | |
| #include <stdint.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #ifdef _WIN32 | |
| #define WIN32_LEAN_AND_MEAN | |
| #define NOMINMAX | |
| #include <windows.h> | |
| #else | |
| #include <unistd.h> | |
| #include <sys/stat.h> | |
| #endif | |
| // === Core utils === | |
| #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L | |
| #define ZB_COMPILE_ASSERT(x) _Static_assert(x, #x) | |
| #else | |
| #define ZB_CONCAT_(a, b) a##b | |
| #define ZB_CONCAT(a, b) ZB_CONCAT_(a, b) | |
| #define ZB_COMPILE_ASSERT(x) typedef char ZB_CONCAT(zb_assert_, __COUNTER__)[(x) ? 1 : -1] | |
| #pragma message("Note: Compile assert errors may show as 'negative subscript'") | |
| #endif | |
| #ifdef _MSC_VER | |
| #define ZB_DEBUG_BREAK() __debugbreak() | |
| #define ZB_NORETURN __declspec(noreturn) | |
| #define ZB_RESTRICT __restrict | |
| // passing 0 is UB | |
| static inline int zb_ctz32(uint32_t x) { unsigned long idx; _BitScanForward(&idx, x); return (int)idx; } // ?: clang-cl could use the builtin | |
| // passing 0 is UB | |
| static inline int zb_clz32(uint32_t x) { unsigned long idx; _BitScanReverse(&idx, x); return 31 - (int)idx; } | |
| #else | |
| #define ZB_DEBUG_BREAK() __builtin_trap() // ?: does this diverge in behavior from `__debugbreak` (e.g. does one exit the program and the other continues?) | |
| #define ZB_NORETURN __attribute__((noreturn)) | |
| #define ZB_RESTRICT restrict | |
| #define zb_ctz32(x) __builtin_ctz(x) // passing 0 is UB | |
| #define zb_clz32(x) __builtin_clz(x) // passing 0 is UB | |
| #endif | |
| static ZB_NORETURN void zb_fatal(const char* s) { fprintf(stderr, "%s\n", s); exit(1); } | |
| static void zb_onassert(const char* expr, const char* file, int line) { fprintf(stderr, "Assertion failed: %s\nFile: %s, Line: %d", expr, file, line); } | |
| #define ZB_ASSERT(x) (void)(!!(x) || (zb_onassert(#x, __FILE__, __LINE__), ZB_DEBUG_BREAK(), exit(1), 0)) | |
| // === Config === | |
| typedef enum { ZB_PLAT_LINUX, ZB_PLAT_WINDOWS /* , ZB_PLAT_MACOS */ } zb_plat; | |
| typedef enum { ZB_COMPILER_GCC, ZB_COMPILER_CLANG, ZB_COMPILER_CLANGCL, ZB_COMPILER_MSVC } zb_compiler; | |
| typedef enum { ZB_CONFIG_DEBUG, ZB_CONFIG_RELEASE } zb_config; | |
| typedef enum { ZB_SAN_NONE /* , ZB_SAN_ASAN, ZB_SAN_UBSAN, ZB_SAN_TSAN */ } zb_san; | |
| // x86 archs ordered by feature level (SSE2 < SSE4.1 < SSE4.2 < AVX < AVX2) | |
| typedef enum { | |
| ZB_ARCH_X64_SSE2, | |
| ZB_ARCH_X64_SSE4_1, | |
| ZB_ARCH_X64_SSE4_2, | |
| ZB_ARCH_X64_AVX, | |
| ZB_ARCH_X64_AVX2_FMA, | |
| ZB_ARCH_ARM64_V8_0, | |
| ZB_ARCH_ARM64_V8_2, | |
| } zb_arch; | |
| static int zb_arch_is_x86(zb_arch a) { return a <= ZB_ARCH_X64_AVX2_FMA; } | |
| static int zb_arch_is_arm64(zb_arch a) { return a >= ZB_ARCH_ARM64_V8_0; } | |
| // === Target data === | |
| typedef uint8_t zb_tgt_flags; | |
| #define ZB_TGTF_STATIC 0 // static lib (no flags set) | |
| #define ZB_TGTF_INTERFACE 0x2 // e.g. just for headers or natvis | |
| #define ZB_TGTF_EXECUTABLE 0x4 | |
| #define ZB_TGTF_TEST 0x8 | |
| #define ZB_TGTF_LOCKED 0x10 // disallow further modifying the target during graph generation (must be greatest flag) | |
| typedef uint16_t zb_src_flags; | |
| #define ZB_SRCF_C 0 // C file (no flags set) | |
| #define ZB_SRCF_CPP 0x1 // C++ file | |
| #define ZB_SRCF_HEADER 0x2 // doesn't need to compile but may be helpful for IDE | |
| #define ZB_SRCF_HOT 0x4 // compile with optimizations on, even in debug | |
| #define ZB_SRCF_NATVIS 0x8 | |
| #define ZB_SRCF_MISC 0x10 // File that aren't included as headers or sources, but may be inlined, and should be searchable in e.g. visual studio projects. | |
| #define ZB_SRCF_X64 0x20 // x86_64 only, skip on ARM | |
| #define ZB_SRCF_ARM64 0x40 // ARM64 only, skip on x86 | |
| #define ZB_SRCF_SSE4_1 0x80 // requires at least SSE4.1 (implies X64) | |
| #define ZB_SRCF_AVX2_FMA 0x100 // requires at least AVX2+FMA (implies X64) | |
| static zb_arch zb_effective_arch(zb_arch base, zb_src_flags sf) | |
| { | |
| if ((sf & ZB_SRCF_AVX2_FMA) && base < ZB_ARCH_X64_AVX2_FMA) return ZB_ARCH_X64_AVX2_FMA; | |
| if ((sf & ZB_SRCF_SSE4_1) && base < ZB_ARCH_X64_SSE4_1) return ZB_ARCH_X64_SSE4_1; | |
| return base; | |
| } | |
| typedef uint8_t zb_dep_flags; | |
| #define ZB_DEPF_PRIVATE 0 // private (no flags set) | |
| #define ZB_DEPF_PUBLIC 0x1 // include directory passthrough | |
| #define ZB_TGT_NAME_MIN 4 // inclusive, hardcoded by ZB_TARGET | |
| #define ZB_TGT_NAME_MAX 15 // inclusive, hardcoded by ZB_TARGET | |
| #define ZB_TGT_MAX 64 // max targets, increase as needed (fits in zb_tgt_idx) | |
| typedef uint16_t zb_tgt_idx; | |
| #define ZB_SRC_MAX 1024 // max sources, increase as needed (fits in zb_src_idx, used as sentinel) | |
| typedef uint16_t zb_src_idx; | |
| #define ZB_DEP_MAX 256 // max dependencies, increase as needed (fits in zb_dep_idx, used as sentinel) | |
| typedef uint16_t zb_dep_idx; | |
| typedef struct { // todo?: 16-align | |
| char bytes[ZB_TGT_NAME_MAX + 1]; /* null terminated, all bytes are zero after the terminator */ | |
| } zb_tgt_name; | |
| typedef struct { | |
| const char* incdir_public; | |
| const char* incdir_private; | |
| } zb_tgt_incdirs; | |
| typedef struct { | |
| zb_tgt_name tgt_names[ZB_TGT_MAX]; | |
| zb_tgt_incdirs tgt_incdirs[ZB_TGT_MAX]; | |
| zb_tgt_flags tgt_flags[ZB_TGT_MAX]; | |
| zb_dep_idx tgt_dep_head[ZB_TGT_MAX]; | |
| zb_src_idx tgt_src_head[ZB_TGT_MAX]; | |
| const char* src_path[ZB_SRC_MAX]; | |
| zb_src_idx src_next[ZB_SRC_MAX]; | |
| zb_src_flags src_flags[ZB_SRC_MAX]; | |
| zb_dep_idx dep_next[ZB_DEP_MAX]; | |
| zb_dep_flags dep_flags[ZB_DEP_MAX]; | |
| zb_tgt_idx dep_target[ZB_DEP_MAX]; | |
| zb_tgt_idx tgt_count; | |
| zb_src_idx src_count; | |
| zb_dep_idx dep_count; | |
| } zb_ctx; | |
| static zb_tgt_idx zb_ctx_add_tgt_pt1_(zb_ctx* ctx) | |
| { | |
| zb_tgt_idx t = ctx->tgt_count++; | |
| if (t >= ZB_TGT_MAX) zb_fatal("too many targets"); | |
| return t; | |
| } | |
| static void zb_ctx_add_tgt_pt2_(zb_ctx* ctx, zb_tgt_idx t, zb_tgt_flags f, const char* incdir_public, const char* incdir_private) | |
| { | |
| zb_tgt_name* name = &ctx->tgt_names[t]; | |
| for (zb_tgt_idx i = 0; i != t; ++i) // N^2 | |
| if (memcmp(ctx->tgt_names[i].bytes, name->bytes, sizeof(zb_tgt_name)) == 0) { fprintf(stderr, "Target '%s' already defined\n", name->bytes); exit(1); } | |
| int badf = (f & ~(zb_tgt_flags)(ZB_TGTF_LOCKED*2ull - 1)) != 0; // flags greater than ZB_TGTF_LOCKED set (it's reasonable for interfaces to start locked) | |
| badf |= !!(f & ZB_TGTF_INTERFACE) + !!(f & ZB_TGTF_EXECUTABLE) > 1; // interface cannot be combined with executable | |
| badf |= !!(f & ZB_TGTF_TEST) & !!!(f & ZB_TGTF_EXECUTABLE); // tests must be executables (!!! to silence msvc) | |
| if (badf) { fprintf(stderr, "Target '%s' invalid flags\n", name->bytes); exit(1); } | |
| // todo?: validate that incdirs exist | |
| ctx->tgt_incdirs[t].incdir_private = incdir_private; | |
| ctx->tgt_incdirs[t].incdir_public = incdir_public; | |
| ctx->tgt_flags[t] = f; | |
| ctx->tgt_dep_head[t] = ZB_DEP_MAX; | |
| ctx->tgt_src_head[t] = ZB_SRC_MAX; | |
| } | |
| static void zb_tgt_lock_(zb_ctx* ctx, zb_tgt_idx t, int actually_lock) | |
| { | |
| if (ctx->tgt_flags[t] & ZB_TGTF_LOCKED) { fprintf(stderr, "Target '%s' is locked\n", ctx->tgt_names[t].bytes); exit(1); } | |
| if (actually_lock) ctx->tgt_flags[t] |= ZB_TGTF_LOCKED; | |
| } | |
| static void zb_ctx_add_deps_(zb_ctx* ctx, zb_tgt_idx t, zb_dep_flags flags, zb_tgt_idx* deps, size_t deps_count) | |
| { | |
| ZB_ASSERT(t < ctx->tgt_count); | |
| zb_tgt_lock_(ctx, t, 0 /* assert unlocked*/); | |
| for (size_t i = 0; i < deps_count; ++i) | |
| { | |
| ZB_ASSERT(deps[i] < ctx->tgt_count); | |
| if (t <= deps[i]) { fprintf(stderr, "Target '%s' declared before dependency '%s'\n", ctx->tgt_names[t].bytes, ctx->tgt_names[deps[i]].bytes); exit(1); } | |
| zb_dep_idx d = ctx->dep_count++; | |
| if (d >= ZB_DEP_MAX) zb_fatal("too many dependencies"); | |
| zb_dep_idx* tail = &ctx->tgt_dep_head[t]; | |
| #if 1 // Check for duplicates and append (N^2) // todo?: May not be worth it, as bits sets are used when processing. | |
| for (; *tail != ZB_DEP_MAX; tail = &ctx->dep_next[*tail]) | |
| if (ctx->dep_target[*tail] == deps[i]) { fprintf(stderr, "Target '%s' already has dependency '%s'\n", ctx->tgt_names[t].bytes, ctx->tgt_names[deps[i]].bytes); exit(1); } | |
| ctx->dep_next[d] = ZB_DEP_MAX; | |
| #else // Prepend | |
| ctx->dep_next[d] = *tail; | |
| #endif | |
| *tail = d; | |
| ctx->dep_target[d] = deps[i]; | |
| ctx->dep_flags[d] = flags; | |
| } | |
| } | |
| static void zb_ctx_add_srcs_(zb_ctx* ctx, zb_tgt_idx t, zb_src_flags flags, const char** paths, size_t paths_count) | |
| { | |
| zb_tgt_lock_(ctx, t, 0 /* assert unlocked*/); | |
| if (!(flags & (ZB_SRCF_MISC | ZB_SRCF_HEADER | ZB_SRCF_NATVIS)) && (ctx->tgt_flags[t] & ZB_TGTF_INTERFACE)) { fprintf(stderr, "Interface target '%s' may not have compiled sources\n", ctx->tgt_names[t].bytes); exit(1); } | |
| for (size_t i = 0; i < paths_count; ++i) | |
| { | |
| zb_src_idx s = ctx->src_count++; | |
| if (s >= ZB_SRC_MAX) zb_fatal("too many sources"); | |
| // todo?: validate that file exists | |
| zb_src_idx* tail = &ctx->tgt_src_head[t]; | |
| #if 1 // Check for duplicates and append (N^2) | |
| for (; *tail != ZB_SRC_MAX; tail = &ctx->src_next[*tail]) | |
| if (ctx->src_flags[*tail] == flags && strcmp(ctx->src_path[*tail], paths[i]) == 0) { fprintf(stderr, "Target '%s' already has source '%s'\n", ctx->tgt_names[t].bytes, paths[i]); exit(1); } | |
| ctx->src_next[s] = ZB_SRC_MAX; | |
| #else // Prepend | |
| ctx->src_next[s] = *tail; | |
| #endif | |
| ctx->src_path[s] = paths[i]; | |
| ctx->src_flags[s] = flags; | |
| *tail = s; | |
| } | |
| } | |
| ZB_COMPILE_ASSERT(ZB_TGT_NAME_MIN == 4 && ZB_TGT_NAME_MAX == 15); // for memcpy padding | |
| #define ZB_TARGET(/* var (zb_tgt_idx) */ t, /* zb_tgt_flags */ f, /* string literal or NULL */ incdir_public, /* string literal or NULL */ incdir_private) \ | |
| const zb_tgt_idx t = zb_ctx_add_tgt_pt1_(ctx); \ | |
| ZB_COMPILE_ASSERT((size_t)sizeof(#t) - 1 - ZB_TGT_NAME_MIN <= ZB_TGT_NAME_MAX - ZB_TGT_NAME_MIN); \ | |
| memcpy(&ctx->tgt_names[t], #t "\0\0\0\0\0\0\0\0\0\0\0", 16); /* memcpy trick replaces string literal with 128 bit stores and allows cache friendly searches */ \ | |
| zb_ctx_add_tgt_pt2_(ctx, t, f, incdir_public, incdir_private); \ | |
| #define ZB_DEPS(/* zb_tgt_idx */ t, /* zb_dep_flags */ f, /* zb_tgt_idx, < `t` */ ...) { zb_tgt_idx d__[] = {__VA_ARGS__}; zb_ctx_add_deps_(ctx, t, f, d__, sizeof(d__)/sizeof(d__[0])); } // add dependencies | |
| #define ZB_SRCS(t, /* zb_src_flags */ f, /* static string literals */ ...) { const char* p__[] = {__VA_ARGS__}; zb_ctx_add_srcs_(ctx, t, f, p__, sizeof(p__)/sizeof(p__[0])); } // add source files | |
| #define ZB_TARGET_LOCK(/* zb_tgt_idx */ t) zb_tgt_lock_(ctx, t, 1); // prevent mutation | |
| // === Graph generation === | |
| static void zb_generate_graph(zb_ctx* ctx) | |
| { | |
| ctx->tgt_count = 0; // `ctx` is uninitialized memory | |
| ctx->src_count = 0; | |
| ctx->dep_count = 0; | |
| // === Example: minimal project === | |
| // Replace this with your own targets, or use #include "my_targets_zb.inl" here | |
| // Interface target: just exposes an include directory, no compiled sources | |
| ZB_TARGET(ext_stb, ZB_TGTF_INTERFACE | ZB_TGTF_LOCKED, "ext/stb", NULL) | |
| ZB_TARGET(mylib, ZB_TGTF_STATIC, "src/mylib/inc", "src/mylib/src") | |
| ZB_DEPS(mylib, ZB_DEPF_PRIVATE, ext_stb,) | |
| ZB_SRCS(mylib, ZB_SRCF_C | ZB_SRCF_HOT, // `ZB_SRCF_HOT` always compiles with release optimization | |
| "src/mylib/src/foo.c", | |
| "src/mylib/src/bar.c", | |
| ) | |
| ZB_TARGET_LOCK(mylib) | |
| ZB_TARGET(myapp, ZB_TGTF_EXECUTABLE, NULL, "src/myapp") | |
| ZB_DEPS(myapp, ZB_DEPF_PRIVATE, mylib,) | |
| ZB_SRCS(myapp, ZB_SRCF_CPP, | |
| "src/myapp/main.cpp", | |
| ) | |
| ZB_TARGET_LOCK(myapp) | |
| ZB_TARGET(mytest, ZB_TGTF_EXECUTABLE | ZB_TGTF_TEST, NULL, NULL) | |
| ZB_DEPS(mytest, ZB_DEPF_PRIVATE, mylib,) | |
| ZB_SRCS(mytest, ZB_SRCF_CPP, | |
| "src/mylib/test/test_main.cpp", | |
| ) | |
| ZB_TARGET_LOCK(mytest) | |
| } | |
| // === File generation === | |
| #define ZB_OUT_CAP (1024 * 1024) // 1MB is likely enough | |
| typedef struct { | |
| char* buf; | |
| size_t len; | |
| } zb_out; | |
| static void zb_out_str(zb_out* o, const char* ZB_RESTRICT s, size_t n) | |
| { | |
| size_t len = o->len; | |
| size_t len2 = len + n; | |
| if (len2 > ZB_OUT_CAP) zb_fatal("output buffer overflow"); | |
| memcpy(o->buf + len, s, n); | |
| o->len = len2; | |
| } | |
| static void zb_out_c(zb_out* o, char c) | |
| { | |
| if (o->len >= ZB_OUT_CAP) zb_fatal("output buffer overflow"); | |
| o->buf[o->len++] = c; | |
| } | |
| static void zb_out_s(zb_out* o, const char* s) { zb_out_str(o, s, strlen(s)); } | |
| #define ZB_OUT_L(o, s) zb_out_str(o, "" s, sizeof(s) - 1) | |
| typedef struct { | |
| const char* name; | |
| zb_plat plat; | |
| zb_arch arch; | |
| zb_compiler compiler; | |
| zb_config config; | |
| zb_san san; | |
| } zb_cfg; | |
| #define ZB_TGT_BITSET_U32S ((ZB_TGT_MAX + 31) / 32) | |
| typedef struct { | |
| uint32_t bits[ZB_TGT_BITSET_U32S]; | |
| } zb_tgt_bitset; | |
| static inline int zb_bitset_get(zb_tgt_bitset* bs, zb_tgt_idx t) { return (bs->bits[t / 32] >> (t % 32)) & 1; } | |
| static inline void zb_bitset_set(zb_tgt_bitset* bs, zb_tgt_idx t) { bs->bits[t / 32] |= (1U << (t % 32)); } | |
| static inline void zb_bitset_clear(zb_tgt_bitset* bs, zb_tgt_idx t) { bs->bits[t / 32] &= ~(1U << (t % 32)); } | |
| // The `t` bit is set | |
| static void zb_collect_deps(zb_ctx* ctx, zb_tgt_idx t, zb_tgt_bitset* out, uint32_t always_use_private) | |
| { | |
| uint32_t depth = 1, use_private = 1; | |
| zb_tgt_idx stack[16]; | |
| zb_bitset_set(out, t); | |
| stack[0] = t; | |
| do { | |
| zb_dep_idx d = ctx->tgt_dep_head[stack[--depth]]; | |
| while (d != ZB_DEP_MAX) | |
| { | |
| if (use_private || (ctx->dep_flags[d] & ZB_DEPF_PUBLIC)) | |
| { | |
| zb_tgt_idx dep_t = ctx->dep_target[d]; | |
| if (!zb_bitset_get(out, dep_t)) | |
| { | |
| zb_bitset_set(out, dep_t); | |
| if (depth == 16) zb_fatal("dep graph too deep"); | |
| stack[depth++] = dep_t; | |
| } | |
| } | |
| d = ctx->dep_next[d]; | |
| } | |
| use_private = always_use_private; | |
| } while (depth); | |
| } | |
| // === Ninja emission === | |
| // also in `zb_emit_vcxproj` | |
| static const char* zb_archflags(zb_arch a, zb_compiler c) | |
| { | |
| switch (a) | |
| { | |
| case ZB_ARCH_X64_SSE2: | |
| return c == ZB_COMPILER_MSVC ? "/DZM_DISABLE_PERFORMANCE_WARNINGS" : "-msse2 -DZM_DISABLE_PERFORMANCE_WARNINGS"; | |
| case ZB_ARCH_X64_SSE4_1: | |
| return c == ZB_COMPILER_MSVC ? "/DZM_USE_SSE4_1" : "-msse4.1"; | |
| case ZB_ARCH_X64_SSE4_2: | |
| return c == ZB_COMPILER_MSVC ? "/DZM_USE_SSE4_2 /DZM_USE_F16C" : "-msse4.2 -mf16c"; | |
| case ZB_ARCH_X64_AVX: | |
| return c == ZB_COMPILER_MSVC ? "/arch:AVX /DZM_USE_AVX /DZM_USE_F16C" : | |
| c == ZB_COMPILER_CLANGCL ? "/arch:AVX -mf16c" : "-mavx -mf16c"; | |
| case ZB_ARCH_X64_AVX2_FMA: | |
| return c == ZB_COMPILER_MSVC ? "/arch:AVX2 /DZM_USE_AVX2 /DZM_USE_FMA /DZM_USE_F16C /DZM_USE_LZCNT /DZM_USE_TZCNT" : | |
| c == ZB_COMPILER_CLANGCL ? "/arch:AVX2 -mfma -mf16c -mlzcnt -mbmi" : "-mavx2 -mfma -mf16c -mlzcnt -mbmi"; | |
| case ZB_ARCH_ARM64_V8_0: | |
| return "-march=armv8-a"; | |
| case ZB_ARCH_ARM64_V8_2: | |
| return "-march=armv8.2-a+fp16+simd"; | |
| } | |
| return ""; | |
| } | |
| static void zb_emit_ninja(zb_ctx* ctx, const zb_cfg* cfg, zb_out* out) | |
| { | |
| zb_compiler compiler = cfg->compiler; | |
| zb_plat plat = cfg->plat; | |
| zb_arch arch = cfg->arch; | |
| int debug = cfg->config == ZB_CONFIG_DEBUG; | |
| int is_msvc_like = compiler == ZB_COMPILER_CLANGCL || compiler == ZB_COMPILER_MSVC; | |
| int is_clang = compiler == ZB_COMPILER_CLANG || compiler == ZB_COMPILER_CLANGCL; | |
| int is_windows = plat == ZB_PLAT_WINDOWS; | |
| int is_x64 = zb_arch_is_x86(arch); | |
| int is_arm64 = zb_arch_is_arm64(arch); | |
| const char* obj_ext = is_msvc_like ? ".obj" : ".o"; | |
| const char* lib_ext = is_msvc_like ? ".lib" : ".a"; | |
| const char* exe_ext = is_windows ? ".exe" : ""; | |
| const char* inc_flag = is_msvc_like ? "/I" : "-I"; | |
| // Header | |
| ZB_OUT_L(out, "ninja_required_version = 1.5\nbuilddir = out/zb/"); | |
| zb_out_s(out, cfg->name); | |
| ZB_OUT_L(out, "\n\n"); | |
| // Toolchain | |
| if (is_msvc_like) | |
| { | |
| if (is_clang) ZB_OUT_L(out, "cc = clang-cl\ncxx = clang-cl\nar = llvm-lib\n"); | |
| else ZB_OUT_L(out, "cc = cl\ncxx = cl\nar = lib\n"); | |
| ZB_OUT_L(out, "warnings = /W3\n"); | |
| if (is_clang) | |
| ZB_OUT_L(out, "compat =" | |
| " -Wno-unused-command-line-argument" | |
| "\n" | |
| ); | |
| else | |
| ZB_OUT_L(out, "compat =\n"); | |
| if (debug) ZB_OUT_L(out, "optflags = /Od /Zi /FS\nrtlib = /MDd\ndefines =\n"); | |
| else ZB_OUT_L(out, "optflags = /O2\nrtlib = /MD\ndefines = /DNDEBUG\n"); | |
| ZB_OUT_L(out, "archflags = "); | |
| zb_out_s(out, zb_archflags(arch, compiler)); | |
| zb_out_c(out, '\n'); | |
| ZB_OUT_L(out, "cflags = /nologo $warnings $compat /std:c17 $optflags $rtlib $defines\n" | |
| "cxxflags = /nologo $warnings $compat /std:c++20 /EHs-c- /GR- $optflags $rtlib $defines\n"); | |
| if (debug) | |
| ZB_OUT_L(out, "cflags_hot = /nologo $warnings $compat /std:c17 /O2 $rtlib /DNDEBUG /DZM_FORCE_DEBUG_ABI\n" | |
| "cxxflags_hot = /nologo $warnings $compat /std:c++20 /EHs-c- /GR- /O2 $rtlib /DNDEBUG /DZM_FORCE_DEBUG_ABI\n"); | |
| ZB_OUT_L(out, "\n" "rule cc\n command = $cc $cflags $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n"); | |
| if (debug) ZB_OUT_L(out, "rule cc_hot\n command = $cc $cflags_hot $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n"); | |
| ZB_OUT_L(out, "rule cxx\n command = $cxx $cxxflags $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n"); | |
| if (debug) ZB_OUT_L(out, "rule cxx_hot\n command = $cxx $cxxflags_hot $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n"); | |
| ZB_OUT_L(out, "rule ar\n command = $ar /nologo /out:$out $in\n"); | |
| if (debug) ZB_OUT_L(out, "rule link\n command = $cxx /nologo $in /Fe$out /link /DEBUG $libs\n"); | |
| else ZB_OUT_L(out, "rule link\n command = $cxx /nologo $in /Fe$out /link $libs\n"); | |
| } | |
| else | |
| { | |
| if (is_clang) ZB_OUT_L(out, "cc = clang\ncxx = clang++\nar = ar\n"); | |
| else ZB_OUT_L(out, "cc = gcc\ncxx = g++\nar = ar\n"); | |
| ZB_OUT_L(out, "warnings = -Wall\nmathflags = -fno-math-errno -fno-trapping-math\n"); | |
| if (debug) ZB_OUT_L(out, "optflags = -O0 -g\ndefines =\n"); | |
| else ZB_OUT_L(out, "optflags = -O2\ndefines = -DNDEBUG\n"); | |
| ZB_OUT_L(out, "archflags = "); | |
| zb_out_s(out, zb_archflags(arch, compiler)); | |
| zb_out_c(out, '\n'); | |
| ZB_OUT_L(out, "cflags = $warnings -std=c17 $mathflags $optflags $defines\n" | |
| "cxxflags = $warnings -std=c++20 -fno-exceptions -fno-rtti $mathflags $optflags $defines\n"); | |
| if (debug) | |
| ZB_OUT_L(out, "cflags_hot = $warnings -std=c17 $mathflags -O2 -DNDEBUG -DZM_FORCE_DEBUG_ABI\n" | |
| "cxxflags_hot = $warnings -std=c++20 -fno-exceptions -fno-rtti $mathflags -O2 -DNDEBUG -DZM_FORCE_DEBUG_ABI\n"); | |
| ZB_OUT_L(out, "\n" "rule cc\n command = $cc $cflags $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n"); | |
| if (debug) ZB_OUT_L(out, "rule cc_hot\n command = $cc $cflags_hot $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n"); | |
| ZB_OUT_L(out, "rule cxx\n command = $cxx $cxxflags $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n"); | |
| if (debug) ZB_OUT_L(out, "rule cxx_hot\n command = $cxx $cxxflags_hot $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n"); | |
| ZB_OUT_L(out, "rule ar\n command = $ar rcs $out $in\n" | |
| "rule link\n command = $cxx $in -o $out $libs\n"); | |
| } | |
| zb_out_c(out, '\n'); | |
| zb_src_flags sf_skip = ZB_SRCF_NATVIS | ZB_SRCF_MISC; | |
| sf_skip |= ZB_SRCF_HEADER; // TODO TODO?: compile headers for tests | |
| // Targets | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| const char* name = ctx->tgt_names[t].bytes; | |
| zb_tgt_flags flags = ctx->tgt_flags[t]; | |
| if (flags & ZB_TGTF_INTERFACE) continue; | |
| int is_exe = flags & ZB_TGTF_EXECUTABLE; | |
| zb_tgt_bitset inc_deps = {0}; | |
| zb_collect_deps(ctx, t, &inc_deps, 0); | |
| zb_tgt_bitset link_deps = {0}; | |
| if (is_exe) { zb_collect_deps(ctx, t, &link_deps, 1); zb_bitset_clear(&link_deps, t); } | |
| // Library/executable build edge | |
| ZB_OUT_L(out, "build $builddir/"); | |
| zb_out_s(out, is_exe ? "bin/" : "lib/"); | |
| zb_out_s(out, name); | |
| zb_out_s(out, is_exe ? exe_ext : lib_ext); | |
| zb_out_s(out, is_exe ? ": link" : ": ar"); | |
| // Object files | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| zb_src_flags sf = ctx->src_flags[s]; | |
| if (sf & sf_skip) continue; | |
| if ((sf & (ZB_SRCF_X64 | ZB_SRCF_SSE4_1 | ZB_SRCF_AVX2_FMA)) && !is_x64) continue; | |
| if ((sf & ZB_SRCF_ARM64) && !is_arm64) continue; | |
| const char* src = ctx->src_path[s]; | |
| const char* base = src; | |
| for (const char* p = src; *p; ++p) if (*p == '/' || *p == '\\') base = p + 1; | |
| const char* dot = base; | |
| for (const char* p = base; *p; ++p) if (*p == '.') dot = p; | |
| ZB_OUT_L(out, " $builddir/obj/"); | |
| zb_out_s(out, name); | |
| zb_out_c(out, '/'); | |
| zb_out_str(out, base, (size_t)(dot - base)); | |
| zb_out_s(out, obj_ext); | |
| } | |
| // Executable: implicit deps and libs | |
| if (is_exe) | |
| for (int j = 0; j < 2; ++j) | |
| { | |
| zb_out_s(out, j ? "\n libs =" : " |"); | |
| for (uint32_t i = ZB_TGT_BITSET_U32S; i--; ) | |
| { | |
| uint32_t chunk = link_deps.bits[i]; | |
| while (chunk) | |
| { | |
| int bit = 31 - zb_clz32(chunk); | |
| zb_tgt_idx d = i * 32 + bit; | |
| if (!(ctx->tgt_flags[d] & ZB_TGTF_INTERFACE)) | |
| { | |
| ZB_OUT_L(out, " $builddir/lib/"); | |
| zb_out_s(out, ctx->tgt_names[d].bytes); | |
| zb_out_s(out, lib_ext); | |
| } | |
| chunk &= ~(1u << bit); | |
| } | |
| } | |
| } | |
| ZB_OUT_L(out, "\n\n"); | |
| // Source compilation edges | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| zb_src_flags sf = ctx->src_flags[s]; | |
| if (sf & sf_skip) continue; // TODO?: compile headers for tests | |
| if ((sf & (ZB_SRCF_X64 | ZB_SRCF_SSE4_1 | ZB_SRCF_AVX2_FMA)) && !is_x64) continue; | |
| if ((sf & ZB_SRCF_ARM64) && !is_arm64) continue; | |
| const char* src = ctx->src_path[s]; | |
| const char* base = src; | |
| for (const char* p = src; *p; ++p) if (*p == '/' || *p == '\\') base = p + 1; | |
| const char* dot = base; | |
| for (const char* p = base; *p; ++p) if (*p == '.') dot = p; | |
| int is_cpp = sf & ZB_SRCF_CPP; | |
| int is_hot = debug & !!(sf & ZB_SRCF_HOT); | |
| ZB_OUT_L(out, "build $builddir/obj/"); | |
| zb_out_s(out, name); | |
| zb_out_c(out, '/'); | |
| zb_out_str(out, base, (size_t)(dot - base)); | |
| zb_out_s(out, obj_ext); | |
| ZB_OUT_L(out, ": "); | |
| zb_out_s(out, is_cpp ? (is_hot ? "cxx_hot " : "cxx ") : (is_hot ? "cc_hot " : "cc ")); | |
| zb_out_s(out, src); | |
| ZB_OUT_L(out, "\n inc ="); | |
| for (uint32_t i = 0; i < ZB_TGT_BITSET_U32S; ++i) | |
| { | |
| uint32_t chunk = inc_deps.bits[i]; | |
| while (chunk) | |
| { | |
| zb_tgt_idx d = i * 32 + zb_ctz32(chunk); | |
| const char* dir = ctx->tgt_incdirs[d].incdir_public; | |
| if (dir) { zb_out_c(out, ' '); zb_out_s(out, inc_flag); zb_out_s(out, dir); } | |
| chunk &= chunk - 1; | |
| } | |
| } | |
| const char* priv_dir = ctx->tgt_incdirs[t].incdir_private; | |
| if (priv_dir) { zb_out_c(out, ' '); zb_out_s(out, inc_flag); zb_out_s(out, priv_dir); } | |
| zb_out_c(out, '\n'); | |
| if (is_msvc_like) | |
| { | |
| ZB_OUT_L(out, " pdb = $builddir/obj/"); | |
| zb_out_s(out, name); | |
| ZB_OUT_L(out, "/"); | |
| zb_out_s(out, name); | |
| ZB_OUT_L(out, ".pdb\n"); | |
| } | |
| // Override archflags if file requires higher SIMD level than baseline | |
| zb_arch eff = zb_effective_arch(arch, sf); | |
| if (eff != arch) { ZB_OUT_L(out, " archflags = "); zb_out_s(out, zb_archflags(eff, compiler)); zb_out_c(out, '\n'); } | |
| } | |
| } | |
| // Phony targets | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| const char* name = ctx->tgt_names[t].bytes; | |
| zb_tgt_flags flags = ctx->tgt_flags[t]; | |
| if (flags & ZB_TGTF_INTERFACE) continue; | |
| int is_exe = flags & ZB_TGTF_EXECUTABLE; | |
| ZB_OUT_L(out, "build "); | |
| zb_out_s(out, name); | |
| ZB_OUT_L(out, ": phony $builddir/"); | |
| zb_out_s(out, is_exe ? "bin/" : "lib/"); | |
| zb_out_s(out, name); | |
| zb_out_s(out, is_exe ? exe_ext : lib_ext); | |
| zb_out_c(out, '\n'); | |
| } | |
| zb_out_c(out, '\n'); | |
| // === Default target === | |
| ZB_OUT_L(out, "default"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| if (ctx->tgt_flags[t] & ZB_TGTF_EXECUTABLE) { zb_out_c(out, ' '); zb_out_s(out, ctx->tgt_names[t].bytes); } | |
| zb_out_c(out, '\n'); | |
| } | |
| // === compile_commands.json emission === | |
| static void zb_emit_compiledb(zb_ctx* ctx, const zb_cfg* cfg, zb_out* out, const char* cwd) | |
| { | |
| zb_compiler compiler = cfg->compiler; | |
| zb_arch arch = cfg->arch; | |
| int debug = cfg->config == ZB_CONFIG_DEBUG; | |
| int is_msvc_like = compiler == ZB_COMPILER_CLANGCL || compiler == ZB_COMPILER_MSVC; | |
| int is_clang = compiler == ZB_COMPILER_CLANG || compiler == ZB_COMPILER_CLANGCL; | |
| int is_x64 = zb_arch_is_x86(arch); | |
| int is_arm64 = zb_arch_is_arm64(arch); | |
| const char* inc_flag = is_msvc_like ? "/I" : "-I"; | |
| const char* cc = is_msvc_like ? (is_clang ? "clang-cl" : "cl") : is_clang ? "clang" : "gcc"; | |
| const char* cxx = is_msvc_like ? cc : is_clang ? "clang++" : "g++"; | |
| ZB_OUT_L(out, "[\n"); | |
| int first = 1; | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| if (ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) continue; | |
| zb_tgt_bitset inc_deps = {0}; | |
| zb_collect_deps(ctx, t, &inc_deps, 0); | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| zb_src_flags sf = ctx->src_flags[s]; | |
| if (sf & (/*ZB_SRCF_HEADER |*/ ZB_SRCF_NATVIS | ZB_SRCF_MISC)) continue; | |
| if ((sf & (ZB_SRCF_X64 | ZB_SRCF_SSE4_1 | ZB_SRCF_AVX2_FMA)) && !is_x64) continue; | |
| if ((sf & ZB_SRCF_ARM64) && !is_arm64) continue; | |
| const char* src = ctx->src_path[s]; | |
| int is_cpp = sf & ZB_SRCF_CPP; | |
| int is_hot = debug & !!(sf & ZB_SRCF_HOT); | |
| zb_arch effective_arch = zb_effective_arch(arch, sf); | |
| if (!first) ZB_OUT_L(out, ",\n"); | |
| first = 0; | |
| ZB_OUT_L(out, " {\"directory\": \""); | |
| zb_out_s(out, cwd); | |
| ZB_OUT_L(out, "\", \"file\": \""); | |
| zb_out_s(out, src); | |
| ZB_OUT_L(out, "\", \"command\": \""); | |
| zb_out_s(out, is_cpp ? cxx : cc); | |
| // Flags | |
| if (is_msvc_like) | |
| { | |
| ZB_OUT_L(out, " /nologo /W3"); | |
| if (is_clang) ZB_OUT_L(out, " -Wno-unused-command-line-argument"); | |
| zb_out_s(out, is_cpp ? " /std:c++20 /EHs-c- /GR-" : " /std:c17"); | |
| if (is_hot) | |
| { | |
| ZB_OUT_L(out, " /O2 /DNDEBUG /DZM_FORCE_DEBUG_ABI"); | |
| zb_out_s(out, debug ? " /MDd" : " /MD"); | |
| } | |
| else if (debug) ZB_OUT_L(out, " /Od /Zi /MDd"); | |
| else ZB_OUT_L(out, " /O2 /DNDEBUG /MD"); | |
| } | |
| else | |
| { | |
| ZB_OUT_L(out, " -Wall"); | |
| zb_out_s(out, is_cpp ? " -std=c++20 -fno-exceptions -fno-rtti" : " -std=c17"); | |
| ZB_OUT_L(out, " -fno-math-errno -fno-trapping-math"); | |
| if (is_hot) ZB_OUT_L(out, " -O2 -DNDEBUG -DZM_FORCE_DEBUG_ABI"); | |
| else if (debug) ZB_OUT_L(out, " -O0 -g"); | |
| else ZB_OUT_L(out, " -O2 -DNDEBUG"); | |
| } | |
| const char* af = zb_archflags(effective_arch, compiler); | |
| if (*af) { zb_out_c(out, ' '); zb_out_s(out, af); } | |
| // Include paths | |
| for (uint32_t i = 0; i < ZB_TGT_BITSET_U32S; ++i) | |
| { | |
| uint32_t chunk = inc_deps.bits[i]; | |
| while (chunk) | |
| { | |
| zb_tgt_idx d = i * 32 + zb_ctz32(chunk); | |
| const char* dir = ctx->tgt_incdirs[d].incdir_public; | |
| if (dir) { zb_out_c(out, ' '); zb_out_s(out, inc_flag); zb_out_s(out, dir); } | |
| chunk &= chunk - 1; | |
| } | |
| } | |
| const char* priv_dir = ctx->tgt_incdirs[t].incdir_private; | |
| if (priv_dir) { zb_out_c(out, ' '); zb_out_s(out, inc_flag); zb_out_s(out, priv_dir); } | |
| // Compile flag and file | |
| zb_out_s(out, is_msvc_like ? " /c " : " -c "); | |
| zb_out_s(out, src); | |
| ZB_OUT_L(out, "\"}"); | |
| } | |
| } | |
| ZB_OUT_L(out, "\n]\n"); | |
| } | |
| // === vcxproj emission === | |
| static void zb_out_xml_escape(zb_out* o, const char* s) | |
| { | |
| for (; *s; ++s) | |
| { | |
| switch (*s) | |
| { | |
| case '&': ZB_OUT_L(o, "&"); break; | |
| case '<': ZB_OUT_L(o, "<"); break; | |
| case '>': ZB_OUT_L(o, ">"); break; | |
| case '"': ZB_OUT_L(o, """); break; | |
| case '\'': ZB_OUT_L(o, "'"); break; | |
| default: zb_out_c(o, *s); break; | |
| } | |
| } | |
| } | |
| // Convert forward slashes to backslashes for VS paths | |
| static void zb_out_winpath(zb_out* o, const char* s) { for (; *s; ++s) zb_out_c(o, *s == '/' ? '\\' : *s); } | |
| static void zb_emit_vcxproj(zb_ctx* ctx, const zb_cfg* cfg, zb_out* out, const char* project_name) | |
| { | |
| zb_compiler compiler = cfg->compiler; | |
| zb_arch arch = cfg->arch; | |
| int debug = cfg->config == ZB_CONFIG_DEBUG; | |
| int is_msvc_like = compiler == ZB_COMPILER_CLANGCL || compiler == ZB_COMPILER_MSVC; | |
| int is_x64 = zb_arch_is_x86(arch); | |
| int is_arm64 = zb_arch_is_arm64(arch); | |
| const char* platform = is_arm64 ? "ARM64" : "x64"; | |
| // XML header and project configurations (one per executable target) | |
| ZB_OUT_L(out, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" | |
| "<Project DefaultTargets=\"Build\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n" | |
| " <ItemGroup Label=\"ProjectConfigurations\">\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| if (!(ctx->tgt_flags[t] & ZB_TGTF_EXECUTABLE)) continue; | |
| const char* name = ctx->tgt_names[t].bytes; | |
| ZB_OUT_L(out, " <ProjectConfiguration Include=\""); | |
| zb_out_s(out, name); | |
| ZB_OUT_L(out, "|"); zb_out_s(out, platform); | |
| ZB_OUT_L(out, "\">\n <Configuration>"); zb_out_s(out, name); | |
| ZB_OUT_L(out, "</Configuration>\n <Platform>"); zb_out_s(out, platform); | |
| ZB_OUT_L(out, "</Platform>\n </ProjectConfiguration>\n"); | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n" | |
| " <PropertyGroup Label=\"Globals\">\n" | |
| " <VCProjectVersion>17.0</VCProjectVersion>\n" | |
| " <ProjectGuid>{12345678-1234-1234-1234-123456789ABC}</ProjectGuid>\n" | |
| " <RootNamespace>"); | |
| zb_out_xml_escape(out, project_name); | |
| ZB_OUT_L(out, "</RootNamespace>\n" | |
| " <Keyword>MakeFileProj</Keyword>\n" | |
| " </PropertyGroup>\n" | |
| " <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.Default.props\" />\n"); | |
| // Configuration type for each target | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| if (!(ctx->tgt_flags[t] & ZB_TGTF_EXECUTABLE)) continue; | |
| const char* name = ctx->tgt_names[t].bytes; | |
| ZB_OUT_L(out, " <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='"); | |
| zb_out_s(out, name); ZB_OUT_L(out, "|"); zb_out_s(out, platform); | |
| ZB_OUT_L(out, "'\" Label=\"Configuration\">\n" | |
| " <ConfigurationType>Makefile</ConfigurationType>\n" | |
| " <UseDebugLibraries>"); zb_out_s(out, debug ? "true" : "false"); | |
| ZB_OUT_L(out, "</UseDebugLibraries>\n" | |
| " <PlatformToolset>v143</PlatformToolset>\n" | |
| " </PropertyGroup>\n"); | |
| } | |
| ZB_OUT_L(out, " <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.props\" />\n" | |
| " <ImportGroup Label=\"ExtensionSettings\">\n </ImportGroup>\n" | |
| " <ImportGroup Label=\"PropertySheets\">\n" | |
| " <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" />\n" | |
| " </ImportGroup>\n" | |
| " <PropertyGroup Label=\"UserMacros\" />\n"); | |
| // NMake properties for each target | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| if (!(ctx->tgt_flags[t] & ZB_TGTF_EXECUTABLE)) continue; | |
| const char* name = ctx->tgt_names[t].bytes; | |
| ZB_OUT_L(out, " <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='"); | |
| zb_out_s(out, name); ZB_OUT_L(out, "|"); zb_out_s(out, platform); | |
| ZB_OUT_L(out, "'\">\n <NMakeBuildCommandLine>zb build "); | |
| zb_out_s(out, cfg->name); ZB_OUT_L(out, " "); zb_out_s(out, name); | |
| ZB_OUT_L(out, "</NMakeBuildCommandLine>\n" | |
| " <NMakeReBuildCommandLine>zb clean && zb ninja "); | |
| zb_out_s(out, cfg->name); | |
| ZB_OUT_L(out, " && zb build "); zb_out_s(out, cfg->name); ZB_OUT_L(out, " "); zb_out_s(out, name); | |
| ZB_OUT_L(out, "</NMakeReBuildCommandLine>\n" | |
| " <NMakeCleanCommandLine>zb clean</NMakeCleanCommandLine>\n" | |
| " <NMakeOutput>out\\zb\\"); | |
| zb_out_s(out, cfg->name); ZB_OUT_L(out, "\\bin\\"); zb_out_s(out, name); | |
| ZB_OUT_L(out, ".exe</NMakeOutput>\n" | |
| " <NMakePreprocessorDefinitions>"); | |
| zb_out_s(out, debug ? "_DEBUG" : "NDEBUG"); | |
| ZB_OUT_L(out, ";_WIN32;_WIN64"); | |
| if (is_msvc_like) | |
| { | |
| switch (arch) | |
| { | |
| case ZB_ARCH_X64_SSE2: ZB_OUT_L(out, ";ZM_DISABLE_PERFORMANCE_WARNINGS"); break; | |
| case ZB_ARCH_X64_SSE4_1: ZB_OUT_L(out, ";ZM_USE_SSE4_1"); break; | |
| case ZB_ARCH_X64_SSE4_2: ZB_OUT_L(out, ";ZM_USE_SSE4_2;ZM_USE_F16C"); break; | |
| case ZB_ARCH_X64_AVX: ZB_OUT_L(out, ";ZM_USE_AVX;ZM_USE_F16C"); break; | |
| case ZB_ARCH_X64_AVX2_FMA: ZB_OUT_L(out, ";ZM_USE_AVX2;ZM_USE_FMA;ZM_USE_F16C;ZM_USE_LZCNT;ZM_USE_TZCNT"); break; | |
| default: break; | |
| } | |
| } | |
| ZB_OUT_L(out, ";$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>\n" | |
| " <NMakeIncludeSearchPath>"); | |
| int inc_first = 1; | |
| for (zb_tgt_idx i = 0; i < ctx->tgt_count; ++i) | |
| for (int j = 0; j < 2; ++j) | |
| { | |
| const char* dir = j ? ctx->tgt_incdirs[i].incdir_private : ctx->tgt_incdirs[i].incdir_public; | |
| if (dir) { if (!inc_first) zb_out_c(out, ';'); inc_first = 0; zb_out_winpath(out, dir); } | |
| } | |
| ZB_OUT_L(out, ";$(NMakeIncludeSearchPath)</NMakeIncludeSearchPath>\n" | |
| " <AdditionalOptions>/std:c++20</AdditionalOptions>\n" | |
| " <IntDir>out\\vs\\$(Configuration)\\</IntDir>\n" | |
| " <SourcePath />\n" | |
| " <ExcludePath />\n" | |
| " </PropertyGroup>\n"); | |
| } | |
| ZB_OUT_L(out, " <ItemDefinitionGroup>\n </ItemDefinitionGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Source files | |
| { | |
| if ((ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) && ctx->tgt_src_head[t] == ZB_SRC_MAX) continue; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| zb_src_flags sf = ctx->src_flags[s]; | |
| if (sf & (ZB_SRCF_HEADER | ZB_SRCF_NATVIS | ZB_SRCF_MISC)) continue; | |
| if ((sf & (ZB_SRCF_X64 | ZB_SRCF_SSE4_1 | ZB_SRCF_AVX2_FMA)) && !is_x64) continue; | |
| if ((sf & ZB_SRCF_ARM64) && !is_arm64) continue; | |
| ZB_OUT_L(out, " <ClCompile Include=\""); zb_out_winpath(out, ctx->src_path[s]); ZB_OUT_L(out, "\" />\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Headers | |
| { | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| if (!(ctx->src_flags[s] & ZB_SRCF_HEADER)) continue; | |
| ZB_OUT_L(out, " <ClInclude Include=\""); zb_out_winpath(out, ctx->src_path[s]); ZB_OUT_L(out, "\" />\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Natvis | |
| { | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| if (!(ctx->src_flags[s] & ZB_SRCF_NATVIS)) continue; | |
| ZB_OUT_L(out, " <Natvis Include=\""); zb_out_winpath(out, ctx->src_path[s]); ZB_OUT_L(out, "\" />\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Misc | |
| { | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| if (!(ctx->src_flags[s] & ZB_SRCF_MISC)) continue; | |
| ZB_OUT_L(out, " <None Include=\""); zb_out_winpath(out, ctx->src_path[s]); ZB_OUT_L(out, "\" />\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n" | |
| " <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.targets\" />\n" | |
| " <ImportGroup Label=\"ExtensionTargets\">\n </ImportGroup>\n" | |
| "</Project>\n"); | |
| } | |
| // Extract sub-filter from path: find /src/ or /inc/ (or /src_ or /inc_), return next segment if it's a folder | |
| // Returns pointer into path and sets *len, or returns NULL if no sub-filter | |
| static const char* zb_extract_subfilter(const char* path, size_t* len) | |
| { | |
| const char* p = path; | |
| const char* found = NULL; | |
| while (*p) | |
| { | |
| if (p[0] == '/' || p[0] == '\\') | |
| { | |
| if ((p[1] == 's' && p[2] == 'r' && p[3] == 'c') || | |
| (p[1] == 'i' && p[2] == 'n' && p[3] == 'c')) | |
| { | |
| char c4 = p[4]; | |
| if (c4 == '/' || c4 == '\\' || c4 == '_') | |
| { | |
| // Find start of next segment | |
| const char* seg = p + 4; | |
| if (c4 == '_') while (*seg && *seg != '/' && *seg != '\\') ++seg; | |
| if (*seg == '/' || *seg == '\\') ++seg; | |
| if (*seg && *seg != '/' && *seg != '\\') | |
| { | |
| // Check if this is a folder (has more path after) or a file | |
| const char* end = seg; | |
| while (*end && *end != '/' && *end != '\\') ++end; | |
| if (*end) found = seg; // Only set if there's more path after (it's a folder) | |
| } | |
| } | |
| } | |
| } | |
| ++p; | |
| } | |
| if (found) | |
| { | |
| const char* end = found; | |
| while (*end && *end != '/' && *end != '\\') ++end; | |
| *len = end - found; | |
| return found; | |
| } | |
| *len = 0; | |
| return NULL; | |
| } | |
| static void zb_emit_vcxproj_filters(zb_ctx* ctx, const zb_cfg* cfg, zb_out* out) | |
| { | |
| zb_arch arch = cfg->arch; | |
| int is_x64 = zb_arch_is_x86(arch); | |
| int is_arm64 = zb_arch_is_arm64(arch); | |
| ZB_OUT_L(out, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n"); | |
| // Collect unique sub-filters per target | |
| #define ZB_MAX_SUBFILTERS 128 | |
| struct { zb_tgt_idx tgt; const char* name; size_t len; } subfilters[ZB_MAX_SUBFILTERS]; | |
| size_t subfilter_count = 0; | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| if ((ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) && ctx->tgt_src_head[t] == ZB_SRC_MAX) continue; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| size_t len; | |
| const char* sf = zb_extract_subfilter(ctx->src_path[s], &len); | |
| if (!sf) continue; | |
| int found = 0; | |
| for (size_t i = 0; i < subfilter_count; ++i) // N^2 | |
| if (subfilters[i].tgt == t && subfilters[i].len == len && memcmp(subfilters[i].name, sf, len) == 0) { found = 1; break; } | |
| if (!found && subfilter_count < ZB_MAX_SUBFILTERS) | |
| { | |
| subfilters[subfilter_count].tgt = t; | |
| subfilters[subfilter_count].name = sf; | |
| subfilters[subfilter_count++].len = len; | |
| } | |
| } | |
| } | |
| // Emit filter declarations | |
| ZB_OUT_L(out, " <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) | |
| { | |
| if ((ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) && ctx->tgt_src_head[t] == ZB_SRC_MAX) continue; | |
| ZB_OUT_L(out, " <Filter Include=\""); | |
| zb_out_s(out, ctx->tgt_names[t].bytes); | |
| ZB_OUT_L(out, "\">\n <UniqueIdentifier>{"); | |
| char guid[64]; | |
| snprintf(guid, sizeof(guid), "%08X-0000-0000-0000-%012X", (unsigned)t, (unsigned)t); | |
| zb_out_s(out, guid); | |
| ZB_OUT_L(out, "}</UniqueIdentifier>\n </Filter>\n"); | |
| } | |
| // Emit sub-filter declarations | |
| for (size_t i = 0; i < subfilter_count; ++i) | |
| { | |
| ZB_OUT_L(out, " <Filter Include=\""); | |
| zb_out_s(out, ctx->tgt_names[subfilters[i].tgt].bytes); | |
| zb_out_c(out, '\\'); | |
| zb_out_str(out, subfilters[i].name, subfilters[i].len); | |
| ZB_OUT_L(out, "\">\n <UniqueIdentifier>{"); | |
| char guid[64]; | |
| snprintf(guid, sizeof(guid), "%08X-0000-0000-%04X-%012X", (unsigned)subfilters[i].tgt, (unsigned)i, (unsigned)i); | |
| zb_out_s(out, guid); | |
| ZB_OUT_L(out, "}</UniqueIdentifier>\n </Filter>\n"); | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| // Helper macro to emit filter path (target or target\subfilter) | |
| #define EMIT_FILTER_PATH(tgt_name, path) do { \ | |
| size_t sf_len; \ | |
| const char* sf = zb_extract_subfilter(path, &sf_len); \ | |
| zb_out_s(out, tgt_name); \ | |
| if (sf) { zb_out_c(out, '\\'); zb_out_str(out, sf, sf_len); } \ | |
| } while (0) | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Source files with filters | |
| { | |
| if ((ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) && ctx->tgt_src_head[t] == ZB_SRC_MAX) continue; | |
| const char* tgt_name = ctx->tgt_names[t].bytes; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| zb_src_flags sf = ctx->src_flags[s]; | |
| if (sf & (ZB_SRCF_HEADER | ZB_SRCF_NATVIS | ZB_SRCF_MISC)) continue; | |
| if ((sf & (ZB_SRCF_X64 | ZB_SRCF_SSE4_1 | ZB_SRCF_AVX2_FMA)) && !is_x64) continue; | |
| if ((sf & ZB_SRCF_ARM64) && !is_arm64) continue; | |
| ZB_OUT_L(out, " <ClCompile Include=\""); | |
| zb_out_winpath(out, ctx->src_path[s]); | |
| ZB_OUT_L(out, "\">\n <Filter>"); | |
| EMIT_FILTER_PATH(tgt_name, ctx->src_path[s]); | |
| ZB_OUT_L(out, "</Filter>\n </ClCompile>\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Headers with filters | |
| { | |
| const char* tgt_name = ctx->tgt_names[t].bytes; | |
| int has_headers = 0; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| if (ctx->src_flags[s] & ZB_SRCF_HEADER) { has_headers = 1; break; } | |
| if (!has_headers) continue; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| if (!(ctx->src_flags[s] & ZB_SRCF_HEADER)) continue; | |
| ZB_OUT_L(out, " <ClInclude Include=\""); | |
| zb_out_winpath(out, ctx->src_path[s]); | |
| ZB_OUT_L(out, "\">\n <Filter>"); | |
| EMIT_FILTER_PATH(tgt_name, ctx->src_path[s]); | |
| ZB_OUT_L(out, "</Filter>\n </ClInclude>\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Natvis with filters | |
| { | |
| const char* tgt_name = ctx->tgt_names[t].bytes; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| if (!(ctx->src_flags[s] & ZB_SRCF_NATVIS)) continue; | |
| ZB_OUT_L(out, " <Natvis Include=\""); | |
| zb_out_winpath(out, ctx->src_path[s]); | |
| ZB_OUT_L(out, "\">\n <Filter>"); | |
| EMIT_FILTER_PATH(tgt_name, ctx->src_path[s]); | |
| ZB_OUT_L(out, "</Filter>\n </Natvis>\n"); | |
| } | |
| } | |
| ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n"); | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count; ++t) // Misc with filters | |
| { | |
| const char* tgt_name = ctx->tgt_names[t].bytes; | |
| for (zb_src_idx s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s]) | |
| { | |
| if (!(ctx->src_flags[s] & ZB_SRCF_MISC)) continue; | |
| ZB_OUT_L(out, " <None Include=\""); | |
| zb_out_winpath(out, ctx->src_path[s]); | |
| ZB_OUT_L(out, "\">\n <Filter>"); | |
| EMIT_FILTER_PATH(tgt_name, ctx->src_path[s]); | |
| ZB_OUT_L(out, "</Filter>\n </None>\n"); | |
| } | |
| } | |
| #undef EMIT_FILTER_PATH | |
| #undef ZB_MAX_SUBFILTERS | |
| ZB_OUT_L(out, " </ItemGroup>\n</Project>\n"); | |
| } | |
| static void zb_emit_sln(zb_out* out, const char* project_name) | |
| { | |
| ZB_OUT_L(out, "\xef\xbb\xbf" // UTF-8 BOM | |
| "Microsoft Visual Studio Solution File, Format Version 12.00\n" | |
| "# Visual Studio Version 17\n" | |
| "VisualStudioVersion = 17.0.31903.59\n" | |
| "MinimumVisualStudioVersion = 10.0.40219.1\n" | |
| "Project(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = \""); | |
| zb_out_s(out, project_name); | |
| ZB_OUT_L(out, "\", \""); | |
| zb_out_s(out, project_name); | |
| ZB_OUT_L(out, ".vcxproj\", \"{12345678-1234-1234-1234-123456789ABC}\"\nEndProject\nGlobal\nEndGlobal\n"); | |
| } | |
| // === Presets === | |
| static const zb_cfg zb_presets[] = { | |
| // TODO: implement asan/ubsan | |
| {"windev", ZB_PLAT_WINDOWS, ZB_ARCH_X64_AVX2_FMA, ZB_COMPILER_MSVC, ZB_CONFIG_DEBUG, ZB_SAN_NONE}, | |
| {"sse42-clangcl-rel", ZB_PLAT_WINDOWS, ZB_ARCH_X64_SSE4_2, ZB_COMPILER_CLANGCL, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"sse42-msvc-rel", ZB_PLAT_WINDOWS, ZB_ARCH_X64_SSE4_2, ZB_COMPILER_MSVC, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"linuxdev", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA, ZB_COMPILER_CLANG, ZB_CONFIG_DEBUG, ZB_SAN_NONE}, | |
| {"sse42-clang-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_SSE4_2, ZB_COMPILER_CLANG, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"sse42-gcc-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_SSE4_2, ZB_COMPILER_GCC, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| // Linux x64 validation | |
| {"sse2-clang-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_SSE2, ZB_COMPILER_CLANG, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"sse2-gcc-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_SSE2, ZB_COMPILER_GCC, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"avx-clang-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX, ZB_COMPILER_CLANG, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"avx-gcc-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX, ZB_COMPILER_GCC, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| // ARM64 validation | |
| #if 0 | |
| {"armv82-clang-dbg", ZB_PLAT_LINUX, ZB_ARCH_ARM64_V8_2, ZB_COMPILER_CLANG, ZB_CONFIG_DEBUG, ZB_SAN_NONE}, | |
| {"armv82-clang-rel", ZB_PLAT_LINUX, ZB_ARCH_ARM64_V8_2, ZB_COMPILER_CLANG, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| {"armv82-gcc-dbg", ZB_PLAT_LINUX, ZB_ARCH_ARM64_V8_2, ZB_COMPILER_GCC, ZB_CONFIG_DEBUG, ZB_SAN_NONE}, | |
| #endif | |
| {"armv82-gcc-rel", ZB_PLAT_LINUX, ZB_ARCH_ARM64_V8_2, ZB_COMPILER_GCC, ZB_CONFIG_RELEASE, ZB_SAN_NONE}, | |
| }; | |
| #define ZB_PRESET_COUNT (sizeof(zb_presets) / sizeof(zb_presets[0])) | |
| static const zb_cfg* zb_find_preset(const char* name) | |
| { | |
| for (size_t i = 0; i < ZB_PRESET_COUNT; ++i) | |
| if (strcmp(zb_presets[i].name, name) == 0) | |
| return &zb_presets[i]; | |
| return NULL; | |
| } | |
| static int zb_cfg_is_native(const zb_cfg* p) | |
| { | |
| #ifdef _WIN32 | |
| return p->plat == ZB_PLAT_WINDOWS; | |
| #elif defined(__aarch64__) | |
| return p->plat == ZB_PLAT_LINUX && zb_arch_is_arm64(p->arch); | |
| #else | |
| return p->plat == ZB_PLAT_LINUX && zb_arch_is_x86(p->arch); | |
| #endif | |
| } | |
| // === System utils === | |
| static void zb_mkdir_for_file(const char* path) | |
| { | |
| char buf[512]; | |
| size_t len = 0; | |
| for (const char* p = path; *p && len < sizeof(buf) - 1; ++p) | |
| { | |
| buf[len++] = *p; | |
| if (*p == '/' || *p == '\\') | |
| { | |
| buf[len] = 0; | |
| #ifdef _WIN32 | |
| CreateDirectoryA(buf, NULL); | |
| #else | |
| mkdir(buf, 0755); | |
| #endif | |
| } | |
| } | |
| } | |
| static void zb_write_file(const char* path, const char* data, size_t len) | |
| { | |
| zb_mkdir_for_file(path); | |
| FILE* f = fopen(path, "wb"); | |
| if (!f) { fprintf(stderr, "failed to open %s\n", path); exit(1); } | |
| if (fwrite(data, 1, len, f) != len) { fclose(f); fprintf(stderr, "failed to write %s\n", path); exit(1); } | |
| if (fclose(f) != 0) { fprintf(stderr, "failed to close %s\n", path); exit(1); } | |
| } | |
| // === Docker === | |
| static int zb_docker_image_exists(const char* image) | |
| { | |
| char cmd[512]; | |
| #ifdef _WIN32 | |
| snprintf(cmd, sizeof(cmd), "docker image inspect %s >nul 2>&1", image); | |
| #else | |
| snprintf(cmd, sizeof(cmd), "docker image inspect %s >/dev/null 2>&1", image); | |
| #endif | |
| return system(cmd) == 0; | |
| } | |
| static int zb_docker_build_image(const char* image, const char* platform) | |
| { | |
| printf("building docker image: %s (%s)\n", image, platform); | |
| const char* dockerfile = "_zb_dockerfile.tmp"; | |
| FILE* f = fopen(dockerfile, "w"); | |
| if (!f) { fprintf(stderr, "failed to create dockerfile\n"); return 0; } | |
| fprintf(f, | |
| "FROM ubuntu:24.04\n" | |
| "RUN apt-get update && apt-get install -y --no-install-recommends \\\n" | |
| " clang lld g++ ninja-build libvulkan-dev libxcb1-dev \\\n" | |
| " && rm -rf /var/lib/apt/lists/*\n"); | |
| fclose(f); | |
| char cmd[512]; | |
| snprintf(cmd, sizeof(cmd), "docker build --platform %s -t %s -f %s .", platform, image, dockerfile); | |
| int rc = system(cmd); | |
| remove(dockerfile); | |
| return rc == 0; | |
| } | |
| static const char* zb_docker_image(zb_arch a) { return zb_arch_is_arm64(a) ? "zb:arm64" : "zb:x64"; } | |
| static const char* zb_docker_platform(zb_arch a) { return zb_arch_is_arm64(a) ? "linux/arm64" : "linux/amd64"; } | |
| static int zb_ensure_docker_image(const zb_cfg* cfg) | |
| { | |
| const char* image = zb_docker_image(cfg->arch); | |
| if (zb_docker_image_exists(image)) return 1; | |
| return zb_docker_build_image(image, zb_docker_platform(cfg->arch)); | |
| } | |
| static int zb_run_in_docker(const zb_cfg* cfg, zb_ctx* ctx, char* out_buf) | |
| { | |
| if (!zb_ensure_docker_image(cfg)) return 1; | |
| const char* image = zb_docker_image(cfg->arch); | |
| const char* platform = zb_docker_platform(cfg->arch); | |
| char cwd[512]; | |
| #ifdef _WIN32 | |
| GetCurrentDirectoryA(sizeof(cwd), cwd); | |
| for (char* c = cwd; *c; ++c) if (*c == '\\') *c = '/'; | |
| #else | |
| if (!getcwd(cwd, sizeof(cwd))) { cwd[0] = '.'; cwd[1] = 0; } | |
| #endif | |
| // Generate ninja file | |
| zb_out out = {out_buf, 0}; | |
| zb_emit_ninja(ctx, cfg, &out); | |
| char path[512]; | |
| snprintf(path, sizeof(path), "out/zb/%s/build.ninja", cfg->name); | |
| zb_write_file(path, out.buf, out.len); | |
| // Build in docker | |
| char cmd[1024]; | |
| snprintf(cmd, sizeof(cmd), | |
| "docker run --rm --platform %s -v \"%s:/src\" -w /src %s ninja -f out/zb/%s/build.ninja", | |
| platform, cwd, image, cfg->name); | |
| printf(">>> %s\n", cmd); | |
| int rc = system(cmd); | |
| if (rc != 0) return rc; | |
| // Run tests in docker | |
| for (zb_tgt_idx t = 0; t < ctx->tgt_count && rc == 0; ++t) | |
| { | |
| if (!(ctx->tgt_flags[t] & ZB_TGTF_TEST)) continue; | |
| snprintf(cmd, sizeof(cmd), "docker run --rm --platform %s -v \"%s:/src\" -w /src %s out/zb/%s/bin/%s", platform, cwd, image, cfg->name, ctx->tgt_names[t].bytes); | |
| printf(">>> %s\n", cmd); | |
| rc = system(cmd); | |
| } | |
| return rc; | |
| } | |
| // === Main === | |
| int main(int argc, char** argv) | |
| { | |
| const char* action = argc < 2 ? "help" : argv[1]; | |
| int ret = 0; | |
| int arg_i = 2; | |
| static char out_buf[ZB_OUT_CAP]; | |
| static zb_ctx ctx; | |
| if (strcmp(action, "ninja") == 0) | |
| { | |
| const char* preset_name = arg_i < argc ? argv[arg_i++] : NULL; | |
| if (arg_i < argc) goto bad_arg; | |
| zb_generate_graph(&ctx); | |
| char path[512]; | |
| zb_out out; | |
| out.buf = out_buf; | |
| if (preset_name) | |
| { | |
| const zb_cfg* cfg = zb_find_preset(preset_name); | |
| if (!cfg) { fprintf(stderr, "unknown preset: %s\n", preset_name); return 1; } | |
| out.len = 0; | |
| zb_emit_ninja(&ctx, cfg, &out); | |
| snprintf(path, sizeof(path), "out/zb/%s/build.ninja", cfg->name); | |
| zb_write_file(path, out.buf, out.len); | |
| printf("wrote %s (%zu bytes)\n", path, out.len); | |
| } | |
| else | |
| for (size_t i = 0; i < ZB_PRESET_COUNT; ++i) | |
| { | |
| const zb_cfg* cfg = &zb_presets[i]; | |
| if (!zb_cfg_is_native(cfg)) continue; | |
| out.len = 0; | |
| zb_emit_ninja(&ctx, cfg, &out); | |
| snprintf(path, sizeof(path), "out/zb/%s/build.ninja", cfg->name); | |
| zb_write_file(path, out.buf, out.len); | |
| printf("wrote %s (%zu bytes)\n", path, out.len); | |
| } | |
| } | |
| else if (strcmp(action, "compiledb") == 0) | |
| { | |
| if (arg_i >= argc) goto missing_arg; | |
| const char* preset_name = argv[arg_i++]; | |
| if (arg_i < argc) goto bad_arg; | |
| const zb_cfg* cfg = zb_find_preset(preset_name); | |
| if (!cfg) { fprintf(stderr, "unknown preset: %s\n", preset_name); return 1; } | |
| zb_generate_graph(&ctx); | |
| char cwd[512]; | |
| #ifdef _WIN32 | |
| GetCurrentDirectoryA(sizeof(cwd), cwd); | |
| for (char* c = cwd; *c; ++c) if (*c == '\\') *c = '/'; | |
| #else | |
| if (!getcwd(cwd, sizeof(cwd))) { cwd[0] = '.'; cwd[1] = 0; } | |
| #endif | |
| zb_out out = {out_buf, 0}; | |
| zb_emit_compiledb(&ctx, cfg, &out, cwd); | |
| zb_write_file("compile_commands.json", out.buf, out.len); | |
| printf("wrote compile_commands.json (%zu bytes)\n", out.len); | |
| } | |
| else if (strcmp(action, "vcxproj") == 0) | |
| { | |
| if (arg_i >= argc) goto missing_arg; | |
| const char* preset_name = argv[arg_i++]; | |
| if (arg_i >= argc) goto missing_arg; | |
| const char* project_name = argv[arg_i++]; | |
| if (arg_i < argc) goto bad_arg; | |
| const zb_cfg* cfg = zb_find_preset(preset_name); | |
| if (!cfg) { fprintf(stderr, "unknown preset: %s\n", preset_name); return 1; } | |
| if (cfg->plat != ZB_PLAT_WINDOWS) { fprintf(stderr, "vcxproj requires a Windows preset\n"); return 1; } | |
| zb_generate_graph(&ctx); | |
| zb_out out; | |
| out.buf = out_buf; | |
| char path[512]; | |
| // Generate ninja | |
| out.len = 0; | |
| zb_emit_ninja(&ctx, cfg, &out); | |
| snprintf(path, sizeof(path), "out/zb/%s/build.ninja", cfg->name); | |
| zb_write_file(path, out.buf, out.len); | |
| printf("wrote %s (%zu bytes)\n", path, out.len); | |
| // Generate .vcxproj | |
| out.len = 0; | |
| zb_emit_vcxproj(&ctx, cfg, &out, project_name); | |
| snprintf(path, sizeof(path), "%s.vcxproj", project_name); | |
| zb_write_file(path, out.buf, out.len); | |
| printf("wrote %s (%zu bytes)\n", path, out.len); | |
| // Generate .vcxproj.filters | |
| out.len = 0; | |
| zb_emit_vcxproj_filters(&ctx, cfg, &out); | |
| snprintf(path, sizeof(path), "%s.vcxproj.filters", project_name); | |
| zb_write_file(path, out.buf, out.len); | |
| printf("wrote %s (%zu bytes)\n", path, out.len); | |
| // Generate .sln (so it doesn't prompt you to save it ...?) | |
| out.len = 0; | |
| zb_emit_sln(&out, project_name); | |
| snprintf(path, sizeof(path), "%s.sln", project_name); | |
| zb_write_file(path, out.buf, out.len); | |
| printf("wrote %s (%zu bytes)\n", path, out.len); | |
| } | |
| else if (strcmp(action, "build") == 0) // TODO: Regenerate ninja and use docker if needed | |
| { | |
| if (arg_i >= argc) goto missing_arg; | |
| const char* preset_name = argv[arg_i++]; | |
| const char* target = arg_i < argc ? argv[arg_i++] : NULL; | |
| if (arg_i < argc) goto bad_arg; | |
| const zb_cfg* cfg = zb_find_preset(preset_name); | |
| if (!cfg) { fprintf(stderr, "unknown preset: %s\n", preset_name); return 1; } | |
| char cmd[512]; | |
| if (target) snprintf(cmd, sizeof(cmd), "ninja -f out/zb/%s/build.ninja %s", cfg->name, target); | |
| else snprintf(cmd, sizeof(cmd), "ninja -f out/zb/%s/build.ninja", cfg->name); | |
| ret = system(cmd); | |
| } | |
| else if (strcmp(action, "run") == 0) | |
| { | |
| if (arg_i >= argc) goto missing_arg; | |
| const char* preset_name = argv[arg_i++]; | |
| if (arg_i >= argc) goto missing_arg; | |
| const char* target = argv[arg_i++]; | |
| if (arg_i < argc) goto bad_arg; | |
| const zb_cfg* cfg = zb_find_preset(preset_name); | |
| if (!cfg) { fprintf(stderr, "unknown preset: %s\n", preset_name); return 1; } | |
| char cmd[512]; | |
| // Build first | |
| snprintf(cmd, sizeof(cmd), "ninja -f out/zb/%s/build.ninja %s", cfg->name, target); | |
| ret = system(cmd); | |
| if (ret != 0) goto done; | |
| const char* ext = cfg->plat == ZB_PLAT_WINDOWS ? ".exe" : ""; | |
| #if 0 // Launch debugger | |
| #ifdef _WIN32 // (assumes raddb.exe is in the project dir) | |
| snprintf(cmd, sizeof(cmd), "raddbg out\\zb\\%s\\bin\\%s%s", cfg->name, target, ext); | |
| #else | |
| snprintf(cmd, sizeof(cmd), "gdb ./out/zb/%s/bin/%s%s", cfg->name, target, ext); | |
| #endif | |
| #else // Run | |
| #ifdef _WIN32 | |
| snprintf(cmd, sizeof(cmd), "out\\zb\\%s\\bin\\%s%s", cfg->name, target, ext); | |
| #else | |
| snprintf(cmd, sizeof(cmd), "./out/zb/%s/bin/%s%s", cfg->name, target, ext); | |
| #endif | |
| #endif | |
| ret = system(cmd); | |
| } | |
| else if (strcmp(action, "presets") == 0) | |
| { | |
| if (arg_i < argc) goto bad_arg; | |
| printf("Presets:\n"); | |
| for (size_t i = 0; i < ZB_PRESET_COUNT; ++i) | |
| { | |
| const zb_cfg* cfg = &zb_presets[i]; | |
| printf(" %-20s %s\n", cfg->name, zb_cfg_is_native(cfg) ? "" : "[cross]"); | |
| } | |
| } | |
| else if (strcmp(action, "targets") == 0) | |
| { | |
| if (arg_i < argc) goto bad_arg; | |
| zb_generate_graph(&ctx); | |
| printf("Targets:\n"); | |
| for (zb_tgt_idx t = 0; t < ctx.tgt_count; ++t) | |
| { | |
| zb_tgt_flags flags = ctx.tgt_flags[t]; | |
| const char* type; | |
| if (flags & ZB_TGTF_INTERFACE) continue; //type = "interface"; | |
| else if (flags & ZB_TGTF_TEST) type = "test"; | |
| else if (flags & ZB_TGTF_EXECUTABLE) type = "executable"; | |
| else type = "library"; | |
| printf(" %-16s%s\n", ctx.tgt_names[t].bytes, type); | |
| } | |
| } | |
| else if (strcmp(action, "clean") == 0) | |
| { | |
| if (arg_i < argc) goto bad_arg; | |
| #ifdef _WIN32 | |
| system("rmdir /s /q out\\zb 2>nul"); | |
| #else | |
| system("rm -rf out/zb"); | |
| #endif | |
| } | |
| else if (strcmp(action, "test") == 0) | |
| { | |
| const char* preset_filter = arg_i < argc ? argv[arg_i++] : NULL; | |
| const char* test_filter = arg_i < argc ? argv[arg_i++] : NULL; | |
| if (arg_i < argc) goto bad_arg; | |
| zb_generate_graph(&ctx); | |
| const char* failed[ZB_PRESET_COUNT]; | |
| size_t failed_count = 0; | |
| size_t run_count = 0; | |
| for (size_t i = 0; i < ZB_PRESET_COUNT; ++i) | |
| { | |
| const zb_cfg* cfg = &zb_presets[i]; | |
| if (preset_filter && strcmp(cfg->name, preset_filter) != 0) continue; | |
| printf("\n========== %s ==========\n", cfg->name); | |
| int rc; | |
| if (!zb_cfg_is_native(cfg)) | |
| rc = zb_run_in_docker(cfg, &ctx, out_buf); | |
| else | |
| { | |
| zb_out out = {out_buf, 0}; | |
| zb_emit_ninja(&ctx, cfg, &out); | |
| char path[512]; | |
| snprintf(path, sizeof(path), "out/zb/%s/build.ninja", cfg->name); | |
| zb_write_file(path, out.buf, out.len); | |
| char cmd[512]; | |
| snprintf(cmd, sizeof(cmd), "ninja -f %s", path); | |
| rc = system(cmd); | |
| if (rc == 0) | |
| { | |
| const char* ext = cfg->plat == ZB_PLAT_WINDOWS ? ".exe" : ""; | |
| for (zb_tgt_idx t = 0; t < ctx.tgt_count && rc == 0; ++t) | |
| { | |
| if (!(ctx.tgt_flags[t] & ZB_TGTF_TEST)) continue; | |
| const char* name = ctx.tgt_names[t].bytes; | |
| if (test_filter && strcmp(name, test_filter) != 0) continue; | |
| #ifdef _WIN32 | |
| snprintf(cmd, sizeof(cmd), "out\\zb\\%s\\bin\\%s%s", cfg->name, name, ext); | |
| #else | |
| snprintf(cmd, sizeof(cmd), "out/zb/%s/bin/%s%s", cfg->name, name, ext); | |
| #endif | |
| printf(">>> %s\n", cmd); | |
| rc = system(cmd); | |
| } | |
| } | |
| } | |
| ++run_count; | |
| if (rc != 0) | |
| { | |
| failed[failed_count++] = cfg->name; | |
| printf(">>> %s: FAIL\n", cfg->name); | |
| } | |
| else | |
| printf(">>> %s: PASS\n", cfg->name); | |
| } | |
| printf("\n========== SUMMARY ==========\n"); | |
| printf("%zu/%zu passed\n", run_count - failed_count, run_count); | |
| if (failed_count > 0) | |
| { | |
| printf("\nFailed:\n"); | |
| for (size_t i = 0; i < failed_count; ++i) | |
| printf(" - %s\n", failed[i]); | |
| ret = 1; | |
| } | |
| } | |
| else | |
| { | |
| if (arg_i < argc) goto bad_arg; | |
| ret = strcmp(action, "help") == 0 ? 0 : 1; | |
| if (ret) fprintf(stderr, "Unknown command: %s\n\n", action); | |
| goto help; | |
| } | |
| goto done; | |
| missing_arg: | |
| fprintf(stderr, "Missing option\n"); | |
| ret = 1; | |
| goto help; | |
| bad_arg: | |
| fprintf(stderr, "Unknown option: %s\n", argv[arg_i]); | |
| ret = 1; | |
| help: | |
| printf("Usage: %s <command> [options]\n\n", *argv); | |
| printf("Commands:\n"); | |
| printf(" presets List all presets\n"); | |
| printf(" targets List all targets\n"); | |
| printf(" clean Remove build artifacts\n"); | |
| printf(" help Show this help\n"); | |
| printf(" ninja [preset] Generate build.ninja (all native if no preset)\n"); | |
| printf(" run <preset> <target> Build and launch debugger (use ninja command first)\n"); | |
| printf(" build <preset> [target] Build (all targets if none specified) (use ninja command first)\n"); | |
| printf(" compiledb [preset] Generate compile_commands.json\n"); | |
| printf(" vcxproj <preset> <name> Generate Visual Studio project and build.ninja\n"); | |
| printf(" test <preset> Build and run tests\n"); | |
| printf(" ci Build and test all presets\n"); | |
| done: | |
| return ret; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment