Skip to content

Instantly share code, notes, and snippets.

@Tomarty
Last active January 14, 2026 19:05
Show Gist options
  • Select an option

  • Save Tomarty/ce00d310647a3337e1c31614164dfdd4 to your computer and use it in GitHub Desktop.

Select an option

Save Tomarty/ce00d310647a3337e1c31614164dfdd4 to your computer and use it in GitHub Desktop.
Custom C/C++ build/CI system in C (WIP)
/*
* 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, "&amp;"); break;
case '<': ZB_OUT_L(o, "&lt;"); break;
case '>': ZB_OUT_L(o, "&gt;"); break;
case '"': ZB_OUT_L(o, "&quot;"); break;
case '\'': ZB_OUT_L(o, "&apos;"); 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 &amp;&amp; zb ninja ");
zb_out_s(out, cfg->name);
ZB_OUT_L(out, " &amp;&amp; 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