977 lines
30 KiB
C
977 lines
30 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.
|
|
|
|
// loaderMain.c -- DVX bootstrap loader entry point
|
|
//
|
|
// Loads all DXE modules from two directories:
|
|
// libs/ *.lib -- core libraries (libtasks, libdvx, dvxshell)
|
|
// widgets/ *.wgt -- widget type plugins (box, button, listview, etc.)
|
|
//
|
|
// Each module may have a .dep file (same base name, .dep extension)
|
|
// listing base names of modules that must be loaded before it.
|
|
// The loader resolves the dependency graph and loads in topological
|
|
// order. After loading, any module that exports wgtRegister() has
|
|
// it called. Finally, the loader finds and calls shellMain().
|
|
|
|
#include "stddclmr.h"
|
|
#include "dvxPlat.h"
|
|
#include "dvxPrefs.h"
|
|
#include "../tools/hlpcCompile.h"
|
|
|
|
#include <ctype.h>
|
|
#include <dlfcn.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/stat.h>
|
|
|
|
// The loader is not a DXE -- use plain realloc/free for stb_ds so that
|
|
// all translation units (loaderMain.o, dvxPrefs.o) share the same heap.
|
|
#define STB_DS_IMPLEMENTATION
|
|
#include "stb_ds_wrap.h"
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define LIBS_DIR "LIBS"
|
|
#define WIDGET_DIR "WIDGETS"
|
|
#define LOG_PATH "dvx.log"
|
|
|
|
// ============================================================
|
|
// Splash screen (delegates to platformSplash* in dvxPlatformDos.c)
|
|
// ============================================================
|
|
|
|
// Palette indices for progress bar (indices into SPLASH.RAW palette)
|
|
#define SPLASH_BAR_BG 50 // dark gray (RGB 68,68,68)
|
|
#define SPLASH_BAR_FG 135 // light gray (RGB 168,168,168)
|
|
#define SPLASH_BAR_OUT 45 // darker gray (RGB 60,60,60)
|
|
|
|
// Progress bar geometry
|
|
#define PBAR_X 10
|
|
#define PBAR_Y 188
|
|
#define PBAR_W 300
|
|
#define PBAR_H 6
|
|
|
|
// ============================================================
|
|
// Module entry for dependency resolution
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
char path[DVX_MAX_PATH];
|
|
char baseName[16];
|
|
char **deps;
|
|
bool loaded;
|
|
void *handle;
|
|
} ModuleT;
|
|
|
|
// Scratch state for processHcf callback -- collects output=, imagedir=, and
|
|
// the accumulated source= glob expansions in one pass.
|
|
typedef struct {
|
|
const char *hcfDir;
|
|
char outputFile[DVX_MAX_PATH];
|
|
char imgDir[DVX_MAX_PATH];
|
|
char **inputFiles;
|
|
} ProcessHcfCtxT;
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static int32_t sSplashActive = 0;
|
|
static int32_t sSplashTotal = 0;
|
|
static int32_t sSplashLoaded = 0;
|
|
|
|
static int32_t sLibCount = 0;
|
|
static int32_t sWidgetCount = 0;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods);
|
|
static void collectGlobFiles(char ***outFiles, const char *pattern, const char *excludePattern);
|
|
static void countHcfCallback(const char *key, const char *val, const char *exclude, void *user);
|
|
static int32_t countHcfInputFiles(const char *hcfPath);
|
|
static int32_t countTotalHelpSteps(const char *dirPath);
|
|
void dvxLog(const char *fmt, ...);
|
|
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize);
|
|
static void *findSymbol(void **handles, const char *symbol);
|
|
static void freeMods(ModuleT *mods);
|
|
static void hcfForEachEntry(const char *hcfPath, void (*cb)(const char *key, const char *val, const char *exclude, void *user), void *user);
|
|
static void helpRecompileIfNeeded(void);
|
|
static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total);
|
|
static void **loadAllModules(void);
|
|
static void loadInOrder(ModuleT *mods);
|
|
static void logAndReadDeps(ModuleT *mods);
|
|
bool platformGlobMatch(const char *pattern, const char *name);
|
|
static void processHcf(const char *hcfPath, const char *hcfDir);
|
|
static void processHcfCallback(const char *key, const char *val, const char *exclude, void *user);
|
|
static void processHcfDir(const char *dirPath);
|
|
static void readDeps(ModuleT *mod);
|
|
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods);
|
|
static void splashDrawScreen(void);
|
|
static void splashShutdownIfActive(void);
|
|
static void splashUpdateProgress(void);
|
|
static void validateDeps(const ModuleT *libs, const ModuleT *widgets);
|
|
int main(int argc, char *argv[]);
|
|
|
|
|
|
// A dep is satisfied if either:
|
|
// 1. No module with that base name exists (external, assumed OK)
|
|
// 2. The module with that base name is already loaded
|
|
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods) {
|
|
for (int32_t d = 0; d < arrlen(mod->deps); d++) {
|
|
for (int32_t j = 0; j < arrlen(mods); j++) {
|
|
if (strcasecmp(mods[j].baseName, mod->deps[d]) == 0) {
|
|
if (!mods[j].loaded) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Collect matching filenames into an stb_ds array of strdup'd paths.
|
|
static void collectGlobFiles(char ***outFiles, const char *pattern, const char *excludePattern) {
|
|
char dirPart[DVX_MAX_PATH];
|
|
const char *globPart = NULL;
|
|
|
|
snprintf(dirPart, sizeof(dirPart), "%s", pattern);
|
|
char *lastSep = platformPathDirEnd(dirPart);
|
|
|
|
if (lastSep) {
|
|
*lastSep = '\0';
|
|
globPart = lastSep + 1;
|
|
} else {
|
|
globPart = pattern;
|
|
dirPart[0] = '.';
|
|
dirPart[1] = '\0';
|
|
}
|
|
|
|
char **names = dvxReadDir(dirPart);
|
|
|
|
if (!names) {
|
|
return;
|
|
}
|
|
|
|
int32_t nEntries = (int32_t)arrlen(names);
|
|
|
|
for (int32_t i = 0; i < nEntries; i++) {
|
|
// Skip hidden files (dvxReadDir already strips "." and "..")
|
|
if (names[i][0] == '.') {
|
|
continue;
|
|
}
|
|
|
|
char fullPath[DVX_MAX_PATH];
|
|
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]);
|
|
|
|
struct stat st;
|
|
|
|
if (stat(fullPath, &st) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
char subPattern[DVX_MAX_PATH];
|
|
snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart);
|
|
collectGlobFiles(outFiles, subPattern, excludePattern);
|
|
} else if (platformGlobMatch(globPart, names[i])) {
|
|
if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
|
|
arrput(*outFiles, strdup(fullPath));
|
|
}
|
|
}
|
|
}
|
|
|
|
dvxReadDirFree(names);
|
|
}
|
|
|
|
|
|
// Callback for countHcfInputFiles: expand source= globs into the user file list.
|
|
static void countHcfCallback(const char *key, const char *val, const char *exclude, void *user) {
|
|
if (strcasecmp(key, "source") == 0) {
|
|
collectGlobFiles((char ***)user, val, exclude);
|
|
}
|
|
}
|
|
|
|
|
|
// Count input files for a single .hcf config (for progress total calculation).
|
|
static int32_t countHcfInputFiles(const char *hcfPath) {
|
|
char **files = NULL;
|
|
hcfForEachEntry(hcfPath, countHcfCallback, &files);
|
|
|
|
int32_t count = (int32_t)arrlen(files);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
free(files[i]);
|
|
}
|
|
|
|
arrfree(files);
|
|
return count;
|
|
}
|
|
|
|
|
|
// Count total progress steps across all .hcf files under a directory.
|
|
static int32_t countTotalHelpSteps(const char *dirPath) {
|
|
char **names = dvxReadDir(dirPath);
|
|
|
|
if (!names) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t total = 0;
|
|
int32_t nEntries = (int32_t)arrlen(names);
|
|
|
|
for (int32_t i = 0; i < nEntries; i++) {
|
|
if (names[i][0] == '.') {
|
|
continue;
|
|
}
|
|
|
|
char fullPath[DVX_MAX_PATH];
|
|
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
|
|
|
|
struct stat st;
|
|
|
|
if (stat(fullPath, &st) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
total += countTotalHelpSteps(fullPath);
|
|
} else if (dvxHasExt(names[i], ".hcf")) {
|
|
int32_t fileCount = countHcfInputFiles(fullPath);
|
|
total += hlpcProgressTotal(fileCount);
|
|
}
|
|
}
|
|
|
|
dvxReadDirFree(names);
|
|
return total;
|
|
}
|
|
|
|
|
|
static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) {
|
|
// Find last directory separator
|
|
const char *start = path;
|
|
const char *p = path;
|
|
|
|
while (*p) {
|
|
if (*p == '/' || *p == '\\') {
|
|
start = p + 1;
|
|
}
|
|
|
|
p++;
|
|
}
|
|
|
|
// Copy up to the extension
|
|
int32_t extLen = strlen(ext);
|
|
int32_t len = strlen(start);
|
|
|
|
if (len > extLen && strcasecmp(start + len - extLen, ext) == 0) {
|
|
len -= extLen;
|
|
}
|
|
|
|
if (len >= outSize) {
|
|
len = outSize - 1;
|
|
}
|
|
|
|
for (int32_t i = 0; i < len; i++) {
|
|
out[i] = tolower((unsigned char)start[i]);
|
|
}
|
|
|
|
out[len] = '\0';
|
|
}
|
|
|
|
|
|
static void *findSymbol(void **handles, const char *symbol) {
|
|
for (int32_t i = 0; i < arrlen(handles); i++) {
|
|
void *sym = dlsym(handles[i], symbol);
|
|
|
|
if (sym) {
|
|
return sym;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static void freeMods(ModuleT *mods) {
|
|
for (int32_t i = 0; i < arrlen(mods); i++) {
|
|
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
|
|
free(mods[i].deps[d]);
|
|
}
|
|
|
|
arrfree(mods[i].deps);
|
|
}
|
|
|
|
arrfree(mods);
|
|
}
|
|
|
|
|
|
// Parse a .hcf INI-like file and invoke cb for each key=value entry.
|
|
// Lines starting with # and blank lines are skipped. For source= entries,
|
|
// the value may include a "!excludePattern" suffix which is split out and
|
|
// passed separately. For other keys, exclude is "".
|
|
static void hcfForEachEntry(const char *hcfPath, void (*cb)(const char *key, const char *val, const char *exclude, void *user), void *user) {
|
|
FILE *f = fopen(hcfPath, "r");
|
|
|
|
if (!f) {
|
|
return;
|
|
}
|
|
|
|
char line[512];
|
|
|
|
while (fgets(line, (int)sizeof(line), f)) {
|
|
dvxTrimRight(line);
|
|
|
|
if (line[0] == '#' || line[0] == '\0') {
|
|
continue;
|
|
}
|
|
|
|
// Split "key = value" - key is the run of chars up to space or '='
|
|
const char *p = line;
|
|
int32_t klen = 0;
|
|
|
|
while (p[klen] && p[klen] != ' ' && p[klen] != '=') {
|
|
klen++;
|
|
}
|
|
|
|
char key[32] = {0};
|
|
|
|
if (klen >= (int32_t)sizeof(key)) {
|
|
klen = (int32_t)sizeof(key) - 1;
|
|
}
|
|
|
|
memcpy(key, p, klen);
|
|
key[klen] = '\0';
|
|
|
|
p += klen;
|
|
|
|
while (*p == ' ' || *p == '=') { p++; }
|
|
|
|
// For source=, split out !exclude suffix
|
|
if (strcasecmp(key, "source") == 0) {
|
|
char pattern[DVX_MAX_PATH] = {0};
|
|
char exclude[DVX_MAX_PATH] = {0};
|
|
const char *bang = strchr(p, '!');
|
|
|
|
if (bang) {
|
|
int32_t patLen = (int32_t)(bang - p);
|
|
|
|
while (patLen > 0 && p[patLen - 1] == ' ') { patLen--; }
|
|
|
|
memcpy(pattern, p, patLen);
|
|
pattern[patLen] = '\0';
|
|
bang++;
|
|
|
|
while (*bang == ' ') { bang++; }
|
|
|
|
snprintf(exclude, sizeof(exclude), "%s", bang);
|
|
} else {
|
|
snprintf(pattern, sizeof(pattern), "%s", p);
|
|
}
|
|
|
|
cb(key, pattern, exclude[0] ? exclude : NULL, user);
|
|
} else {
|
|
cb(key, p, NULL, user);
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
|
|
static void helpRecompileIfNeeded(void) {
|
|
PrefsHandleT *ini = prefsLoad(DVX_INI_PATH);
|
|
|
|
if (!ini) {
|
|
dvxLog("helpRecompile: cannot load INI");
|
|
return;
|
|
}
|
|
|
|
int32_t storedLibs = prefsGetInt(ini, "help", "libCount", -1);
|
|
int32_t storedWidgets = prefsGetInt(ini, "help", "widgetCount", -1);
|
|
|
|
// First startup: no [help] section. Save counts, skip recompile.
|
|
if (storedLibs == -1 && storedWidgets == -1) {
|
|
dvxLog("helpRecompile: first startup, saving initial counts (libs=%d widgets=%d)",
|
|
(int)sLibCount, (int)sWidgetCount);
|
|
prefsSetInt(ini, "help", "libCount", sLibCount);
|
|
prefsSetInt(ini, "help", "widgetCount", sWidgetCount);
|
|
prefsSave(ini);
|
|
prefsClose(ini);
|
|
return;
|
|
}
|
|
|
|
// Counts unchanged: no recompile needed
|
|
if (storedLibs == sLibCount && storedWidgets == sWidgetCount) {
|
|
prefsClose(ini);
|
|
return;
|
|
}
|
|
|
|
dvxLog("helpRecompile: counts changed (libs: %d->%d, widgets: %d->%d)",
|
|
(int)storedLibs, (int)sLibCount, (int)storedWidgets, (int)sWidgetCount);
|
|
|
|
// Count total progress steps across all .hcf compilations
|
|
int32_t totalSteps = countTotalHelpSteps("APPS");
|
|
|
|
if (totalSteps == 0) {
|
|
dvxLog("helpRecompile: no help sources found");
|
|
prefsSetInt(ini, "help", "libCount", sLibCount);
|
|
prefsSetInt(ini, "help", "widgetCount", sWidgetCount);
|
|
prefsSave(ini);
|
|
prefsClose(ini);
|
|
return;
|
|
}
|
|
|
|
// Reset progress bar for recompile phase
|
|
sSplashLoaded = 0;
|
|
sSplashTotal = totalSteps;
|
|
platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG);
|
|
|
|
// Recursively scan APPS/ for .hcf files and process each
|
|
processHcfDir("APPS");
|
|
|
|
// Save new counts
|
|
prefsSetInt(ini, "help", "libCount", sLibCount);
|
|
prefsSetInt(ini, "help", "widgetCount", sWidgetCount);
|
|
prefsSave(ini);
|
|
prefsClose(ini);
|
|
|
|
dvxLog("helpRecompile: done");
|
|
}
|
|
|
|
|
|
// Progress callback for hlpcCompile -- updates the splash progress bar.
|
|
static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total) {
|
|
(void)ctx;
|
|
(void)total;
|
|
(void)current;
|
|
sSplashLoaded++;
|
|
splashUpdateProgress();
|
|
}
|
|
|
|
|
|
static void **loadAllModules(void) {
|
|
void **handles = NULL;
|
|
|
|
// Scan both directories first to get total module count for progress bar
|
|
ModuleT *libs = NULL;
|
|
scanDir(LIBS_DIR, ".lib", &libs);
|
|
logAndReadDeps(libs);
|
|
|
|
ModuleT *widgets = NULL;
|
|
scanDir(WIDGET_DIR, ".wgt", &widgets);
|
|
logAndReadDeps(widgets);
|
|
|
|
sLibCount = (int32_t)arrlen(libs);
|
|
sWidgetCount = (int32_t)arrlen(widgets);
|
|
sSplashTotal = sLibCount + sWidgetCount;
|
|
|
|
// Abort early on typos or missing prerequisite .lib/.wgt files.
|
|
validateDeps(libs, widgets);
|
|
|
|
// Phase 1: load libraries in dependency order
|
|
loadInOrder(libs);
|
|
|
|
for (int32_t i = 0; i < arrlen(libs); i++) {
|
|
if (libs[i].handle) {
|
|
arrput(handles, libs[i].handle);
|
|
}
|
|
}
|
|
|
|
freeMods(libs);
|
|
|
|
// Phase 2: load widgets in dependency order (all libs already loaded)
|
|
loadInOrder(widgets);
|
|
|
|
for (int32_t i = 0; i < arrlen(widgets); i++) {
|
|
if (widgets[i].handle) {
|
|
arrput(handles, widgets[i].handle);
|
|
}
|
|
}
|
|
|
|
freeMods(widgets);
|
|
|
|
return handles;
|
|
}
|
|
|
|
|
|
// Repeatedly scans the module list for entries whose dependencies
|
|
// are all satisfied, loads them, and marks them done. Stops when
|
|
// all modules are loaded or no progress can be made (circular dep).
|
|
static void loadInOrder(ModuleT *mods) {
|
|
typedef void (*RegFnT)(void);
|
|
int32_t total = arrlen(mods);
|
|
int32_t loaded = 0;
|
|
bool progress;
|
|
|
|
do {
|
|
progress = false;
|
|
|
|
for (int32_t i = 0; i < total; i++) {
|
|
if (mods[i].loaded) {
|
|
continue;
|
|
}
|
|
|
|
if (!allDepsLoaded(&mods[i], mods)) {
|
|
continue;
|
|
}
|
|
|
|
dvxLog("Loading: %s", mods[i].path);
|
|
mods[i].handle = dlopen(mods[i].path, RTLD_NOW | RTLD_GLOBAL);
|
|
|
|
if (!mods[i].handle) {
|
|
const char *err = dlerror();
|
|
dvxLog(" FAILED: %s", err ? err : "(unknown)");
|
|
splashShutdownIfActive();
|
|
fprintf(stderr, "FATAL: Failed to load %s\n %s\n", mods[i].path, err ? err : "(unknown error)");
|
|
exit(1);
|
|
}
|
|
|
|
RegFnT regFn = (RegFnT)dlsym(mods[i].handle, DVX_SYM("wgtRegister"));
|
|
|
|
if (regFn) {
|
|
regFn();
|
|
|
|
// Record the .wgt path for any newly registered ifaces
|
|
typedef int32_t (*IfaceCountFnT)(void);
|
|
typedef const void *(*IfaceAtFnT)(int32_t, const char **);
|
|
typedef const char *(*IfaceGetPathFnT)(const char *);
|
|
typedef void (*IfaceSetPathFnT)(const char *, const char *);
|
|
|
|
IfaceCountFnT countFn = (IfaceCountFnT)dlsym(NULL, DVX_SYM("wgtIfaceCount"));
|
|
IfaceAtFnT atFn = (IfaceAtFnT)dlsym(NULL, DVX_SYM("wgtIfaceAt"));
|
|
IfaceGetPathFnT getPathFn = (IfaceGetPathFnT)dlsym(NULL, DVX_SYM("wgtIfaceGetPath"));
|
|
IfaceSetPathFnT setPathFn = (IfaceSetPathFnT)dlsym(NULL, DVX_SYM("wgtIfaceSetPath"));
|
|
|
|
if (countFn && atFn && getPathFn && setPathFn) {
|
|
int32_t ic = countFn();
|
|
|
|
for (int32_t k = 0; k < ic; k++) {
|
|
const char *ifaceName = NULL;
|
|
atFn(k, &ifaceName);
|
|
|
|
if (ifaceName && !getPathFn(ifaceName)) {
|
|
setPathFn(ifaceName, mods[i].path);
|
|
}
|
|
}
|
|
}
|
|
} else if (strstr(mods[i].path, ".wgt") || strstr(mods[i].path, ".WGT")) {
|
|
dvxLog(" No _wgtRegister in %s", mods[i].baseName);
|
|
}
|
|
|
|
mods[i].loaded = true;
|
|
loaded++;
|
|
progress = true;
|
|
sSplashLoaded++;
|
|
splashUpdateProgress();
|
|
}
|
|
} while (progress && loaded < total);
|
|
|
|
if (loaded < total) {
|
|
fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", (int)(total - loaded), (int)total);
|
|
|
|
for (int32_t i = 0; i < total; i++) {
|
|
if (!mods[i].loaded) {
|
|
fprintf(stderr, " %s\n", mods[i].path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void logAndReadDeps(ModuleT *mods) {
|
|
dvxLog("Discovered %d modules:", arrlen(mods));
|
|
|
|
for (int32_t i = 0; i < arrlen(mods); i++) {
|
|
dvxLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName);
|
|
}
|
|
|
|
for (int32_t i = 0; i < arrlen(mods); i++) {
|
|
readDeps(&mods[i]);
|
|
|
|
if (arrlen(mods[i].deps) > 0) {
|
|
char line[512];
|
|
int32_t pos = snprintf(line, sizeof(line), " %s deps:", mods[i].baseName);
|
|
|
|
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
|
|
// dvxStrAppendf bounds the accumulate and returns -1 on
|
|
// truncation (or a prior failure), so a long dep list can
|
|
// never overrun the fixed line buffer.
|
|
pos = dvxStrAppendf(line, sizeof(line), pos, " %s", mods[i].deps[d]);
|
|
|
|
if (pos < 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
dvxLog("%s", line);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void processHcf(const char *hcfPath, const char *hcfDir) {
|
|
ProcessHcfCtxT ctx;
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
ctx.hcfDir = hcfDir;
|
|
|
|
hcfForEachEntry(hcfPath, processHcfCallback, &ctx);
|
|
|
|
int32_t inputCount = (int32_t)arrlen(ctx.inputFiles);
|
|
|
|
if (!ctx.outputFile[0] || inputCount == 0) {
|
|
for (int32_t i = 0; i < inputCount; i++) { free(ctx.inputFiles[i]); }
|
|
arrfree(ctx.inputFiles);
|
|
return;
|
|
}
|
|
|
|
dvxLog("helpRecompile: %s -> %s (%d files)", hcfPath, ctx.outputFile, (int)inputCount);
|
|
|
|
int32_t rc = hlpcCompile((const char **)ctx.inputFiles, inputCount, ctx.outputFile,
|
|
ctx.imgDir[0] ? ctx.imgDir : NULL, NULL, HLPC_QUIET,
|
|
hlpcProgressCallback, NULL);
|
|
|
|
if (rc != 0) {
|
|
dvxLog("helpRecompile: FAILED (rc=%d)", (int)rc);
|
|
}
|
|
|
|
for (int32_t i = 0; i < inputCount; i++) {
|
|
free(ctx.inputFiles[i]);
|
|
}
|
|
|
|
arrfree(ctx.inputFiles);
|
|
}
|
|
|
|
|
|
// Callback for processHcf: accumulate output=, imagedir=, and expanded source= entries.
|
|
static void processHcfCallback(const char *key, const char *val, const char *exclude, void *user) {
|
|
ProcessHcfCtxT *ctx = (ProcessHcfCtxT *)user;
|
|
|
|
if (strcasecmp(key, "output") == 0) {
|
|
snprintf(ctx->outputFile, sizeof(ctx->outputFile), "%s" DVX_PATH_SEP "%s", ctx->hcfDir, val);
|
|
} else if (strcasecmp(key, "imagedir") == 0) {
|
|
snprintf(ctx->imgDir, sizeof(ctx->imgDir), "%s", val);
|
|
} else if (strcasecmp(key, "source") == 0) {
|
|
collectGlobFiles(&ctx->inputFiles, val, exclude);
|
|
}
|
|
}
|
|
|
|
|
|
// Recursively scan a directory for .hcf files and process each one.
|
|
static void processHcfDir(const char *dirPath) {
|
|
char **names = dvxReadDir(dirPath);
|
|
|
|
if (!names) {
|
|
return;
|
|
}
|
|
|
|
int32_t nEntries = (int32_t)arrlen(names);
|
|
|
|
for (int32_t i = 0; i < nEntries; i++) {
|
|
if (names[i][0] == '.') {
|
|
continue;
|
|
}
|
|
|
|
char fullPath[DVX_MAX_PATH];
|
|
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
|
|
|
|
struct stat st;
|
|
|
|
if (stat(fullPath, &st) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
processHcfDir(fullPath);
|
|
} else if (dvxHasExt(names[i], ".hcf")) {
|
|
processHcf(fullPath, dirPath);
|
|
}
|
|
}
|
|
|
|
dvxReadDirFree(names);
|
|
}
|
|
|
|
|
|
// The .dep file has the same path as the module but with a .dep
|
|
// extension. Each line is a dependency base name. Empty lines
|
|
// and lines starting with # are ignored.
|
|
static void readDeps(ModuleT *mod) {
|
|
// Build dep file path: replace extension with .dep
|
|
char depPath[DVX_MAX_PATH];
|
|
strncpy(depPath, mod->path, sizeof(depPath) - 1);
|
|
depPath[sizeof(depPath) - 1] = '\0';
|
|
|
|
char *dot = strrchr(depPath, '.');
|
|
|
|
if (!dot) {
|
|
return;
|
|
}
|
|
|
|
strcpy(dot, ".dep");
|
|
|
|
FILE *f = fopen(depPath, "r");
|
|
|
|
if (!f) {
|
|
return;
|
|
}
|
|
|
|
char line[64];
|
|
|
|
while (fgets(line, sizeof(line), f)) {
|
|
// A physical line longer than the buffer is split across fgets
|
|
// calls. Drain the remainder so an over-long comment's tail (which
|
|
// does not start with '#') is not mistaken for a separate dep name.
|
|
// fgetc returns EOF immediately at end of file, so this is safe even
|
|
// for an unterminated final line.
|
|
if (strchr(line, '\n') == NULL) {
|
|
int32_t ch;
|
|
|
|
while ((ch = fgetc(f)) != '\n' && ch != EOF) {
|
|
// Discard the rest of the over-long physical line.
|
|
}
|
|
}
|
|
|
|
// Strip trailing whitespace (CR/LF/space/tab)
|
|
int32_t len = dvxTrimRight(line);
|
|
|
|
// Skip empty lines and comments
|
|
if (len == 0 || line[0] == '#') {
|
|
continue;
|
|
}
|
|
|
|
// Lowercase the dep name for case-insensitive matching
|
|
for (int32_t i = 0; i < len; i++) {
|
|
line[i] = tolower((unsigned char)line[i]);
|
|
}
|
|
|
|
arrput(mod->deps, strdup(line));
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
|
|
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
|
|
// Collect all entries first, close the handle, then process.
|
|
// DOS has limited file handles; keeping a DIR open during
|
|
// recursion or stat() causes intermittent failures.
|
|
char **names = dvxReadDir(dirPath);
|
|
|
|
if (!names) {
|
|
return;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(names);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
char path[DVX_MAX_PATH];
|
|
snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
|
|
|
|
if (dvxHasExt(names[i], ext)) {
|
|
ModuleT mod;
|
|
memset(&mod, 0, sizeof(mod));
|
|
snprintf(mod.path, sizeof(mod.path), "%s", path);
|
|
extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName));
|
|
arrput(*mods, mod);
|
|
} else {
|
|
struct stat st;
|
|
|
|
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
scanDir(path, ext, mods);
|
|
}
|
|
}
|
|
}
|
|
|
|
dvxReadDirFree(names);
|
|
}
|
|
|
|
|
|
static void splashDrawScreen(void) {
|
|
if (platformSplashLoadRaw("SYSTEM/SPLASH.RAW")) {
|
|
platformSplashFillRect(PBAR_X - 1, PBAR_Y - 1, PBAR_W + 2, PBAR_H + 2, SPLASH_BAR_OUT);
|
|
platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG);
|
|
}
|
|
}
|
|
|
|
|
|
static void splashShutdownIfActive(void) {
|
|
if (sSplashActive) {
|
|
platformSplashShutdown();
|
|
sSplashActive = 0;
|
|
}
|
|
}
|
|
|
|
|
|
static void splashUpdateProgress(void) {
|
|
if (!sSplashActive || sSplashTotal <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t fillW = (PBAR_W * sSplashLoaded) / sSplashTotal;
|
|
|
|
if (fillW > PBAR_W) {
|
|
fillW = PBAR_W;
|
|
}
|
|
|
|
platformSplashFillRect(PBAR_X, PBAR_Y, fillW, PBAR_H, SPLASH_BAR_FG);
|
|
}
|
|
|
|
|
|
// allDepsLoaded silently treats an unknown dep name as "external, assumed
|
|
// OK" so that libc / platform symbols don't trip the topological sort.
|
|
// That's fine for intentional externals but masks typos and missing
|
|
// .lib/.wgt files. This pre-flight check ensures every name in any .dep
|
|
// resolves to a module we actually discovered; anything else is fatal.
|
|
static void validateDeps(const ModuleT *libs, const ModuleT *widgets) {
|
|
const ModuleT *pools[2] = { libs, widgets };
|
|
const char *kinds[2] = { "lib", "widget" };
|
|
int32_t errors = 0;
|
|
|
|
for (int32_t pi = 0; pi < 2; pi++) {
|
|
const ModuleT *mods = pools[pi];
|
|
|
|
for (int32_t i = 0; i < arrlen(mods); i++) {
|
|
for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
|
|
const char *depName = mods[i].deps[d];
|
|
bool found = false;
|
|
|
|
for (int32_t qi = 0; qi < 2 && !found; qi++) {
|
|
const ModuleT *pool = pools[qi];
|
|
|
|
for (int32_t j = 0; j < arrlen(pool); j++) {
|
|
if (strcasecmp(pool[j].baseName, depName) == 0) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
dvxLog("FATAL: %s %s requires missing dep: %s", kinds[pi], mods[i].baseName, depName);
|
|
|
|
// Restore text mode before the first print or the
|
|
// message prints invisibly under the graphics-mode
|
|
// splash.
|
|
splashShutdownIfActive();
|
|
|
|
fprintf(stderr, "FATAL: %s %s requires missing dep: %s\n", kinds[pi], mods[i].baseName, depName);
|
|
errors++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors > 0) {
|
|
// The splash was already shut down when the first error printed.
|
|
fprintf(stderr, "%d unresolved dependency reference(s); aborting.\n", (int)errors);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char *argv[]) {
|
|
// Change to the directory containing the executable so relative
|
|
// paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly.
|
|
char exeDir[DVX_MAX_PATH];
|
|
strncpy(exeDir, argv[0], sizeof(exeDir) - 1);
|
|
exeDir[sizeof(exeDir) - 1] = '\0';
|
|
|
|
char *sep = platformPathDirEnd(exeDir);
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
platformChdir(exeDir);
|
|
}
|
|
|
|
// Truncate log, then use append-per-write
|
|
FILE *logInit = fopen(LOG_PATH, "w");
|
|
|
|
if (logInit) {
|
|
fclose(logInit);
|
|
}
|
|
|
|
// Suppress Ctrl+C before anything else
|
|
platformSetLogPath(LOG_PATH);
|
|
platformInit();
|
|
|
|
// Switch to VGA mode 13h and show graphical splash
|
|
platformSplashInit();
|
|
sSplashActive = 1;
|
|
splashDrawScreen();
|
|
|
|
dvxLog("DVX Loader starting...");
|
|
|
|
// Register platform + libc/libm/runtime symbols for DXE resolution
|
|
platformRegisterDxeExports();
|
|
dvxLog("Platform exports registered.");
|
|
|
|
// Load all modules from libs/ and widgets/ in dependency order.
|
|
// Each module may have a .dep file specifying load-before deps.
|
|
// Widget modules that export wgtRegister() get it called.
|
|
void **handles = loadAllModules();
|
|
|
|
if (!handles || arrlen(handles) == 0) {
|
|
splashShutdownIfActive();
|
|
fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR);
|
|
arrfree(handles);
|
|
return 1;
|
|
}
|
|
|
|
// Check if help files need recompilation (module count changed)
|
|
helpRecompileIfNeeded();
|
|
|
|
// Find and call shellMain from whichever module exports it
|
|
typedef int (*ShellMainFnT)(int, char **);
|
|
ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, DVX_SYM("shellMain"));
|
|
|
|
if (!shellMain) {
|
|
dvxLog("ERROR: No module exports shellMain");
|
|
|
|
splashShutdownIfActive();
|
|
|
|
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
|
|
dlclose(handles[i]);
|
|
}
|
|
|
|
arrfree(handles);
|
|
return 1;
|
|
}
|
|
|
|
int result = shellMain(argc, argv);
|
|
|
|
// Clean up in reverse load order
|
|
for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
|
|
dlclose(handles[i]);
|
|
}
|
|
|
|
arrfree(handles);
|
|
return result;
|
|
}
|