A C library that implements the Type-Length-Value wire format.
0

Configure Feed

Select the types of activity you want to include in your feed.

tlv / nob.c
15 kB 486 lines
1#define _POSIX_C_SOURCE 200809L 2#define NOB_IMPLEMENTATION 3#define NOB_UNSTRIP_PREFIX 4#define NOB_NO_ECHO 5#include "nob.h" 6 7#include <ctype.h> 8#include <string.h> 9 10/* --- settings --- */ 11 12#define NAME "tlv" 13#define STD "c2x" 14#define CC "gcc" 15#define BUILD_DIR ".build" 16#define SRC_DIR "src" 17#define INC_DIR "include" 18#define LIB_DIR "lib" 19#define TEST_DIR "tests" 20 21#define WARN_FLAGS \ 22 "-Wall", "-Wextra", "-Wfloat-equal", "-Wundef", "-Wshadow", \ 23 "-Wpointer-arith", "-Wwrite-strings", "-Wswitch-default", \ 24 "-Wconversion", "-Wunreachable-code", "-pedantic" 25 26#define BASE_FLAGS "-std=" STD, WARN_FLAGS, "-I" INC_DIR 27#define SAN_FLAGS "-fsanitize=address,undefined" 28#define LNK_FLAGS "-L" LIB_DIR "/static", "-L" LIB_DIR "/dynamic", "-lm", "-ldynarr" 29 30/* to expose POSIX/GNU APIs: add "-D_POSIX_C_SOURCE=200809L" or "-D_GNU_SOURCE" to BASE_FLAGS */ 31 32/* --- file collection helpers --- */ 33 34static bool is_c_file(const char *name) { 35 size_t n = strlen(name); 36 return n >= 3 && strcmp(name + n - 2, ".c") == 0; 37} 38 39static bool is_test_c_file(const char *name) { 40 return is_c_file(name) && strncmp(name, "test_", 5) == 0; 41} 42 43static bool is_ch_file(const char *name) { 44 size_t n = strlen(name); 45 if (n < 3) return false; 46 return strcmp(name + n - 2, ".c") == 0 || strcmp(name + n - 2, ".h") == 0; 47} 48 49typedef bool (*FileFilter)(const char *name); 50 51static bool collect_files(const char *dir, FileFilter filter, Nob_File_Paths *paths) { 52 Nob_Dir_Entry de = {0}; 53 54 if (!nob_dir_entry_open(dir, &de)) 55 return false; 56 57 while (nob_dir_entry_next(&de)) { 58 if (strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0) 59 continue; 60 61 if (filter && !filter(de.name)) 62 continue; 63 64 nob_da_append(paths, nob_temp_sprintf("%s/%s", dir, de.name)); 65 } 66 67 bool ok = !de.error; 68 nob_dir_entry_close(de); 69 return ok; 70} 71 72/* --- helpers --- */ 73 74static const char *git_version(void) { 75 FILE *f = popen("git describe --tags --always --dirty 2>/dev/null", "r"); 76 if (!f) return "unknown"; 77 char buf[128] = {0}; 78 if (!fgets(buf, sizeof(buf), f)) { pclose(f); return "unknown"; } 79 pclose(f); 80 size_t n = strlen(buf); 81 if (n > 0 && buf[n - 1] == '\n') buf[n - 1] = '\0'; 82 return nob_temp_sprintf("%s", buf); 83} 84 85/* --- semver --- */ 86 87/* Validates MAJOR.MINOR.PATCH[-prerelease][+build] */ 88static bool is_valid_semver(const char *s) { 89 if (!s || !*s || !isdigit((unsigned char)*s)) return false; 90 int dots = 0; 91 bool in_digits = false; 92 for (; *s; s++) { 93 if (isdigit((unsigned char)*s)) { 94 in_digits = true; 95 } else if (*s == '.' && dots < 2) { 96 if (!in_digits) return false; 97 dots++; 98 in_digits = false; 99 } else if ((*s == '-' || *s == '+') && dots == 2 && in_digits) { 100 s++; 101 while (*s && (isalnum((unsigned char)*s) || *s == '.' || *s == '-' || *s == '+')) 102 s++; 103 return *s == '\0'; 104 } else { 105 return false; 106 } 107 } 108 return dots == 2 && in_digits; 109} 110 111/* --- commands --- */ 112 113static bool build_objects(Nob_File_Paths *srcs, Nob_File_Paths *objs) { 114 if (!nob_mkdir_if_not_exists(BUILD_DIR)) return false; 115 for (size_t i = 0; i < srcs->count; i++) { 116 const char *src = srcs->items[i]; 117 const char *obj = nob_temp_sprintf(BUILD_DIR "/%s.o", nob_path_name(src)); 118 /* strip ".c", add ".o" */ 119 size_t len = strlen(obj); 120 char *o = nob_temp_strdup(obj); 121 o[len - 2] = '.'; o[len - 1] = 'o'; 122 123 Nob_Cmd cmd = {0}; 124 nob_cmd_append(&cmd, CC, BASE_FLAGS, "-O2", "-DNDEBUG", "-fPIC", 125 nob_temp_sprintf("-DVERSION=\"%s\"", git_version()), 126 "-c", src, "-o", o); 127 if (!nob_cmd_run(&cmd)) return false; 128 nob_da_append(objs, o); 129 } 130 return true; 131} 132 133static bool cmd_static(void) { 134 Nob_File_Paths srcs = {0}; 135 if (!collect_files(SRC_DIR, is_c_file, &srcs)) return false; 136 137 Nob_File_Paths objs = {0}; 138 if (!build_objects(&srcs, &objs)) return false; 139 140 Nob_Cmd cmd = {0}; 141 nob_cmd_append(&cmd, "ar", "rcs", BUILD_DIR "/lib" NAME ".a"); 142 nob_da_append_many(&cmd, objs.items, objs.count); 143 if (!nob_cmd_run(&cmd)) return false; 144 145 nob_log(NOB_INFO, "Built: " BUILD_DIR "/lib" NAME ".a"); 146 return true; 147} 148 149static bool cmd_dynamic(void) { 150 Nob_File_Paths srcs = {0}; 151 if (!collect_files(SRC_DIR, is_c_file, &srcs)) return false; 152 153 Nob_File_Paths objs = {0}; 154 if (!build_objects(&srcs, &objs)) return false; 155 156 Nob_Cmd cmd = {0}; 157 nob_cmd_append(&cmd, CC, "-shared", "-o", BUILD_DIR "/lib" NAME ".so"); 158 nob_da_append_many(&cmd, objs.items, objs.count); 159 nob_cmd_append(&cmd, LNK_FLAGS); 160 if (!nob_cmd_run(&cmd)) return false; 161 162 nob_log(NOB_INFO, "Built: " BUILD_DIR "/lib" NAME ".so"); 163 return true; 164} 165 166static bool cmd_build(void) { 167 return cmd_static() && cmd_dynamic(); 168} 169 170static bool cmd_test(void) { 171 if (!nob_mkdir_if_not_exists(BUILD_DIR)) return false; 172 if (!nob_mkdir_if_not_exists(BUILD_DIR "/tests")) return false; 173 174 Nob_File_Paths srcs = {0}; 175 if (!collect_files(SRC_DIR, is_c_file, &srcs)) return false; 176 177 Nob_File_Paths test_files = {0}; 178 if (!collect_files(TEST_DIR, is_test_c_file, &test_files)) return false; 179 180 int failed = 0; 181 for (size_t i = 0; i < test_files.count; i++) { 182 const char *tf = test_files.items[i]; 183 const char *base = nob_path_name(tf); 184 size_t blen = strlen(base); 185 char tname[256]; 186 snprintf(tname, sizeof(tname), "%.*s", (int)(blen - 2), base); 187 188 printf(" %-30s", tname); 189 fflush(stdout); 190 191 const char *out = nob_temp_sprintf(BUILD_DIR "/tests/%s", tname); 192 193 Nob_Cmd cmd = {0}; 194 nob_cmd_append(&cmd, CC, BASE_FLAGS, "-I" TEST_DIR, "-g", "-O1", SAN_FLAGS); 195 nob_cmd_append(&cmd, tf); 196 nob_da_append_many(&cmd, srcs.items, srcs.count); 197 nob_cmd_append(&cmd, LNK_FLAGS, "-o", out); 198 if (!nob_cmd_run(&cmd)) { printf("FAILED (compile)\n"); failed++; continue; } 199 200 Nob_Cmd run = {0}; 201 nob_cmd_append(&run, out); 202 if (nob_cmd_run(&run)) printf("ok\n"); 203 else { printf("FAILED\n"); failed++; } 204 } 205 206 if (failed > 0) { 207 nob_log(NOB_ERROR, "%d test(s) failed", failed); 208 return false; 209 } 210 return true; 211} 212 213static bool cmd_valgrind(void) { 214 if (!nob_mkdir_if_not_exists(BUILD_DIR "/tests")) return false; 215 216 Nob_File_Paths srcs = {0}; 217 if (!collect_files(SRC_DIR, is_c_file, &srcs)) return false; 218 219 Nob_File_Paths test_files = {0}; 220 if (!collect_files(TEST_DIR, is_test_c_file, &test_files)) return false; 221 222 for (size_t i = 0; i < test_files.count; i++) { 223 const char *tf = test_files.items[i]; 224 const char *base = nob_path_name(tf); 225 size_t blen = strlen(base); 226 char tname[256]; 227 snprintf(tname, sizeof(tname), "%.*s-vg", (int)(blen - 2), base); 228 229 const char *out = nob_temp_sprintf(BUILD_DIR "/tests/%s", tname); 230 231 Nob_Cmd cmd = {0}; 232 nob_cmd_append(&cmd, CC, BASE_FLAGS, "-I" TEST_DIR, "-g", "-O1"); 233 nob_cmd_append(&cmd, tf); 234 nob_da_append_many(&cmd, srcs.items, srcs.count); 235 nob_cmd_append(&cmd, LNK_FLAGS, "-o", out); 236 if (!nob_cmd_run(&cmd)) return false; 237 238 nob_log(NOB_INFO, "==> valgrind: %s", tname); 239 Nob_Cmd vg = {0}; 240 nob_cmd_append(&vg, "valgrind", 241 "--leak-check=full", "--show-leak-kinds=all", 242 "--track-origins=yes", "--error-exitcode=1", 243 out); 244 if (!nob_cmd_run(&vg)) return false; 245 } 246 return true; 247} 248 249static bool cmd_cppcheck(void) { 250 Nob_Cmd cmd = {0}; 251 nob_cmd_append(&cmd, "cppcheck", 252 "--enable=all", "--std=" STD, "--inconclusive", "--error-exitcode=1", 253 "-I" INC_DIR, SRC_DIR "/"); 254 return nob_cmd_run(&cmd); 255} 256 257static bool cmd_fmt(bool check_only) { 258 Nob_File_Paths files = {0}; 259 if (!collect_files(SRC_DIR, is_ch_file, &files)) return false; 260 if (!collect_files(INC_DIR, is_ch_file, &files)) return false; 261 262 Nob_Cmd cmd = {0}; 263 nob_cmd_append(&cmd, "clang-format"); 264 if (check_only) nob_cmd_append(&cmd, "--dry-run", "--Werror"); 265 else nob_cmd_append(&cmd, "-i"); 266 nob_da_append_many(&cmd, files.items, files.count); 267 return nob_cmd_run(&cmd); 268} 269 270static bool cmd_compile_commands(void) { 271 Nob_Cmd cmd = {0}; 272 nob_cmd_append(&cmd, "bear", "--", "./nob", "build"); 273 return nob_cmd_run(&cmd); 274} 275 276static bool cmd_docs(void) { 277 if (!nob_mkdir_if_not_exists(BUILD_DIR "/docs")) return false; 278 279 const char *doxyfile = BUILD_DIR "/docs/doxyfile"; 280 const char *content = 281 "PROJECT_NAME = " NAME "\n" 282 "INPUT = " SRC_DIR " " INC_DIR "\n" 283 "OUTPUT_DIRECTORY = " BUILD_DIR "/docs\n" 284 "GENERATE_HTML = YES\n" 285 "GENERATE_LATEX = NO\n" 286 "RECURSIVE = YES\n" 287 "EXTRACT_ALL = YES\n" 288 "QUIET = YES\n"; 289 if (!nob_write_entire_file(doxyfile, content, strlen(content))) return false; 290 291 Nob_Cmd cmd = {0}; 292 nob_cmd_append(&cmd, "doxygen", doxyfile); 293 if (!nob_cmd_run(&cmd)) return false; 294 295 nob_log(NOB_INFO, "docs: " BUILD_DIR "/docs/html/index.html"); 296 return true; 297} 298 299static bool cmd_pack(const char *version, const char *prefix) { 300 if (!cmd_static()) return false; 301 if (!cmd_dynamic()) return false; 302 303 char out[512]; 304 if (version && *version) { 305 if (!is_valid_semver(version)) { 306 nob_log(NOB_ERROR, "error: '%s' is not valid semver (e.g. 1.2.3)", version); 307 return false; 308 } 309 snprintf(out, sizeof(out), NAME "-%s.tar.gz", version); 310 } else { 311 snprintf(out, sizeof(out), NAME ".tar.gz"); 312 } 313 314 /* create staging directory */ 315 char stage[] = "/tmp/nob_pack_XXXXXX"; 316 if (!mkdtemp(stage)) { 317 nob_log(NOB_ERROR, "mkdtemp failed: %s", strerror(errno)); 318 return false; 319 } 320 321 const char *root = (prefix && *prefix) 322 ? nob_temp_sprintf("%s/%s", stage, prefix) 323 : stage; 324 325 if (!nob_mkdir_if_not_exists(nob_temp_sprintf("%s/include", root))) goto fail; 326 if (!nob_mkdir_if_not_exists(nob_temp_sprintf("%s/lib", root))) goto fail; 327 if (!nob_mkdir_if_not_exists(nob_temp_sprintf("%s/lib/static", root))) goto fail; 328 if (!nob_mkdir_if_not_exists(nob_temp_sprintf("%s/lib/dynamic", root))) goto fail; 329 330 if (!nob_copy_directory_recursively(INC_DIR, nob_temp_sprintf("%s/include", root))) goto fail; 331 332 const char *a_name = (version && *version) 333 ? nob_temp_sprintf("lib" NAME "-%s.a", version) 334 : "lib" NAME ".a"; 335 const char *so_name = (version && *version) 336 ? nob_temp_sprintf("lib" NAME "-%s.so", version) 337 : "lib" NAME ".so"; 338 339 if (nob_file_exists(BUILD_DIR "/lib" NAME ".a")) 340 if (!nob_copy_file(BUILD_DIR "/lib" NAME ".a", 341 nob_temp_sprintf("%s/lib/static/%s", root, a_name))) goto fail; 342 343 if (nob_file_exists(BUILD_DIR "/lib" NAME ".so")) 344 if (!nob_copy_file(BUILD_DIR "/lib" NAME ".so", 345 nob_temp_sprintf("%s/lib/dynamic/%s", root, so_name))) goto fail; 346 347 { 348 const char *tar_dir = (prefix && *prefix) ? prefix : "."; 349 Nob_Cmd cmd = {0}; 350 nob_cmd_append(&cmd, "tar", "-czf", out, "-C", stage, tar_dir); 351 if (!nob_cmd_run(&cmd)) goto fail; 352 } 353 354 { 355 Nob_Cmd cmd = {0}; 356 nob_cmd_append(&cmd, "rm", "-rf", stage); 357 nob_cmd_run(&cmd); /* best effort cleanup */ 358 } 359 360 nob_log(NOB_INFO, "packed: %s", out); 361 return true; 362 363fail: 364 { 365 Nob_Cmd cmd = {0}; 366 nob_cmd_append(&cmd, "rm", "-rf", stage); 367 nob_cmd_run(&cmd); 368 } 369 return false; 370} 371 372static bool cmd_clean(void) { 373 Nob_Cmd cmd = {0}; 374 nob_cmd_append(&cmd, "rm", "-rf", BUILD_DIR); 375 return nob_cmd_run(&cmd); 376} 377 378static bool cmd_strace(void) { 379 /* build each test without sanitizers — cleaner strace output */ 380 if (!nob_mkdir_if_not_exists(BUILD_DIR "/tests")) return false; 381 382 Nob_File_Paths srcs = {0}; 383 if (!collect_files(SRC_DIR, is_c_file, &srcs)) return false; 384 385 Nob_File_Paths test_files = {0}; 386 if (!collect_files(TEST_DIR, is_test_c_file, &test_files)) return false; 387 388 for (size_t i = 0; i < test_files.count; i++) { 389 const char *tf = test_files.items[i]; 390 const char *base = nob_path_name(tf); 391 size_t blen = strlen(base); 392 char tname[256]; 393 snprintf(tname, sizeof(tname), "%.*s-st", (int)(blen - 2), base); 394 395 const char *out = nob_temp_sprintf(BUILD_DIR "/tests/%s", tname); 396 397 Nob_Cmd cmd = {0}; 398 nob_cmd_append(&cmd, CC, BASE_FLAGS, "-I" TEST_DIR, "-g", "-O1"); 399 nob_cmd_append(&cmd, tf); 400 nob_da_append_many(&cmd, srcs.items, srcs.count); 401 nob_cmd_append(&cmd, LNK_FLAGS, "-o", out); 402 if (!nob_cmd_run(&cmd)) return false; 403 404 nob_log(NOB_INFO, "==> strace: %s", tname); 405 Nob_Cmd st = {0}; 406 nob_cmd_append(&st, "strace", out); 407 if (!nob_cmd_run(&st)) return false; 408 } 409 return true; 410} 411 412static bool cmd_ctags(void) { 413 Nob_Cmd cmd = {0}; 414 nob_cmd_append(&cmd, "ctags", "-R", SRC_DIR, INC_DIR); 415 return nob_cmd_run(&cmd); 416} 417 418/* --- usage --- */ 419 420static void usage(const char *program) { 421 fprintf(stderr, "Usage: %s [command]\n", program); 422 fprintf(stderr, "\n"); 423 fprintf(stderr, "Commands:\n"); 424 fprintf(stderr, " build Build static and dynamic libraries (default)\n"); 425 fprintf(stderr, " static Build " BUILD_DIR "/lib" NAME ".a\n"); 426 fprintf(stderr, " dynamic Build " BUILD_DIR "/lib" NAME ".so\n"); 427 fprintf(stderr, " test Compile and run all tests/test_*.c\n"); 428 fprintf(stderr, " valgrind Per-test Valgrind run (no sanitizers)\n"); 429 fprintf(stderr, " strace Per-test strace (no sanitizers)\n"); 430 fprintf(stderr, " cppcheck cppcheck static analysis\n"); 431 fprintf(stderr, " ctags Generate tags file for editor navigation\n"); 432 fprintf(stderr, " fmt clang-format in-place\n"); 433 fprintf(stderr, " fmt-check Check formatting (for CI)\n"); 434 fprintf(stderr, " compile-commands Generate compile_commands.json via bear\n"); 435 fprintf(stderr, " docs Generate HTML docs in " BUILD_DIR "/docs/html/\n"); 436 fprintf(stderr, " pack [ver] [pfx] Pack library as tar.gz (semver optional)\n"); 437 fprintf(stderr, " clean Remove " BUILD_DIR "/\n"); 438} 439 440/* --- main --- */ 441 442int main(int argc, char **argv) { 443 NOB_GO_REBUILD_URSELF(argc, argv); 444 445 const char *program = nob_shift(argv, argc); 446 const char *cmd = argc > 0 ? nob_shift(argv, argc) : "build"; 447 448 if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0 || strcmp(cmd, "help") == 0) { 449 usage(program); 450 return 0; 451 } else if (strcmp(cmd, "build") == 0) { 452 return cmd_build() ? 0 : 1; 453 } else if (strcmp(cmd, "static") == 0) { 454 return cmd_static() ? 0 : 1; 455 } else if (strcmp(cmd, "dynamic") == 0) { 456 return cmd_dynamic() ? 0 : 1; 457 } else if (strcmp(cmd, "test") == 0) { 458 return cmd_test() ? 0 : 1; 459 } else if (strcmp(cmd, "valgrind") == 0) { 460 return cmd_valgrind() ? 0 : 1; 461 } else if (strcmp(cmd, "strace") == 0) { 462 return cmd_strace() ? 0 : 1; 463 } else if (strcmp(cmd, "cppcheck") == 0) { 464 return cmd_cppcheck() ? 0 : 1; 465 } else if (strcmp(cmd, "ctags") == 0) { 466 return cmd_ctags() ? 0 : 1; 467 } else if (strcmp(cmd, "fmt") == 0) { 468 return cmd_fmt(false) ? 0 : 1; 469 } else if (strcmp(cmd, "fmt-check") == 0) { 470 return cmd_fmt(true) ? 0 : 1; 471 } else if (strcmp(cmd, "compile-commands") == 0) { 472 return cmd_compile_commands() ? 0 : 1; 473 } else if (strcmp(cmd, "docs") == 0) { 474 return cmd_docs() ? 0 : 1; 475 } else if (strcmp(cmd, "pack") == 0) { 476 const char *ver = argc > 0 ? nob_shift(argv, argc) : ""; 477 const char *pfx = argc > 0 ? nob_shift(argv, argc) : ""; 478 return cmd_pack(ver, pfx) ? 0 : 1; 479 } else if (strcmp(cmd, "clean") == 0) { 480 return cmd_clean() ? 0 : 1; 481 } else { 482 nob_log(NOB_ERROR, "unknown command: %s", cmd); 483 usage(program); 484 return 1; 485 } 486}