diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84bb131..0723c0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Docker meta id: meta @@ -64,6 +66,10 @@ jobs: flavor: | latest=false + - name: Compute version + id: version + run: echo "wibo_version=$(git describe --tags --always)" >> "$GITHUB_OUTPUT" + - name: Build Docker image id: docker-build uses: docker/build-push-action@v6 @@ -71,6 +77,7 @@ jobs: file: ${{ matrix.dockerfile }} build-args: | BUILD_TYPE=${{ matrix.build_type }} + WIBO_VERSION=${{ steps.version.outputs.wibo_version }} target: build - name: Tests @@ -84,6 +91,7 @@ jobs: file: ${{ matrix.dockerfile }} build-args: | BUILD_TYPE=${{ matrix.build_type }} + WIBO_VERSION=${{ steps.version.outputs.wibo_version }} target: export outputs: | type=local,dest=dist @@ -102,6 +110,7 @@ jobs: file: ${{ matrix.dockerfile }} build-args: | BUILD_TYPE=${{ matrix.build_type }} + WIBO_VERSION=${{ steps.version.outputs.wibo_version }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/AGENTS.md b/AGENTS.md index 42600d5..33cdf3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ - `cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON` configures a 32-bit toolchain; ensure multilib packages are present. - `cmake --build build --target wibo` compiles the program and tests. - `./build/wibo /path/to/program.exe` runs a Windows binary. Use `WIBO_DEBUG=1` (or `--debug`/`-D`) for verbose logging. Use `--chdir`/`-C` to set the working directory. -- `cmake -B build -DBUILD_TESTING=ON` + `ctest --test-dir build --output-on-failure` runs the self-checking WinAPI fixtures (requires `i686-w64-mingw32-gcc` and `i686-w64-mingw32-windres`). +- `ctest --test-dir build --output-on-failure` runs the self-checking WinAPI fixtures (requires `i686-w64-mingw32-gcc` and `i686-w64-mingw32-windres`). - `clang-format -i path/to/file.cpp` and `clang-tidy path/to/file.cpp -p build` keep contributions aligned with the repo's tooling. ## Coding Style & Naming Conventions diff --git a/CMakeLists.txt b/CMakeLists.txt index f95f2df..f41690b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,40 @@ set(CMAKE_SHARED_LINKER_FLAGS_INIT "-m32") project(wibo LANGUAGES C CXX) +set(WIBO_VERSION "" CACHE STRING "Version string for the wibo binary; if empty, attempts to use git describe") + +if(NOT "${WIBO_VERSION}" STREQUAL "") + set(WIBO_VERSION_STRING "${WIBO_VERSION}") +elseif(DEFINED ENV{WIBO_VERSION} AND NOT "$ENV{WIBO_VERSION}" STREQUAL "") + set(WIBO_VERSION_STRING "$ENV{WIBO_VERSION}") +else() + find_package(Git QUIET) + if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe --tags --dirty --always + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE WIBO_GIT_DESCRIBE_RESULT + OUTPUT_VARIABLE WIBO_GIT_DESCRIBE_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(WIBO_GIT_DESCRIBE_RESULT EQUAL 0 AND NOT "${WIBO_GIT_DESCRIBE_OUTPUT}" STREQUAL "") + set(WIBO_VERSION_STRING "${WIBO_GIT_DESCRIBE_OUTPUT}") + endif() + endif() +endif() + +if(NOT DEFINED WIBO_VERSION_STRING OR "${WIBO_VERSION_STRING}" STREQUAL "") + set(WIBO_VERSION_STRING "unknown") +endif() + +set(WIBO_GENERATED_HEADER_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated) +file(MAKE_DIRECTORY ${WIBO_GENERATED_HEADER_DIR}) +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/src/version_info.h.in + ${WIBO_GENERATED_HEADER_DIR}/version_info.h + @ONLY +) + option(WIBO_ENABLE_FIXTURE_TESTS "Enable Win32 fixture tests (requires i686-w64-mingw32)" ON) option(WIBO_ENABLE_LIBURING "Enable liburing for asynchronous I/O" OFF) set(WIBO_ENABLE_LTO "AUTO" CACHE STRING "Enable link-time optimization (LTO)") @@ -142,7 +176,7 @@ add_executable(wibo target_compile_definitions(wibo PRIVATE _GNU_SOURCE _FILE_OFFSET_BITS=64 _TIME_BITS=64) target_compile_features(wibo PRIVATE cxx_std_20) target_compile_options(wibo PRIVATE -Wall -Wextra -fno-pie -maccumulate-outgoing-args) -target_include_directories(wibo PRIVATE dll src) +target_include_directories(wibo PRIVATE dll src ${WIBO_GENERATED_HEADER_DIR}) target_link_libraries(wibo PRIVATE mimalloc-obj) target_link_options(wibo PRIVATE -no-pie) if (WIBO_ENABLE_LIBURING) diff --git a/Dockerfile b/Dockerfile index 6d4ae2e..4499c71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,9 @@ ARG BUILD_TYPE=Release # Enable link-time optimization (LTO) (AUTO, ON, OFF) ARG ENABLE_LTO=AUTO +# Version string (if not provided, defaults to "unknown") +ARG WIBO_VERSION + # Build static binary RUN cmake -S /wibo -B /wibo/build -G Ninja \ -DCMAKE_BUILD_TYPE:STRING="$BUILD_TYPE" \ @@ -33,6 +36,7 @@ RUN cmake -S /wibo -B /wibo/build -G Ninja \ -DMI_LIBC_MUSL:BOOL=ON \ -DWIBO_ENABLE_LIBURING:BOOL=ON \ -DWIBO_ENABLE_LTO:STRING="$ENABLE_LTO" \ + -DWIBO_VERSION:STRING="$WIBO_VERSION" \ && cmake --build /wibo/build --verbose \ && ( [ "$BUILD_TYPE" != "Release" ] || strip -g /wibo/build/wibo ) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index a1f6cf9..9f75a38 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -30,10 +30,14 @@ ARG BUILD_TYPE=Release # Enable link-time optimization (LTO) (AUTO, ON, OFF) ARG ENABLE_LTO=AUTO +# Version string (if not provided, defaults to "unknown") +ARG WIBO_VERSION + RUN cmake -S /wibo -B /wibo/build -G Ninja \ -DCMAKE_BUILD_TYPE:STRING="$BUILD_TYPE" \ -DWIBO_ENABLE_LIBURING:BOOL=ON \ -DWIBO_ENABLE_LTO:STRING="$ENABLE_LTO" \ + -DWIBO_VERSION:STRING="$WIBO_VERSION" \ && cmake --build /wibo/build --verbose \ && ( [ "$BUILD_TYPE" != "Release" ] || strip -g /wibo/build/wibo ) diff --git a/src/main.cpp b/src/main.cpp index dc8a439..f0571ce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "modules.h" #include "processes.h" #include "strutil.h" +#include "version_info.h" #include #include @@ -124,33 +125,71 @@ TIB tib; const size_t MAPS_BUFFER_SIZE = 0x10000; -static void printHelp(const char *argv0) { +static std::string getExeName(const char *argv0) { std::filesystem::path exePath(argv0 ? argv0 : "wibo"); - std::string exeName = exePath.filename().string(); - fprintf(stdout, "Usage: %s [options] [arguments...]\n", exeName.c_str()); - fprintf(stdout, " %s path [subcommand options] [path...]\n", exeName.c_str()); - fprintf(stdout, "\n"); - fprintf(stdout, "Options:\n"); - fprintf(stdout, " --help\t\tShow this help message and exit\n"); - fprintf(stdout, " -C, --chdir DIR\tChange working directory before launching the program\n"); - fprintf(stdout, " -D, --debug\tEnable shim debug logging (same as WIBO_DEBUG=1)\n"); - fprintf(stdout, " --cmdline STRING\tUse STRING as the exact guest command line\n"); - fprintf(stdout, - " --\t\tStop option parsing; following arguments are interpreted as the exact guest command line\n"); - fprintf(stdout, "\n"); - fprintf(stdout, "Subcommands:\n"); - fprintf(stdout, " path\t\tConvert between host and Windows-style paths (see '%s path --help')\n", exeName.c_str()); + return exePath.filename().string(); } -static void printPathHelp(const char *argv0) { - std::filesystem::path exePath(argv0 ? argv0 : "wibo"); - std::string exeName = exePath.filename().string(); - fprintf(stdout, "Usage: %s path (-u | --unix | -w | --windows) [path...]\n", exeName.c_str()); - fprintf(stdout, "\n"); - fprintf(stdout, "Path Options:\n"); - fprintf(stdout, " -u, --unix\tConvert Windows paths to host paths\n"); - fprintf(stdout, " -w, --windows\tConvert host paths to Windows paths\n"); - fprintf(stdout, " -h, --help\tShow this help message and exit\n"); +static void printHelp(const char *argv0, bool error) { + const auto exeName = getExeName(argv0); + FILE *out = error ? stderr : stdout; + if (error) { + fprintf(out, "See '%s --help' for usage information.\n", exeName.c_str()); + return; + } + fprintf(out, "wibo %s\n\n", wibo::kVersionString); + fprintf(out, "Usage:\n"); + fprintf(out, " %s [options] [arguments...]\n", exeName.c_str()); + fprintf(out, " %s path [subcommand options] [path...]\n", exeName.c_str()); + fprintf(out, "\n"); + fprintf(out, "General Options:\n"); + fprintf(out, " -h, --help Show this help message and exit\n"); + fprintf(out, " -V, --version Show version information and exit\n"); + fprintf(out, "\n"); + fprintf(out, "Runtime Options:\n"); + fprintf(out, " -C, --chdir DIR Change working directory before launching the program\n"); + fprintf(out, " -D, --debug Enable debug logging (same as WIBO_DEBUG=1)\n"); + fprintf(out, " --cmdline STRING Use STRING as the exact guest command line\n"); + fprintf(out, " (includes the program name, e.g. \"test.exe a b c\")\n"); + fprintf(out, " -- Stop option parsing; following arguments are used\n"); + fprintf(out, " verbatim as the guest command line, including the\n"); + fprintf(out, " program name\n"); + fprintf(out, "\n"); + fprintf(out, "Subcommands:\n"); + fprintf(out, " path Convert between host and Windows-style paths\n"); + fprintf(out, " (see '%s path --help' for details)\n", exeName.c_str()); + fprintf(out, "\n"); + fprintf(out, "Examples:\n"); + fprintf(out, " # Typical usage\n"); + fprintf(out, " %s path/to/test.exe a b c\n", exeName.c_str()); + fprintf(out, " %s -C path/to test.exe a b c\n", exeName.c_str()); + fprintf(out, "\n"); + fprintf(out, " # Advanced forms: full control over the guest command line\n"); + fprintf(out, " %s path/to/test.exe -- test.exe a b c\n", exeName.c_str()); + fprintf(out, " %s --cmdline 'test.exe a b c' path/to/test.exe\n", exeName.c_str()); + fprintf(out, " %s -- test.exe a b c\n", exeName.c_str()); +} + +static void printPathHelp(const char *argv0, bool error) { + const auto exeName = getExeName(argv0); + FILE *out = error ? stderr : stdout; + if (error) { + fprintf(out, "See '%s path --help' for usage information.\n", exeName.c_str()); + return; + } + fprintf(out, "Usage:\n"); + fprintf(out, " %s path [options] [path...]\n", exeName.c_str()); + fprintf(out, "\n"); + fprintf(out, "Path Options (exactly one required):\n"); + fprintf(out, " -u, --unix Convert Windows paths to host (Unix-style) paths\n"); + fprintf(out, " -w, --windows Convert host (Unix-style) paths to Windows paths\n"); + fprintf(out, "\n"); + fprintf(out, "General Options:\n"); + fprintf(out, " -h, --help Show this help message and exit\n"); + fprintf(out, "\n"); + fprintf(out, "Examples:\n"); + fprintf(out, " %s path -u 'Z:\\home\\user'\n", exeName.c_str()); + fprintf(out, " %s path -w /home/user\n", exeName.c_str()); } static int handlePathCommand(int argc, char **argv, const char *argv0) { @@ -169,25 +208,29 @@ static int handlePathCommand(int argc, char **argv, const char *argv0) { continue; } if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) { - printPathHelp(argv0); + printPathHelp(argv0, false); return 0; } if (arg[0] == '-' && arg[1] != '\0') { - fprintf(stderr, "Unknown option for 'path' subcommand: %s\n", arg); - printPathHelp(argv0); + fprintf(stderr, "Error: unknown option '%s'.\n", arg); + printPathHelp(argv0, true); return 1; } inputs.push_back(arg); } if (convertToUnix == convertToWindows) { - fprintf(stderr, "Specify exactly one of --unix or --windows for the 'path' subcommand\n"); - printPathHelp(argv0); + if (!convertToUnix) { + printPathHelp(argv0, false); + } else { + fprintf(stderr, "Error: cannot specify both --unix and --windows for path conversion.\n"); + printPathHelp(argv0, true); + } return 1; } if (inputs.empty()) { - fprintf(stderr, "No path specified for conversion\n"); - printPathHelp(argv0); + fprintf(stderr, "Error: no paths specified for conversion.\n"); + printPathHelp(argv0, true); return 1; } @@ -325,22 +368,27 @@ int main(int argc, char **argv) { parsingOptions = false; continue; } + if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) { + printHelp(argv[0], false); + return 0; + } + if (strcmp(arg, "-V") == 0 || strcmp(arg, "--version") == 0) { + fprintf(stdout, "wibo %s\n", wibo::kVersionString); + return 0; + } if (strncmp(arg, "--cmdline=", 10) == 0) { cmdLine = arg + 10; continue; } if (strcmp(arg, "--cmdline") == 0) { if (i + 1 >= argc) { - fprintf(stderr, "Option %s requires a command line argument\n", arg); + fprintf(stderr, "Error: '%s' requires a command line argument.\n", arg); + printHelp(argv[0], true); return 1; } cmdLine = argv[++i]; continue; } - if (strcmp(arg, "--help") == 0) { - printHelp(argv[0]); - return 0; - } if (strcmp(arg, "-D") == 0 || strcmp(arg, "--debug") == 0) { optionDebug = true; continue; @@ -351,7 +399,8 @@ int main(int argc, char **argv) { } if (strcmp(arg, "-C") == 0 || strcmp(arg, "--chdir") == 0) { if (i + 1 >= argc) { - fprintf(stderr, "Option %s requires a directory argument\n", arg); + fprintf(stderr, "Error: '%s' requires a directory argument.\n", arg); + printHelp(argv[0], true); return 1; } chdirPath = argv[++i]; @@ -362,9 +411,8 @@ int main(int argc, char **argv) { continue; } if (arg[0] == '-' && arg[1] != '\0') { - fprintf(stderr, "Unknown option: %s\n", arg); - fprintf(stderr, "\n"); - printHelp(argv[0]); + fprintf(stderr, "Error: unknown option '%s'.\n", arg); + printHelp(argv[0], true); return 1; } } @@ -374,8 +422,13 @@ int main(int argc, char **argv) { } if (programIndex == -1 && cmdLine.empty()) { - printHelp(argv[0]); - return argc <= 1 ? 0 : 1; + if (argc == 1) { + printHelp(argv[0], false); + return 0; + } + fprintf(stderr, "Error: no program or command line specified.\n"); + printHelp(argv[0], true); + return 1; } // Try to resolve our own executable path diff --git a/src/version_info.h.in b/src/version_info.h.in new file mode 100644 index 0000000..2bb8329 --- /dev/null +++ b/src/version_info.h.in @@ -0,0 +1,7 @@ +#pragma once + +namespace wibo { + +constexpr const char kVersionString[] = "@WIBO_VERSION_STRING@"; + +} // namespace wibo