-
-
Save Tomarty/ce00d310647a3337e1c31614164dfdd4 to your computer and use it in GitHub Desktop.
Custom C/C++ build/CI system in C (WIP)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * This file ("zb.c") was created by Thomas Martell (Tomarty) 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, "&"); break; | |
| case '<': ZB_OUT_L(o, "<"); break; | |
| case '>': ZB_OUT_L(o, ">"); break; | |
| case '"': ZB_OUT_L(o, """); break; | |
| case '\'': ZB_OUT_L(o, "'"); break; | |
| default: zb_out_c(o, *s); break; | |
| } | |
| } | |
| } | |
| 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 && zb ninja "); | |
| zb_out_s(out, cfg->name); | |
| ZB_OUT_L(out, " && zb build "); zb_out_s(out, cfg->name); ZB_OUT_L(out, " "); zb_out_s(out, name); | |
| ZB_OUT_L(out, "</NMakeReBuildCommandLine>\n <NMakeCleanCommandLine>zb clean</NMakeCleanCommandLine>\n <NMakeOutput>out\\zb\\"); | |
| zb_out_s(out, cfg->name); ZB_OUT_L(out, "\\bin\\"); zb_out_s(out, name); | |
| ZB_OUT_L(out, ".exe</NMakeOutput>\n <NMakePreprocessorDefinitions>"); | |
| zb_out_s(out, debug ? "_DEBUG" : "NDEBUG"); | |
| 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