diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10a7be6..9c16c5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,21 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + include: + - { os: ubuntu-latest, name: Linux } + - { os: macos-latest, name: macOS } + - { os: windows-latest, name: Windows (MSVC) } runs-on: ${{ matrix.os }} + name: ${{ matrix.name }} 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: cmake --build build --config Release + - name: Smoke test (Unix) + if: runner.os != 'Windows' run: ./build/cli/uc2 -h + - name: Smoke test (Windows) + if: runner.os == 'Windows' + run: .\build\cli\Release\uc2.exe -h diff --git a/.gitignore b/.gitignore index e40e07b..e3f5fc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build build/ +build-*/ cmake-build-*/ # IDE diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 1d4be59..3125213 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -15,12 +15,22 @@ target_compile_features(uc2-cli PRIVATE c_std_99) if(WIN32) target_sources(uc2-cli PRIVATE src/compat/compat_win32.c) + # Shared compat headers (err.h, fnmatch.h) — both MSVC and MinGW lack these target_include_directories(uc2-cli PRIVATE src/compat/include) + if(MSVC) + # MSVC standalone headers (unistd.h, utime.h, getopt.h) — no #include_next + target_sources(uc2-cli PRIVATE src/compat/getopt.c) + target_include_directories(uc2-cli PRIVATE src/compat/include/msvc) + else() + # MinGW/Clang: headers that wrap system headers via #include_next + target_include_directories(uc2-cli PRIVATE src/compat/include/posix) + endif() target_compile_definitions(uc2-cli PRIVATE + NO_OLDNAMES g_err g_errx g_warn g_warnx g_vwarn g_vwarnx g_verr g_verrx g_getprogname g_setlinebuf g_fnmatch g_compat__utf8_console g_compat__wpath g_fopen - g_access g_mkdir g_utime + g_access g_unlink g_chdir g_mkdir g_chmod g_utime ) elseif(DJGPP) target_sources(uc2-cli PRIVATE src/compat/compat_dos.c) diff --git a/cli/src/compat/compat_win32.c b/cli/src/compat/compat_win32.c index d535b11..d5b923c 100644 --- a/cli/src/compat/compat_win32.c +++ b/cli/src/compat/compat_win32.c @@ -1,4 +1,7 @@ -/* Copyright © Jan Bobrowski 2020 / Licence: LGPL */ +/* Win32 compatibility layer for UC2 CLI. + Provides POSIX/BSD functions missing from MSVC and MinGW. + All file operations use wide-char Windows APIs for UTF-8 support. + Copyright (c) Jan Bobrowski 2020 / Licence: LGPL */ #define NO_OLDNAMES #include @@ -110,7 +113,7 @@ const char *getprogname(void) { static char name[256]; if (!name[0]) { -#ifdef _pgmptr +#ifdef _WIN32 char *p = _pgmptr; char *q = p; int n; @@ -215,6 +218,18 @@ wchar_t *compat__wpath(const char *path); #ifdef g_compat__utf8_console #include +#ifdef _MSC_VER +/* MSVC: use CRT initializer table (.CRT$XCU) instead of GCC constructor */ +static void __cdecl compat__utf8_console_init(void) +{ + setvbuf(stdout, 0, _IOFBF, 1<<16); + setvbuf(stderr, 0, _IOFBF, 1<<16); + SetConsoleOutputCP(CP_UTF8); +} +#pragma section(".CRT$XCU", read) +__declspec(allocate(".CRT$XCU")) +static void (__cdecl *compat__utf8_console_p)(void) = compat__utf8_console_init; +#else __attribute__((constructor)) void compat__utf8_console(void) { @@ -223,6 +238,7 @@ void compat__utf8_console(void) SetConsoleOutputCP(CP_UTF8); } #endif +#endif #ifdef g_compat__wpath wchar_t *compat__wpath(const char *path) @@ -253,6 +269,22 @@ int access(const char *path, int mode) } #endif +#ifdef g_unlink +int unlink(const char *path) +{ + wchar_t *wpath = compat__wpath(path); + return wpath ? _wunlink(wpath) : -1; +} +#endif + +#ifdef g_chdir +int chdir(const char *path) +{ + wchar_t *wpath = compat__wpath(path); + return wpath ? _wchdir(wpath) : -1; +} +#endif + #ifdef g_mkdir int mkdir(const char *path, int mode) { @@ -266,14 +298,27 @@ int mkdir(const char *path, int mode) } #endif +#ifdef g_chmod +int chmod(const char *path, int mode) +{ + wchar_t *wpath = compat__wpath(path); + return wpath ? _wchmod(wpath, mode) : -1; +} +#endif + #ifdef g_utime #include +#ifdef _MSC_VER +/* MSVC's hides utimbuf behind NO_OLDNAMES */ +#include +struct utimbuf { time_t actime; time_t modtime; }; +#endif int utime(const char *path, struct utimbuf *ut) { wchar_t *wpath = compat__wpath(path); if (!wpath) return -1; - struct __utimbuf32 wut = {.actime = ut->actime, .modtime = ut->modtime}; + struct __utimbuf32 wut = {.actime = (long)ut->actime, .modtime = (long)ut->modtime}; return _wutime32(wpath, &wut); } #endif diff --git a/cli/src/compat/getopt.c b/cli/src/compat/getopt.c new file mode 100644 index 0000000..5395843 --- /dev/null +++ b/cli/src/compat/getopt.c @@ -0,0 +1,66 @@ +/* Minimal POSIX getopt() for MSVC. + Supports short options with optional arguments (e.g., "d:"). */ + +#include +#include +#include "include/msvc/getopt.h" + +char *optarg; +int optind = 1, opterr = 1, optopt; + +int getopt(int argc, char *const argv[], const char *optstring) +{ + static int optpos = 0; + + if (optind >= argc || !argv[optind]) + return -1; + + if (argv[optind][0] != '-' || !argv[optind][1]) + return -1; + + if (argv[optind][1] == '-' && !argv[optind][2]) { + optind++; + return -1; + } + + if (!optpos) + optpos = 1; + + int c = argv[optind][optpos]; + const char *p = strchr(optstring, c); + + if (!p || c == ':') { + optopt = c; + if (opterr && optstring[0] != ':') + fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c); + if (!argv[optind][++optpos]) { + optind++; + optpos = 0; + } + return '?'; + } + + if (p[1] == ':') { + if (argv[optind][optpos + 1]) { + optarg = &argv[optind][optpos + 1]; + } else if (++optind >= argc) { + optopt = c; + if (opterr && optstring[0] != ':') + fprintf(stderr, "%s: option requires an argument -- '%c'\n", + argv[0], c); + optpos = 0; + return optstring[0] == ':' ? ':' : '?'; + } else { + optarg = argv[optind]; + } + optind++; + optpos = 0; + } else { + if (!argv[optind][++optpos]) { + optind++; + optpos = 0; + } + } + + return c; +} diff --git a/cli/src/compat/include/err.h b/cli/src/compat/include/err.h index 473d622..749e49d 100644 --- a/cli/src/compat/include/err.h +++ b/cli/src/compat/include/err.h @@ -2,7 +2,8 @@ #define _ERR_H #ifdef __GNUC__ #define err_noreturn __attribute__((noreturn)) -//#define err_noreturn [[noreturn]] +#elif defined(_MSC_VER) +#define err_noreturn __declspec(noreturn) #else #define err_noreturn #endif diff --git a/cli/src/compat/include/msvc/getopt.h b/cli/src/compat/include/msvc/getopt.h new file mode 100644 index 0000000..9766ed8 --- /dev/null +++ b/cli/src/compat/include/msvc/getopt.h @@ -0,0 +1,10 @@ +/* Minimal POSIX getopt for MSVC */ +#ifndef _COMPAT_GETOPT_H +#define _COMPAT_GETOPT_H + +extern char *optarg; +extern int optind, opterr, optopt; + +int getopt(int argc, char *const argv[], const char *optstring); + +#endif diff --git a/cli/src/compat/include/msvc/unistd.h b/cli/src/compat/include/msvc/unistd.h new file mode 100644 index 0000000..bd3fad4 --- /dev/null +++ b/cli/src/compat/include/msvc/unistd.h @@ -0,0 +1,32 @@ +/* Minimal POSIX unistd.h for MSVC */ +#ifndef _COMPAT_UNISTD_H +#define _COMPAT_UNISTD_H + +#include +#include + +#ifndef F_OK +#define F_OK 0 +#endif +#ifndef R_OK +#define R_OK 4 +#endif +#ifndef W_OK +#define W_OK 2 +#endif +#ifndef X_OK +#define X_OK 0 +#endif + +#ifndef PATH_MAX +#define PATH_MAX 260 +#endif + +/* Provided by compat_win32.c (UTF-8-aware via wide-char APIs) */ +int access(const char *path, int mode); +int unlink(const char *path); +int chdir(const char *path); +int mkdir(const char *path, int mode); +int chmod(const char *path, int mode); + +#endif diff --git a/cli/src/compat/include/msvc/utime.h b/cli/src/compat/include/msvc/utime.h new file mode 100644 index 0000000..c5f5e91 --- /dev/null +++ b/cli/src/compat/include/msvc/utime.h @@ -0,0 +1,14 @@ +/* POSIX utime.h for MSVC (which only provides sys/utime.h) */ +#ifndef _COMPAT_UTIME_H +#define _COMPAT_UTIME_H + +#include + +struct utimbuf { + time_t actime; + time_t modtime; +}; + +int utime(const char *path, struct utimbuf *ut); + +#endif diff --git a/cli/src/compat/include/sys/stat.h b/cli/src/compat/include/posix/sys/stat.h similarity index 100% rename from cli/src/compat/include/sys/stat.h rename to cli/src/compat/include/posix/sys/stat.h diff --git a/cli/src/compat/include/sys/utime.h b/cli/src/compat/include/posix/sys/utime.h similarity index 100% rename from cli/src/compat/include/sys/utime.h rename to cli/src/compat/include/posix/sys/utime.h diff --git a/cli/src/compat/include/unistd.h b/cli/src/compat/include/posix/unistd.h similarity index 100% rename from cli/src/compat/include/unistd.h rename to cli/src/compat/include/posix/unistd.h diff --git a/cmake/UC2Platform.cmake b/cmake/UC2Platform.cmake index 7bb841b..2dd1999 100644 --- a/cmake/UC2Platform.cmake +++ b/cmake/UC2Platform.cmake @@ -8,4 +8,5 @@ if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") endif() elseif(MSVC) add_compile_options(/W3) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS) endif()