287 lines
9 KiB
C
287 lines
9 KiB
C
// 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.
|
|
|
|
// 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/parser.h"
|
|
#include "../compiler/symtab.h"
|
|
#include "../runtime/vm.h"
|
|
#include "../../../../libs/kpunch/libdvx/dvxPrefs.h"
|
|
#include "../../../../libs/kpunch/libdvx/dvxTypes.h"
|
|
#include "../../../../libs/kpunch/libdvx/platform/dvxPlat.h"
|
|
#include "../basBuild.h"
|
|
#include "../basRes.h"
|
|
|
|
#include "stb_ds_wrap.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <unistd.h>
|
|
|
|
|
|
// Kernel view of the running image on hosted OSes.
|
|
#define SELF_EXE_PATH "/proc/self/exe"
|
|
|
|
// Function prototypes (alphabetical)
|
|
static void buildLog(const char *msg);
|
|
static const char *selfPath(const char *argv0);
|
|
static void usage(void);
|
|
int main(int argc, char **argv);
|
|
|
|
|
|
static void buildLog(const char *msg) {
|
|
printf("%s\n", msg);
|
|
}
|
|
|
|
|
|
// Path of this executable, which carries the embedded STUB and NOICON
|
|
// resources. On a hosted OS argv[0] is whatever the shell typed (a bare
|
|
// name when invoked through PATH), so the kernel's view of the running
|
|
// image is preferred; DJGPP always passes a full path in argv[0].
|
|
static const char *selfPath(const char *argv0) {
|
|
#ifndef __DJGPP__
|
|
if (access(SELF_EXE_PATH, R_OK) == 0) {
|
|
return SELF_EXE_PATH;
|
|
}
|
|
#endif
|
|
|
|
return argv0;
|
|
}
|
|
|
|
|
|
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");
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 2) {
|
|
usage();
|
|
return 1;
|
|
}
|
|
|
|
const char *dbpPath = NULL;
|
|
const char *outputPath = NULL;
|
|
bool release = false;
|
|
|
|
for (int32_t 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. prefsLoad treats a missing file as an
|
|
// empty INI, so probe it first or a bad path would surface as a
|
|
// confusing "no modules" build error.
|
|
FILE *dbpProbe = fopen(dbpPath, "rb");
|
|
|
|
if (!dbpProbe) {
|
|
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
|
|
return 1;
|
|
}
|
|
|
|
fclose(dbpProbe);
|
|
|
|
PrefsHandleT *prefs = prefsLoad(dbpPath);
|
|
|
|
if (!prefs) {
|
|
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
|
|
return 1;
|
|
}
|
|
|
|
// Derive project directory
|
|
char projectDir[DVX_MAX_PATH];
|
|
snprintf(projectDir, sizeof(projectDir), "%s", dbpPath);
|
|
char *sep = platformPathDirEnd(projectDir);
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
projectDir[0] = '.';
|
|
projectDir[1] = '\0';
|
|
}
|
|
|
|
// Read project metadata
|
|
const char *projName = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, "App");
|
|
const char *author = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, "");
|
|
const char *publisher = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, "");
|
|
const char *version = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, "");
|
|
const char *copyright = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, "");
|
|
const char *description = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, "");
|
|
const char *iconPath = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, "");
|
|
const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
|
|
|
|
// Derive output path
|
|
char outBuf[DVX_MAX_PATH];
|
|
|
|
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");
|
|
|
|
// Concatenate the project's sources (modules first, then form code)
|
|
BasProjectSourceT project;
|
|
|
|
if (!basBuildConcatProject(prefs, projectDir, &project)) {
|
|
fprintf(stderr, "Error: %s\n", project.error);
|
|
basBuildFreeProject(&project);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|
|
|
|
int32_t fileCount = project.fileCount;
|
|
|
|
// Compile
|
|
printf("Compiling %d file(s)...\n", (int)fileCount);
|
|
|
|
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
|
|
|
if (!parser) {
|
|
fprintf(stderr, "Error: out of memory.\n");
|
|
goto failProject;
|
|
}
|
|
|
|
basParserInit(parser, project.source, project.sourceLen);
|
|
parser->optionExplicit = project.optionExplicit;
|
|
|
|
if (!basParse(parser)) {
|
|
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
goto failProject;
|
|
}
|
|
|
|
BasModuleT *mod = basParserBuildModule(parser);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
|
|
if (!mod) {
|
|
fprintf(stderr, "Error: failed to build module.\n");
|
|
goto failProject;
|
|
}
|
|
|
|
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
|
|
|
|
// Raw .frm texts; the shared build pipeline strips comments and, for
|
|
// release builds, obfuscates them.
|
|
char **frmSources = NULL; // stb_ds: owned form text
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(project.frmPaths); i++) {
|
|
char *fdata = platformReadFile(project.frmPaths[i], NULL);
|
|
|
|
if (fdata) {
|
|
arrput(frmSources, fdata);
|
|
}
|
|
}
|
|
|
|
BasBuildSpecT spec;
|
|
memset(&spec, 0, sizeof(spec));
|
|
spec.projName = projName;
|
|
spec.author = author;
|
|
spec.publisher = publisher;
|
|
spec.version = version;
|
|
spec.copyright = copyright;
|
|
spec.description = description;
|
|
spec.projectDir = projectDir;
|
|
spec.iconPath = iconPath;
|
|
spec.helpFile = helpFile;
|
|
spec.selfPath = selfPath(argv[0]);
|
|
spec.module = mod;
|
|
spec.frmSources = (const char *const *)frmSources;
|
|
spec.frmCount = (int32_t)arrlen(frmSources);
|
|
spec.release = release;
|
|
spec.log = buildLog;
|
|
|
|
const char *failure = basBuildApp(outputPath, &spec);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) {
|
|
free(frmSources[i]);
|
|
}
|
|
|
|
arrfree(frmSources);
|
|
basModuleFree(mod);
|
|
basBuildFreeProject(&project);
|
|
prefsClose(prefs);
|
|
|
|
if (failure) {
|
|
fprintf(stderr, "Error: %s\n", failure);
|
|
return 1;
|
|
}
|
|
|
|
// Report the true on-disk size of the finished app (stub, bytecode,
|
|
// debug info, icon, metadata and every FORMn resource).
|
|
int32_t outBytes = 0;
|
|
FILE *szf = fopen(outputPath, "rb");
|
|
|
|
if (szf) {
|
|
fseek(szf, 0, SEEK_END);
|
|
long endPos = ftell(szf);
|
|
|
|
if (endPos > 0) {
|
|
outBytes = (int32_t)endPos;
|
|
}
|
|
|
|
fclose(szf);
|
|
}
|
|
|
|
printf("Created %s (%d bytes)\n", outputPath, (int)outBytes);
|
|
return 0;
|
|
|
|
// ----- Error cleanup path -----
|
|
// The caller has already printed an appropriate error message.
|
|
failProject:
|
|
basBuildFreeProject(&project);
|
|
prefsClose(prefs);
|
|
return 1;
|
|
}
|