// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // basBuild.c -- shared "compile a project into an .app" back end // // See basBuild.h for the public contract. This pipeline used to live // inline in two places: ideMain.c (the IDE's Make Executable path) and // bascomp.c (the standalone command-line compiler). Both implementations // were essentially identical, so they are consolidated here. // // Pipeline: // 1. Strip comments from every .frm text. // 2. Serialize the module; release builds deserialize a private copy, // strip its debug info, obfuscate form/control names (rewriting the // .frm texts too), compact the bytecode and re-serialize. // 3. Debug builds serialize the debug info. // 4. Extract the STUB resource from selfPath and write it as outPath. // 5. Append the resource set in canonical order: // BAS_RES_NAME (always, falls back to BAS_DEFAULT_APP_NAME) // BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION // BAS_RES_ICON32 (project icon, else the NOICON fallback) // BAS_RES_HELPFILE (basename only) // BAS_RES_MODULE, BAS_RES_DEBUG (debug builds) // FORM0, FORM1, ... cut at the end of the outer Begin Form block // so the .frm's BASIC code section (already compiled into // MODULE) never ships as a resource // 6. Copy the help file next to outPath. #include "basBuild.h" #include "basRes.h" #include "compiler/compact.h" #include "compiler/obfuscate.h" #include "compiler/strip.h" #include "runtime/serialize.h" #include "dvxRes.h" #include "dvxTypes.h" #include "dvxPlat.h" #include "stb_ds_wrap.h" #include #include #include #include // Slack added to a .frm buffer for basStripFrmComments output. #define BAS_BUILD_STRIP_MARGIN 16 // Generic resource-append failure (disk full, I/O error). #define BAS_BUILD_ERR_WRITE_RES "Failed writing resources to output file." // Longest progress line handed to the log callback. #define BAS_BUILD_LOG_BUF 128 // Separator used when joining project-relative paths. '/' is accepted by // DJGPP as well as every host OS bascomp runs on; DVX_PATH_SEP is not. #define BAS_BUILD_SEP "/" // Growth unit of the project source buffer. #define BAS_BUILD_SOURCE_CHUNK 8192 // Room for a BEGINFORM "name" / BEGINCLASS "name" line. #define BAS_BUILD_WRAP_SLACK (BAS_MAX_IDENT + 32) // Longest [Modules]/[Forms] key, "File" plus a decimal index. #define BAS_BUILD_KEY_LEN 32 // emitIcon's error text when it names the unreadable icon path (the // other build errors are string literals) static char sIconError[BAS_BUILD_ERROR_LEN]; // basBuildConcatProject passes, in source order. typedef enum { BAS_BUILD_PASS_MODULES, BAS_BUILD_PASS_CLASSES, BAS_BUILD_PASS_FORMS, BAS_BUILD_PASS_COUNT } BasBuildPassE; // The directives around a form's and a class module's code. static const char *const sWrapBegin[] = { [BAS_BUILD_WRAP_FORM] = "BEGINFORM", [BAS_BUILD_WRAP_CLASS] = "BEGINCLASS" }; static const char *const sWrapEnd[] = { [BAS_BUILD_WRAP_FORM] = "ENDFORM\n", [BAS_BUILD_WRAP_CLASS] = "ENDCLASS\n" }; // Function prototypes (alphabetical) static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len); static int32_t appendText(const char *path, const char *name, const char *value); const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec); bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, int32_t *textLine, int32_t *textLines); bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out); void basBuildDirOf(const char *path, char *out, int32_t outSize); void basBuildFreeProject(BasProjectSourceT *out); bool basBuildMapLine(const BasSourceMapT *map, int32_t line, const char **path, int32_t *fileLine); bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projectDir, int32_t projectDirSize); static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...); static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec); static int32_t countLines(const char *text, int32_t len); static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec); static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount); static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms); static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen); static const char *writeStub(const char *outPath, const BasBuildSpecT *spec); // Appends text to the project source and makes sure it ends on a newline // so the next file starts on a fresh line. static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len) { int32_t need = ps->sourceLen + len + 2; if (need > ps->sourceCap) { int32_t newCap = ps->sourceCap ? ps->sourceCap : BAS_BUILD_SOURCE_CHUNK; while (newCap < need) { newCap *= 2; } char *grown = (char *)realloc(ps->source, (size_t)newCap); if (!grown) { snprintf(ps->error, sizeof(ps->error), "Out of memory."); return false; } ps->source = grown; ps->sourceCap = newCap; } memcpy(ps->source + ps->sourceLen, text, (size_t)len); ps->sourceLen += len; for (int32_t i = 0; i < len; i++) { if (text[i] == '\n') { ps->lineCount++; } } if (ps->sourceLen > 0 && ps->source[ps->sourceLen - 1] != '\n') { ps->source[ps->sourceLen++] = '\n'; ps->lineCount++; } ps->source[ps->sourceLen] = '\0'; return true; } static int32_t appendText(const char *path, const char *name, const char *value) { if (!value || !value[0]) { return 0; } return dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1); } const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) { if (!outPath || !spec || !spec->module || !spec->selfPath) { return "Invalid build request."; } // Strip comments from every .frm text. Comments are source-only and // must not ship in the embedded resource for either build mode. uint8_t **frmData = NULL; // stb_ds: stripped form text int32_t *frmLens = NULL; // stb_ds: length of stripped form text BasObfFrmT *obfFrms = NULL; // stb_ds: obfuscated variants (release) for (int32_t i = 0; i < spec->frmCount; i++) { const char *src = spec->frmSources[i]; if (!src) { continue; } int32_t srcLen = (int32_t)strlen(src); int32_t stripCap = srcLen + BAS_BUILD_STRIP_MARGIN; uint8_t *stripped = (uint8_t *)malloc(stripCap); // A form silently missing from the app would only surface as a // LoadForm failure at runtime, so a failed allocation fails the // build. if (!stripped) { freeFrmWork(frmData, frmLens, obfFrms); return "Out of memory."; } int32_t strippedLen = basStripFrmComments(src, srcLen, stripped, stripCap); BasObfFrmT empty = { NULL, 0 }; arrput(frmData, stripped); arrput(frmLens, strippedLen); arrput(obfFrms, empty); } int32_t frmCount = (int32_t)arrlen(frmData); uint8_t *modData = NULL; int32_t modLen = 0; uint8_t *dbgData = NULL; int32_t dbgLen = 0; const char *failure = prepareModule(spec, frmData, frmLens, obfFrms, frmCount, &modData, &modLen, &dbgData, &dbgLen); if (!failure) { failure = writeStub(outPath, spec); } if (!failure) { failure = emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount); } if (!failure) { failure = copyHelpFile(outPath, spec); } free(modData); free(dbgData); freeFrmWork(frmData, frmLens, obfFrms); return failure; } bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, int32_t *textLine, int32_t *textLines) { if (wrap != BAS_BUILD_WRAP_NONE) { char line[BAS_BUILD_WRAP_SLACK]; int32_t n = snprintf(line, sizeof(line), "%s \"%s\"\n", sWrapBegin[wrap], name); if (n < 0 || n >= (int32_t)sizeof(line)) { snprintf(ps->error, sizeof(ps->error), "Name too long: %s", name); return false; } if (!appendSource(ps, line, n)) { return false; } } int32_t startLine = ps->lineCount + 1; if (!appendSource(ps, text, len)) { return false; } if (textLine) { *textLine = startLine; } if (textLines) { *textLines = ps->lineCount + 1 - startLine; } if (wrap != BAS_BUILD_WRAP_NONE) { return appendSource(ps, sWrapEnd[wrap], (int32_t)strlen(sWrapEnd[wrap])); } return true; } bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out) { static const char *const sections[BAS_BUILD_PASS_COUNT] = { [BAS_BUILD_PASS_MODULES] = BAS_INI_SECTION_MODULES, [BAS_BUILD_PASS_CLASSES] = BAS_INI_SECTION_CLASSES, [BAS_BUILD_PASS_FORMS] = BAS_INI_SECTION_FORMS }; memset(out, 0, sizeof(*out)); out->optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false); // .bas modules first, then .cls class modules, then .frm code sections. for (int32_t pass = 0; pass < BAS_BUILD_PASS_COUNT; pass++) { bool isForm = (pass == BAS_BUILD_PASS_FORMS); bool isClass = (pass == BAS_BUILD_PASS_CLASSES); for (int32_t i = 0; ; i++) { char key[BAS_BUILD_KEY_LEN]; snprintf(key, sizeof(key), "File%d", (int)i); const char *val = prefsGetString(prefs, sections[pass], key, NULL); if (!val) { break; } BasSourceMapT entry; memset(&entry, 0, sizeof(entry)); snprintf(entry.path, sizeof(entry.path), "%s%s%s", projectDir, BAS_BUILD_SEP, val); int32_t srcLen = 0; char *src = platformReadFile(entry.path, &srcLen); if (!src) { snprintf(out->error, sizeof(out->error), "Cannot read %s", entry.path); return false; } const char *code = src; char formName[BAS_MAX_IDENT] = ""; BasBuildWrapE wrap = BAS_BUILD_WRAP_NONE; entry.firstFileLine = 1; if (isClass) { // The class code follows the .cls header if (!basExtractClassName(src, srcLen, val, formName, BAS_MAX_IDENT)) { snprintf(out->error, sizeof(out->error), "No class name for %s", entry.path); free(src); return false; } int32_t codeOff = basClassCodePos(src, srcLen); code = src + codeOff; entry.firstFileLine = countLines(src, codeOff) + 1; wrap = BAS_BUILD_WRAP_CLASS; } if (isForm) { // A .frm without a Begin Form line would otherwise compile // its code unwrapped, as if it were a module. if (!basExtractFormName(src, formName, BAS_MAX_IDENT)) { snprintf(out->error, sizeof(out->error), "No Begin Form line in %s", entry.path); free(src); return false; } // The BASIC code section follows the outer Begin Form block. int32_t codeOff = basFindFormEndPos(src, srcLen); code = src + codeOff; entry.firstFileLine = countLines(src, codeOff) + 1; wrap = BAS_BUILD_WRAP_FORM; } if (!basBuildAppendFile(out, code, (int32_t)strlen(code), wrap, formName, &entry.startLine, &entry.lineCount)) { free(src); return false; } arrput(out->map, entry); if (isForm) { char *frmPath = (char *)malloc(strlen(entry.path) + 1); if (!frmPath) { snprintf(out->error, sizeof(out->error), "Out of memory."); free(src); return false; } strcpy(frmPath, entry.path); arrput(out->frmPaths, frmPath); arrput(out->frmTexts, src); } else { free(src); } out->fileCount++; } } if (out->fileCount == 0) { snprintf(out->error, sizeof(out->error), "Project has no source files."); return false; } return true; } void basBuildDirOf(const char *path, char *out, int32_t outSize) { snprintf(out, outSize, "%s", path); char *sep = platformPathDirEnd(out); if (sep) { *sep = '\0'; } else { snprintf(out, outSize, "."); } } void basBuildFreeProject(BasProjectSourceT *out) { for (int32_t i = 0; i < (int32_t)arrlen(out->frmPaths); i++) { free(out->frmPaths[i]); } for (int32_t i = 0; i < (int32_t)arrlen(out->frmTexts); i++) { free(out->frmTexts[i]); } arrfree(out->frmPaths); arrfree(out->frmTexts); arrfree(out->map); free(out->source); memset(out, 0, sizeof(*out)); } bool basBuildMapLine(const BasSourceMapT *map, int32_t line, const char **path, int32_t *fileLine) { for (int32_t i = 0; i < (int32_t)arrlen(map); i++) { const BasSourceMapT *m = &map[i]; if (line >= m->startLine && line < m->startLine + m->lineCount) { *path = m->path; *fileLine = m->firstFileLine + (line - m->startLine); return true; } } return false; } bool basBuildOpenProject(const char *dbpPath, PrefsHandleT **prefs, char *projectDir, int32_t projectDirSize) { // prefsLoad treats a missing file as an empty INI, so probe it first // or a bad path would surface as a confusing "no source files" error. FILE *probe = fopen(dbpPath, "rb"); if (!probe) { return false; } fclose(probe); *prefs = prefsLoad(dbpPath); if (!*prefs) { return false; } basBuildDirOf(dbpPath, projectDir, projectDirSize); return true; } static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) { if (!spec->log) { return; } char msg[BAS_BUILD_LOG_BUF]; va_list ap; va_start(ap, fmt); vsnprintf(msg, sizeof(msg), fmt, ap); va_end(ap); spec->log(msg); } // Copy the project's help file next to the output app; the HELPFILE // resource itself names only the basename, which the stub resolves // relative to the app. static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec) { if (!spec->helpFile || !spec->helpFile[0]) { return NULL; } char helpSrc[DVX_MAX_PATH]; snprintf(helpSrc, sizeof(helpSrc), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->helpFile); char outDir[DVX_MAX_PATH]; basBuildDirOf(outPath, outDir, sizeof(outDir)); char helpDst[DVX_MAX_PATH]; snprintf(helpDst, sizeof(helpDst), "%s" BAS_BUILD_SEP "%s", outDir, platformPathBaseName(spec->helpFile)); if (!platformCopyFile(helpSrc, helpDst)) { return "Failed copying help file to output directory."; } return NULL; } // Number of newlines in text[0..len). static int32_t countLines(const char *text, int32_t len) { int32_t n = 0; for (int32_t i = 0; i < len; i++) { if (text[i] == '\n') { n++; } } return n; } // Embed the project icon, or the NOICON fallback carried by the running // executable when the project names none. Returns NULL on success, else // an error message (a named icon that cannot be read names the path). static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) { char iconPath[DVX_MAX_PATH]; void *iconData = NULL; int32_t iconLen = 0; if (spec->iconPath && spec->iconPath[0]) { snprintf(iconPath, sizeof(iconPath), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->iconPath); iconData = platformReadFile(iconPath, &iconLen); if (!iconData) { // A named icon that cannot be read is a build failure -- // shipping the app without its ICON32 resource would never // tell anyone. snprintf(sIconError, sizeof(sIconError), "Cannot read icon file %s", iconPath); return sIconError; } } else { DvxResHandleT *selfRes = dvxResOpen(spec->selfPath); if (selfRes) { uint32_t size = 0; iconData = dvxResRead(selfRes, BAS_RES_NOICON, &size); iconLen = (int32_t)size; dvxResClose(selfRes); } if (!iconData) { return NULL; } } int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen); free(iconData); return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES; } static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) { // Accumulate every append result so a disk-full or I/O failure on any // resource is reported instead of being silently treated as success. int32_t rc = 0; // Project metadata. Name is required -- fall back to a generic label // so the compiled app always has something sensible to display. const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : BAS_DEFAULT_APP_NAME; rc |= appendText(outPath, BAS_RES_NAME, projName); rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author); rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher); rc |= appendText(outPath, BAS_RES_VERSION, spec->version); rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright); rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description); const char *iconFailure = emitIcon(outPath, spec); if (iconFailure) { return iconFailure; } if (spec->helpFile && spec->helpFile[0]) { rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile)); } rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen); if (dbgData && dbgLen > 0) { rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen); } // Form resources. Release builds embed the obfuscated variant when // one was produced. Number the OUTPUT densely with outIdx rather // than the source index: skipping an empty form by source index would // leave a numbering gap (FORM0, FORM2, ...) and the stub's reader // stops at the first missing index, silently dropping every later // form. int32_t outIdx = 0; for (int32_t i = 0; i < frmCount; i++) { const uint8_t *data = frmData[i]; int32_t len = frmLens[i]; if (spec->release && obfFrms[i].data) { data = (const uint8_t *)obfFrms[i].data; len = obfFrms[i].len; } if (!data || len <= 0) { continue; } // The stub's loader scans FORM0..FORM(BAS_MAX_FORM_RESOURCES-1) // and stops there, so anything past that cap would be written but // unreachable at runtime. Fail the build loudly instead of // shipping forms the app can never load. if (outIdx >= BAS_MAX_FORM_RESOURCES) { return "Too many forms for one application."; } char resName[BAS_RES_FORM_NAME_LEN]; int32_t formLen = basFindFormEndPos((const char *)data, len); snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx); rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, data, (uint32_t)formLen); outIdx++; } return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES; } // Releases the stripped / obfuscated form work arrays of basBuildApp. static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms) { for (int32_t i = 0; i < (int32_t)arrlen(frmData); i++) { free(frmData[i]); free(obfFrms[i].data); } arrfree(frmData); arrfree(frmLens); arrfree(obfFrms); } // Serialize the module (and, for debug builds, its debug info). Release // builds run strip -> obfuscate -> compact on a private copy so the // caller's module stays intact; obfuscation also fills obfFrms. static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen) { uint8_t *modData = basModuleSerialize(spec->module, outModLen); if (!modData) { return "Failed to serialize module."; } if (!spec->release) { *outMod = modData; *outDbg = basDebugSerialize(spec->module, outDbgLen); // basDebugSerialize fails only on allocation; a debug build that // silently lacks its DEBUG resource is not a debug build. return *outDbg ? NULL : "Failed to serialize debug info."; } BasModuleT *modCopy = basModuleDeserialize(modData, *outModLen); free(modData); if (!modCopy) { return "Failed to prepare release module."; } basStripModule(modCopy); buildLog(spec, " stripped debug info"); if (frmCount > 0) { const char **frmTexts = NULL; for (int32_t i = 0; i < frmCount; i++) { arrput(frmTexts, (const char *)frmData[i]); } bool obfOk = basObfuscateNames(modCopy, frmTexts, frmLens, frmCount, obfFrms); arrfree(frmTexts); // A failed obfuscation leaves the module and forms with // inconsistent names; shipping them would break at runtime. if (!obfOk) { basModuleFree(modCopy); return "Out of memory while obfuscating names."; } buildLog(spec, " obfuscated %d form(s)", (int)frmCount); } int32_t removed = basCompactBytecode(modCopy); if (removed > 0) { buildLog(spec, " compacted bytecode (-%d bytes)", (int)removed); } *outMod = basModuleSerialize(modCopy, outModLen); basModuleFree(modCopy); if (!*outMod) { return "Failed to serialize release module."; } return NULL; } // Extract the STUB resource from the running executable and write it as // the output file; every resource is appended behind it. static const char *writeStub(const char *outPath, const BasBuildSpecT *spec) { DvxResHandleT *selfRes = dvxResOpen(spec->selfPath); if (!selfRes) { return "Cannot open own executable to read embedded stub."; } uint32_t stubSize = 0; void *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubSize); dvxResClose(selfRes); if (!stubData || stubSize == 0) { free(stubData); return "STUB resource not found in own executable."; } FILE *outFile = fopen(outPath, "wb"); if (!outFile) { free(stubData); return "Cannot create output file."; } size_t stubWritten = fwrite(stubData, 1, stubSize, outFile); int32_t stubClose = fclose(outFile); free(stubData); if (stubWritten != stubSize || stubClose != 0) { return "Failed writing stub to output file."; } return NULL; }