diff options
-rw-r--r-- | .gitignore | 13 | ||||
-rw-r--r-- | Makefile | 145 | ||||
-rw-r--r-- | deps.mk | 37 | ||||
-rwxr-xr-x | mkdeps.sh | 15 | ||||
-rw-r--r-- | src/api.js | 6 | ||||
-rw-r--r-- | src/catalog.c | 501 | ||||
-rw-r--r-- | src/catalog.h | 33 | ||||
-rw-r--r-- | src/config.h.in | 12 | ||||
-rw-r--r-- | src/db.js | 45 | ||||
-rw-r--r-- | src/i18n.c | 68 | ||||
-rw-r--r-- | src/i18n.h | 55 | ||||
-rw-r--r-- | src/ircd.js | 4 | ||||
-rw-r--r-- | src/logerr.c | 301 | ||||
-rw-r--r-- | src/logerr.h | 23 | ||||
-rw-r--r-- | src/napi-sqlite.c | 103 | ||||
-rw-r--r-- | src/papo.en.msg | 70 | ||||
-rw-r--r-- | src/sql/config.sql | 34 | ||||
-rwxr-xr-x | tests/assert-deps.sh | 45 | ||||
-rwxr-xr-x | tests/c-lint.sh | 65 | ||||
-rwxr-xr-x | tests/cli-opts.sh | 4 | ||||
-rw-r--r-- | tests/slurp.c | 69 | ||||
-rw-r--r-- | tests/slurp.h | 11 | ||||
-rw-r--r-- | tests/tests-lib.c | 28 | ||||
-rw-r--r-- | tests/tests-lib.h | 15 | ||||
-rwxr-xr-x | tools/cdeps.sh | 225 |
25 files changed, 1831 insertions, 96 deletions
@@ -1,6 +1,17 @@ +/*.bin +/*.so +/src/config.h +/src/*.o +/src/*.lo +/src/*.to +/src/*.ta +/src/*.t +/src/*.c.txt +/src/*.cat /doc/*.[0-9] /doc/*.3js /src/index.js /node_modules/ -/src/napi-sqlite.lo /src/napi-sqlite.node +/tests/tests-lib.o +/tests/slurp.o @@ -15,15 +15,22 @@ SRCDIR = $(PREFIX)/src/$(NAME) SHAREDIR = $(PREFIX)/share LOCALEDIR = $(SHAREDIR)/locale MANDIR = $(SHAREDIR)/man +CFLAGS.a = $(CFLAGS) CFLAGS.so = $(CFLAGS) -fPIC +LDFLAGS.a = $(LDFLAGS) LDFLAGS.so = $(LDFLAGS) -shared +LDLIBS.a = $(LDLIBS) +LDLIBS.so = $(LDLIBS) +EXT.so = .so +EXEC = ./ ## Where to store the installation. Empty by default. DESTDIR = +LDLIBS = .SUFFIXES: -.SUFFIXES: .in .c .lo .msg .cat +.SUFFIXES: .in .c .o .lo .to .ta .t .msg .cat .in: sed \ @@ -35,11 +42,23 @@ DESTDIR = < $< > $@ if [ -x $< ]; then chmod +x $@; fi +.c.o: + $(CC) $(CFLAGS.a) -o $@ -c $< + .c.lo: - $(CC) $(CFLAGS.so) -o $@ -c $< + $(CC) $(CFLAGS.so) -o $@ -c $< + +.c.to: + $(CC) $(CFLAGS.a) -DTEST -o $@ -c $< + +.ta.t: + $(CC) $(LDFLAGS.a) -o $@ $< $(LDLIBS.a) +all: +include deps.mk + manpages.en.in = \ doc/$(NAME).README.en.7.in \ doc/$(NAME).CHANGELOG.en.7.in \ @@ -52,25 +71,38 @@ manpages.en.in = \ manpages.in = $(manpages.en.in) manpages = $(manpages.in:.in=) -sources.js = \ - src/api.js \ - src/cli.js \ - src/ircd.js \ - src/utils.js \ - src/web.js \ - -tests.js = \ - tests/js/ircd.js \ - tests/js/utils.js \ - tests/js/web.js \ +catalogs.en.msg = src/$(NAME).en.msg +catalogs.msg = $(catalogs.en.msg) +catalogs.cat = $(catalogs.msg:.msg=.cat) +sources.o = $(sources.c:.c=.o) +sources.lo = $(sources.c:.c=.lo) +sources.to = $(sources.c:.c=.to) +sources.ta = $(sources.c:.c=.ta) +sources.t = $(sources.c:.c=.t) sources = \ - $(sources.js) \ + $(sources.c) \ + $(sources.c:.c=.h) \ + src/config.h.in \ + src/config.h \ + $(catalogs.msg) \ + $(sources.js) \ derived-assets = \ + $(NAME).bin \ + lib$(NAME)$(EXT.so) \ + src/config.h \ $(manpages) \ + $(catalogs.cat) \ + $(sources.o) \ + $(sources.lo) \ + $(sources.to) \ + $(sources.ta) \ + $(sources.t) \ + tests/tests-lib.o \ + tests/slurp.o \ src/index.js \ node_modules/dir.sentinel \ node_modules/ \ @@ -79,8 +111,10 @@ derived-assets = \ src/napi-sqlite.node \ side-assets = \ - ircd.sock \ - web.sock \ + src/logerr.c.txt \ + src/catalog.c.txt \ + ircd.sock \ + web.sock \ @@ -89,10 +123,14 @@ side-assets = \ all: $(derived-assets) -$(manpages) src/napi-sqlite.lo: Makefile +$(NAME).bin: + ln -fs src/cli $@ -src/napi-sqlite.node: src/napi-sqlite.lo - $(CC) $(LDFLAGS.so) -o $@ src/napi-sqlite.lo +$(manpages) src/config.h: Makefile deps.mk +$(sources.o) $(sources.lo) $(sources.to): src/config.h Makefile deps.mk +tests/tests-lib.o: tests/tests-lib.h src/config.h Makefile deps.mk +$(sources.to): tests/tests-lib.h +$(sources.ta): tests/tests-lib.o src/index.js: ln -fs api.js $@ @@ -108,6 +146,22 @@ node_modules/$(NAME): node_modules/dir.sentinel node_modules/: node_modules/dir.sentinel node_modules/$(NAME) +src/napi-sqlite.node: lib$(NAME)$(EXT.so) + ln -f lib$(NAME)$(EXT.so) $@ + +lib$(NAME)$(EXT.so): $(sources.lo) src/napi-sqlite.lo + $(CC) $(LDFLAGS.so) -o $@ $(sources.lo) src/napi-sqlite.lo $(LDLIBS.so) + +$(sources.ta): + $(AR) $(ARFLAGS) $@ $? + +src/$(NAME).en.cat: src/i18n.t + env DUMP_TRANSLATABLE_STRINGS=1 $(EXEC)src/i18n.t > $*.msg.new + cmp -s $*.msg.new $*.msg || mv $*.msg.new $*.msg + rm -f $*.msg.new + gencat $@ $*.msg + touch $@ + .SUFFIXES: .js .js-t @@ -115,7 +169,34 @@ tests.js-t = $(tests.js:.js=.js-t) $(tests.js-t): node $*.js -check-t: $(tests.js-t) +check-js-t: $(tests.js-t) + +.SUFFIXES: .t-run +sources.t-run = $(sources.c:.c=.t-run) +$(sources.t-run): + $(EXEC)$*.t + +check-t-run: $(sources.t-run) + +check-t: check-js-t check-t-run + + + +.SUFFIXES: .c-lint +sources.c-lint = $(sources.c:.c=.c-lint) +$(sources.c-lint): + sh tests/c-lint.sh $*.c + +check-lint: $(sources.c-lint) + + +integration-tests = \ + tests/cli-opts.sh \ + +$(integration-tests): $(NAME).bin ALWAYS + sh $@ $(EXEC)$(NAME).bin + +check-integration: $(integration-tests) tests/assert-clean.sh tests/assert-install.sh tests/assert-uninstall.sh: all @@ -134,7 +215,7 @@ check-asserts: $(assert-tests) ## Run all tests. Each test suite is isolated, so that a parallel ## build can run tests at the same time. The required artifacts ## are created if missing. -check: check-t check-asserts +check: check-t check-lint check-integration check-asserts ## Remove *all* derived artifacts produced during the build. @@ -149,38 +230,44 @@ install: all mkdir -p \ '$(DESTDIR)$(BINDIR)' \ '$(DESTDIR)$(JSLIBDIR)' - cp src/*.js '$(DESTDIR)$(JSLIBDIR)' + cp -P src/*.js '$(DESTDIR)$(JSLIBDIR)' ln -fs '$(DESTDIR)$(JSLIBDIR)'/cli.js '$(DESTDIR)$(BINDIR)'/$(NAME) for f in $(sources); do \ dir='$(DESTDIR)$(SRCDIR)'/"`dirname "$${f#src/}"`"; \ mkdir -p "$$dir"; \ cp -P "$$f" "$$dir"; \ done + for l in en $(TRANSLATIONS); do \ + dir='$(DESTDIR)$(LOCALEDIR)'/"$$l"/LC_MESSAGES; \ + mkdir -p "$$dir"; \ + cp src/$(NAME)."$$l".cat "$$dir"/$(NAME).cat; \ + done sh tools/manpages.sh -ip '$(DESTDIR)$(MANDIR)' $(manpages) ## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror ## of the "install" target, and removes *all* that was installed. ## A dedicated test asserts that this is always true. uninstall: - rm -f \ - '$(DESTDIR)$(BINDIR)'/$(NAME) + rm -rf \ + '$(DESTDIR)$(BINDIR)'/$(NAME) \ + '$(DESTDIR)$(SRCDIR)' rm -rf '$(DESTDIR)$(JSLIBDIR)' - for f in $(sources); do \ - rm -f '$(DESTDIR)$(SRCDIR)'/$${f#src/}; \ + for l in en $(TRANSLATIONS); do \ + rm -f '$(DESTDIR)$(LOCALEDIR)'/"$$l"/LC_MESSAGES/$(NAME).cat; \ done sh tools/manpages.sh -up '$(DESTDIR)$(MANDIR)' $(manpages) -run-ircd: +run-ircd: all rm -f ircd.sock ./src/cli.js ircd ircd.sock -run-web: +run-web: all rm -f web.sock ./src/cli.js web web.sock ## Run the web and IRC server locally. -run: node_modules/$(NAME) src/index.js +run: all $(MAKE) run-ircd & $(MAKE) run-web & wait @@ -0,0 +1,37 @@ +sources.c = \ + src/catalog.c \ + src/i18n.c \ + src/logerr.c \ + +sources.js = \ + src/api.js \ + src/db.js \ + src/ircd.js \ + src/utils.js \ + src/web.js \ + +tests.js = \ + tests/js/ircd.js \ + tests/js/utils.js \ + tests/js/web.js \ + +src/catalog.o src/catalog.lo src/catalog.to: src/catalog.h +src/i18n.o src/i18n.lo src/i18n.to: src/i18n.h +src/logerr.o src/logerr.lo src/logerr.to: src/logerr.h + +src/catalog.ta: src/catalog.to +src/i18n.ta: src/i18n.to +src/logerr.ta: src/logerr.to + +src/catalog.t-run: src/catalog.t +src/i18n.t-run: src/i18n.t +src/logerr.t-run: src/logerr.t + + +src/catalog.o src/catalog.lo src/catalog.to: src/logerr.h src/../tests/slurp.h +src/i18n.o src/i18n.lo src/i18n.to: src/logerr.h src/catalog.h +src/logerr.o src/logerr.lo src/logerr.to: src/../tests/slurp.h + +src/catalog.ta: src/logerr.o src/../tests/slurp.o +src/i18n.ta: src/logerr.o src/catalog.o +src/logerr.ta: src/../tests/slurp.o diff --git a/mkdeps.sh b/mkdeps.sh new file mode 100755 index 0000000..12d10f0 --- /dev/null +++ b/mkdeps.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu + +varlist() { + printf '%s = \\\n' "$1" + sed 's|^\(.*\)$|\t\1 \\|' + printf '\n' +} + +export LANG=POSIX.UTF-8 +find src/*.c -not -name napi-sqlite.c | sort | varlist 'sources.c' +find src/*.js -not -name index.js | sort | varlist 'sources.js' +find tests/js/*.js | sort | varlist 'tests.js' + +sh tools/cdeps.sh `find src/*.c -not -name napi-sqlite.c | sort` @@ -2,19 +2,19 @@ const { eq } = require("./utils.js"); const ircd = require("./ircd.js"); const web = require("./web.js"); -const main = () => { +const main = async () => { if (process.argv.length === 3 && process.argv[2] === "-V") { console.log("papo 1970-01-01 0.1.0"); return; } if (process.argv[2] === "ircd") { - ircd.app(process.argv[3]); + await ircd.app(process.argv[3]); return; } if (process.argv[2] === "web") { - web.app(process.argv[3]); + await web.app(process.argv[3]); return; } diff --git a/src/catalog.c b/src/catalog.c new file mode 100644 index 0000000..c3ec9b2 --- /dev/null +++ b/src/catalog.c @@ -0,0 +1,501 @@ +#include "catalog.h" +#include "logerr.h" + +#include <assert.h> +#include <errno.h> +#include <nl_types.h> +#include <stdlib.h> +#include <string.h> + +#ifdef TEST +#include "../tests/tests-lib.h" +#include "../tests/slurp.h" + +static const char *const +FNAME = __FILE__ ".txt"; + +enum TEST_MSGCATALOG_ID { + MSG_X_FIRST = 1, + MSG_X_1, + MSG_X_2, + MSG_X_LAST, + MSG_STANDALONE, +}; + +static const char *const +TEST_MSGS[] = { + "", + [MSG_X_FIRST]="First line\n", + [MSG_X_1]="a second\n", + [MSG_X_2]="a third\n", + [MSG_X_LAST]="and the last one\n", + [MSG_STANDALONE]="single line message\n", + NULL +}; +#endif + + + +static const char *const +CATALOG_NAME = NAME_MACRO_STRING; + +static nl_catd +catalog_descriptor = NULL; + +static const char *const +NLSPATH = LOCALEDIR_MACRO_STRING "/%l_%t/LC_MESSAGES/%N.cat" ":" + LOCALEDIR_MACRO_STRING "/%l/LC_MESSAGES/%N.cat"; + +static const char *const +NLSPATH_KEY = "NLSPATH"; + + +int +i18n_init(void) { + int rc = 0; + + static const int should_overwrite = 0; + if (setenv(NLSPATH_KEY, NLSPATH, should_overwrite)) { + logerr("setenv(\"%s\", \"%s\", 0): %s\n", NLSPATH_KEY, + NLSPATH, strerror(errno)); + rc = -1; + goto out; + } + + catalog_descriptor = catopen(CATALOG_NAME, 0); + if (catalog_descriptor && catalog_descriptor == (nl_catd)-1) { + logerr("catopen(\"%s\", 0): %s\n", CATALOG_NAME, strerror(errno)); + catalog_descriptor = NULL; + rc = -1; + goto out; + } + +out: + return rc; +} + +#ifdef TEST +static int +test_i18n_init(void) { + int rc = 0; + + test_start("i18n_init()"); + + { + testing("simple call without touching the environment"); + + static const int should_overwrite = 1; + if (setenv(NLSPATH_KEY, "src/%N.en.cat", should_overwrite)) { + logerr("setenv(\"%s\", \"src/%%N.en.cat\", 1): %s\n", + NLSPATH_KEY, strerror(errno)); + rc = -1; + goto out; + } + + if (i18n_init()) { + logerr("i18n_init()\n"); + rc = -1; + goto out; + } + + test_ok(); + } + +out: + if (i18n_destroy()) { + logerr("i18n_destroy()\n"); + rc = -1; + } + return rc; +} +#endif + + +int +i18n_destroy(void) { + int rc = 0; + + if (catalog_descriptor) { + if (catclose(catalog_descriptor)) { + logerr("catclose(...): %s\n", strerror(errno)); + rc = -1; + goto out; + } + } + +out: + if (catalog_descriptor) { + catalog_descriptor = NULL; + } + return rc; +} + +#ifdef TEST +static int +test_i18n_destroy(void) { + int rc = 0; + + test_start("i18n_destroy()"); + + { + testing("simple call without init first"); + + if (i18n_destroy()) { + logerr("i18n_destroy()\n"); + rc = -1; + goto out; + } + + test_ok(); + } + +out: + return rc; +} +#endif + + +/** + * Infallible: always returns a valid string, no matter what. + */ +const char * +s(const char* const MSGS[], const int msg_id) { + assert(msg_id > 0); + // FIXME: assert within bounds! + // printf("sizeof(MSGS): %ld\n", sizeof(MSGS)); + if (!catalog_descriptor) { + return MSGS[msg_id]; + } + + errno = 0; + const char *const ret = + catgets(catalog_descriptor, NL_SETD, msg_id, MSGS[msg_id]); + if (errno) { + logerr("catgets(%d): %s\n", msg_id, strerror(errno)); + } + + return ret; +} + +#ifdef TEST +static int +test_s(void) { + int rc = 0; + + test_start("_()"); + FILE *file = NULL; + char *str = NULL; + + { + testing("empty string"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + // FIXME: implement correct test + + + + test_ok(); + } + +out: + if (str) { + free(str); + } + if (file) { + if (fclose(file)) { + logerr("fclose(file): %s\n", strerror(errno)); + rc = -1; + } + } + return rc; +} +#endif + + +int +s_print_msgs( + const char *const MSGS[], + FILE *restrict stream, + const int msg_begin, + const int msg_end +) { + int rc = 0; + + for (int i = msg_begin; i <= msg_end; i++) { + if (fprintf(stream, "%s", s(MSGS, i)) < 0) { + logerr("fprintf(stream, \"%%s\", _(%d)): %s\n", i, + strerror(errno)); + rc = -1; + goto out; + } + } + +out: + return rc; +} + +#ifdef TEST +static int +test_s_print_msgs(void) { + int rc = 0; + + test_start("s_print_msgs()"); + FILE *file = NULL; + char *str = NULL; + + { + testing("message in range"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + if (s_print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_LAST)) { + logerr("print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_LAST)\n"); + rc = -1; + goto out; + } + + const int ret = fclose(file); + file = NULL; + if (ret) { + logerr("fclose(file): %s\n", strerror(errno)); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + logerr("slurp_for_tests(FNAME, &str)\n"); + rc = -1; + goto out; + } + + const char *const expected = + "First line\n" + "a second\n" + "a third\n" + "and the last one\n" + ; + + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + { + testing("range begin and end is the same"); + + file = fopen(FNAME, "w"); + if (!file) { + logerr("fopen(FNAME, \"w\"): %s\n", strerror(errno)); + rc = -1; + goto out; + } + + if (s_print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_FIRST)) { + logerr("s_print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_FIRST)\n"); + rc = -1; + goto out; + } + + const int ret = fclose(file); + file = NULL; + if (ret) { + logerr("fclose(file): %s\n", strerror(errno)); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + logerr("slurp_for_tests(FNAME, &str)\n"); + rc = -1; + goto out; + } + + const char *const expected = + "First line\n"; + + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + +out: + if (str) { + free(str); + } + if (file) { + if (fclose(file)) { + logerr("fclose(file): %s\n", strerror(errno)); + rc = -1; + } + } + return rc; +} +#endif + + +int +s_print_msg(const char *const MSGS[], FILE *const fd, const int msg_id) { + return s_print_msgs(MSGS, fd, msg_id, msg_id); +} + +#ifdef TEST +static int +test_s_print_msg(void) { + int rc = 0; + + test_start("s_print_msg()"); + FILE *file = NULL; + char *str = NULL; + + { + testing("simple individual message"); + + file = fopen(FNAME, "w"); + if (!file) { + logerr("fopen(FNAME, \"w\"): %s\n"); + rc = -1; + goto out; + } + + if (s_print_msg(TEST_MSGS, file, MSG_STANDALONE)) { + logerr("s_print_msg(TEST_MSGS, file, MSG_STANDALONE)\n"); + rc = -1; + goto out; + } + + const int ret = fclose(file); + file = NULL; + if (ret) { + logerr("fopen(file): %s\n", strerror(errno)); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + logerr("slurp_for_tests(FNAME, &str)\n"); + rc = -1; + goto out; + } + + const char *const expected = + "single line message\n"; + + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + +out: + if (str) { + free(str); + } + if (file) { + if (fclose(file)) { + logerr("fclose(file): %s\n", strerror(errno)); + rc = -1; + } + } + return rc; +} +#endif + + +int +dump_translatable_strings(const char *const MSGS[]) { + int rc = 0; + + for (size_t i = 1; MSGS[i]; i++) { + if (printf("%ld ", i) < 0) { + logerr("printf(\"%%ld\", %d): %s\n", i); + rc = -1; + goto out; + } + + for (size_t j = 0; MSGS[i][j]; j++) { + if (MSGS[i][j] == '\n') { + if (printf("\\n") < 0) { + logerr("printf(\"\\\\n\"): %s\n", + strerror(errno)); + rc = -1; + goto out; + } + } else { + if (printf("%c", MSGS[i][j]) < 0) { + logerr("printf(\"%%c\", " + "MSGS[%ld][%ld]): %s\n", + i, j, strerror(errno)); + rc = -1; + goto out; + } + } + } + + if (printf("\n\n") < 0) { + logerr("printf(\"\\n\\n\"): %s\n", strerror(errno)); + rc = -1; + goto out; + } + } + +out: + return rc; +} + +#ifdef TEST +int +main(void) { + int rc = 0; + + if (test_i18n_init()) { + logerr("test_i18n_init()\n"); + rc = -1; + goto out; + } + + if (test_i18n_destroy()) { + logerr("test_i18n_destroy()\n"); + rc = -1; + goto out; + } + + if (test_s()) { + logerr("test_s()\n"); + rc = -1; + goto out; + } + + if (test_s_print_msgs()) { + logerr("test_s_print_msgs()\n"); + rc = -1; + goto out; + } + + if (test_s_print_msg()) { + logerr("test_s_print_msg()\n"); + rc = -1; + goto out; + } + +out: + return !!rc; +} +#endif diff --git a/src/catalog.h b/src/catalog.h new file mode 100644 index 0000000..9f237a9 --- /dev/null +++ b/src/catalog.h @@ -0,0 +1,33 @@ +#ifndef CATALOG_H +#define CATALOG_H + +#include "config.h" + +#include <stdio.h> + + +int +i18n_init(void); + +int +i18n_destroy(void); + +const char * +s(const char *const MSGS[], const int msg_id); + +int +s_print_msgs( + const char *const MSGS[], + FILE *restrict stream, + const int msg_begin, + const int msg_end +); + +int +s_print_msg(const char *const MSGS[], FILE *restrict stream, const int msg_id); + +int +dump_translatable_strings(const char *const MSGS[]); + + +#endif diff --git a/src/config.h.in b/src/config.h.in new file mode 100644 index 0000000..4867dc0 --- /dev/null +++ b/src/config.h.in @@ -0,0 +1,12 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define _XOPEN_SOURCE 700 +#define _POSIX_C_SOURCE 200809L + +#define VERSION_MACRO_STRING "@VERSION@" +#define DATE_MACRO_STRING "@DATE@" +#define NAME_MACRO_STRING "@NAME@" +#define LOCALEDIR_MACRO_STRING "@LOCALEDIR@" + +#endif diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..e735ab1 --- /dev/null +++ b/src/db.js @@ -0,0 +1,45 @@ +const fs = require("node:fs"); +const sqlite = require("./napi-sqlite.node"); + +// const value = 8; +// console.log(`${value} time 2 =`, sqlite.my_function(value)); + +const CONFIG_FILE = __dirname + "/sql/config.sql"; +const MIGRATIONS_DIR = __dirname + "/sql/migrations/"; + +let db = null; +const init = async () => { + console.log({ + sqlite, + }); + console.log(`sqlite.myfn(2): ${sqlite.myfn(2)}`); + console.log(`sqlite.open(2): ${sqlite.open(2)}`); + /* + const config = fs.readFileSync(CONFIG_FILE, "UTF-8"); + const migrations = fs.readdirSync(MIGRATIONS_DIR, "UTF-8"); + + await exec(config); + await exec(` + CREATE TABLE IF NOT EXISTS migrations ( + filename TEXT PRIMARY KEY + ); + `); + const done = await run(` + SELECT filename FROM migrations; + `); + + // FIXME: sort + const pending = new Set(migrations).difference(new Set(done)); + + await exec("BEGIN TRANSACTION;"); + for (const p of pending) { + await exec(fs.readFileSync(MIGRATIONS_DIR + p, "UTF-8")); + await exec(`INSERT INTO migrations (filename) VALUES (?)`, p); + } + await exec("COMMIT TRANSACTION;"); + */ +}; + +module.exports = { + init, +}; diff --git a/src/i18n.c b/src/i18n.c new file mode 100644 index 0000000..245ad84 --- /dev/null +++ b/src/i18n.c @@ -0,0 +1,68 @@ +#include "i18n.h" + +#ifdef TEST +#include "logerr.h" +#include "catalog.h" + +#include <stdlib.h> +#endif + +const char *const +MSGS[] = { + "", + [MSG_USAGE_FIRST]="Usage:\n", + [MSG_USAGE_1]=" " NAME_MACRO_STRING " -p FILE [-o DIRECTORY]\n", + [MSG_USAGE_2]=" " NAME_MACRO_STRING " -l FILE [-o DIRECTORY]\n", + [MSG_USAGE_LAST]=" " NAME_MACRO_STRING " [-hV]\n", + [MSG_HELP_FIRST]="\n", + [MSG_HELP_1]="\n", + [MSG_HELP_2]="Options:\n", + [MSG_HELP_3]=" -p FILE parser file to be processed\n", + [MSG_HELP_4]=" -l FILE lexer file to be processed\n", + [MSG_HELP_5]=" -o DIRECTORY output where to place the\n", + [MSG_HELP_6]=" generated files (default .)\n", + [MSG_HELP_7]=" -h, --help show this help message\n", + [MSG_HELP_8]=" -V, --version print the version number\n", + [MSG_HELP_9]="\n", + [MSG_HELP_10]="\n", + [MSG_HELP_11]="Run the " NAME_MACRO_STRING "(1) parser program.\n", + [MSG_HELP_12]="\n", + [MSG_HELP_13]="Here is the explanation for what it does, and the synopsis\n", + [MSG_HELP_14]="of its usage.\n", + [MSG_HELP_15]="\n", + [MSG_HELP_16]="See \"man " NAME_MACRO_STRING "\" for usage information and\n", + [MSG_HELP_17]="\"man " NAME_MACRO_STRING ".tutorial\" for a beginner introduction.\n", + [MSG_HELP_18]="\n", + [MSG_HELP_19]="\n", + [MSG_HELP_20]="Examples:\n", + [MSG_HELP_21]="\n", + [MSG_HELP_22]=" Do a one-line parser:\n", + [MSG_HELP_23]="\n", + [MSG_HELP_24]=" $ " NAME_MACRO_STRING " run md.grammar < README.md\n", + [MSG_HELP_25]="\n", + [MSG_HELP_26]="\n", + [MSG_HELP_27]=" Compile the grammer:\n", + [MSG_HELP_28]="\n", + [MSG_HELP_LAST]=" $ " NAME_MACRO_STRING " build csv.grammar > dunno.alsodunno\n", + [MSG_VERSION]= NAME_MACRO_STRING " " VERSION_MACRO_STRING " " DATE_MACRO_STRING "\n", + NULL +}; + + +#ifdef TEST +int +main(void) { + int rc = 0; + + if (getenv("DUMP_TRANSLATABLE_STRINGS")) { + if (dump_translatable_strings(MSGS)) { + logerr("dump_translatable_strings(MSGS)\n"); + rc = -1; + goto out; + } + } + +out: + return !!rc; +} +#endif diff --git a/src/i18n.h b/src/i18n.h new file mode 100644 index 0000000..8245564 --- /dev/null +++ b/src/i18n.h @@ -0,0 +1,55 @@ +#ifndef I18N_H +#define I18N_H + +#include "config.h" +#include "catalog.h" + +#include <stdio.h> + + +enum MSGCATALOG_ID { + MSG_USAGE_FIRST = 1, + MSG_USAGE_1, + MSG_USAGE_2, + MSG_USAGE_LAST, + MSG_HELP_FIRST, + MSG_HELP_1, + MSG_HELP_2, + MSG_HELP_3, + MSG_HELP_4, + MSG_HELP_5, + MSG_HELP_6, + MSG_HELP_7, + MSG_HELP_8, + MSG_HELP_9, + MSG_HELP_10, + MSG_HELP_11, + MSG_HELP_12, + MSG_HELP_13, + MSG_HELP_14, + MSG_HELP_15, + MSG_HELP_16, + MSG_HELP_17, + MSG_HELP_18, + MSG_HELP_19, + MSG_HELP_20, + MSG_HELP_21, + MSG_HELP_22, + MSG_HELP_23, + MSG_HELP_24, + MSG_HELP_25, + MSG_HELP_26, + MSG_HELP_27, + MSG_HELP_28, + MSG_HELP_LAST, + MSG_VERSION, +}; + + +extern const char *const +MSGS[]; + +#define _(msg_id) s(MSGS, msg_id) + + +#endif diff --git a/src/ircd.js b/src/ircd.js index affc986..6a02bd6 100644 --- a/src/ircd.js +++ b/src/ircd.js @@ -1,11 +1,13 @@ const net = require("node:net"); +const db = require("./db.js"); const server = net.createServer(socket => { socket.write("olar\r\n"); socket.pipe(socket); }); -const app = udsPath => { +const app = async udsPath => { + await db.init(); server.listen(udsPath, () => { console.log("I'm ircd."); }); diff --git a/src/logerr.c b/src/logerr.c new file mode 100644 index 0000000..936bd28 --- /dev/null +++ b/src/logerr.c @@ -0,0 +1,301 @@ +#include "logerr.h" + +#include <stdarg.h> +#include <stdlib.h> + +#ifdef TEST +#include "../tests/tests-lib.h" +#include "../tests/slurp.h" +#include <assert.h> +#include <errno.h> +#include <string.h> +#endif + + +void +vlogerr( + const char *const file, + const char *const function, + const int lineno, + FILE *restrict stream, + const char *restrict format, + ... +) { + (void)fprintf(stream, "%s:%s:%d: ", file, function, lineno); + + va_list args; + va_start(args, format); + (void)vfprintf(stream, format, args); + va_end(args); +} + +#ifdef TEST +static const char *const +FNAME = __FILE__ ".txt"; + +static int +test_vlogerr(void) { + int rc = 0; + + test_start("vlogerr()"); + FILE *file = NULL; + char *str = NULL; + + { + testing("empty varargs"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + vlogerr(__FILE__, __func__, __LINE__, file, + ""); + + const int ret = fclose(file); + file = NULL; + if (ret) { + perror("fclose(file)"); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + perror("slurp_for_tests(FNAME, &str)"); + rc = -1; + goto out; + } + + const char *const expected = + "src/logerr.c:test_vlogerr:54: "; + + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + { + testing("a newline only"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + vlogerr(__FILE__, __func__, __LINE__, file, + "\n"); + + const int ret = fclose(file); + file = NULL; + if (ret) { + perror("fclose(file)"); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + perror("slurp_for_tests(FNAME, &str)"); + rc = -1; + goto out; + } + + const char *const expected = + "src/logerr.c:test_vlogerr:91: \n"; + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + { + testing("static format string"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + vlogerr(__FILE__, __func__, __LINE__, file, + "some static string\n"); + + const int ret = fclose(file); + file = NULL; + if (ret) { + perror("fclose(file)"); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + perror("slurp_for_tests(FNAME, &str)"); + rc = -1; + goto out; + } + + const char *const expected = + "src/logerr.c:test_vlogerr:127: some static string\n"; + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + { + testing("single arg format string"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + vlogerr(__FILE__, __func__, __LINE__, file, + "fn(%s)\n", "an-arg"); + + const int ret = fclose(file); + file = NULL; + if (ret) { + perror("fclose(file)"); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + perror("slurp_for_tests(FNAME, &str)"); + rc = -1; + goto out; + } + + const char *const expected = + "src/logerr.c:test_vlogerr:163: fn(an-arg)\n"; + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + { + testing("multiple format strings"); + + file = fopen(FNAME, "w"); + if (!file) { + perror("fopen(FNAME, \"w\")"); + rc = -1; + goto out; + } + + vlogerr(__FILE__, __func__, __LINE__, file, + "int (%d), string (%s) and char (%c)\n", + 123, + "another-str", + 'z'); + + const int ret = fclose(file); + file = NULL; + if (ret) { + perror("fclose(file)"); + rc = -1; + goto out; + } + + if (slurp_for_tests(FNAME, &str)) { + perror("slurp_for_tests(FNAME, &str)"); + rc = -1; + goto out; + } + + const char *const expected = + "src/logerr.c:test_vlogerr:199: " + "int (123), string (another-str) and char (z)\n"; + assert(strcmp(expected, str) == 0); + + free(str); + str = NULL; + + test_ok(); + } + +out: + if (str) { + free(str); + } + if (file) { + if (fclose(file)) { + perror("fclose(file)"); + rc = -1; + } + } + return rc; +} + +static int +test_logerr(void) { + int rc = 0; + + test_start("logerr()"); + + { + testing("can be called with an empty string"); + + logerr(""); + + test_ok(); + } + { + testing("can be called with a static string"); + + logerr("some err\n"); + + test_ok(); + } + { + testing("can be called with a formatted string"); + + logerr("some err: %s\n", strerror(errno)); + + test_ok(); + } + { + testing("can be called with formatting arguments"); + + logerr("int: %d\nstr: %s\n", 123, "an example string"); + + test_ok(); + } + + return rc; +} + + +int +main(void) { + int rc = 0; + + if (test_vlogerr()) { + perror("test_vlogerr()"); + rc = -1; + goto out; + } + + if (test_logerr()) { + perror("test_logerr()"); + rc = -1; + goto out; + } + +out: + return !!rc; +} +#endif diff --git a/src/logerr.h b/src/logerr.h new file mode 100644 index 0000000..77f98bd --- /dev/null +++ b/src/logerr.h @@ -0,0 +1,23 @@ +#ifndef LOGERR_H +#define LOGERR_H + +#include "config.h" + +#include <stdio.h> + + +void +vlogerr( + + const char *const file, + const char *const function, + const int lineno, + FILE *restrict stream, + const char *restrict format, + ... +); + +#define logerr(...) vlogerr(__FILE__, __func__, __LINE__, stderr, __VA_ARGS__) + + +#endif diff --git a/src/napi-sqlite.c b/src/napi-sqlite.c index 7c0b872..df3b042 100644 --- a/src/napi-sqlite.c +++ b/src/napi-sqlite.c @@ -1,7 +1,18 @@ +#include <stdio.h> + #include <node/node_api.h> +/* +FIXME +static const napi_type_tag SQLITE_DB_TYPE_TAG = { + 0x0e9614d459f746cc, 0x88b814a5dc5c4cf7 +}; +*/ + static napi_value -my_function(napi_env env, napi_callback_info info) { +myfn(napi_env env, napi_callback_info info) { + napi_value ret = NULL; + napi_status status; size_t argc = 1; int number; @@ -11,51 +22,99 @@ my_function(napi_env env, napi_callback_info info) { status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); if (status != napi_ok) { napi_throw_error(env, NULL, "Failed to parse arguments FIXME i18n"); - // FIXME: does execution somehow halt here? Or is it missing a - // return? + goto out; } status = napi_get_value_int32(env, argv[0], &number); if (status != napi_ok) { napi_throw_error(env, NULL, "Invalid number was passed as argument FIXME i18n"); - // FIXME: return? + goto out; } number = number * 2; status = napi_create_int32(env, number, &my_number); if (status != napi_ok) { napi_throw_error(env, NULL, "Unable to create return value FIXME i18n"); - // FIXME: return? + goto out; } + ret = my_number; + +out: + return ret; +} - return my_number; +static napi_value +open(napi_env env, napi_callback_info info) { + (void)env; + (void)info; + return NULL; } static napi_value +close(napi_env env, napi_callback_info info) { + (void)env; + (void)info; + return NULL; +} + +static const struct { + const char *label; + napi_value(*const handle)(napi_env env, napi_callback_info info); +} fns[] = { + { .label = "myfn", .handle = myfn, }, + { .label = "open", .handle = open, }, + { .label = "close", .handle = close, }, + { NULL, NULL }, +}; + +static napi_value init(napi_env env, napi_value exports) { + napi_value ret = exports; + napi_status status; - napi_value fn; - status = napi_create_function(env, NULL, 0, my_function, NULL, &fn); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to wrap native function FIXME i18n"); - // FIXME: return? - } + for (size_t i = 0; fns[i].label && fns[i].handle; i++) { + napi_value fn; + status = napi_create_function( + env, + fns[i].label, + NAPI_AUTO_LENGTH, + fns[i].handle, + "xucrutes", + &fn + ); + if (status != napi_ok) { + ret = NULL; + napi_throw_error( + env, + "SQLITE_FN_CREATE", + "Unable to wrap native function FIXME i18n" + ); + goto out; + } - status = napi_set_named_property(env, exports, "my_function", fn); - if (status != napi_ok) { - napi_throw_error(env, NULL, "Unable to populate exports FIXME i18n"); - // FIXME: return? + status = napi_set_named_property( + env, + exports, + fns[i].label, + fn + ); + if (status != napi_ok) { + ret = NULL; + napi_throw_error( + env, + "SQLITE_FN_SETNAME", + "Unable to populate exports FIXME i18n" + ); + goto out; + } } - return exports; +out: + return ret; } -napi_value -sqlite_napi_init(napi_env env, napi_value exports) { +NAPI_MODULE_INIT() { return init(env, exports); } - -//NAPI_MODULE(NODE_GYP_MODULE_NAME, sqlite_napi_init) -NAPI_MODULE(FIXME_CAN_THIS_BE_ANYTHING, sqlite_napi_init) diff --git a/src/papo.en.msg b/src/papo.en.msg new file mode 100644 index 0000000..f1679a3 --- /dev/null +++ b/src/papo.en.msg @@ -0,0 +1,70 @@ +1 Usage:\n + +2 papo -p FILE [-o DIRECTORY]\n + +3 papo -l FILE [-o DIRECTORY]\n + +4 papo [-hV]\n + +5 \n + +6 \n + +7 Options:\n + +8 -p FILE parser file to be processed\n + +9 -l FILE lexer file to be processed\n + +10 -o DIRECTORY output where to place the\n + +11 generated files (default .)\n + +12 -h, --help show this help message\n + +13 -V, --version print the version number\n + +14 \n + +15 \n + +16 Run the papo(1) parser program.\n + +17 \n + +18 Here is the explanation for what it does, and the synopsis\n + +19 of its usage.\n + +20 \n + +21 See "man papo" for usage information and\n + +22 "man papo.tutorial" for a beginner introduction.\n + +23 \n + +24 \n + +25 Examples:\n + +26 \n + +27 Do a one-line parser:\n + +28 \n + +29 $ papo run md.grammar < README.md\n + +30 \n + +31 \n + +32 Compile the grammer:\n + +33 \n + +34 $ papo build csv.grammar > dunno.alsodunno\n + +35 papo 0.1.0 1970-01-01\n + diff --git a/src/sql/config.sql b/src/sql/config.sql new file mode 100644 index 0000000..53eb279 --- /dev/null +++ b/src/sql/config.sql @@ -0,0 +1,34 @@ +; "Litestream requires periodic but short write locks on the database when +; checkpointing occurs": +; https://litestream.io/tips/#busy-timeout +PRAGMA busy_timeout = 5000; + +; "Litestream only works with the SQLite WAL journaling mode": +; https://litestream.io/tips/#wal-journal-mode +PRAGMA journal_mode = WAL; + +; "(...) change the synchronous mode to NORMAL (it typically defaults to FULL)": +; https://litestream.io/tips/#synchronous-pragma +; "WAL mode is safe from corruption with synchronous=NORMAL": +; https://www.sqlite.org/pragma.html#pragma_synchronous +PRAGMA synchronous = NORMAL; + +; "(...) can perform a checkpoint in between Litestream-initiated checkpoints +; and cause Litestream to miss a WAL file": +; https://litestream.io/tips/#disable-autocheckpoints-for-high-write-load-servers +PRAGMA wal_autocheckpoint = 0; + +; "This pragma does a low-level formatting and consistency check of the +; database": +; https://www.sqlite.org/pragma.html#pragma_integrity_check +PRAGMA integrity_check; + +; "The foreign_key_check pragma checks the database, or the table called + \"table-name\", for foreign key constraints that are violated": +; https://www.sqlite.org/pragma.html#pragma_foreign_key_check +PRAGMA foreign_key_check; + + +CREATE TABLE IF NO EXISTS migrations ( + name TEXT PRIMARY KEY +); diff --git a/tests/assert-deps.sh b/tests/assert-deps.sh index 9bfc5ea..b73933d 100755 --- a/tests/assert-deps.sh +++ b/tests/assert-deps.sh @@ -1,45 +1,10 @@ #!/bin/sh set -eu -if [ ! -e .git ]; then - echo "Not in a Git repository, skipping \"$0\"." >&2 - exit -fi - - . tools/lib.sh -F="$(mkstemp)" -trap 'rm -f "$F"' EXIT - - -awk ' - $0 == "sources.js = \\" { sources = 1; next } - $0 == "tests.js = \\" { tests = 1; next } - sources == 1 && $0 == "" { sources = 2; next } - tests == 1 && $0 == "" { tests = 2; next } - - sources == 1 || tests == 1 { - print $1 - } - - END { - if (sources != 2) { - print "Could not find $(sources.js) in Makefile." \ - > "/dev/stderr" - exit 2 - } - if (tests != 2) { - print "Could not find $(tests.js) in Makefile." \ - > "/dev/stderr" - exit 2 - } - } -' Makefile | LANG=POSIX.UTF-8 sort > "$F" - -printf '%s: all JavaScript sources are listed in the Makefile...' \ - "$(yellow "$0")" >&2 -git ls-files src/*.js tests/js/*.js | - LANG=POSIX.UTF-8 sort | - diff -U10 "$F" - -printf ' %s\n' "$(green 'OK')" >&2 +{ + printf '%s: all deps.mk is up-to-date...' "$(yellow "$0")" + sh mkdeps.sh | diff -U10 deps.mk - + printf ' %s\n' "$(green 'OK')" +} >&2 diff --git a/tests/c-lint.sh b/tests/c-lint.sh new file mode 100755 index 0000000..37822f3 --- /dev/null +++ b/tests/c-lint.sh @@ -0,0 +1,65 @@ +#!/bin/sh +set -eu + +awk ' +BEGIN { + ret = 0 + msg = "function not on the start of the line:" +} + +/^[a-zA-Z0-9_]+ [^=]+\(/ { + if (ret == 0) { + print msg + } + printf "%s:%s:%s\n", FILENAME, FNR, $0 + ret = 1 +} + +END { + exit ret +} +' "$@" + + +awk ' +BEGIN { + ret = 0 + static = 1 + msg = "non-static function is not declared in a header:" +} + +/^[a-zA-Z0-9_]+\(.*$/ && static == 0 { + split($0, line, /\(/) + fn_name = line[1] + if (fn_name != "main" && fn_name != "LLVMFuzzerTestOneInput") { + header = substr(FILENAME, 0, length(FILENAME) - 2) ".h" + if (system("grep -q ^\"" fn_name "\" \"" header "\"")) { + if (ret == 0) { + print msg + } + printf "%s:%s:%s\n", FILENAME, FNR, $0 + ret = 1 + } + } +} + +/^static / { + static = 1 +} + +!/^static / { + static = 0 +} + +END { + exit ret +} +' "$@" + + +RE='[a-z]+\(\) {' +if grep -Eq "$RE" "$@"; then + echo 'Functions with no argument without explicit "void" parameter:' >&2 + grep -En "$RE" "$@" + exit 1 +fi diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh new file mode 100755 index 0000000..a25b66c --- /dev/null +++ b/tests/cli-opts.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +"$@" -V diff --git a/tests/slurp.c b/tests/slurp.c new file mode 100644 index 0000000..683a126 --- /dev/null +++ b/tests/slurp.c @@ -0,0 +1,69 @@ +#include "slurp.h" + +#include <stdio.h> +#include <stdlib.h> + +int +slurp_for_tests(const char *const FNAME, char **strref) { + int rc = 0; + + FILE *file = NULL; + char *str = NULL; + + file = fopen(FNAME, "r"); + if (!file) { + perror("fopen(FNAME, \"r\")"); + rc = -1; + goto out; + } + + if (fseek(file, 0L, SEEK_END)) { + perror("fseek(file, 0L, SEEK_END)"); + rc = -1; + goto out; + } + + const long lsize = ftell(file); + if (lsize == -1) { + perror("ftell(file)"); + rc = -1; + goto out; + } + const size_t size = (size_t)lsize + sizeof(char); + + if (fseek(file, 0L, SEEK_SET)) { + perror("fseek(file, 0L, SEEK_SET)"); + rc = -1; + goto out; + } + + str = malloc(size); + if (!str) { + perror("malloc(...)"); + rc = -1; + goto out; + } + + if (fread(str, sizeof(char), size - 1, file) != size - 1) { + perror("fread(...)"); + rc = -1; + goto out; + } + str[size - 1] = '\0'; + *strref = str; + +out: + if (file) { + if (fclose(file)) { + perror("flcose(file"); + rc = -1; + } + } + + if (rc) { + if (str) { + free(str); + } + } + return rc; +} diff --git a/tests/slurp.h b/tests/slurp.h new file mode 100644 index 0000000..056c77d --- /dev/null +++ b/tests/slurp.h @@ -0,0 +1,11 @@ +#ifndef SLURP_H +#define SLURP_H + +#include "../src/config.h" + + +int +slurp_for_tests(const char *const FNAME, char **strref); + + +#endif diff --git a/tests/tests-lib.c b/tests/tests-lib.c new file mode 100644 index 0000000..e46fb2d --- /dev/null +++ b/tests/tests-lib.c @@ -0,0 +1,28 @@ +#include "tests-lib.h" +#include <stdio.h> +#include <assert.h> + +#define COLOUR_RESET "\033[0m" +#define COLOUR_GREEN "\033[0;32m" +#define COLOUR_YELLOW "\033[0;33m" + +void +test_start(const char *const name) { + assert(fprintf(stdout, "%s:\n", name) > 0); +} + +void +testing(const char *const message) { + assert( + fprintf( + stdout, + COLOUR_YELLOW "testing" COLOUR_RESET ": %s...", + message + ) > 0 + ); +} + +void +test_ok(void) { + assert(fprintf(stdout, " " COLOUR_GREEN "OK" COLOUR_RESET ".\n") > 0); +} diff --git a/tests/tests-lib.h b/tests/tests-lib.h new file mode 100644 index 0000000..a1e67ba --- /dev/null +++ b/tests/tests-lib.h @@ -0,0 +1,15 @@ +#ifndef TESTS_LIB_H +#define TESTS_LIB_H + +#include "../src/config.h" + +void +test_start(const char *const name); + +void +testing(const char *const message); + +void +test_ok(void); + +#endif diff --git a/tools/cdeps.sh b/tools/cdeps.sh new file mode 100755 index 0000000..dd48d74 --- /dev/null +++ b/tools/cdeps.sh @@ -0,0 +1,225 @@ +#!/bin/sh +set -eu + +. tools/lib.sh + +usage() { + cat <<-'EOF' + Usage: + tools/cdeps.sh FILE... + tools/cdeps.sh -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + FILE toplevel entrypoint file + + + Given a list of C FILEs, generate the Makefile dependencies + between them. + + We have 3 types of object files: + - .o: plain object files; + - .lo: object files compiled as relocatable so it can be + included in a shared library; + - .to: compiled with -DTEST so it can expose its embedded unit + tests; + + We also have 2 aggregate files: + - .ta: an ar(1)-chive that includes all the .o dependencies the + $NAME.ta, plus the $NAME.to object. The goal is to have + "dep1.o", "dep2.o", ... "depN.o" included in the archive, + alongside "$NAME.to", so that recompiling "depN.o" would + replace only this file in the archive; + - .t: an executable "$NAME.t" derived from just linking together + all the objects inside a ".ta". Since the "main()" function + was only exposed in the "$NAME.to" via the -DTEST flag, this + executable is the runnable instance of all unit tests present + in "$NAME.c". Its exit code determines if its test suite + execution is successful. + + Also in order to run the unit tests without having to relink + them on each run, we have: + - .t-run: a dedicated virtual target that does nothing but + execute the tests. In order to assert the binaries exist, + each "$NAME.t-run" virtual target depends on the equivalent + "$NAME".t physical target. + + There are 2 types of dependencies that are generated: + 1. self dependencies; + 2. inter dependencies. + + The self dependencies are the ones across different + manifestations of the same file so all derived assets are + correctly kept up-to-date: + - $NAME.o $NAME.lo $NAME.to: $NAME.h + + As the .SUFFIXES rule already covers the dependency to the + orinal $NAME.c file, all we do is say that whenever the public + interface of these binaries change, they need to be + recompiled; + - $NAME.ta: $NAME.to + + We make sure to include in each test archive (ta) file its own + binary with unit tests. We include the "depN.o" dependencies + later; + - $NAME.t-run: $NAME.t + + Enforce that the binary exists before we run them. + + After we establish the self dependencies, we scrub each file's + content looking for `#include "..."` lines that denote + dependency to other C file. Once we do that we'll have: + - $NAME.o $NAME.lo $NAME.to: dep1.h dep2.h ... depN.h + + We'll recompile our file when its public header changes. When + only the body of the code changes we don't recompile, only + later relink; + - $NAME.ta: dep1.o dep2.o ... depN.o + + Make sure to include all required dependencies in the $NAME.t + binary so that the later linking works properly. + + So if we have file1.c, file2.c and file3.c with their respective + headers, where file2.c and file3.c depend of file1.c, i.e. they + have `#include "file.h"` in their code, and file3.c depend of + file2.c, the expected output is: + + file1.o file1.lo file1.to: file1.h + file2.o file2.lo file2.to: file2.h + file3.o file3.lo file3.to: file3.h + + file1.ta: file1.to + file2.ta: file2.to + file3.ta: file3.to + + file1.t-run: file1.t + file2.t-run: file2.t + file3.t-run: file3.t + + + file1.o file1.lo file1.to: + file2.o file2.lo file2.to: file1.h + file3.o file3.lo file3.to: file1.h file2.h + + file1.ta: + file2.ta: file1.o + file3.ta: file1.o file2.o + + This ensures that only the minimal amount of files need to get + recompiled, but no less. + + + Examples: + + Get deps for all files in 'src/' but 'src/main.c': + + $ sh tools/cdeps.sh `find src/*.c -not -name 'main.c'` + + + Emit dependencies for all C files in a Git repository: + + $ sh tools/cdeps.sh `git ls-files | grep '\.c$'` + EOF +} + + +for flag in "$@"; do + case "$flag" in + (--) + break + ;; + (--help) + usage + help + exit + ;; + (*) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + (h) + usage + help + exit + ;; + (*) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +FILE="${1:-}" +eval "$(assert_arg "$FILE" 'FILE')" + + + +each_f() { + fn="$1" + shift + for file in "$@"; do + f="${file%.c}" + "$fn" "$f" + done + printf '\n' +} + +self_header_deps() { + printf '%s.o\t%s.lo\t%s.to:\t%s.h\n' "$1" "$1" "$1" "$1" +} + +self_ta_deps() { + printf '%s.ta:\t%s.to\n' "$1" "$1" +} + +self_trun_deps() { + printf '%s.t-run:\t%s.t\n' "$1" "$1" +} + +deps_for() { + ext="$2" + for file in $(awk -F'"' '/^#include "/ { print $2 }' "$1.c"); do + if [ "$file" = 'config.h' ]; then + continue + fi + if [ "$(basename "$file")" = 'tests-lib.h' ]; then + continue + fi + f="$(dirname "$1")/$file" + if [ "$f" = "$1.h" ]; then + continue + fi + printf '%s\n' "${f%.h}$2" + done +} + +rebuild_deps() { + printf '\n' + printf '%s.o\t%s.lo\t%s.to:' "$1" "$1" "$1" + printf ' %s' $(deps_for "$1" .h) | sed 's|^ $||' +} + +archive_deps() { + printf '\n' + printf '%s.ta:' "$1" + printf ' %s' $(deps_for "$1" .o) | sed 's|^ $||' +} + + +each_f self_header_deps "$@" +each_f self_ta_deps "$@" +each_f self_trun_deps "$@" + +each_f rebuild_deps "$@" +each_f archive_deps "$@" |