diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-17 13:47:41 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-17 13:47:41 +0800 |
| commit | ff67f703fbdbb4d0239594213dfba6b6b0dcc702 (patch) | |
| tree | f289fd296fe130bee20c4b6c9a0411ed2654ebbb | |
| parent | 6a133b95c137dd1decfea4af9f66ecc7073baeb4 (diff) | |
build.c: the build script without sh or make
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | build.c | 736 |
2 files changed, 739 insertions, 0 deletions
@@ -1 +1,4 @@ obj/ +build +build.exe +build.exe.old @@ -0,0 +1,736 @@ +/* SPDX-License-Identifier: MIT */ +/* + * build.c, the engine's build program. it does what build.sh and the Makefile do without needing sh or make, only a C compiler: + * + * cc -o build build.c + * ./build [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...] + * + * it rebuilds itself when this file or kon/konbuild.h changes + */ + +#define _POSIX_C_SOURCE 200809L +#define KONBUILD_IMPLEMENTATION +#include "kon/konbuild.h" + +#define PROGRAM "konengine" +#define SRCDIR "src" +#define KONDIR "kon" +#define EXTDIR "ext" + +/*** modules ***/ + +typedef struct module +{ + const char *name; + const char *needs; /* the modules it cannot do without, separated by spaces */ +} module_t; + +static const module_t known[] = { + {"font", ""}, {"toml", ""}, {"tile", ""}, {"pack", ""}, {"ui", "font"}, {"debug", "font assets"}, + {"assets", "pack"}, {"image", ""}, {"audio", ""}, {"mic", ""}, {"sound", "audio"}, {"model", "assets"}, + {"2d", ""}, {"fx", "2d"}, {"particles", ""}, {"picture", "2d"}, {"rm", ""}, {"rmmap", ""}, {"rmtile", ""}, + {"rmdraw", "rmmap rmtile"}, {"rmfield", "rmmap"}, {"rmplugin", ""}, {"rmdata", "battle party save"}, + {"script", ""}, {"battle", ""}, {"party", "battle save"}, {"save", ""}, {"stage", "2d"}, {"textbox", "2d font"}, + {"platformer", "tile"}, {"rpg", "tile"}, {"3d", "tile"}, {"sector", ""}, {"world", "3d sector tile"} +}; +#define KNOWN (sizeof(known) / sizeof(known[0])) + +#define MODULES_MAX 64 + +static const char *chosen[MODULES_MAX]; +static int chosenCount; + +/* the next word of a string cut at sep, writing a 0 over the separator. strtok keeps its place in a hidden variable, and these are nested */ +static char *word(char **at, char sep) +{ + char *start = *at, *end; + + while (start && *start == sep) start++; + if (!start || !*start) { + *at = NULL; + return NULL; + } + end = strchr(start, sep); + if (end) { + *end = 0; + *at = end + 1; + } else { + *at = NULL; + } + return start; +} + +static const module_t *findModule(const char *name) +{ + size_t i; + + for (i = 0; i < KNOWN; i++) { + if (strcmp(known[i].name, name) == 0) return &known[i]; + } + return NULL; +} + +static int hasModule(const char *name) +{ + int i; + + for (i = 0; i < chosenCount; i++) { + if (strcmp(chosen[i], name) == 0) return 1; + } + return 0; +} + +static void addModule(const char *name); + +static void addNeeds(const char *needs) +{ + char *copy = kon_fmt("%s", needs), *w; + + while ((w = word(©, ' ')) != NULL) addModule(w); +} + +static void addModule(const char *name) +{ + const module_t *m = findModule(name); + + if (!m) { + fprintf(stderr, "build: unknown module '%s'\n", name); + exit(1); + } + if (hasModule(m->name)) return; + if (chosenCount >= MODULES_MAX) { + fprintf(stderr, "build: too many modules\n"); + exit(1); + } + chosen[chosenCount++] = m->name; + addNeeds(m->needs); +} + +static void usage(void) +{ + size_t i; + + fprintf(stderr, "usage: build [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...]\n"); + fprintf(stderr, "modules: "); + for (i = 0; i < KNOWN; i++) fprintf(stderr, " %s", known[i].name); + fprintf(stderr, "\nplatforms: native linux bsd windows\n"); + fprintf(stderr, "targets: all run clean tools pack examples\n"); + exit(1); +} + +/*** settings ***/ + +static const char *platform = "native"; +static const char *objdir = "obj"; +static int debug, developer; +static const char *exe = ""; +static const char *compiler; +static KonCmd modeFlags, defs, libs; +static long long newestHeader; +static const char *flagsFile; + +static void push(KonCmd *c, const char *s) +{ + kon_pushCmd(c, (char *)s, NULL); +} + +static void pushAll(KonCmd *to, const KonCmd *from) +{ + size_t i; + + for (i = 0; i < from->count; i++) push(to, from->items[i]); +} + +static void pushWords(KonCmd *to, const char *words) +{ + char *copy = kon_fmt("%s", words), *w; + + while ((w = word(©, ' ')) != NULL) push(to, w); +} + +static char *upper(const char *s) +{ + char *u = kon_fmt("%s", s), *p; + + for (p = u; *p; p++) { + if (*p >= 'a' && *p <= 'z') *p = (char)(*p - 'a' + 'A'); + } + return u; +} + +static char *objPath(const char *name) +{ + return kon_fmt("%s/%s.o", objdir, name); +} + +static char *exePath(const char *name) +{ + return kon_fmt("%s/%s%s", objdir, name, exe); +} + +/*** compiling ***/ + +/* the warnings the engine's own code is held to. the stb files and miniaudio are not ours, they get -w */ +static void ourFlags(KonCmd *c, int withDefs) +{ + push(c, "-std=c99"); + push(c, "-Wall"); + push(c, "-Wextra"); + push(c, "-Wpedantic"); + pushAll(c, &modeFlags); + if (withDefs) pushAll(c, &defs); + push(c, "-I" KONDIR); + push(c, "-I" EXTDIR); +} + +static void extFlags(KonCmd *c) +{ + push(c, "-std=gnu99"); + push(c, "-w"); + pushAll(c, &modeFlags); + pushAll(c, &defs); + push(c, "-I" EXTDIR); +} + +static int stale(const char *output, const char **inputs, size_t count, int headers) +{ + long long m = kon_mtime(output); + + if (m < 0) return 1; + if (headers && newestHeader > m) return 1; + return kon_needsRebuild(output, inputs, count); +} + +static void run(KonCmd *c) +{ + if (!kon_runCmdSync(c)) exit(1); + kon_freeCmd(c); +} + +/* returns 1 when it compiled something */ +static int compile(const char *source, const char *object, int ext, const KonCmd *extra) +{ + KonCmd c = {0}; + const char *inputs[1]; + + inputs[0] = source; + if (!stale(object, inputs, 1, 1)) return 0; + kon_mkdirs(objdir); + printf("# compile %s\n", object); + fflush(stdout); + push(&c, compiler); + if (ext) extFlags(&c); + else ourFlags(&c, 1); + if (extra) pushAll(&c, extra); + push(&c, "-c"); + push(&c, source); + push(&c, "-o"); + push(&c, object); + run(&c); + return 1; +} + +static int linkProgram(const char *output, const char **objects, size_t count, const KonCmd *flags, int withLibs) +{ + KonCmd c = {0}; + size_t i; + + if (!stale(output, objects, count, 0)) return 0; + printf("# link %s\n", output); + fflush(stdout); + push(&c, compiler); + push(&c, "-o"); + push(&c, output); + if (flags) pushAll(&c, flags); + for (i = 0; i < count; i++) push(&c, objects[i]); + if (withLibs) pushAll(&c, &libs); + run(&c); + return 1; +} + +/*** what goes into the program ***/ + +#define OBJECTS_MAX 128 + +typedef struct unit +{ + const char *name; /* the object, without the folder and the .o */ + const char *source; + int ext; +} unit_t; + +static unit_t units[OBJECTS_MAX]; +static int unitCount; + +static void addUnit(const char *name, const char *source, int ext) +{ + int i; + + for (i = 0; i < unitCount; i++) { + if (strcmp(units[i].name, name) == 0) return; + } + if (unitCount >= OBJECTS_MAX) { + fprintf(stderr, "build: too many files\n"); + exit(1); + } + units[unitCount].name = name; + units[unitCount].source = source; + units[unitCount].ext = ext; + unitCount++; +} + +/* a library that is a single header has its implementation in src/impl, a module has its .c next to its header */ +static void addKon(const char *name) +{ + const char *impl = kon_fmt("%s/impl/%s.c", SRCDIR, name); + + addUnit(name, kon_exists(impl) ? impl : kon_fmt("%s/%s.c", KONDIR, name), 0); +} + +static void addModuleUnits(const char *m) +{ + if (strcmp(m, "assets") == 0) { + addKon("konassets"); + } else if (strcmp(m, "model") == 0) { + addKon("konmodel"); + addKon("konassets"); + } else if (strcmp(m, "image") == 0) { + addUnit("stb_image", SRCDIR "/impl/stb_image.c", 1); + } else if (strcmp(m, "audio") == 0 || strcmp(m, "mic") == 0) { + addUnit("miniaudio", SRCDIR "/impl/miniaudio.c", 1); + addKon(kon_fmt("kon%s", m)); + if (strcmp(m, "audio") == 0 && kon_exists(EXTDIR "/stb_vorbis.c")) addUnit("stb_vorbis", EXTDIR "/stb_vorbis.c", 1); + } else { + addKon(kon_fmt("kon%s", m)); + } +} + +static void setup(void) +{ + int i, native = 0; + const char *x11 = ""; + KonCmd platformDefs = {0}; + +#if defined(_WIN32) + const char *here = "windows"; +#elif defined(__linux__) + const char *here = "linux"; +#else + const char *here = "bsd"; +#endif + + if (strcmp(platform, "native") == 0) platform = here; + native = strcmp(platform, here) == 0; + + if (strcmp(platform, "linux") == 0 || strcmp(platform, "bsd") == 0) { + push(&libs, "-lX11"); + push(&libs, "-lXext"); + push(&libs, "-lXrandr"); + push(&libs, "-lm"); + push(&platformDefs, "-D_POSIX_C_SOURCE=200809L"); +#if defined(__NetBSD__) + x11 = "/usr/X11R7"; +#elif defined(__OpenBSD__) + x11 = "/usr/X11R6"; +#elif defined(__FreeBSD__) || defined(__DragonFly__) + x11 = "/usr/local"; +#endif + if (*x11 && native) { + push(&platformDefs, kon_fmt("-I%s/include", x11)); + libs.count = 0; + push(&libs, kon_fmt("-L%s/lib", x11)); + push(&libs, kon_fmt("-Wl,-R%s/lib", x11)); + push(&libs, "-lX11"); + push(&libs, "-lXext"); + push(&libs, "-lXrandr"); + push(&libs, "-lm"); + } + } else if (strcmp(platform, "windows") == 0) { + push(&libs, "-lgdi32"); + push(&libs, "-lm"); + exe = ".exe"; + } else { + fprintf(stderr, "build: unknown platform '%s'\n", platform); + usage(); + } + + compiler = getenv("CC"); + if (!compiler || !*compiler) { + if (strcmp(platform, "windows") == 0) compiler = native ? "gcc" : "x86_64-w64-mingw32-gcc"; + else compiler = "cc"; + } + + if (debug) { + push(&modeFlags, "-g"); + push(&modeFlags, "-O1"); + push(&modeFlags, "-DKON_DEBUG"); + developer = 1; + } else { + push(&modeFlags, "-O2"); + push(&modeFlags, "-DNDEBUG"); + } + if (developer) push(&modeFlags, "-DKON_DEVELOPER"); + + /* the modules are all in, with what they need */ + pushAll(&defs, &platformDefs); + kon_freeCmd(&platformDefs); + for (i = 0; i < chosenCount; i++) push(&defs, kon_fmt("-DKON_MODULE_%s", upper(chosen[i]))); + if (hasModule("3d") || hasModule("model") || hasModule("sector")) push(&defs, "-DKONSOFREN_3D"); + if (hasModule("audio") && kon_exists(EXTDIR "/stb_vorbis.c")) push(&defs, "-DKON_VORBIS"); + if (hasModule("audio") || hasModule("mic")) { + if (strcmp(platform, "linux") == 0) { + push(&libs, "-lpthread"); + push(&libs, "-ldl"); + } else if (strcmp(platform, "bsd") == 0) { + push(&libs, "-lpthread"); + } + } + + addUnit("engine", SRCDIR "/engine.c", 0); + addUnit("konwinlib", SRCDIR "/impl/konwinlib.c", 0); + addUnit("konsofren", SRCDIR "/impl/konsofren.c", 0); + addUnit("kontime", SRCDIR "/impl/kontime.c", 0); + addUnit("koninput", SRCDIR "/impl/koninput.c", 0); + addUnit("konprogram", KONDIR "/konprogram.c", 0); + for (i = 0; i < chosenCount; i++) addModuleUnits(chosen[i]); + + { + long long a = kon_newestIn(KONDIR, ".h"), b = kon_newestIn(EXTDIR, ".h"); + + newestHeader = a > b ? a : b; + } +} + +/* what was built with, so a changed flag or module list starts the objects again */ +static void checkFlags(void) +{ + KonCmd all = {0}; + char *signature; + size_t i, size = 1; + FILE *f; + char old[8192]; + + push(&all, compiler); + pushAll(&all, &modeFlags); + pushAll(&all, &defs); + pushAll(&all, &libs); + for (i = 0; i < (size_t)unitCount; i++) push(&all, units[i].name); + for (i = 0; i < all.count; i++) size += strlen(all.items[i]) + 1; + signature = (char *)malloc(size); + if (!signature) exit(1); + signature[0] = '\0'; + for (i = 0; i < all.count; i++) { + strcat(signature, all.items[i]); + strcat(signature, " "); + } + kon_freeCmd(&all); + + kon_mkdirs(objdir); + flagsFile = kon_fmt("%s/.flags", objdir); + old[0] = '\0'; + f = fopen(flagsFile, "r"); + if (f) { + size_t n = fread(old, 1, sizeof(old) - 1, f); + + old[n] = '\0'; + fclose(f); + } + if (strcmp(old, signature) != 0) { + KonList list = {0}; + + if (kon_listFiles(objdir, &list)) { + for (i = 0; i < list.count; i++) { + size_t n = strlen(list.items[i]); + + if (n > 2 && strcmp(list.items[i] + n - 2, ".o") == 0) kon_remove(list.items[i]); + } + kon_freeList(&list); + } + f = fopen(flagsFile, "w"); + if (f) { + fputs(signature, f); + fclose(f); + } + } + free(signature); +} + +/*** targets ***/ + +static const char *objects[OBJECTS_MAX + 16]; +static int objectCount; + +static void buildObjects(int withEngine) +{ + int i; + + objectCount = 0; + for (i = 0; i < unitCount; i++) { + const char *obj = objPath(units[i].name); + + if (!withEngine && strcmp(units[i].name, "engine") == 0) continue; + compile(units[i].source, obj, units[i].ext, NULL); + objects[objectCount++] = obj; + } +} + +static void targetAll(void) +{ + buildObjects(1); + linkProgram(exePath(PROGRAM), objects, (size_t)objectCount, NULL, 1); +} + +static void targetRun(void) +{ + KonCmd c = {0}; + + targetAll(); + push(&c, exePath(PROGRAM)); + if (!kon_runCmdSync(&c)) exit(1); + kon_freeCmd(&c); +} + +static const char *examples[] = {"modelview", "sound", "ui", "platform", "dialog", "script", "corridor", "hills", "sectorview"}; + +static void targetExamples(void) +{ + size_t i; + + buildObjects(0); + for (i = 0; i < sizeof(examples) / sizeof(examples[0]); i++) { + const char *obj = objPath(examples[i]); + const char *src = kon_fmt("examples/%s.c", examples[i]); + const char *all[OBJECTS_MAX + 16]; + int n = 0, k; + + compile(src, obj, 0, NULL); + all[n++] = obj; + for (k = 0; k < objectCount; k++) all[n++] = objects[k]; + linkProgram(exePath(examples[i]), all, (size_t)n, NULL, 1); + } +} + +/* a program made straight from some files, with the headers they read checked too. the converters do not link the engine */ +static void tool(const char *name, const char **sources, size_t count, const KonCmd *flags, int withEngineFlags, int withMath) +{ + KonCmd c = {0}; + const char *out = exePath(name); + size_t i; + + if (!stale(out, sources, count, 1) && kon_newestIn("tools", ".h") <= kon_mtime(out)) return; + kon_mkdirs(objdir); + printf("# link %s\n", out); + fflush(stdout); + push(&c, compiler); + ourFlags(&c, withEngineFlags); + if (flags) pushAll(&c, flags); + push(&c, "-o"); + push(&c, out); + for (i = 0; i < count; i++) push(&c, sources[i]); + if (withMath) push(&c, "-lm"); + run(&c); +} + +static void targetTools(void) +{ + KonCmd extra = {0}; + const char *cgltfObj = objPath("cgltf"); + const char *s[8]; + KonCmd cg = {0}; + + /* the packer uses the module flags, the others are built without them */ + push(&extra, "-DKONPACK_WRITER"); + s[0] = "tools/konpack.c"; + s[1] = KONDIR "/konpack.c"; + tool("konpack", s, 2, &extra, 1, 0); + kon_freeCmd(&extra); + + { + const char *in[1]; + + in[0] = "tools/cgltf.c"; + if (stale(cgltfObj, in, 1, 0)) { + printf("# compile %s\n", cgltfObj); + fflush(stdout); + kon_mkdirs(objdir); + push(&cg, compiler); + push(&cg, "-std=gnu99"); + push(&cg, "-w"); + pushAll(&cg, &modeFlags); + push(&cg, "-I" EXTDIR); + push(&cg, "-c"); + push(&cg, "tools/cgltf.c"); + push(&cg, "-o"); + push(&cg, cgltfObj); + run(&cg); + } + } + push(&extra, "-DKONSOFREN_3D"); + push(&extra, "-DKONMODEL_WRITER"); + s[0] = "tools/konmodel.c"; + s[1] = KONDIR "/konmodel.c"; + s[2] = KONDIR "/konassets.c"; + s[3] = KONDIR "/konpack.c"; + s[4] = SRCDIR "/impl/konsofren.c"; + s[5] = cgltfObj; + tool("konmodel", s, 6, &extra, 0, 1); + kon_freeCmd(&extra); + + s[0] = "tools/mzsurvey.c"; + tool("mzsurvey", s, 1, NULL, 0, 0); + + push(&extra, "-Itools"); + s[0] = "tools/mzscript.c"; + s[1] = KONDIR "/konscript.c"; + tool("mzscript", s, 2, &extra, 0, 0); + s[0] = "tools/kons2c.c"; + tool("kons2c", s, 2, &extra, 0, 0); + kon_freeCmd(&extra); + + s[0] = "tools/mzmap.c"; + tool("mzmap", s, 1, NULL, 0, 0); + + s[0] = "tools/mzdata.c"; + s[1] = KONDIR "/konbattle.c"; + tool("mzdata", s, 2, NULL, 0, 1); +} + +static void targetPack(void) +{ + KonList list = {0}; + KonCmd c = {0}; + const char *assets = getenv("ASSETDIR") ? getenv("ASSETDIR") : "assets"; + const char *listFile = kon_fmt("%s/assets.lst", objdir); + const char *packFile = kon_fmt("%s/data.kpk", objdir); + const char *extra = getenv("PACKFLAGS"); + FILE *f; + size_t i; + + targetTools(); + if (!kon_listFiles(assets, &list)) { + fprintf(stderr, "build: cannot read %s\n", assets); + exit(1); + } + f = fopen(listFile, "w"); + if (!f) { + fprintf(stderr, "build: cannot write %s\n", listFile); + exit(1); + } + for (i = 0; i < list.count; i++) fprintf(f, "%s\n", list.items[i]); + fclose(f); + kon_freeList(&list); + + push(&c, exePath("konpack")); + push(&c, "-c"); + push(&c, packFile); + if (extra && *extra) pushWords(&c, extra); + push(&c, "-b"); + push(&c, assets); + push(&c, "-l"); + push(&c, listFile); + run(&c); +} + +static void removeFile(const char *path) +{ + if (kon_exists(path)) { + printf("remove %s\n", path); + kon_remove(path); + } +} + +static void targetClean(void) +{ + static const char *tools[] = {"konpack", "konmodel", "mzsurvey", "mzscript", "kons2c", "mzmap", "mzdata"}; + KonList list = {0}; + size_t i; + + if (kon_listFiles(objdir, &list)) { + for (i = 0; i < list.count; i++) { + size_t n = strlen(list.items[i]); + + if (n > 2 && strcmp(list.items[i] + n - 2, ".o") == 0) kon_remove(list.items[i]); + } + kon_freeList(&list); + } + removeFile(exePath(PROGRAM)); + for (i = 0; i < sizeof(tools) / sizeof(tools[0]); i++) removeFile(exePath(tools[i])); + for (i = 0; i < sizeof(examples) / sizeof(examples[0]); i++) removeFile(exePath(examples[i])); + removeFile(kon_fmt("%s/data.kpk", objdir)); + removeFile(kon_fmt("%s/assets.lst", objdir)); +} + +/*** main ***/ + +static void addModuleList(const char *list) +{ + char *copy = kon_fmt("%s", list), *w; + + while ((w = word(©, ',')) != NULL) addModule(w); +} + +int main(int argc, char **argv) +{ + const char *targets[16]; + int targetCount = 0, i; + + KON_REBUILDSELF(argc, argv); + setvbuf(stdout, NULL, _IOLBF, 0); + + for (i = 1; i < argc; i++) { + const char *a = argv[i]; + + if (a[0] != '-' || a[1] == '\0') { + if (targetCount >= 16) usage(); + targets[targetCount++] = a; + continue; + } + { + const char *p; + + for (p = a + 1; *p; p++) { + if (*p == 'd') { + debug = 1; + } else if (*p == 'D') { + developer = 1; + } else if (*p == 'p' || *p == 'm' || *p == 'o') { + const char *value = p[1] ? p + 1 : (i + 1 < argc ? argv[++i] : NULL); + + if (!value) usage(); + if (*p == 'p') platform = value; + else if (*p == 'o') objdir = value; + else addModuleList(value); + break; + } else { + usage(); + } + } + } + } + if (targetCount == 0) targets[targetCount++] = "all"; + + setup(); + if (hasModule("debug") && !(debug || developer)) printf("note: debug module does nothing without -d or -D\n"); + printf("===> platform %s, %s build, modules:", platform, debug ? "debug" : "release"); + if (chosenCount == 0) printf(" none"); + for (i = 0; i < chosenCount; i++) printf(" %s", chosen[i]); + printf("\n"); + checkFlags(); + + for (i = 0; i < targetCount; i++) { + const char *t = targets[i]; + + if (strcmp(t, "all") == 0) targetAll(); + else if (strcmp(t, "run") == 0) targetRun(); + else if (strcmp(t, "clean") == 0) targetClean(); + else if (strcmp(t, "tools") == 0) targetTools(); + else if (strcmp(t, "pack") == 0) targetPack(); + else if (strcmp(t, "examples") == 0) targetExamples(); + else { + fprintf(stderr, "build: unknown target '%s'\n", t); + usage(); + } + } + kon_freeTemp(); + return 0; +} |
