Skip to content

Instantly share code, notes, and snippets.

@Tomarty
Last active February 15, 2026 00:16
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) in 2026. Public domain.
* The file generators (e.g. ninja.build, compile_commands.json, vcxproj) were created using LLMs.
*/
/*
* Custom zero-allocation build system with automated tests using docker/podman containers
* 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
*
* Podman and ARM emulated testing setup:
* 1. Install Podman CLI and run 'podman machine init'
* 2. After booting, run 'zb podman' (starts the podman machine and sets up QEMU)
* 3. Run 'zb test' to compile all presets/targets and run tests
*
* Notes:
* See `zb_generate_graph` for project setup.
* Warnings are currently very strict, with exceptions disabled, etc. See 'Compiler Flags' section.
* Clang/GCC use '-fno-math-errno', but functions like `sqrtf` still branch on MSVC.
* (MSVC's undocumented `__sqrt_ss` intrinsic in "immintrin.h" is recommended in place of 'sqrtf').
* Works on linux but not yet tested with docker/podman there. It can't spawn MSVC/clang-cl containers.
* Edit the `ZB_DOCKER_CMD` define if you need docker instead of podman.
* Currently missing per-source/target flag support. With unity built dependencies it's not too much of an issue.
* 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`)
*/
#ifdef __GNUC__
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
#define _POSIX_C_SOURCE 200809L
#endif
#pragma GCC optimize("Os") // optimize for code size (likely to build faster)
#elif defined(_MSC_VER)
#pragma optimize("s", on)
#endif
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef __cplusplus
#define ZB_C extern "C"
#else
#define ZB_C
#endif
#if defined(_MSC_VER) || defined(__cplusplus)
#define ZB_RESTRICT __restrict
#else
#define ZB_RESTRICT restrict
#endif
#if defined(_MSC_VER) || (defined(__cplusplus) && __cplusplus >= 201103L)
#define ZB_COMPILE_ASSERT(x) static_assert(x, #x)
#elif 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
ZB_COMPILE_ASSERT(sizeof(int) >= 4 && sizeof(char) == 1);
#ifdef _WIN32
#ifdef _MSC_VER // fast compile
typedef unsigned long DWORD;
typedef int BOOL;
typedef union { struct { unsigned long LowPart; long HighPart; }; long long QuadPart; } LARGE_INTEGER;
ZB_C __declspec(dllimport) DWORD __stdcall GetFileAttributesA(const char*);
ZB_C __declspec(dllimport) BOOL __stdcall CreateDirectoryA(const char*, void*);
ZB_C __declspec(dllimport) DWORD __stdcall GetCurrentDirectoryA(DWORD, char*);
ZB_C __declspec(dllimport) BOOL __stdcall QueryPerformanceCounter(LARGE_INTEGER*);
ZB_C __declspec(dllimport) BOOL __stdcall QueryPerformanceFrequency(LARGE_INTEGER*);
#define INVALID_FILE_ATTRIBUTES ((DWORD)-1)
#define FILE_ATTRIBUTE_DIRECTORY 0x10
#else
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#endif
#else
#include <unistd.h>
#include <sys/stat.h>
#endif
#if defined(__GNUC__) || defined(__clang__) // gcc, clang, clang-cl
#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_ALIGN(n) __attribute__((__aligned__(n)))
#define zb_ctz(x) __builtin_ctz(x) // passing 0 is UB
#define zb_clz(x) __builtin_clz(x) // passing 0 is UB
#elif defined(_MSC_VER) // msvc
#define ZB_DEBUG_BREAK() __debugbreak()
#define ZB_NORETURN __declspec(noreturn)
#define ZB_ALIGN(n) __declspec(align(n))
ZB_C unsigned char _BitScanReverse(unsigned long*, unsigned long);
ZB_C unsigned char _BitScanForward(unsigned long*, unsigned long);
#pragma intrinsic(_BitScanReverse, _BitScanForward)
// passing 0 is UB
static inline int zb_ctz(uint32_t x) { unsigned long idx; _BitScanForward(&idx, x); return (int)idx; }
// passing 0 is UB
static inline int zb_clz(uint32_t x) { unsigned long idx; _BitScanReverse(&idx, x); return 31 - (int)idx; }
#else
#error Unexpected compiler
#endif
static ZB_NORETURN void zb_fatal(const char* s) { fprintf(stderr, "%s\n", s); exit(1); }
static void zb_logassert(const char* expr, const char* file, int line) { fprintf(stderr, "Assertion failed: %s\nFile: %s, Line: %d\n", expr, file, line); }
#define ZB_ASSERT(x) (void)(!!(x) || (zb_logassert(#x, __FILE__, __LINE__), ZB_DEBUG_BREAK(), exit(1), 0))
// === Config ===
typedef enum zb_plat { ZB_PLAT_LINUX, ZB_PLAT_WINDOWS /* , ZB_PLAT_MACOS */ } zb_plat;
typedef enum zb_compiler { ZB_COMPILER_GCC, ZB_COMPILER_CLANG, ZB_COMPILER_CLANGCL, ZB_COMPILER_MSVC } zb_compiler;
typedef enum zb_config { ZB_CONFIG_DEBUG, ZB_CONFIG_RELEASE } zb_config;
typedef enum zb_san { 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 {
ZB_ARCH_X64_SSE2,
ZB_ARCH_X64_SSE4_1,
ZB_ARCH_X64_SSE4_2, // currently implies POPCNT
ZB_ARCH_X64_AVX, // currently implies POPCNT
ZB_ARCH_X64_AVX2_FMA_COMPAT, // currently implies POPCNT
ZB_ARCH_X64_AVX2_FMA_BUNDLE, // currently implies POPCNT + F16C + LZCNT + BMI1 + BMI2 + MOVBE
ZB_ARCH_X64_AVX512,
ZB_ARCH_ARM64_V8_0,
ZB_ARCH_ARM64_V8_2,
} zb_arch;
#define ZB_ARCH_IS_X86(a) ((a) < ZB_ARCH_ARM64_V8_0) // otherwise arm
// === Target data ===
typedef uint8_t zb_tgt_flags; // storage type
typedef unsigned zb_tgt_flags__native;
#define ZB_TGTF_STATIC 0u // static lib (no flags set)
#define ZB_TGTF_INTERFACE 0x2u // e.g. just for headers or natvis
#define ZB_TGTF_EXECUTABLE 0x4u
#define ZB_TGTF_TEST 0x8u
#define ZB_TGTF_LOCKED 0x10u // disallow further modifying the target during graph generation (must be greatest flag)
typedef uint16_t zb_src_flags; // storage type
typedef unsigned zb_src_flags__native;
#define ZB_SRCF_C 0u // C file (no flags set)
#define ZB_SRCF_CXX 0x1u // C++ file
#define ZB_SRCF_HEADER 0x2u // doesn't need to compile but may be helpful for IDE
#define ZB_SRCF_HOT 0x4u // compile with optimizations on in debug
#define ZB_SRCF_O1 0x8u // compile with reduced optimization (or optimizing for minimal code size) in release (mainly to speed up CI)
#define ZB_SRCF_NATVIS 0x10u
#define ZB_SRCF_MISC 0x20u // Files 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 0x40u // x86_64 only, skip on ARM // todo?: remove
#define ZB_SRCF_ARM64 0x80u // ARM64 only, skip on x86 // todo?: remove, unused at time of writing
#define ZB_SRCF_SSE4_1 0x100u // requires at least SSE4.1 (implies x64)
#define ZB_SRCF_AVX2_FMA 0x200u // requires at least AVX2+FMA (implies x64) (see `ZB_ARCH_X64_AVX2_FMA_COMPAT`)
static zb_arch zb_effective_arch(zb_arch base, zb_src_flags__native sf)
{
// FIXME: TUs that override archflags could set a flag so that it knows to mark weak or C++-style inline functions static, as to not interfere with other TUs if linking deduplicates them.
if ((sf & ZB_SRCF_AVX2_FMA) && base < ZB_ARCH_X64_AVX2_FMA_COMPAT) return ZB_ARCH_X64_AVX2_FMA_COMPAT;
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; // storage type
typedef unsigned zb_dep_flags__native;
#define ZB_DEPF_PRIVATE 0u // private (no flags set)
#define ZB_DEPF_PUBLIC 0x1u // include directory passthrough
#define ZB_TGT_NAME_MIN 4u // inclusive, hardcoded by ZB_TARGET
#define ZB_TGT_NAME_MAX 15u // inclusive, hardcoded by ZB_TARGET
#define ZB_TGT_MAX 64u // max targets, increase as needed (fits in zb_tgt_idx)
typedef uint16_t zb_tgt_idx; // storage type; native type is `unsigned`
#define ZB_SRC_MAX 1024u // max sources, increase as needed (fits in zb_src_idx, used as sentinel)
typedef uint16_t zb_src_idx; // storage type; native type is `unsigned`
#define ZB_DEP_MAX 256u // max dependencies, increase as needed (fits in zb_dep_idx, used as sentinel)
typedef uint16_t zb_dep_idx; // storage type; native type is `unsigned`
ZB_COMPILE_ASSERT(ZB_TGT_NAME_MAX + 1 == 16);
typedef struct ZB_ALIGN(16) {
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 {
unsigned tgt_count;
unsigned src_count;
unsigned dep_count;
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_ctx;
static zb_tgt_idx zb_ctx_add_tgt_pt1_(zb_ctx* ctx)
{
unsigned t = ctx->tgt_count++;
if (t >= ZB_TGT_MAX) zb_fatal("too many targets");
return (zb_tgt_idx)t;
}
static void zb_ctx_add_tgt_pt2_(zb_ctx* ctx, zb_tgt_idx t, zb_tgt_flags__native f, const char* incdir_public, const char* incdir_private)
{
zb_tgt_name* name = &ctx->tgt_names[t];
for (unsigned 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 * UINT64_C(2) - 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
if (badf) { fprintf(stderr, "Target '%s' invalid flags\n", name->bytes); exit(1); }
ctx->tgt_incdirs[t].incdir_private = incdir_private;
ctx->tgt_incdirs[t].incdir_public = incdir_public;
ctx->tgt_flags[t] = (zb_tgt_flags)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__native 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); }
unsigned 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] = (zb_dep_flags)flags;
}
}
static void zb_ctx_add_srcs_(zb_ctx* ctx, zb_tgt_idx t, zb_src_flags__native 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)
{
unsigned s = ctx->src_count++;
if (s >= ZB_SRC_MAX) zb_fatal("too many sources");
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] = (zb_src_flags)flags;
*tail = (zb_src_idx)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__native */ 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", 8); /* replace string literal with immediate stores */ \
memcpy((char*)&ctx->tgt_names[t] + 8, (#t "\0\0\0\0\0\0\0\0\0\0\0") + 8, 8); \
zb_ctx_add_tgt_pt2_(ctx, t, f, incdir_public, incdir_private);
#define ZB_DEPS(/* zb_tgt_idx */ t, /* zb_dep_flags__native */ f, /* zb_tgt_idx, < `t` */ ...) { \
zb_tgt_idx d__[] = {__VA_ARGS__}; \
zb_ctx_add_deps_(ctx, t, f, d__, sizeof(d__)/sizeof(d__[0])); \
}
#define ZB_SRCS(t, /* zb_src_flags__native */ f, /* static string literals */ ...) { \
const char* p__[] = {__VA_ARGS__}; \
zb_ctx_add_srcs_(ctx, t, f, p__, sizeof(p__)/sizeof(p__[0])); \
}
#define ZB_TARGET_LOCK(/* zb_tgt_idx */ t) zb_tgt_lock_(ctx, t, 1);
// === Graph generation ===
static void zb_generate_graph(zb_ctx* ctx)
{
ctx->tgt_count = 0;
ctx->src_count = 0;
ctx->dep_count = 0;
#if 0 // my current project
ZB_TARGET(zm_vulkan, ZB_TGTF_INTERFACE | ZB_TGTF_LOCKED, "ext/Vulkan-Headers/include", NULL)
ZB_TARGET(zm_gpu_vma, ZB_TGTF_INTERFACE | ZB_TGTF_LOCKED, "ext/VulkanMemoryAllocator/inc", NULL)
ZB_TARGET(mikktspace_dir, ZB_TGTF_INTERFACE | ZB_TGTF_LOCKED, "ext/MikkTSpace/inc", NULL)
ZB_TARGET(wuffs_dir, ZB_TGTF_INTERFACE | ZB_TGTF_LOCKED, "ext/wuffs/inc", NULL)
#include "src/zm/zm_zb.inl"
#include "ext/ext_zb.inl"
#include "src/cmn/cmn_zb.inl"
#include "src/phy/phy_zb.inl"
#include "src/tool/tool_zb.inl"
#include "src/rm/rm_zb.inl"
#include "src/sfx/sfx_zb.inl"
#include "src/gfx/gfx_zb.inl"
#include "src/app/app_zb.inl"
ZB_TARGET(Build, ZB_TGTF_INTERFACE, NULL, NULL)
ZB_SRCS(Build, ZB_SRCF_MISC, // make viewable for vcxproj
"zb.c",
"src/zm/zm_zb.inl",
"ext/ext_zb.inl",
"src/cmn/cmn_zb.inl",
"src/phy/phy_zb.inl",
"src/tool/tool_zb.inl",
"src/rm/rm_zb.inl",
"src/sfx/sfx_zb.inl",
"src/gfx/gfx_zb.inl",
"src/app/app_zb.inl",
)
ZB_TARGET_LOCK(Build)
#else
// === 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 optimization and NDEBUG (use '#ifdef ZM_FORCE_DEBUG_ABI' to enable custom assert macros)
"src/mylib/src/foo.c",
"src/mylib/src/bar.c",
)
ZB_TARGET_LOCK(mylib) // (optional helper to prevent further modification)
ZB_TARGET(myapp, ZB_TGTF_EXECUTABLE, NULL, "src/myapp")
ZB_DEPS(myapp, ZB_DEPF_PRIVATE, mylib,)
ZB_SRCS(myapp, ZB_SRCF_CXX,
"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_CXX,
"src/mylib/test/test_main.cpp",
)
ZB_TARGET_LOCK(mytest)
#endif
}
static int zb_validate_graph(zb_ctx* ctx)
{
int err = 0;
for (unsigned t = 0, tgt_count = ctx->tgt_count; t < tgt_count; ++t)
for (int i = 0; i < 2; ++i)
{
const char* dir = i ? ctx->tgt_incdirs[t].incdir_private : ctx->tgt_incdirs[t].incdir_public;
if (!dir) continue;
#ifdef _WIN32
DWORD attr = GetFileAttributesA(dir);
if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY))
#else
struct stat st;
if (stat(dir, &st) || !S_ISDIR(st.st_mode))
#endif
{
fprintf(stderr, "target '%s' include dir not found: '%s'\n", ctx->tgt_names[t].bytes, dir);
err = 1;
}
}
for (unsigned s = 0, src_count = ctx->src_count; s < src_count; ++s)
{
const char* path = ctx->src_path[s];
zb_src_flags__native sf = ctx->src_flags[s];
#ifdef _WIN32
DWORD attr = GetFileAttributesA(path);
if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY))
#else
struct stat st;
if (stat(path, &st) || !S_ISREG(st.st_mode))
#endif
{
fprintf(stderr, "source missing: '%s'\n", path);
err = 1;
}
const char* dot = strrchr(path, '.');
if (dot && (((sf & ZB_SRCF_CXX) && strcmp(dot, ".c") == 0) || (!(sf & ZB_SRCF_CXX) && strcmp(dot, ".cpp") == 0)))
{
fprintf(stderr, "source C/CPP mismatch: '%s'\n", path);
err = 1;
}
}
return err;
}
// === Compiler Flags ===
#define ZB_F_C_VER "c17" // note: `-Wstrict-prototypes` only needed <c23
#define ZB_F_CXX_VER "c++20"
// C
#define ZB_F_C_GCC " -std=" ZB_F_C_VER " -Wstrict-prototypes -Wwrite-strings -Wc++-compat -D_POSIX_C_SOURCE=200809L"
#define ZB_F_C_CLANG " -std=" ZB_F_C_VER " -Wstrict-prototypes -Wwrite-strings -Wc++-compat -D_POSIX_C_SOURCE=200809L"
#define ZB_F_C_CLANGCL " /std:" ZB_F_C_VER " -Wstrict-prototypes -Wwrite-strings -Wc++-compat"
#define ZB_F_C_MSVC " /std:" ZB_F_C_VER
// C++
#define ZB_F_CXX_GCC " -std=" ZB_F_CXX_VER " -fno-exceptions -fno-rtti"
#define ZB_F_CXX_CLANG " -std=" ZB_F_CXX_VER " -fno-exceptions -fno-rtti"
#define ZB_F_CXX_CLANGCL " /std:" ZB_F_CXX_VER " /EHs-c- /GR-"
#define ZB_F_CXX_MSVC " /std:" ZB_F_CXX_VER " /EHs-c- /GR-"
// Warnings
// (-Wvla may be disabled within source if needed)
#define ZB_F_WARN_GNU_LIKE " -Werror -Wcast-qual -Wshadow -Wdouble-promotion -Wconversion -Wundef -Wno-missing-field-initializers -Wvla"
#define ZB_F_WARN_CLANG_LIKE " -Wassign-enum -Wno-unused-command-line-argument -Wframe-larger-than=90000"
#define ZB_F_WARN_GCC " -Wall -Wextra -Warith-conversion -Wduplicated-cond -Wduplicated-branches -Wstack-usage=90000"
#define ZB_F_WARN_CLANG " -Wall -Wextra" // don't use for clang-cl
// 44456=local shadows local; 44457=local shadows param
// 4201=nameless struct/union; 4324=padded due to alignment; 4701/4703=potentially uninitialized variables
#define ZB_F_WARN_MSVC " /WX /W4 /w44456 /w44457 /wd4201 /wd4324 /wd4701 /wd4703"
// Common flags for both C and C++
#define ZB_F_BASE_GCC " -g -ffunction-sections -fdata-sections" ZB_F_WARN_GCC ZB_F_WARN_GNU_LIKE " -fno-math-errno -fno-trapping-math"
#define ZB_F_BASE_CLANG " -g -ffunction-sections -fdata-sections" ZB_F_WARN_CLANG ZB_F_WARN_GNU_LIKE ZB_F_WARN_CLANG_LIKE " -fno-math-errno -fno-trapping-math"
#define ZB_F_BASE_CLANGCL " /Zi /FS /W4" ZB_F_WARN_GNU_LIKE ZB_F_WARN_CLANG_LIKE " /clang:-fno-math-errno /clang:-fno-trapping-math"
#define ZB_F_BASE_MSVC " /Zi /FS" ZB_F_WARN_MSVC
static void zb_opt_flags(zb_compiler c, zb_config cfg, zb_san san, const char** out_opt, const char** out_opt_o1)
{
int is_clangcl = c == ZB_COMPILER_CLANGCL;
int is_msvc_like = (c == ZB_COMPILER_MSVC) | is_clangcl;
int debug = cfg == ZB_CONFIG_DEBUG;
int ubsan_clangcl = (san == ZB_SAN_UBSAN) & is_clangcl; // UBSan on clang-cl requires static runtime (/MT) due to prebuilt runtime
if (is_msvc_like)
{
*out_opt = debug
? (ubsan_clangcl ? " /Od /MT"
: san == ZB_SAN_ASAN ? " /Od /MD" : " /Od /MDd")
: ubsan_clangcl ? " /O2 /MT /DNDEBUG /GS-" : " /O2 /MD /DNDEBUG /GS-";
*out_opt_o1 = debug
? (ubsan_clangcl ? " /O1 /MT /DNDEBUG /DZM_FORCE_DEBUG_ABI"
: san == ZB_SAN_ASAN ? " /O1 /MD /DNDEBUG /DZM_FORCE_DEBUG_ABI" : " /O1 /MDd /DNDEBUG /DZM_FORCE_DEBUG_ABI")
: (ubsan_clangcl ? " /O1 /MT /DNDEBUG" : " /O1 /MD /DNDEBUG");
}
else
{
*out_opt = debug ? " -O0" : " -O2 -DNDEBUG";
*out_opt_o1 = debug ? " -O1 -DNDEBUG -DZM_FORCE_DEBUG_ABI" : " -Os -DNDEBUG";
}
}
static const char* zb_san_flags(zb_compiler c, zb_san san)
{
// todo?: error for unsupported
switch (san)
{
case ZB_SAN_ASAN:
return c == ZB_COMPILER_MSVC ? " /fsanitize=address"
: c == ZB_COMPILER_CLANGCL ? " -fsanitize=address /clang:-fno-omit-frame-pointer"
: " -fsanitize=address -fno-omit-frame-pointer";
case ZB_SAN_UBSAN:
if (c == ZB_COMPILER_MSVC) break; // unsupported
return " -fsanitize=undefined";
case ZB_SAN_TSAN:
if (c == ZB_COMPILER_MSVC || c == ZB_COMPILER_CLANGCL) break; // unsupported
return " -fsanitize=thread";
case ZB_SAN_NONE:
default:
break;
}
return "";
}
/* Used so that TUs know that C++/MSVC non-static inline is dangerous for shared headers compiled
* in TUs with different arch flags, because COMDAT dedupes functions based on symbol, not codegen. */
#define ZB_F_ARCH_OVERRIDE "ZM_ARCH_OVERRIDE"
// note: TUs are expected to cascade ZM_USE_AVX2 -> ZM_USE_AVX -> ZM_USE_SSE4_2 -> ZM_USE_SSE4_1
// Arch: SSE2
#define ZB_F_ARCH_SSE2_GNU " -msse2 -DZM_DISABLE_PERFORMANCE_WARNINGS"
#define ZB_F_ARCH_SSE2_MSVC " /DZM_DISABLE_PERFORMANCE_WARNINGS"
#define ZB_F_ARCH_SSE2_CLANGCL " -msse2 -DZM_DISABLE_PERFORMANCE_WARNINGS"
#define ZB_F_ARCH_SSE2_VCX_MSVC "ZM_DISABLE_PERFORMANCE_WARNINGS"
#define ZB_F_ARCH_SSE2_VCX_CLANGCL "ZM_DISABLE_PERFORMANCE_WARNINGS"
// Arch: SSE4.1
#define ZB_F_ARCH_SSE4_1_GNU " -msse4.1 -DZM_USE_SSE4_1"
#define ZB_F_ARCH_SSE4_1_MSVC " /DZM_USE_SSE4_1"
#define ZB_F_ARCH_SSE4_1_CLANGCL " -msse4.1 -DZM_USE_SSE4_1"
#define ZB_F_ARCH_SSE4_1_VCX_MSVC "ZM_USE_SSE4_1"
#define ZB_F_ARCH_SSE4_1_VCX_CLANGCL "ZM_USE_SSE4_1;" \
"__SSSE3__;__SSE4_1__"
// Arch: SSE4.2 + POPCNT
#define ZB_F_ARCH_SSE4_2_GNU " -msse4.2 -mpopcnt -DZM_USE_SSE4_2 -DZM_USE_POPCNT"
#define ZB_F_ARCH_SSE4_2_MSVC " /DZM_USE_SSE4_2 /DZM_USE_POPCNT"
#define ZB_F_ARCH_SSE4_2_CLANGCL ZB_F_ARCH_SSE4_2_GNU
#define ZB_F_ARCH_SSE4_2_VCX_MSVC "ZM_USE_SSE4_2;ZM_USE_POPCNT"
#define ZB_F_ARCH_SSE4_2_VCX_CLANGCL "ZM_USE_SSE4_2;ZM_USE_POPCNT;__SSSE3__;__SSE4_1__;__SSE4_2__;__POPCNT__"
// Arch: AVX + POPCNT
#define ZB_F_ARCH_AVX_GNU " -mavx -mpopcnt -DZM_USE_AVX -DZM_USE_POPCNT"
#define ZB_F_ARCH_AVX_MSVC " /arch:AVX /DZM_USE_AVX /DZM_USE_POPCNT"
#define ZB_F_ARCH_AVX_CLANGCL ZB_F_ARCH_AVX_GNU
#define ZB_F_ARCH_AVX_VCX_MSVC "ZM_USE_AVX;ZM_USE_POPCNT"
#define ZB_F_ARCH_AVX_VCX_CLANGCL "ZM_USE_AVX;ZM_USE_POPCNT;__SSSE3__;__SSE4_1__;__SSE4_2__;__POPCNT__;__AVX__;__XSAVE__"
// Arch: AVX2 + FMA + POPCNT (for CPU detection selected TUs) (Note that "/arch:AVX" is used on MSVC to be safe, as "/arch:AVX2" implies additional features.)
#define ZB_F_ARCH_AVX2_FMA_COMPAT_GNU " -mavx2 -mfma -mpopcnt -DZM_USE_AVX2 -DZM_USE_FMA -DZM_USE_POPCNT"
#define ZB_F_ARCH_AVX2_FMA_COMPAT_MSVC " /arch:AVX /DZM_USE_AVX2 /DZM_USE_FMA /DZM_USE_POPCNT"
#define ZB_F_ARCH_AVX2_FMA_COMPAT_CLANGCL ZB_F_ARCH_AVX2_FMA_COMPAT_GNU
#define ZB_F_ARCH_AVX2_FMA_COMPAT_VCX_MSVC "ZM_USE_AVX2;ZM_USE_FMA;ZM_USE_POPCNT"
#define ZB_F_ARCH_AVX2_FMA_COMPAT_VCX_CLANGCL "ZM_USE_AVX2;ZM_USE_FMA;ZM_USE_POPCNT;" \
"__SSSE3__;__SSE4_1__;__SSE4_2__;__POPCNT__;__AVX__;__XSAVE__;__AVX2__;__FMA__"
// Arch: AVX2 + FMA + F16C + POPCNT + LZCNT + BMI1; plus BMI2 + MOVBE to match MSVC's "/arch:AVX2"
#define ZB_F_ARCH_AVX2_FMA_BUNDLE_GNU ZB_F_ARCH_AVX2_FMA_COMPAT_GNU " -mf16c -mlzcnt -mbmi -mbmi2 -mmovbe -DZM_USE_F16C -DZM_USE_LZCNT -DZM_USE_BMI1 -DZM_USE_BMI2 -DZM_USE_MOVBE"
#define ZB_F_ARCH_AVX2_FMA_BUNDLE_MSVC " /arch:AVX2 /DZM_USE_AVX2 /DZM_USE_FMA /DZM_USE_F16C /DZM_USE_POPCNT /DZM_USE_LZCNT /DZM_USE_BMI1 /DZM_USE_BMI2 /DZM_USE_MOVBE"
#define ZB_F_ARCH_AVX2_FMA_BUNDLE_CLANGCL ZB_F_ARCH_AVX2_FMA_BUNDLE_GNU
#define ZB_F_ARCH_AVX2_FMA_BUNDLE_VCX_MSVC ZB_F_ARCH_AVX2_FMA_COMPAT_VCX_MSVC ";ZM_USE_F16C;ZM_USE_LZCNT;ZM_USE_BMI1;ZM_USE_BMI2;ZM_USE_MOVBE"
#define ZB_F_ARCH_AVX2_FMA_BUNDLE_VCX_CLANGCL ZB_F_ARCH_AVX2_FMA_COMPAT_VCX_CLANGCL ";ZM_USE_F16C;ZM_USE_LZCNT;ZM_USE_BMI1;ZM_USE_BMI2;ZM_USE_MOVBE" \
";__F16C__;__BMI__;__BMI2__;__LZCNT__;__MOVBE__"
// Arch: AVX-512 (F+CD+VL+DQ+BW) + FMA + F16C + POPCNT + LZCNT + BMI1 + BMI2 + MOVBE
#define ZB_F_ARCH_AVX512_GNU ZB_F_ARCH_AVX2_FMA_BUNDLE_GNU " -mavx512f -mavx512cd -mavx512vl -mavx512dq -mavx512bw -DZM_USE_AVX512"
#define ZB_F_ARCH_AVX512_MSVC " /arch:AVX512 /DZM_USE_AVX512 /DZM_USE_AVX2 /DZM_USE_FMA /DZM_USE_F16C /DZM_USE_POPCNT /DZM_USE_LZCNT /DZM_USE_BMI1 /DZM_USE_BMI2 /DZM_USE_MOVBE"
#define ZB_F_ARCH_AVX512_CLANGCL ZB_F_ARCH_AVX512_GNU
#define ZB_F_ARCH_AVX512_VCX_MSVC ZB_F_ARCH_AVX2_FMA_BUNDLE_VCX_MSVC ";ZM_USE_AVX512"
#define ZB_F_ARCH_AVX512_VCX_CLANGCL ZB_F_ARCH_AVX2_FMA_BUNDLE_VCX_CLANGCL ";ZM_USE_AVX512;__AVX512F__;__AVX512CD__;__AVX512VL__;__AVX512DQ__;__AVX512BW__"
// Arch: ARM
#define ZB_F_ARCH_ARM8_0_GNU " -march=armv8-a" // todo?: disable performance warnings
#define ZB_F_ARCH_ARM8_2_GNU " -march=armv8.2-a+fp16+simd"
// === Presets ===
typedef struct {
const char* name;
zb_plat plat;
zb_arch arch;
zb_compiler compiler;
zb_config config;
zb_san san;
} zb_cfg;
static const zb_cfg zb_presets[] = {
// === Windows Development ===
{"windev", ZB_PLAT_WINDOWS, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_MSVC, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"windev-asan", ZB_PLAT_WINDOWS, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_MSVC, ZB_CONFIG_DEBUG, ZB_SAN_ASAN},
{"windev-ubsan", ZB_PLAT_WINDOWS, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_CLANGCL, ZB_CONFIG_DEBUG, ZB_SAN_UBSAN},
// === Linux Development ===
{"linuxdev", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_CLANG, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"linuxdev-asan", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_CLANG, ZB_CONFIG_DEBUG, ZB_SAN_ASAN},
{"linuxdev-ubsan", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_CLANG, ZB_CONFIG_DEBUG, ZB_SAN_UBSAN},
{"linuxdev-tsan", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_CLANG, ZB_CONFIG_DEBUG, ZB_SAN_TSAN},
// === Release + Debug pairs ===
{"sse42-msvc-dbg", ZB_PLAT_WINDOWS, ZB_ARCH_X64_SSE4_2, ZB_COMPILER_MSVC, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"sse42-msvc-rel", ZB_PLAT_WINDOWS, ZB_ARCH_X64_SSE4_2, ZB_COMPILER_MSVC, ZB_CONFIG_RELEASE, ZB_SAN_NONE},
{"sse42-clang-dbg", ZB_PLAT_LINUX, ZB_ARCH_X64_SSE4_2, 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},
// ("avx2-msvc-dbg" is "windev")
{"avx2-msvc-rel", ZB_PLAT_WINDOWS, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_MSVC, ZB_CONFIG_RELEASE, ZB_SAN_NONE},
// ("avx2-clang-dbg" is "linuxdev")
{"avx2-clang-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_CLANG, ZB_CONFIG_RELEASE, ZB_SAN_NONE},
// === Compile checks ===
{"sse2-msvc-dbg", ZB_PLAT_WINDOWS, ZB_ARCH_X64_SSE2, ZB_COMPILER_MSVC, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"sse2-gcc-rel", ZB_PLAT_LINUX, ZB_ARCH_X64_SSE2, ZB_COMPILER_GCC, ZB_CONFIG_RELEASE, ZB_SAN_NONE},
{"avx-msvc-dbg", ZB_PLAT_WINDOWS, ZB_ARCH_X64_AVX, ZB_COMPILER_MSVC, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"avx2-gcc-dbg", ZB_PLAT_LINUX, ZB_ARCH_X64_AVX2_FMA_BUNDLE, ZB_COMPILER_GCC, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"armv82-gcc-dbg", ZB_PLAT_LINUX, ZB_ARCH_ARM64_V8_2, ZB_COMPILER_GCC, ZB_CONFIG_DEBUG, ZB_SAN_NONE},
{"armv82-gcc-rel", ZB_PLAT_LINUX, ZB_ARCH_ARM64_V8_2, ZB_COMPILER_GCC, ZB_CONFIG_RELEASE, ZB_SAN_NONE},
{"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},
};
#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_X86(p->arch);
#else
return p->plat == ZB_PLAT_LINUX && ZB_ARCH_IS_X86(p->arch);
#endif
}
static int zb_cfg_needs_cross(const zb_cfg* p)
{
#ifdef _WIN32
return p->plat == ZB_PLAT_LINUX;
#elif defined(__aarch64__)
return ZB_ARCH_IS_X86(p->arch);
#else
return !ZB_ARCH_IS_X86(p->arch);
#endif
}
// === File generation ===
#define ZB_OUT_CAP 4194304u // 4MB is generous (used for file generation and docker command concatenation)
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* ZB_RESTRICT s) { zb_out_str(o, s, strlen(s)); }
#define ZB_OUT_L(o, s) zb_out_str(o, "" s, sizeof(s) - 1)
#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, unsigned t) { return (bs->bits[t / 32] >> (t % 32)) & 1; }
static inline void zb_bitset_set(zb_tgt_bitset* bs, unsigned t) { bs->bits[t / 32] |= (1U << (t % 32)); }
static inline void zb_bitset_clear(zb_tgt_bitset* bs, unsigned t) { bs->bits[t / 32] &= ~(1U << (t % 32)); }
// The `t` bit is set
static void zb_collect_deps(zb_ctx* ctx, unsigned 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 {
unsigned d = ctx->tgt_dep_head[stack[--depth]];
while (d != ZB_DEP_MAX)
{
if (use_private || (ctx->dep_flags[d] & ZB_DEPF_PUBLIC))
{
unsigned 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++] = (zb_tgt_idx)dep_t;
}
}
d = ctx->dep_next[d];
}
use_private = always_use_private;
} while (depth);
}
// === Ninja emission ===
#define ZB_VERBOSE_NINJA_ON 0
static const char* zb_archflags(zb_arch a, zb_compiler c)
{
int is_msvc = c == ZB_COMPILER_MSVC;
int is_clangcl = c == ZB_COMPILER_CLANGCL;
switch (a) // see `zb_archflags_vcx`
{
case ZB_ARCH_X64_SSE2: return is_msvc ? ZB_F_ARCH_SSE2_MSVC : is_clangcl ? ZB_F_ARCH_SSE2_CLANGCL : ZB_F_ARCH_SSE2_GNU;
case ZB_ARCH_X64_SSE4_1: return is_msvc ? ZB_F_ARCH_SSE4_1_MSVC : is_clangcl ? ZB_F_ARCH_SSE4_1_CLANGCL : ZB_F_ARCH_SSE4_1_GNU;
case ZB_ARCH_X64_SSE4_2: return is_msvc ? ZB_F_ARCH_SSE4_2_MSVC : is_clangcl ? ZB_F_ARCH_SSE4_2_CLANGCL : ZB_F_ARCH_SSE4_2_GNU;
case ZB_ARCH_X64_AVX: return is_msvc ? ZB_F_ARCH_AVX_MSVC : is_clangcl ? ZB_F_ARCH_AVX_CLANGCL : ZB_F_ARCH_AVX_GNU;
case ZB_ARCH_X64_AVX2_FMA_COMPAT: return is_msvc ? ZB_F_ARCH_AVX2_FMA_COMPAT_MSVC : is_clangcl ? ZB_F_ARCH_AVX2_FMA_COMPAT_CLANGCL : ZB_F_ARCH_AVX2_FMA_COMPAT_GNU;
case ZB_ARCH_X64_AVX2_FMA_BUNDLE: return is_msvc ? ZB_F_ARCH_AVX2_FMA_BUNDLE_MSVC : is_clangcl ? ZB_F_ARCH_AVX2_FMA_BUNDLE_CLANGCL : ZB_F_ARCH_AVX2_FMA_BUNDLE_GNU;
case ZB_ARCH_X64_AVX512: return is_msvc ? ZB_F_ARCH_AVX512_MSVC : is_clangcl ? ZB_F_ARCH_AVX512_CLANGCL : ZB_F_ARCH_AVX512_GNU;
case ZB_ARCH_ARM64_V8_0: return ZB_F_ARCH_ARM8_0_GNU;
case ZB_ARCH_ARM64_V8_2: return ZB_F_ARCH_ARM8_2_GNU;
}
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 = compiler == ZB_COMPILER_MSVC;
int is_clangcl = compiler == ZB_COMPILER_CLANGCL;
int is_msvc_like = is_msvc | is_clangcl;
int is_windows = plat == ZB_PLAT_WINDOWS;
int is_x64 = ZB_ARCH_IS_X86(arch);
int needs_cross = zb_cfg_needs_cross(cfg);
unsigned flag_o1 = debug ? ZB_SRCF_HOT : ZB_SRCF_O1;
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";
const char* f_c = is_msvc ? ZB_F_C_MSVC : is_clangcl ? ZB_F_C_CLANGCL : compiler == ZB_COMPILER_CLANG ? ZB_F_C_CLANG : ZB_F_C_GCC;
const char* f_cxx = is_msvc ? ZB_F_CXX_MSVC : is_clangcl ? ZB_F_CXX_CLANGCL : compiler == ZB_COMPILER_CLANG ? ZB_F_CXX_CLANG : ZB_F_CXX_GCC;
const char* f_math = is_msvc ? ZB_F_BASE_MSVC : is_clangcl ? ZB_F_BASE_CLANGCL : compiler == ZB_COMPILER_CLANG ? ZB_F_BASE_CLANG : ZB_F_BASE_GCC;
const char* f_opt;
const char* f_opt_o1;
zb_opt_flags(compiler, cfg->config, cfg->san, &f_opt, &f_opt_o1);
const char* f_san = zb_san_flags(compiler, cfg->san);
// 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_clangcl) ZB_OUT_L(out, "cc = clang-cl\ncxx = clang-cl\nar = llvm-lib\n");
else if (is_msvc) ZB_OUT_L(out, "cc = cl\ncxx = cl\nar = lib\n");
else if (needs_cross && !is_x64)
{
// Cross-compiling to ARM64
if (compiler == ZB_COMPILER_CLANG)
ZB_OUT_L(out, "cc = clang --target=aarch64-linux-gnu\ncxx = clang++ --target=aarch64-linux-gnu\nar = llvm-ar\n");
else
ZB_OUT_L(out, "cc = aarch64-linux-gnu-gcc\ncxx = aarch64-linux-gnu-g++\nar = aarch64-linux-gnu-ar\n");
}
else if (compiler == ZB_COMPILER_CLANG) ZB_OUT_L(out, "cc = clang\ncxx = clang++\nar = ar\n");
else ZB_OUT_L(out, "cc = gcc\ncxx = g++\nar = ar\n");
// Archflags variable (may be overridden per-file)
ZB_OUT_L(out, "archflags =");
zb_out_s(out, zb_archflags(arch, compiler));
zb_out_c(out, '\n');
#if ZB_VERBOSE_NINJA_ON + 0
#define ZB_DESC(s) ""
#else
#define ZB_DESC(s) " description = " s " $out\n"
#endif
// Rules
if (is_msvc_like)
{
ZB_OUT_L(out, "\nrule cc\n command = $cc /nologo /showIncludes");
zb_out_s(out, f_c); zb_out_s(out, f_math); zb_out_s(out, f_opt); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n" ZB_DESC("CC"));
ZB_OUT_L(out, "rule cc_o1\n command = $cc /nologo /showIncludes");
zb_out_s(out, f_c); zb_out_s(out, f_math); zb_out_s(out, f_opt_o1); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n" ZB_DESC("CC"));
ZB_OUT_L(out, "rule cxx\n command = $cxx /nologo /showIncludes");
zb_out_s(out, f_cxx); zb_out_s(out, f_math); zb_out_s(out, f_opt); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n" ZB_DESC("CXX"));
ZB_OUT_L(out, "rule cxx_o1\n command = $cxx /nologo /showIncludes");
zb_out_s(out, f_cxx); zb_out_s(out, f_math); zb_out_s(out, f_opt_o1); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc /c $in /Fo$out /Fd$pdb\n deps = msvc\n" ZB_DESC("CXX"));
ZB_OUT_L(out, "rule ar\n command = $ar /nologo /out:$out $in\n" ZB_DESC("AR"));
ZB_OUT_L(out, "rule link\n command = $cxx /nologo");
zb_out_s(out, f_san);
if (debug)
ZB_OUT_L(out, " $in /Fe$out /link /DEBUG /OPT:REF /OPT:NOICF");
else
ZB_OUT_L(out, " $in /Fe$out /link /DEBUG /OPT:REF /OPT:ICF");
if (cfg->san == ZB_SAN_UBSAN && is_clangcl) ZB_OUT_L(out, " /ignore:4217 /ignore:4286");
ZB_OUT_L(out, " $libs\n" ZB_DESC("LINK"));
}
else
{
ZB_OUT_L(out, "\nrule cc\n command = $cc");
zb_out_s(out, f_c); zb_out_s(out, f_math); zb_out_s(out, f_opt); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n" ZB_DESC("CC"));
ZB_OUT_L(out, "rule cc_o1\n command = $cc");
zb_out_s(out, f_c); zb_out_s(out, f_math); zb_out_s(out, f_opt_o1); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n" ZB_DESC("CC"));
ZB_OUT_L(out, "rule cxx\n command = $cxx");
zb_out_s(out, f_cxx); zb_out_s(out, f_math); zb_out_s(out, f_opt); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n" ZB_DESC("CXX"));
ZB_OUT_L(out, "rule cxx_o1\n command = $cxx");
zb_out_s(out, f_cxx); zb_out_s(out, f_math); zb_out_s(out, f_opt_o1); zb_out_s(out, f_san);
ZB_OUT_L(out, " $archflags $inc -c $in -o $out -MD -MF $out.d\n depfile = $out.d\n deps = gcc\n" ZB_DESC("CXX"));
ZB_OUT_L(out, "rule ar\n command = $ar rcs $out $in\n" ZB_DESC("AR") "rule link\n command = $cxx");
if (compiler == ZB_COMPILER_CLANG) ZB_OUT_L(out, " -fuse-ld=lld");
ZB_OUT_L(out, " $in -o $out -Wl,--gc-sections");
if (!debug && compiler == ZB_COMPILER_CLANG) ZB_OUT_L(out, " -Wl,--icf=all");
ZB_OUT_L(out, " $libs");
zb_out_s(out, f_san);
ZB_OUT_L(out, "\n" ZB_DESC("LINK"));
}
zb_out_c(out, '\n');
#undef ZB_DESC
zb_src_flags__native sf_skip = ZB_SRCF_NATVIS | ZB_SRCF_MISC;
sf_skip |= ZB_SRCF_HEADER; // TODO TODO?: compile headers for tests
unsigned tgt_count = ctx->tgt_count;
// Targets
for (unsigned t = 0; t < tgt_count; ++t)
{
const char* name = ctx->tgt_names[t].bytes;
zb_tgt_flags__native 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 (unsigned s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s])
{
zb_src_flags__native 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_x64) 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)
{
unsigned bit = 31u - zb_clz(chunk);
unsigned 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 (unsigned s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s])
{
zb_src_flags__native 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_x64) 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_hot = sf & flag_o1;
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, sf & ZB_SRCF_CXX ? (is_hot ? "cxx_o1 " : "cxx ") : (is_hot ? "cc_o1 " : "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)
{
const char* dir = ctx->tgt_incdirs[i * 32 + zb_ctz(chunk)].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_s(out, is_msvc_like ? " /D" ZB_F_ARCH_OVERRIDE : " -D" ZB_F_ARCH_OVERRIDE); zb_out_c(out, '\n');
}
}
}
// Phony targets
for (unsigned t = 0; t < tgt_count; ++t)
{
const char* name = ctx->tgt_names[t].bytes;
zb_tgt_flags__native 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 (unsigned t = 0; t < 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* ZB_RESTRICT cwd)
{
zb_compiler compiler = cfg->compiler;
zb_arch arch = cfg->arch;
int debug = cfg->config == ZB_CONFIG_DEBUG;
int is_msvc = compiler == ZB_COMPILER_MSVC;
int is_clangcl = compiler == ZB_COMPILER_CLANGCL;
int is_msvc_like = is_msvc | is_clangcl;
int is_x64 = ZB_ARCH_IS_X86(arch);
unsigned flag_o1 = debug ? ZB_SRCF_HOT : ZB_SRCF_O1;
const char* inc_flag = is_msvc_like ? " /I" : " -I";
const char* cc = is_clangcl ? "clang-cl" : is_msvc ? "cl" : compiler == ZB_COMPILER_CLANG ? "clang" : "gcc";
const char* cxx = is_clangcl ? "clang-cl" : is_msvc ? "cl" : compiler == ZB_COMPILER_CLANG ? "clang++" : "g++";
const char* f_c = is_msvc ? ZB_F_C_MSVC : is_clangcl ? ZB_F_C_CLANGCL : compiler == ZB_COMPILER_CLANG ? ZB_F_C_CLANG : ZB_F_C_GCC;
const char* f_cxx = is_msvc ? ZB_F_CXX_MSVC : is_clangcl ? ZB_F_CXX_CLANGCL : compiler == ZB_COMPILER_CLANG ? ZB_F_CXX_CLANG : ZB_F_CXX_GCC;
const char* f_math = is_msvc ? ZB_F_BASE_MSVC : is_clangcl ? ZB_F_BASE_CLANGCL : compiler == ZB_COMPILER_CLANG ? ZB_F_BASE_CLANG : ZB_F_BASE_GCC;
const char* f_opt;
const char* f_opt_o1;
zb_opt_flags(compiler, cfg->config, cfg->san, &f_opt, &f_opt_o1);
const char* f_san = zb_san_flags(compiler, cfg->san);
ZB_OUT_L(out, "[\n");
int first = 1;
for (unsigned t = 0, tgt_count = ctx->tgt_count; t < 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 (unsigned s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s])
{
zb_src_flags__native 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_x64) continue;
const char* src = ctx->src_path[s];
int is_cpp = sf & ZB_SRCF_CXX;
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
zb_out_s(out, is_cpp ? f_cxx : f_c);
zb_out_s(out, f_math);
zb_out_s(out, sf & flag_o1 ? f_opt_o1 : f_opt);
zb_out_s(out, zb_archflags(effective_arch, compiler));
if (effective_arch != arch) zb_out_s(out, is_msvc_like ? " /D" ZB_F_ARCH_OVERRIDE : " -D" ZB_F_ARCH_OVERRIDE);
zb_out_s(out, f_san);
// Include paths
for (uint32_t i = 0; i < ZB_TGT_BITSET_U32S; ++i)
{
uint32_t chunk = inc_deps.bits[i];
while (chunk)
{
const char* dir = ctx->tgt_incdirs[i * 32 + zb_ctz(chunk)].incdir_public;
if (dir) { 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_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* ZB_RESTRICT 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;
}
}
}
static const char* zb_archflags_vcx(zb_arch a, zb_compiler c)
{
int is_msvc = c == ZB_COMPILER_MSVC;
switch (a) // see `zb_archflags`
{
case ZB_ARCH_X64_SSE2: return is_msvc ? ZB_F_ARCH_SSE2_VCX_MSVC : ZB_F_ARCH_SSE2_VCX_CLANGCL;
case ZB_ARCH_X64_SSE4_1: return is_msvc ? ZB_F_ARCH_SSE4_1_VCX_MSVC : ZB_F_ARCH_SSE4_1_VCX_CLANGCL;
case ZB_ARCH_X64_SSE4_2: return is_msvc ? ZB_F_ARCH_SSE4_2_VCX_MSVC : ZB_F_ARCH_SSE4_2_VCX_CLANGCL;
case ZB_ARCH_X64_AVX: return is_msvc ? ZB_F_ARCH_AVX_VCX_MSVC : ZB_F_ARCH_AVX_VCX_CLANGCL;
case ZB_ARCH_X64_AVX2_FMA_COMPAT: return is_msvc ? ZB_F_ARCH_AVX2_FMA_COMPAT_VCX_MSVC : ZB_F_ARCH_AVX2_FMA_COMPAT_VCX_CLANGCL;
case ZB_ARCH_X64_AVX2_FMA_BUNDLE: return is_msvc ? ZB_F_ARCH_AVX2_FMA_BUNDLE_VCX_MSVC : ZB_F_ARCH_AVX2_FMA_BUNDLE_VCX_CLANGCL;
case ZB_ARCH_X64_AVX512: return is_msvc ? ZB_F_ARCH_AVX512_VCX_MSVC : ZB_F_ARCH_AVX512_VCX_CLANGCL;
case ZB_ARCH_ARM64_V8_0:
case ZB_ARCH_ARM64_V8_2:
default: return "";
}
}
// Convert forward slashes to backslashes for VS paths
static void zb_out_winpath(zb_out* o, const char* ZB_RESTRICT s) { for (; *s; ++s) zb_out_c(o, *s == '/' ? '\\' : *s); }
#define ZB_VCX_GUID "{12345678-1234-1234-1234-123456789ABC}"
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_clangcl = compiler == ZB_COMPILER_CLANGCL;
int is_x64 = ZB_ARCH_IS_X86(arch);
const char* platform = is_x64 ? "x64" : "ARM64";
unsigned tgt_count = ctx->tgt_count;
// 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 (unsigned t = 0; t < 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>" ZB_VCX_GUID "</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 (unsigned t = 0; t < 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");
const char* arch_defs = zb_archflags_vcx(arch, compiler);
// NMake properties for each target
for (unsigned t = 0; t < 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");
if (is_clangcl) ZB_OUT_L(out, ";__clang__");
if (*arch_defs) { zb_out_c(out, ';'); zb_out_s(out, arch_defs); }
ZB_OUT_L(out, ";$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>\n <NMakeIncludeSearchPath>");
int inc_first = 1;
for (unsigned i = 0; i < tgt_count; ++i) // todo?: generate dependency bitset and only add relevant paths
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:" ZB_F_CXX_VER " /std:" ZB_F_C_VER "</AdditionalOptions>\n"
" <OutDir>out\\vs\\$(Configuration)\\</OutDir>\n"
" <IntDir>out\\vs\\$(Configuration)\\</IntDir>\n"
" <SourcePath />\n <ExcludePath />\n");
if (cfg->san == ZB_SAN_ASAN)
ZB_OUT_L(out, " <LocalDebuggerEnvironment>PATH=%PATH%;$(VC_ExecutablePath_x64)</LocalDebuggerEnvironment>\n");
ZB_OUT_L(out, " </PropertyGroup>\n");
}
ZB_OUT_L(out, " <ItemDefinitionGroup>\n </ItemDefinitionGroup>\n <ItemGroup>\n");
for (unsigned t = 0; t < tgt_count; ++t) // Source files
{
if ((ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) && ctx->tgt_src_head[t] == ZB_SRC_MAX) continue;
for (unsigned s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s])
{
zb_src_flags__native 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_x64) continue;
ZB_OUT_L(out, " <ClCompile Include=\""); zb_out_winpath(out, ctx->src_path[s]);
zb_arch eff = zb_effective_arch(arch, sf);
int has_arch_override = is_x64 & (eff != arch);
int is_c = !(sf & ZB_SRCF_CXX);
if (!has_arch_override && !is_c)
ZB_OUT_L(out, "\" />\n");
else
{
ZB_OUT_L(out, "\">\n");
if (is_c) ZB_OUT_L(out, " <CompileAs>CompileAsC</CompileAs>\n");
if (has_arch_override)
{
ZB_OUT_L(out, " <PreprocessorDefinitions>");
zb_out_s(out, zb_archflags_vcx(eff, compiler) /* may have duplicates from the target base */);
ZB_OUT_L(out, ";%(PreprocessorDefinitions)</PreprocessorDefinitions>\n");
}
ZB_OUT_L(out, " </ClCompile>\n");
}
}
}
ZB_OUT_L(out, " </ItemGroup>\n <ItemGroup>\n");
for (unsigned t = 0; t < tgt_count; ++t) // Headers
for (unsigned 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 (unsigned t = 0; t < tgt_count; ++t) // Natvis
for (unsigned 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 (unsigned t = 0; t < tgt_count; ++t) // Misc
for (unsigned 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* ZB_RESTRICT path, size_t* len)
{
const char* found = NULL;
for (const char* p = path; *p; ++p)
if ((*p == '/' || *p == '\\') && (
(p[1] == 's' && p[2] == 'r' && p[3] == 'c') || // "src"
(p[1] == 'i' && p[2] == 'n' && p[3] == 'c') // "inc"
) && (p[4] == '/' || p[4] == '\\' || p[4] == '_'))
{
// Find start of next segment
const char* seg = p + 4;
if (p[4] == '_') while (*seg && *seg != '/' && *seg != '\\') ++seg;
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; // Set if there's more path after (it's a folder)
}
}
if (!found) { *len = 0; return NULL; }
const char* end = found;
while (*end && *end != '/' && *end != '\\') ++end;
*len = end - found;
return found;
}
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);
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;
unsigned tgt_count = ctx->tgt_count;
for (unsigned t = 0; t < tgt_count; ++t)
{
if ((ctx->tgt_flags[t] & ZB_TGTF_INTERFACE) && ctx->tgt_src_head[t] == ZB_SRC_MAX) continue;
for (unsigned 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 = (zb_tgt_idx)t;
subfilters[subfilter_count].name = sf;
subfilters[subfilter_count++].len = len;
}
}
}
// Emit filter declarations
ZB_OUT_L(out, " <ItemGroup>\n");
for (unsigned t = 0; t < 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 sfs_len; \
const char* sfs = zb_extract_subfilter(path, &sfs_len); \
zb_out_s(out, tgt_name); \
if (sfs) { zb_out_c(out, '\\'); zb_out_str(out, sfs, sfs_len); } \
} while(0)
for (unsigned t = 0; t < 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 (unsigned s = ctx->tgt_src_head[t]; s < ZB_SRC_MAX; s = ctx->src_next[s])
{
zb_src_flags__native 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_x64) 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 (unsigned t = 0; t < tgt_count; ++t) // Headers with filters
{
const char* tgt_name = ctx->tgt_names[t].bytes;
int has_headers = 0;
for (unsigned 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 (unsigned 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 (unsigned t = 0; t < tgt_count; ++t) // Natvis with filters
{
const char* tgt_name = ctx->tgt_names[t].bytes;
for (unsigned 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 (unsigned t = 0; t < tgt_count; ++t) // Misc with filters
{
const char* tgt_name = ctx->tgt_names[t].bytes;
for (unsigned 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\", \"" ZB_VCX_GUID "\"\nEndProject\nGlobal\nEndGlobal\n");
}
// === 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 ===
#define ZB_DOCKER_CMD "podman" // or "docker" (may want to clean output dir if changing after use)
static int zb_docker_image_exists(const char* image)
{
char cmd[512];
#ifdef _WIN32
snprintf(cmd, sizeof(cmd), ZB_DOCKER_CMD " image inspect %s >nul 2>&1", image);
#else
snprintf(cmd, sizeof(cmd), ZB_DOCKER_CMD " 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, int include_toolchain)
{
printf("building 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"
" libvulkan%s libxcb1%s%s \\\n"
" && rm -rf /var/lib/apt/lists/*\n",
include_toolchain ? "-dev" : "1",
include_toolchain ? "-dev" : "",
include_toolchain ? " clang lld llvm g++ ninja-build libclang-rt-dev gcc-aarch64-linux-gnu g++-aarch64-linux-gnu" : "");
fclose(f);
char cmd[512];
snprintf(cmd, sizeof(cmd), ZB_DOCKER_CMD " build --platform %s -t %s -f %s .", platform, image, dockerfile);
int rc = system(cmd);
remove(dockerfile);
return rc == 0;
}
static int zb_run_in_docker(const zb_cfg* cfg, zb_ctx* ctx, char* /* ZB_RESTRICT */ out_buf)
{
if (!zb_docker_image_exists("zb:builder") && !zb_docker_build_image("zb:builder", "linux/amd64", 1))
return 1;
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_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);
}
snprintf(out_buf, ZB_OUT_CAP,
ZB_DOCKER_CMD " run --rm --platform linux/amd64 -v \"%s:/src\" -w /src zb:builder ninja -f out/zb/%s/build.ninja",
cwd, cfg->name);
printf(">>> %s\n", out_buf);
int rc = system(out_buf);
if (rc) return rc;
const char* run_image = "zb:builder";
const char* run_platform = "linux/amd64";
if (!ZB_ARCH_IS_X86(cfg->arch))
{
run_image = "zb:arm64";
run_platform = "linux/arm64";
if (!zb_docker_image_exists("zb:arm64") && !zb_docker_build_image("zb:arm64", "linux/arm64", 0))
return 1;
}
// Build test list in second half of buffer
char* tests = out_buf + ZB_OUT_CAP / 2;
size_t tests_len = 0;
for (unsigned t = 0, tgt_count = ctx->tgt_count; t < tgt_count; ++t)
{
if (!(ctx->tgt_flags[t] & ZB_TGTF_TEST)) continue;
if (tests_len) { memcpy(tests + tests_len, " && ", 4); tests_len += 4; }
tests_len += snprintf(tests + tests_len, ZB_OUT_CAP / 2 - tests_len, "out/zb/%s/bin/%s", cfg->name, ctx->tgt_names[t].bytes);
}
if (tests_len)
{
snprintf(out_buf, ZB_OUT_CAP / 2,
ZB_DOCKER_CMD " run --rm --security-opt seccomp=unconfined --platform %s -v \"%s:/src\" -w /src %s sh -c \"%s\"",
run_platform, cwd, run_image, tests);
printf(">>> %s\n", out_buf);
rc = system(out_buf);
}
return rc;
}
// === Performance timing ===
#ifdef _WIN32
static uint64_t zb_clock(void) { LARGE_INTEGER t; QueryPerformanceCounter(&t); return t.QuadPart; }
static uint64_t zb_clock_to_ns(uint64_t t)
{
LARGE_INTEGER f;
QueryPerformanceFrequency(&f);
uint64_t q = f.QuadPart;
return (t < UINT64_MAX / UINT64_C(1000000000)) ? t * UINT64_C(1000000000) / q
: t / q * UINT64_C(1000000000) + t % q * UINT64_C(1000000000) / q;
}
#else
#include <time.h>
static uint64_t zb_clock(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * UINT64_C(1000000000) + (uint64_t)ts.tv_nsec;
}
#define zb_clock_to_ns(t) (t)
#endif
// === 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];
snprintf(cmd, sizeof(cmd), "ninja -f out/zb/%s/build.ninja %s", cfg->name, target ? target : "");
uint64_t t0 = zb_clock();
ret = system(cmd);
uint64_t t1 = zb_clock();
uint64_t ns = zb_clock_to_ns(t1 - t0);
if (!ret) printf("built in %llu.%03llu ms\n", (unsigned long long)(ns / 1000000), (unsigned long long)((ns / 1000) % 1000));
}
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) 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;
puts("Presets:");
for (size_t i = 0; i < ZB_PRESET_COUNT; ++i)
{
const zb_cfg* cfg = &zb_presets[i];
// todo?: convert settings to strings
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);
puts("Targets:");
for (unsigned t = 0; t < ctx.tgt_count; ++t)
{
zb_tgt_flags__native 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");
DWORD attr = GetFileAttributesA("out\\zb");
ret = (attr != INVALID_FILE_ATTRIBUTES) & !!(attr & FILE_ATTRIBUTE_DIRECTORY);
#else
system("rm -rf out/zb");
struct stat st;
ret = !stat("out/zb", &st);
#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);
ret = zb_validate_graph(&ctx);
if (ret) goto done;
const char* failed[ZB_PRESET_COUNT];
size_t failed_count = 0, 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 /* mutates 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[640];
snprintf(cmd, sizeof(cmd), "ninja -f %s", path);
rc = system(cmd);
if (rc == 0)
{
const char* ext = cfg->plat == ZB_PLAT_WINDOWS ? ".exe" : "";
for (unsigned 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)
{
failed[failed_count++] = cfg->name;
printf(">>> %s: FAIL\n", cfg->name);
}
else
printf(">>> %s: PASS\n", cfg->name);
}
puts("\n========== SUMMARY ==========");
printf("%zu/%zu passed\n", run_count - failed_count, run_count);
if (failed_count)
{
puts("\nFailed:");
for (size_t i = 0; i < failed_count; ++i)
printf(" - %s\n", failed[i]);
ret = 1;
}
}
else if (strcmp(action, "podman") == 0)
{
if (arg_i < argc) goto bad_arg;
ret = system("podman machine start");
if (ret) puts("Hint: run 'podman machine init' after installing Podman CLI if this is your first time");
ret |= system("podman machine ssh -- sudo podman run --rm --privileged multiarch/qemu-user-static --reset -p yes");
}
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);
puts("Commands:"
"\n help"
"\n presets List presets"
"\n targets List targets"
"\n test [preset] Build and run tests (all if no preset)"
"\n clean Remove build artifacts"
"\n ninja [preset] Generate build.ninja (all native if no preset)"
"\n run <preset> <target> Build and launch debugger (use ninja command first)"
"\n build <preset> [target] Build (all targets if none specified) (use ninja command first)"
"\n compiledb [preset] Generate compile_commands.json"
"\n vcxproj <preset> <name> Generate Visual Studio project and build.ninja"
"\n podman Start podman and register QEMU for ARM cross-testing"
);
done:
return ret;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment