667 lines
19 KiB
C
667 lines
19 KiB
C
// bascomp.c -- DVX BASIC command-line compiler
|
|
//
|
|
// Compiles a .dbp project into a standalone .app file.
|
|
//
|
|
// Usage: BASCOMP project.dbp [-o output.app] [-release]
|
|
//
|
|
// The project file and all referenced source files (.bas, .frm)
|
|
// are loaded relative to the directory containing the .dbp file.
|
|
// The stub (basstub.app) is read from the same directory as the
|
|
// compiler executable.
|
|
|
|
#include "../compiler/compact.h"
|
|
#include "../compiler/lexer.h"
|
|
#include "../compiler/obfuscate.h"
|
|
#include "../compiler/parser.h"
|
|
#include "../compiler/strip.h"
|
|
#include "../compiler/opcodes.h"
|
|
#include "../runtime/vm.h"
|
|
#include "../runtime/values.h"
|
|
#include "../runtime/serialize.h"
|
|
#include "../../core/dvxRes.h"
|
|
#include "../../core/dvxPrefs.h"
|
|
#include "../../core/dvxTypes.h"
|
|
#include "../../tools/dvxResWrite.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
// ============================================================
|
|
// Limits
|
|
// ============================================================
|
|
|
|
#define MAX_FILES 64
|
|
#define MAX_PATH_LEN 260
|
|
#define MAX_NAME 64
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void concatGrow(char **buf, int32_t *cap, int32_t need);
|
|
static char *readFile(const char *path, int32_t *outLen);
|
|
static const char *extractFormCode(const char *frmText);
|
|
static void usage(void);
|
|
|
|
|
|
// ============================================================
|
|
// extractFormCode -- skip past the form layout to the code section
|
|
// ============================================================
|
|
|
|
static void concatGrow(char **buf, int32_t *cap, int32_t need) {
|
|
while (*cap < need) {
|
|
*cap *= 2;
|
|
}
|
|
|
|
*buf = (char *)realloc(*buf, *cap);
|
|
}
|
|
|
|
|
|
static const char *extractFormCode(const char *frmText) {
|
|
if (!frmText) {
|
|
return NULL;
|
|
}
|
|
|
|
const char *p = frmText;
|
|
int32_t depth = 0;
|
|
bool inForm = false;
|
|
|
|
while (*p) {
|
|
// Skip leading whitespace
|
|
while (*p == ' ' || *p == '\t') { p++; }
|
|
|
|
if (strncasecmp(p, "Begin ", 6) == 0) {
|
|
if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) {
|
|
inForm = true;
|
|
}
|
|
|
|
depth++;
|
|
} else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) {
|
|
depth--;
|
|
|
|
if (depth <= 0 && inForm) {
|
|
// Skip past this line
|
|
while (*p && *p != '\n') { p++; }
|
|
|
|
if (*p == '\n') { p++; }
|
|
|
|
return p;
|
|
}
|
|
}
|
|
|
|
// Skip to next line
|
|
while (*p && *p != '\n') { p++; }
|
|
|
|
if (*p == '\n') { p++; }
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// readFile
|
|
// ============================================================
|
|
|
|
static char *readFile(const char *path, int32_t *outLen) {
|
|
FILE *f = fopen(path, "rb");
|
|
|
|
if (!f) {
|
|
return NULL;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
char *buf = (char *)malloc(size + 1);
|
|
|
|
if (!buf) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
int32_t n = (int32_t)fread(buf, 1, size, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
|
|
if (outLen) {
|
|
*outLen = n;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// usage
|
|
// ============================================================
|
|
|
|
static void usage(void) {
|
|
fprintf(stderr, "DVX BASIC Compiler\n\n");
|
|
fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n");
|
|
fprintf(stderr, " project.dbp DVX BASIC project file\n");
|
|
fprintf(stderr, " -o output.app Output file (default: project name + .app)\n");
|
|
fprintf(stderr, " -release Strip debug information\n");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// main
|
|
// ============================================================
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 2) {
|
|
usage();
|
|
return 1;
|
|
}
|
|
|
|
const char *dbpPath = NULL;
|
|
const char *outputPath = NULL;
|
|
bool release = false;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
|
|
outputPath = argv[++i];
|
|
} else if (strcmp(argv[i], "-release") == 0) {
|
|
release = true;
|
|
} else if (argv[i][0] != '-') {
|
|
dbpPath = argv[i];
|
|
} else {
|
|
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
|
usage();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (!dbpPath) {
|
|
fprintf(stderr, "Error: no project file specified.\n");
|
|
usage();
|
|
return 1;
|
|
}
|
|
|
|
// Load the project file
|
|
PrefsHandleT *prefs = prefsLoad(dbpPath);
|
|
|
|
if (!prefs) {
|
|
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
|
|
return 1;
|
|
}
|
|
|
|
// Derive project directory
|
|
char projectDir[MAX_PATH_LEN];
|
|
snprintf(projectDir, sizeof(projectDir), "%s", dbpPath);
|
|
char *sep = strrchr(projectDir, '/');
|
|
char *sep2 = strrchr(projectDir, '\\');
|
|
|
|
if (sep2 > sep) {
|
|
sep = sep2;
|
|
}
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
projectDir[0] = '.';
|
|
projectDir[1] = '\0';
|
|
}
|
|
|
|
// Read project metadata
|
|
const char *projName = prefsGetString(prefs, "Project", "Name", "App");
|
|
const char *author = prefsGetString(prefs, "Project", "Author", "");
|
|
const char *company = prefsGetString(prefs, "Project", "Company", "");
|
|
const char *version = prefsGetString(prefs, "Project", "Version", "");
|
|
const char *copyright = prefsGetString(prefs, "Project", "Copyright", "");
|
|
const char *description = prefsGetString(prefs, "Project", "Description", "");
|
|
const char *iconPath = prefsGetString(prefs, "Project", "Icon", "");
|
|
const char *helpFile = prefsGetString(prefs, "Project", "HelpFile", "");
|
|
const char *startupForm = prefsGetString(prefs, "Settings", "StartupForm", "");
|
|
(void)startupForm; // used implicitly by stub's basFormRtLoadAllForms
|
|
|
|
// Derive output path
|
|
char outBuf[MAX_PATH_LEN];
|
|
|
|
if (!outputPath) {
|
|
snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName);
|
|
outputPath = outBuf;
|
|
}
|
|
|
|
printf("Project: %s\n", projName);
|
|
printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug");
|
|
|
|
// Collect source files
|
|
typedef struct {
|
|
char path[MAX_PATH_LEN];
|
|
bool isForm;
|
|
} SrcFileT;
|
|
|
|
SrcFileT files[MAX_FILES];
|
|
int32_t fileCount = 0;
|
|
|
|
// Modules
|
|
for (int32_t i = 0; i < MAX_FILES; i++) {
|
|
char key[16];
|
|
snprintf(key, sizeof(key), "File%d", (int)i);
|
|
const char *val = prefsGetString(prefs, "Modules", key, NULL);
|
|
|
|
if (!val) {
|
|
break;
|
|
}
|
|
|
|
snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val);
|
|
files[fileCount].isForm = false;
|
|
fileCount++;
|
|
}
|
|
|
|
// Forms
|
|
for (int32_t i = 0; i < MAX_FILES; i++) {
|
|
char key[16];
|
|
snprintf(key, sizeof(key), "File%d", (int)i);
|
|
const char *val = prefsGetString(prefs, "Forms", key, NULL);
|
|
|
|
if (!val) {
|
|
break;
|
|
}
|
|
|
|
snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val);
|
|
files[fileCount].isForm = true;
|
|
fileCount++;
|
|
}
|
|
|
|
if (fileCount == 0) {
|
|
fprintf(stderr, "Error: project has no source files.\n");
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
// Concatenate sources (modules first, then form code)
|
|
int32_t concatCap = 8192;
|
|
char *concatBuf = (char *)malloc(concatCap);
|
|
|
|
if (!concatBuf) {
|
|
fprintf(stderr, "Error: out of memory.\n");
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
// Pass 0: .bas modules, Pass 1: .frm code sections
|
|
for (int32_t pass = 0; pass < 2; pass++) {
|
|
for (int32_t i = 0; i < fileCount; i++) {
|
|
if (pass == 0 && files[i].isForm) { continue; }
|
|
if (pass == 1 && !files[i].isForm) { continue; }
|
|
|
|
int32_t srcLen = 0;
|
|
char *srcBuf = readFile(files[i].path, &srcLen);
|
|
|
|
if (!srcBuf) {
|
|
fprintf(stderr, "Error: cannot read %s\n", files[i].path);
|
|
free(concatBuf);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
const char *code = srcBuf;
|
|
|
|
if (files[i].isForm) {
|
|
// Extract form name from "Begin Form <name>"
|
|
char formName[MAX_NAME] = "";
|
|
const char *bp = srcBuf;
|
|
|
|
while (*bp) {
|
|
while (*bp == ' ' || *bp == '\t') { bp++; }
|
|
|
|
if (strncasecmp(bp, "Begin Form ", 11) == 0) {
|
|
bp += 11;
|
|
|
|
while (*bp == ' ' || *bp == '\t') { bp++; }
|
|
|
|
int32_t ni = 0;
|
|
|
|
while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < MAX_NAME - 1) {
|
|
formName[ni++] = *bp++;
|
|
}
|
|
|
|
formName[ni] = '\0';
|
|
break;
|
|
}
|
|
|
|
while (*bp && *bp != '\n') { bp++; }
|
|
|
|
if (*bp == '\n') { bp++; }
|
|
}
|
|
|
|
code = extractFormCode(srcBuf);
|
|
|
|
if (!code) {
|
|
code = "";
|
|
}
|
|
|
|
int32_t codeLen = (int32_t)strlen(code);
|
|
concatGrow(&concatBuf, &concatCap, pos + codeLen + 128);
|
|
|
|
// Inject BEGINFORM directive before form code
|
|
if (formName[0]) {
|
|
pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName);
|
|
}
|
|
|
|
memcpy(concatBuf + pos, code, codeLen);
|
|
pos += codeLen;
|
|
|
|
if (pos > 0 && concatBuf[pos - 1] != '\n') {
|
|
concatBuf[pos++] = '\n';
|
|
}
|
|
|
|
// Inject ENDFORM directive after form code
|
|
if (formName[0]) {
|
|
pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n");
|
|
}
|
|
} else {
|
|
int32_t codeLen = (int32_t)strlen(code);
|
|
concatGrow(&concatBuf, &concatCap, pos + codeLen + 2);
|
|
|
|
memcpy(concatBuf + pos, code, codeLen);
|
|
pos += codeLen;
|
|
|
|
if (pos > 0 && concatBuf[pos - 1] != '\n') {
|
|
concatBuf[pos++] = '\n';
|
|
}
|
|
}
|
|
|
|
free(srcBuf);
|
|
}
|
|
}
|
|
|
|
concatBuf[pos] = '\0';
|
|
|
|
// Compile
|
|
printf("Compiling %d file(s)...\n", (int)fileCount);
|
|
|
|
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
|
|
|
if (!parser) {
|
|
fprintf(stderr, "Error: out of memory.\n");
|
|
free(concatBuf);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
basParserInit(parser, concatBuf, pos);
|
|
|
|
if (!basParse(parser)) {
|
|
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
free(concatBuf);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
free(concatBuf);
|
|
|
|
BasModuleT *mod = basParserBuildModule(parser);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
|
|
if (!mod) {
|
|
fprintf(stderr, "Error: failed to build module.\n");
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
|
|
|
|
// Strip for release
|
|
if (release) {
|
|
basStripModule(mod);
|
|
printf(" stripped debug info\n");
|
|
}
|
|
|
|
// Read all .frm texts up front; they're used for obfuscation and
|
|
// then embedded as FORM0, FORM1, ... resources below.
|
|
int32_t frmCount = 0;
|
|
char *frmData[MAX_FILES];
|
|
int32_t frmLens[MAX_FILES];
|
|
int32_t frmFileIdx[MAX_FILES]; // index into files[] for this frm
|
|
|
|
for (int32_t i = 0; i < fileCount; i++) {
|
|
if (!files[i].isForm) {
|
|
continue;
|
|
}
|
|
|
|
int32_t flen = 0;
|
|
char *fdata = readFile(files[i].path, &flen);
|
|
|
|
if (!fdata) {
|
|
continue;
|
|
}
|
|
|
|
// Strip comments from the .frm text unconditionally. Comments
|
|
// are source-only; they shouldn't ship in the embedded resource
|
|
// for either debug or release builds.
|
|
int32_t stripCap = flen + 16;
|
|
uint8_t *stripped = (uint8_t *)malloc(stripCap);
|
|
|
|
if (!stripped) {
|
|
free(fdata);
|
|
continue;
|
|
}
|
|
|
|
int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap);
|
|
free(fdata);
|
|
|
|
frmData[frmCount] = (char *)stripped;
|
|
frmLens[frmCount] = strippedLen;
|
|
frmFileIdx[frmCount] = i;
|
|
frmCount++;
|
|
}
|
|
|
|
// Obfuscate form/control names in release mode
|
|
BasObfFrmT obfFrms[MAX_FILES];
|
|
|
|
for (int32_t i = 0; i < MAX_FILES; i++) {
|
|
obfFrms[i].data = NULL;
|
|
obfFrms[i].len = 0;
|
|
}
|
|
|
|
if (release && frmCount > 0) {
|
|
const char *frmTexts[MAX_FILES];
|
|
|
|
for (int32_t i = 0; i < frmCount; i++) {
|
|
frmTexts[i] = frmData[i];
|
|
}
|
|
|
|
basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms);
|
|
printf(" obfuscated %d form(s)\n", (int)frmCount);
|
|
}
|
|
|
|
// Remove OP_LINE instructions and compact the bytecode.
|
|
if (release) {
|
|
int32_t removed = basCompactBytecode(mod);
|
|
|
|
if (removed > 0) {
|
|
printf(" compacted bytecode (-%d bytes)\n", (int)removed);
|
|
}
|
|
}
|
|
|
|
// Serialize module
|
|
int32_t modLen = 0;
|
|
uint8_t *modData = basModuleSerialize(mod, &modLen);
|
|
|
|
if (!modData) {
|
|
fprintf(stderr, "Error: failed to serialize module.\n");
|
|
basModuleFree(mod);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
// Serialize debug info
|
|
int32_t dbgLen = 0;
|
|
uint8_t *dbgData = NULL;
|
|
|
|
if (!release) {
|
|
dbgData = basDebugSerialize(mod, &dbgLen);
|
|
}
|
|
|
|
basModuleFree(mod);
|
|
|
|
// Find the stub -- look in the compiler's own directory
|
|
char compilerDir[MAX_PATH_LEN];
|
|
snprintf(compilerDir, sizeof(compilerDir), "%s", argv[0]);
|
|
sep = strrchr(compilerDir, '/');
|
|
sep2 = strrchr(compilerDir, '\\');
|
|
|
|
if (sep2 > sep) {
|
|
sep = sep2;
|
|
}
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
compilerDir[0] = '.';
|
|
compilerDir[1] = '\0';
|
|
}
|
|
|
|
char stubPath[MAX_PATH_LEN];
|
|
snprintf(stubPath, sizeof(stubPath), "%s/BASSTUB.APP", compilerDir);
|
|
|
|
int32_t stubLen = 0;
|
|
char *stubData = readFile(stubPath, &stubLen);
|
|
|
|
if (!stubData) {
|
|
fprintf(stderr, "Error: cannot find stub at %s\n", stubPath);
|
|
free(modData);
|
|
free(dbgData);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
// Write stub to output file
|
|
FILE *out = fopen(outputPath, "wb");
|
|
|
|
if (!out) {
|
|
fprintf(stderr, "Error: cannot create %s\n", outputPath);
|
|
free(stubData);
|
|
free(modData);
|
|
free(dbgData);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
fwrite(stubData, 1, stubLen, out);
|
|
fclose(out);
|
|
free(stubData);
|
|
|
|
// Attach resources
|
|
dvxResAppendEntry(outputPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1);
|
|
|
|
if (author[0]) { dvxResAppendEntry(outputPath, "author", DVX_RES_TEXT, author, (uint32_t)strlen(author) + 1); }
|
|
if (company[0]) { dvxResAppendEntry(outputPath, "company", DVX_RES_TEXT, company, (uint32_t)strlen(company) + 1); }
|
|
if (version[0]) { dvxResAppendEntry(outputPath, "version", DVX_RES_TEXT, version, (uint32_t)strlen(version) + 1); }
|
|
if (copyright[0]) { dvxResAppendEntry(outputPath, "copyright", DVX_RES_TEXT, copyright, (uint32_t)strlen(copyright) + 1); }
|
|
if (description[0]) { dvxResAppendEntry(outputPath, "description", DVX_RES_TEXT, description, (uint32_t)strlen(description) + 1); }
|
|
|
|
// Icon
|
|
if (iconPath[0]) {
|
|
char iconFullPath[MAX_PATH_LEN];
|
|
snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath);
|
|
int32_t iconLen = 0;
|
|
char *iconData = readFile(iconFullPath, &iconLen);
|
|
|
|
if (iconData) {
|
|
dvxResAppendEntry(outputPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconLen);
|
|
free(iconData);
|
|
}
|
|
}
|
|
|
|
// Help file name (file is expected alongside the .app)
|
|
if (helpFile[0]) {
|
|
const char *helpBase = helpFile;
|
|
const char *hs = strrchr(helpBase, '/');
|
|
const char *hs2 = strrchr(helpBase, '\\');
|
|
|
|
if (hs2 > hs) {
|
|
hs = hs2;
|
|
}
|
|
|
|
if (hs) {
|
|
helpBase = hs + 1;
|
|
}
|
|
|
|
dvxResAppendEntry(outputPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1);
|
|
|
|
// Copy help file to output directory
|
|
char helpSrc[MAX_PATH_LEN];
|
|
snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile);
|
|
|
|
char outDir[MAX_PATH_LEN];
|
|
snprintf(outDir, sizeof(outDir), "%s", outputPath);
|
|
char *outSep = strrchr(outDir, '/');
|
|
char *outSep2 = strrchr(outDir, '\\');
|
|
|
|
if (outSep2 > outSep) {
|
|
outSep = outSep2;
|
|
}
|
|
|
|
if (outSep) {
|
|
*outSep = '\0';
|
|
} else {
|
|
outDir[0] = '.';
|
|
outDir[1] = '\0';
|
|
}
|
|
|
|
char helpDst[MAX_PATH_LEN];
|
|
snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase);
|
|
|
|
int32_t hLen = 0;
|
|
char *hData = readFile(helpSrc, &hLen);
|
|
|
|
if (hData) {
|
|
FILE *hf = fopen(helpDst, "wb");
|
|
|
|
if (hf) {
|
|
fwrite(hData, 1, hLen, hf);
|
|
fclose(hf);
|
|
}
|
|
|
|
free(hData);
|
|
}
|
|
}
|
|
|
|
// MODULE resource
|
|
dvxResAppendEntry(outputPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen);
|
|
free(modData);
|
|
|
|
// DEBUG resource
|
|
if (dbgData) {
|
|
dvxResAppendEntry(outputPath, "DEBUG", DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
|
|
free(dbgData);
|
|
}
|
|
|
|
// Form resources -- use obfuscated bytes in release mode, raw otherwise
|
|
for (int32_t fi = 0; fi < frmCount; fi++) {
|
|
char resName[16];
|
|
snprintf(resName, sizeof(resName), "FORM%d", (int)fi);
|
|
|
|
if (release && obfFrms[fi].data) {
|
|
dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, obfFrms[fi].data, (uint32_t)obfFrms[fi].len);
|
|
} else {
|
|
dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, frmData[fi], (uint32_t)frmLens[fi]);
|
|
}
|
|
}
|
|
|
|
// Free .frm buffers
|
|
for (int32_t i = 0; i < frmCount; i++) {
|
|
free(frmData[i]);
|
|
free(obfFrms[i].data);
|
|
}
|
|
|
|
(void)frmFileIdx; // unused (kept in case of future per-file metadata)
|
|
|
|
prefsClose(prefs);
|
|
|
|
printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen);
|
|
return 0;
|
|
}
|