From 145c948804a0b0f893e1d1d6263da37a91fced41 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sun, 8 Mar 2026 08:23:09 -0400 Subject: [PATCH] Add DOS/DJGPP cross-compilation support and CI for Linux + macOS Add a DJGPP CMake toolchain file and DOS compatibility layer (err.h, fnmatch, getprogname/setprogname) so UC2 builds as a native DOS executable via cross-compilation from Linux. The toolchain works around a baked-in /usr/include in the DJGPP GCC binary by using -I instead of -isystem to ensure DJGPP headers take precedence. Add GitHub Actions CI workflow that builds and smoke-tests on both ubuntu-latest and macos-latest. --- .github/workflows/build.yml | 23 ++++ cli/CMakeLists.txt | 4 + cli/src/compat/compat_dos.c | 156 +++++++++++++++++++++++++++ cli/src/compat/include/dos/err.h | 18 ++++ cli/src/compat/include/dos/fnmatch.h | 15 +++ cli/src/main.c | 11 +- cmake/UC2Platform.cmake | 4 + cmake/djgpp.cmake | 34 ++++++ 8 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 cli/src/compat/compat_dos.c create mode 100644 cli/src/compat/include/dos/err.h create mode 100644 cli/src/compat/include/dos/fnmatch.h create mode 100644 cmake/djgpp.cmake diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..10a7be6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Configure + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + - name: Build + run: cmake --build build + - name: Smoke test + run: ./build/cli/uc2 -h diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 308d5b8..1d4be59 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -22,4 +22,8 @@ if(WIN32) g_compat__utf8_console g_compat__wpath g_fopen g_access g_mkdir g_utime ) +elseif(DJGPP) + target_sources(uc2-cli PRIVATE src/compat/compat_dos.c) + # Only add the err.h and fnmatch.h headers, not sys/ overrides + target_include_directories(uc2-cli PRIVATE src/compat/include/dos) endif() diff --git a/cli/src/compat/compat_dos.c b/cli/src/compat/compat_dos.c new file mode 100644 index 0000000..18c39c5 --- /dev/null +++ b/cli/src/compat/compat_dos.c @@ -0,0 +1,156 @@ +/* DOS/DJGPP compatibility layer for UC2. + Provides BSD err.h functions and fnmatch for DJGPP, + which lacks these POSIX/BSD extensions. + Copyright © Jan Bobrowski 2020 / Licence: LGPL + Adapted for DOS by Eremey Valetov 2026 */ + +#include +#include +#include +#include +#include + +/* err/errx/warn/warnx family */ + +#include "err.h" + +static const char *_progname = "uc2"; + +const char *getprogname(void) +{ + return _progname; +} + +void setprogname(const char *argv0) +{ + const char *p = argv0; + for (const char *q = argv0; *q; q++) + if (*q == '/' || *q == '\\') + p = q + 1; + _progname = p; +} + +void vwarn(const char *f, va_list a) +{ + fprintf(stderr, "%s: ", getprogname()); + if (f) { + vfprintf(stderr, f, a); + fprintf(stderr, ": "); + } + fflush(stderr); + perror(0); +} + +void vwarnx(const char *f, va_list a) +{ + fprintf(stderr, "%s: ", getprogname()); + if (f) + vfprintf(stderr, f, a); + fprintf(stderr, "\n"); + fflush(stderr); +} + +void warn(const char *f, ...) +{ + va_list a; + va_start(a, f); + vwarn(f, a); + va_end(a); +} + +void warnx(const char *f, ...) +{ + va_list a; + va_start(a, f); + vwarnx(f, a); + va_end(a); +} + +void verr(int x, const char *f, va_list a) +{ + vwarn(f, a); + exit(x); +} + +void verrx(int x, const char *f, va_list a) +{ + vwarnx(f, a); + exit(x); +} + +void err(int x, const char *f, ...) +{ + va_list a; + va_start(a, f); + verr(x, f, a); +} + +void errx(int x, const char *f, ...) +{ + va_list a; + va_start(a, f); + verrx(x, f, a); +} + +/* fnmatch */ + +#include "fnmatch.h" + +int fnmatch(const char *pattern, const char *string, int flags) +{ + for (;;) { + char c = *pattern++; + switch (c) { + case '\\': + if (*pattern && !(flags & FNM_NOESCAPE)) + c = *pattern++; + default: + if (c != *string++) + return FNM_NOMATCH; + if (!c) + return 0; + continue; + case '?': + c = *string++; + if (!c || (flags & FNM_PATHNAME && c == '/')) + return FNM_NOMATCH; + continue; + case '*': + do { + if (fnmatch(pattern, string, flags) == 0) + return 0; + if (flags & FNM_PATHNAME && *string == '/') + return FNM_NOMATCH; + } while (*string++); + return FNM_NOMATCH; + case '[':; + const char *p = pattern; + if (!*pattern++) + return FNM_NOMATCH; + for (;;) { + c = *pattern; + if (c == ']') + break; + if (!c) + return FNM_NOMATCH; + pattern++; + } + c = *string++; + if (flags & FNM_PATHNAME && c == '/') + return FNM_NOMATCH; + for (;;) { + if (c == *p++) + break; + if (*p == '-' && p + 1 < pattern) { + if (p[-1] <= c && c <= p[1]) + break; + p++; + } + if (p == pattern) + return FNM_NOMATCH; + } + pattern++; + continue; + } + } +} diff --git a/cli/src/compat/include/dos/err.h b/cli/src/compat/include/dos/err.h new file mode 100644 index 0000000..473d622 --- /dev/null +++ b/cli/src/compat/include/dos/err.h @@ -0,0 +1,18 @@ +#ifndef _ERR_H +#define _ERR_H +#ifdef __GNUC__ +#define err_noreturn __attribute__((noreturn)) +//#define err_noreturn [[noreturn]] +#else +#define err_noreturn +#endif +err_noreturn void err(int x, const char* f, ...); +err_noreturn void errx(int x, const char* f, ...); +void warn(const char* f, ...); +void warnx(const char* f, ...); +#include +void verr(int x, const char* f, va_list a); +void verrx(int x, const char* f, va_list a); +void vwarn(const char* f, va_list a); +void vwarnx(const char* f, va_list a); +#endif diff --git a/cli/src/compat/include/dos/fnmatch.h b/cli/src/compat/include/dos/fnmatch.h new file mode 100644 index 0000000..6905693 --- /dev/null +++ b/cli/src/compat/include/dos/fnmatch.h @@ -0,0 +1,15 @@ +#ifndef _FNMATCH_H +#define _FNMATCH_H + +#define FNM_PATHNAME 0x1 +#define FNM_NOESCAPE 0x2 +#define FNM_PERIOD 0x4 +#define FNM_LEADING_DIR 0x8 +#define FNM_CASEFOLD 0x10 + +#define FNM_NOMATCH 1 +#define FNM_NOSYS (-1) + +int fnmatch(const char *, const char *, int); + +#endif diff --git a/cli/src/main.c b/cli/src/main.c index 75b5ae7..412b992 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -16,12 +16,18 @@ #include #include #include +#ifdef __DJGPP__ +#include +#include +void setprogname(const char *argv0); +#else #include #include +#include +#endif #include #include #include -#include #include #include @@ -465,6 +471,9 @@ static bool extract_cb(struct node *ne, void *ctx, enum cause cause) int main(int argc, char *argv[]) { +#ifdef __DJGPP__ + setprogname(argv[0]); +#endif if (argc == 1) goto usage; diff --git a/cmake/UC2Platform.cmake b/cmake/UC2Platform.cmake index d3bcc94..7bb841b 100644 --- a/cmake/UC2Platform.cmake +++ b/cmake/UC2Platform.cmake @@ -2,6 +2,10 @@ if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Wall -Wextra -Wno-unused-parameter) + if(DJGPP) + # DJGPP needs gnu99 for PATH_MAX and other POSIX extensions + add_compile_options(-std=gnu99) + endif() elseif(MSVC) add_compile_options(/W3) endif() diff --git a/cmake/djgpp.cmake b/cmake/djgpp.cmake new file mode 100644 index 0000000..7e62f1f --- /dev/null +++ b/cmake/djgpp.cmake @@ -0,0 +1,34 @@ +# CMake toolchain file for DJGPP cross-compilation (DOS target) +# +# Usage: cmake -B build-dos -DCMAKE_TOOLCHAIN_FILE=cmake/djgpp.cmake +# cmake --build build-dos + +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR i586) + +set(DJGPP_ROOT "/usr/local/djgpp" CACHE PATH "DJGPP installation root") +set(DJGPP_PREFIX "i586-pc-msdosdjgpp" CACHE STRING "DJGPP toolchain prefix") + +set(CMAKE_C_COMPILER "${DJGPP_ROOT}/bin/${DJGPP_PREFIX}-gcc") +set(CMAKE_CXX_COMPILER "${DJGPP_ROOT}/bin/${DJGPP_PREFIX}-g++") +set(CMAKE_ASM_COMPILER "${DJGPP_ROOT}/bin/${DJGPP_PREFIX}-gcc") +set(CMAKE_AR "${DJGPP_ROOT}/bin/${DJGPP_PREFIX}-ar" CACHE FILEPATH "") +set(CMAKE_RANLIB "${DJGPP_ROOT}/bin/${DJGPP_PREFIX}-ranlib" CACHE FILEPATH "") +set(CMAKE_STRIP "${DJGPP_ROOT}/bin/${DJGPP_PREFIX}-strip" CACHE FILEPATH "") + +# This DJGPP cross-compiler has /usr/include baked in and -nostdinc doesn't +# remove it. Using -I (not -isystem) puts the DJGPP paths before /usr/include +# so the correct headers are always found first. +set(_DJGPP_NOSTDINC "-nostdinc -I${DJGPP_ROOT}/lib/gcc/${DJGPP_PREFIX}/12.2.0/include -I${DJGPP_ROOT}/lib/gcc/${DJGPP_PREFIX}/12.2.0/include-fixed -I${DJGPP_ROOT}/${DJGPP_PREFIX}/sys-include") +set(CMAKE_C_FLAGS_INIT "${_DJGPP_NOSTDINC}") +set(CMAKE_ASM_FLAGS_INIT "${_DJGPP_NOSTDINC}") + +set(CMAKE_FIND_ROOT_PATH "${DJGPP_ROOT}/${DJGPP_PREFIX}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_EXECUTABLE_SUFFIX ".exe") + +set(DJGPP TRUE) +set(DOS TRUE)