From d5c8ac701f70044c6f8a56a4c8a70325398fd231 Mon Sep 17 00:00:00 2001 From: mid-kid Date: Mon, 24 Jul 2023 23:37:39 +0200 Subject: [PATCH] Rewrite metrowerks wrapper in C --- .gitignore | 8 +- config.sh | 30 ++- meson/cross.ini | 8 +- meson/cross_wine.ini | 8 +- tools/cw/meson.build | 18 ++ tools/cw/mwrap.c | 546 +++++++++++++++++++++++++++++++++++++++++++ tools/cw/wrapper.sh | 73 ------ 7 files changed, 602 insertions(+), 89 deletions(-) create mode 100644 tools/cw/meson.build create mode 100644 tools/cw/mwrap.c delete mode 100755 tools/cw/wrapper.sh diff --git a/.gitignore b/.gitignore index a29b1971fe..dbceffe534 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Meson files +/build/ +/meson/root.ini +/tools/cw/build/ +/tools/cw/mwrap + # CLion folders .idea/ cmake-build-debug/ @@ -49,8 +55,6 @@ cmake-build-* # Depfiles *.d -root.ini - diff.txt .vscode/ diff --git a/config.sh b/config.sh index 03e818bdb3..574def0abe 100644 --- a/config.sh +++ b/config.sh @@ -1,4 +1,18 @@ #!/usr/bin/env bash +set -e + +dir_build="build" + +wrap=tools/cw + +# Build the wrapper +if ! "$wrap/mwrap" -test 2> /dev/null; then + rm -rf "$wrap/mwrap" "$wrap/build" + "${MESON:-meson}" setup "$wrap/build" "$wrap" + "${MESON:-meson}" compile -C "$wrap/build" + install -m755 "$wrap/build/mwrap" "$wrap/mwrap" + rm -rf "$wrap/build" +fi is_wsl_accessing_windows() { kernel=$(uname -r) @@ -28,12 +42,16 @@ else cross_file="cross.ini" fi -# Launch meson -"${MESON:-meson}" setup build --native-file=meson/"$native_file" --native-file=meson/root.ini --cross-file=meson/"$cross_file" --cross-file=meson/root.ini +mkdir -p "$dir_build" +export MWCONFIG="$(realpath "$dir_build/.mwconfig")" if [ "$native_file" = "native_wine.ini" ]; then -cat > build/.mwconfig << EOF -path_unix="$PWD" -path_win="$("${WINE:-wine}" winepath -w "$PWD")" -EOF + wrap_wine="$(command -v "${WINELOADER:-wine}")" + wrap_path_unx="$PWD" + wrap_path_win="$("$wrap_wine" winepath -w "$wrap_path_unx")" + "$wrap/mwrap" -conf -wine "$wrap_wine" \ + -path_unx "$wrap_path_unx" -path_win "$wrap_path_win" fi + +# Launch meson +"${MESON:-meson}" setup build --native-file=meson/"$native_file" --native-file=meson/root.ini --cross-file=meson/"$cross_file" --cross-file=meson/root.ini diff --git a/meson/cross.ini b/meson/cross.ini index b6307cd61f..27e6266c19 100644 --- a/meson/cross.ini +++ b/meson/cross.ini @@ -1,8 +1,8 @@ [binaries] -c = [root + '/tools/cw/wrapper.sh', 'mwccarm'] -c_ld = [root + '/tools/cw/wrapper.sh', 'mwldarm'] -nasm = [root + '/tools/cw/wrapper.sh', 'mwasmarm'] -ar = [root + '/tools/cw/wrapper.sh', 'mwldarm'] +c = [root + '/tools/cw/mwrap', 'mwccarm'] +c_ld = [root + '/tools/cw/mwrap', 'mwldarm'] +nasm = [root + '/tools/cw/mwrap', 'mwasmarm'] +ar = [root + '/tools/cw/mwrap', 'mwldarm'] strip = ['false'] [host_machine] diff --git a/meson/cross_wine.ini b/meson/cross_wine.ini index 07cecbcafd..27e6266c19 100644 --- a/meson/cross_wine.ini +++ b/meson/cross_wine.ini @@ -1,8 +1,8 @@ [binaries] -c = [root + '/tools/cw/wrapper.sh', 'mwccarm', '-wrap:wine'] -c_ld = [root + '/tools/cw/wrapper.sh', 'mwldarm', '-wrap:wine'] -nasm = [root + '/tools/cw/wrapper.sh', 'mwasmarm', '-wrap:wine'] -ar = [root + '/tools/cw/wrapper.sh', 'mwldarm', '-wrap:wine'] +c = [root + '/tools/cw/mwrap', 'mwccarm'] +c_ld = [root + '/tools/cw/mwrap', 'mwldarm'] +nasm = [root + '/tools/cw/mwrap', 'mwasmarm'] +ar = [root + '/tools/cw/mwrap', 'mwldarm'] strip = ['false'] [host_machine] diff --git a/tools/cw/meson.build b/tools/cw/meson.build new file mode 100644 index 0000000000..ff392fe2e5 --- /dev/null +++ b/tools/cw/meson.build @@ -0,0 +1,18 @@ +project('metrowerks', ['c', 'cpp'], + default_options : ['warning_level=2']) + +if host_machine.system() == 'windows' + winflags = [ + '-mconsole', + '-municode' + ] + add_project_arguments(winflags, language : ['c', 'cpp']) + add_project_link_arguments(winflags, language : ['c', 'cpp']) + add_project_arguments( + '-DUNICODE', + '-D_UNICODE', + '-D_CRT_SECURE_NO_WARNINGS', + language : ['c', 'cpp']) +endif + +executable('mwrap', 'mwrap.c') diff --git a/tools/cw/mwrap.c b/tools/cw/mwrap.c new file mode 100644 index 0000000000..1b2d335106 --- /dev/null +++ b/tools/cw/mwrap.c @@ -0,0 +1,546 @@ +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +extern char **environ; +#endif + +#define PROGRAM_NAME "mwrap" + +#define DEFAULT_VER "2.0/sp2p2" +#define DEFAULT_CFG_FILE ".mwconfig" +#define VER_CFG 0 + +#ifdef _UNICODE +#define FMT_TS "%ls" +#else +#define FMT_TS "%s" +#endif + +// POSIX-compatibility +#ifndef _WIN32 +typedef char _TCHAR; +#define _tmain main +#define _T(...) __VA_ARGS__ +#define _ftprintf(...) fprintf(__VA_ARGS__) +#define _sntprintf(...) snprintf(__VA_ARGS__) +#define _tcscat(...) strcat(__VA_ARGS__) +#define _tcscmp(...) strcmp(__VA_ARGS__) +#define _tcscspn(...) strcspn(__VA_ARGS__) +#define _tcsdup(...) strdup(__VA_ARGS__) +#define _tcslen(...) strlen(__VA_ARGS__) +#define _tcsrchr(...) strrchr(__VA_ARGS__) +#define _tfopen(...) fopen(__VA_ARGS__) +#define _tgetenv(...) getenv(__VA_ARGS__) +#endif + +struct args { + _TCHAR *o; + _TCHAR *precompile; + bool M; + bool MD; + + bool wrap_dbg; + _TCHAR *wrap_ver; +}; + +struct args parse_args(int argc, _TCHAR *argv[], int *out_argc, _TCHAR ***out_argv) +{ + struct args args = { + .o = NULL, + .precompile = NULL, + .M = false, + .MD = false, + .wrap_dbg = false, + .wrap_ver = NULL + }; + + int new_argc = 0; + _TCHAR **new_argv = malloc(sizeof(_TCHAR *) * (argc + 2)); + new_argv[new_argc++] = NULL; + + while (argc >= 1) { + int skip = 0; + int copy = 0; + + if (_tcscmp(argv[0], _T("-o")) == 0 && + argc >= 2) { + args.o = argv[1]; + copy = 2; + } else if (_tcscmp(argv[0], _T("-precompile")) == 0 && + argc >= 2) { + args.precompile = argv[1]; + copy = 2; + } else if (_tcscmp(argv[0], _T("-M")) == 0) { + args.M = true; + copy = 1; + } else if (_tcscmp(argv[0], _T("-MD")) == 0) { + args.MD = true; + copy = 1; + } else if (_tcscmp(argv[0], _T("-wrap:dbg")) == 0) { + args.wrap_dbg = true; + skip = 1; + } else if (_tcscmp(argv[0], _T("-wrap:ver")) == 0 && + argc >= 2) { + args.wrap_ver = argv[1]; + skip = 2; + } else { + copy = 1; + } + + argc -= skip + copy; + while (skip--) argv++; + while (copy--) new_argv[new_argc++] = *argv++; + } + new_argv[new_argc] = NULL; + + *out_argc = new_argc; + *out_argv = new_argv; + return args; +} + +char *file_read(const _TCHAR *fname, size_t *size) +{ + FILE *f = _tfopen(fname, _T("rb")); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + *size = ftell(f); + char *file = malloc(*size); + rewind(f); + fread(file, *size, 1, f); + fclose(f); + return file; +} + +char *tctoutf(const _TCHAR *str) +{ + if (!str) return NULL; +#ifdef _UNICODE + size_t size = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL); + if (!size) return NULL; + char *res = malloc(size); + if (!res) return NULL; + WideCharToMultiByte(CP_UTF8, 0, str, -1, res, size, NULL, NULL); + return res; +#else + return strdup(str); +#endif +} + +_TCHAR *utftotc(const char *str) +{ + if (!str) return NULL; +#ifdef _UNICODE + size_t size = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (!size) return NULL; + _TCHAR *res = malloc(size); + if (!res) return NULL; + MultiByteToWideChar(CP_UTF8, 0, str, -1, res, size); + return res; +#else + return strdup(str); +#endif +} + +struct config { + char *wine; + char *path_unx; + char *path_win; +}; + +const _TCHAR *cfg_file(void) +{ + const _TCHAR *path = _tgetenv(_T("MWCONFIG")); + if (!path) path = _T(DEFAULT_CFG_FILE); + return path; +} + +void cfg_save_writestr(FILE *f, const char *str) +{ + if (str) fputs(str, f); + fputc('\0', f); +} + +void cfg_save(struct config cfg) +{ + FILE *f = _tfopen(cfg_file(), _T("wb")); + if (!f) { + fprintf(stderr, PROGRAM_NAME ": cfg_save: Failed to write config\n"); + exit(EXIT_FAILURE); + } + + const char head[] = {'M', 'W', 'R', VER_CFG}; + fwrite(head, sizeof(head), 1, f); + cfg_save_writestr(f, cfg.wine); + cfg_save_writestr(f, cfg.path_unx); + cfg_save_writestr(f, cfg.path_win); + fclose(f); +} + +char *cfg_load_readstr(char *file, size_t *pos, size_t len) +{ + // Check if a string is present + if (len < *pos + 1) return NULL; + if (file[*pos] == '\0') { + // Treat empty string as empty + (*pos)++; + return NULL; + } + + // Look for the null byte + size_t readlen = strnlen(file + *pos, len - *pos); + if (len - *pos <= readlen) return NULL; + + // If found, we can safely read it + char *readstr = strdup(file + *pos); + *pos += readlen + 1; + return readstr; +} + +struct config cfg_load(void) +{ + struct config cfg; + cfg.wine = NULL; + cfg.path_unx = NULL; + cfg.path_win = NULL; + + size_t file_len = 0; + char *file = file_read(cfg_file(), &file_len); + if (!file) return cfg; + size_t file_pos = 0; + + const char head[] = {'M', 'W', 'R', VER_CFG}; + if (file_len < file_pos + sizeof(head)) return cfg; + if (memcmp(file + file_pos, head, sizeof(head)) != 0) return cfg; + file_pos += sizeof(head); + + cfg.wine = cfg_load_readstr(file, &file_pos, file_len); + cfg.path_unx = cfg_load_readstr(file, &file_pos, file_len); + cfg.path_win = cfg_load_readstr(file, &file_pos, file_len); + + free(file); + return cfg; +} + +void cfg_free(struct config cfg) +{ + free(cfg.path_win); + free(cfg.path_unx); + free(cfg.wine); +} + +void configure(int argc, _TCHAR *argv[]) +{ + _TCHAR *wine = NULL; + _TCHAR *path_unx = NULL; + _TCHAR *path_win = NULL; + + while (argc >= 1) { + if (_tcscmp(argv[0], _T("-wine")) == 0) { + wine = argv[1]; + argv += 2; argc -= 2; + } else if (_tcscmp(argv[0], _T("-path_unx")) == 0) { + path_unx = argv[1]; + argv += 2; argc -= 2; + } else if (_tcscmp(argv[0], _T("-path_win")) == 0) { + path_win = argv[1]; + argv += 2; argc -= 2; + } else { + fprintf(stderr, + PROGRAM_NAME ": configure: Invalid argument: " FMT_TS "\n", + *argv); + exit(EXIT_FAILURE); + } + } + + struct config cfg; + cfg.wine = tctoutf(wine); + cfg.path_unx = tctoutf(path_unx); + cfg.path_win = tctoutf(path_win); + if ((wine && !cfg.wine) || + (path_unx && !cfg.path_unx) || + (path_win && !cfg.path_win)) { + fprintf(stderr, PROGRAM_NAME ": configure: Conversion failed\n"); + exit(EXIT_FAILURE); + } + cfg_save(cfg); + cfg_free(cfg); +} + +_TCHAR *my_dirname(const _TCHAR *str) +{ + _TCHAR *dir = _tcsdup(str); + size_t sep = _tcslen(dir); + while (sep > 0) { + if (dir[sep] == '/') goto found; +#ifdef _WIN32 + if (dir[sep] == '\\') goto found; +#endif + sep--; + } + // Indicate current dir if no slash was found + dir[0] = '.'; + sep = 1; +found: + dir = realloc(dir, sizeof(*dir) * (sep + 1)); + dir[sep] = '\0'; + return dir; +} + +void str_resize(_TCHAR **str, size_t *max, size_t req) +{ + if (*max < req) { + while (*max < req) *max += 1024; + *str = realloc(*str, sizeof(**str) * *max); + } +} + +// Windows argv[] strings are passed as a whole, instead of being split up into +// an array. This means that *we* have to make sure to quote our arguments, and +// for some reason nobody every thought to make a library for this. +_TCHAR *win_argv_build(const _TCHAR *const *argv) +{ + size_t args_max = 1024; + size_t args_pos = 0; + _TCHAR *args = malloc(sizeof(_TCHAR) * args_max); + + bool first = true; + + while (*argv) { + const _TCHAR *arg = *argv; + + // Add a space between arguments + if (!first) { + str_resize(&args, &args_max, args_pos + 1); + args[args_pos++] = ' '; + } + first = false; + + // If it doesn't contain any special chars, copy as-is + size_t arg_len = _tcslen(arg); + if (_tcscspn(arg, _T(" \t\n\r")) == arg_len) { + str_resize(&args, &args_max, args_pos + arg_len); + memcpy(args + args_pos, arg, sizeof(_TCHAR) * arg_len); + args_pos += arg_len; + argv++; + continue; + } + + // Quote the argument + int backslashes = 0; + str_resize(&args, &args_max, args_pos + 1); + args[args_pos++] = '"'; + while (*arg) { + if (*arg == '\\') { + backslashes++; + } else if (*arg == '"') { + // Add extra backslashes to escape the quote + backslashes++; + str_resize(&args, &args_max, args_pos + backslashes); + while (backslashes--) args[args_pos++] = '\\'; + backslashes = 0; + } else { + backslashes = 0; + } + + str_resize(&args, &args_max, args_pos + 1); + args[args_pos++] = *arg; + + arg++; + } + // Add extra backslashes to avoid escaping the final quote + if (backslashes) { + str_resize(&args, &args_max, args_pos + backslashes); + while (backslashes--) args[args_pos++] = '\\'; + } + str_resize(&args, &args_max, args_pos + 1); + args[args_pos++] = '"'; + + argv++; + } + + args = realloc(args, sizeof(*args) * (args_pos + 1)); + args[args_pos] = '\0'; + + return args; +} + +void fix_depfile(_TCHAR *fname, const char *path_unx, const char *path_win) +{ + size_t file_size; + char *file = file_read(fname, &file_size); + if (!file) return; + + FILE *f = _tfopen(fname, _T("wb")); + if (!f) { + free(file); + return; + } + + size_t size_win = strlen(path_win); + bool blankline = false; + + // Replace any instances of path_win at the beginning of a line with + // path_unx, and backslashes with forward slashes. + for (size_t x = 0; x < file_size;) { + if (blankline && file_size - x > size_win && + memcmp(file + x, path_win, size_win) == 0) { + fputs(path_unx, f); + x += size_win; + } else if (file_size - x > 2 && file[x] == '\\' && + file[x + 1] != '\r' && file[x + 1] != '\n') { + fputc('/', f); + x++; + } else { + if (blankline && file[x] != ' ' && file[x] != '\t') { + blankline = false; + } + if (file[x] == '\n') blankline = true; + fputc(file[x++], f); + } + } + + fclose(f); + free(file); +} + +int _tmain(int argc, _TCHAR *argv[]) +{ + if (argc < 2) { + fprintf(stderr, PROGRAM_NAME ": Too few arguments\n"); + return EXIT_FAILURE; + } + + if (_tcscmp(argv[1], _T("-test")) == 0) return EXIT_SUCCESS; + if (_tcscmp(argv[1], _T("-conf")) == 0) { + configure(argc - 2, argv + 2); + return EXIT_SUCCESS; + } + + struct config cfg = cfg_load(); + + _TCHAR *tool_dir = my_dirname(argv[0]); + const _TCHAR *tool_ver = _T(DEFAULT_VER); + const _TCHAR *tool_bin = argv[1]; + + int new_argc; + _TCHAR **new_argv; + struct args args = parse_args(argc - 2, argv + 2, &new_argc, &new_argv); + + if (args.wrap_ver) tool_ver = args.wrap_ver; + + // Make a path of the chosen tool + size_t tool_size = _tcslen(tool_dir) + 1 + _tcslen(tool_ver) + 1 + + _tcslen(tool_bin) + 5; + _TCHAR *tool = malloc(sizeof(_TCHAR) * tool_size); + _sntprintf(tool, tool_size, _T(FMT_TS "/" FMT_TS "/" FMT_TS ".exe"), + tool_dir, tool_ver, tool_bin); + new_argv[0] = tool; + + // Add the wine command if requested + _TCHAR *wine = utftotc(cfg.wine); + if (wine) { + new_argc += 1; + new_argv = realloc(new_argv, sizeof(*new_argv) * (new_argc + 1)); + memmove(new_argv + 1, new_argv, sizeof(*new_argv) * new_argc); + new_argv[0] = wine; + } + +#ifdef _WIN32 + // Set up the environment + SetEnvironmentVariable(_T("MWLibraries"), _T(";")); + + // Execute the tool + _TCHAR *argv_quoted = win_argv_build((const _TCHAR **)new_argv); + if (args.wrap_dbg) fprintf(stderr, FMT_TS "\n", argv_quoted); + STARTUPINFO si; + PROCESS_INFORMATION pi; + DWORD exitcode; + ZeroMemory(&si, sizeof(si)); + ZeroMemory(&pi, sizeof(pi)); + si.cb = sizeof(si); + if (!CreateProcess(new_argv[0], argv_quoted, NULL, NULL, FALSE, 0, + NULL, NULL, &si, &pi)) { + fprintf(stderr, PROGRAM_NAME ": CreateProcess failed\n"); + exit(EXIT_FAILURE); + } + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exitcode); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + if (exitcode) return exitcode; + free(argv_quoted); +#else + // Set up the environment + setenv("MWLibraries", ";", true); + + // Execute the tool + if (args.wrap_dbg) { + for (int x = 0; x < new_argc; x++) { + printf(x ? " %s" : "%s", new_argv[x]); + } + printf("\n"); + } + pid_t pid; + if (posix_spawn(&pid, new_argv[0], NULL, NULL, new_argv, environ) != 0) { + perror(PROGRAM_NAME ": posix_spawn"); + exit(EXIT_FAILURE); + } + int exitcode; + waitpid(pid, &exitcode, 0); + if (exitcode) return exitcode; +#endif + + if (wine) free(wine); + free(tool); + free(new_argv); + free(tool_dir); + + // Fix dependency file if generated + if (cfg.path_unx && cfg.path_win) { + _TCHAR *depfile = NULL; + if (args.M) { + // Only generated a depfile + depfile = _tcsdup(args.o); + } else if (args.MD) { + // Generating a depfile as a side-effect of compilation + _TCHAR *out = NULL; + if (args.o) out = args.o; + if (args.precompile) out = args.precompile; + + // Replace filename extension with .d + if (out) { + out = _tcsdup(out); + _TCHAR *dot = _tcsrchr(out, '.'); + if (!dot) dot = _tcsrchr(out, '\0'); + *dot = '\0'; + + size_t len = dot - out + 3; + out = realloc(out, sizeof(*out) * len); + _tcscat(out, _T(".d")); + } + depfile = out; + } + + if (depfile) { + if (args.wrap_dbg) { + fprintf(stderr, "dep: " FMT_TS "\n", depfile); + fprintf(stderr, "path_unx: %s\n", cfg.path_unx); + fprintf(stderr, "path_win: %s\n", cfg.path_win); + } + fix_depfile(depfile, cfg.path_unx, cfg.path_win); + free(depfile); + } + } + + cfg_free(cfg); + return EXIT_SUCCESS; +} diff --git a/tools/cw/wrapper.sh b/tools/cw/wrapper.sh deleted file mode 100755 index 6fecd51b61..0000000000 --- a/tools/cw/wrapper.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh -set -e -tool_bin="$(basename "$1")"; shift -tool_dir="$(dirname "$0")" -tool_ver="2.0/sp2p2" - -wine= -path_unix= -path_win= - -test -f .mwconfig && . ./.mwconfig - -opt_o= -opt_precompile= -opt_M=false -opt_MD=false -opt_config=false - -# Parse/filter the arguments -n="$#" -while [ "$n" -gt 0 ]; do - skip=0 - copy=0 - case "$1" in - -o) opt_o="$2"; copy=2 ;; - -precompile) opt_precompile="$2"; copy=2 ;; - -MD) opt_MD=true; copy=1 ;; - -M) opt_M=true; copy=1 ;; - -wrap:config) opt_config=true; skip=1 ;; - -wrap:wine) wine="${WINE:-wine}"; skip=1 ;; - -wrap:ver) tool_ver="$2"; skip=2 ;; - *) copy=1 ;; - esac - shift $skip - for x in $(seq $copy); do set -- "$@" "$1"; shift 1; done - : $((n-=$skip)) $((n-=$copy)) -done - -# Set environment variables -export MWLibraries=";" -export LM_LICENSE_FILE="$tool_dir/license.dat" - -# Run the tool -set -- "$tool_dir/$tool_ver/$tool_bin.exe" "$@" -test -n "$wine" && set -- "$wine" "$@" -"$@" - -# Process the dependency file -if [ -n "$path_unix" -a -n "$path_win" ]; then - depfile= - if $opt_M; then - depfile="$opt_o" - elif $opt_MD; then - # Automatically generate the filename - out= - test -n "$opt_o" && out="$opt_o" - test -n "$opt_precompile" && out="$opt_precompile" - depfile="$(printf '%s' "$out" | sed 's/[^\.]*$/d/')" - fi - - if [ -n "$depfile" ]; then - sedescape_regex() { - printf '%s' "$@" | sed -e 's/[^^]/[&]/g; s/\^/\\^/g' - } - sedescape_subst() { - printf '%s' "$@" | sed 's/[&/\]/\\&/g' - } - sed -e "s/\<$(sedescape_regex "$path_win")/$(sedescape_subst "$path_unix")/g" \ - -e 's/\r//g' -e 's/\\/\//g' -e 's/\/$/\\/g' \ - -i "$depfile" - cp "$depfile" "$depfile.new" - fi -fi