A C library that implements the Type-Length-Value wire format.
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}