From 853d6550fdcb3eab291c01801638c353602a68bc Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 14:23:45 +0100 Subject: [PATCH 01/27] all github workflows are static now. --- .github/workflows/build.yml | 66 +++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbe7365..dd5c3d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,10 @@ jobs: name: macOS Apple Silicon - os: windows-latest name: Windows MSVC + toolchain: msvc + - os: windows-latest + name: Windows Clang + toolchain: clang steps: - name: Checkout repository @@ -24,16 +28,16 @@ jobs: with: submodules: recursive - - name: Install Dependencies (Linux) + - name: Install Build Tools (Linux) if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y cmake ninja-build g++ clang libsqlite3-dev git libgrpc++-dev protobuf-compiler-grpc libspdlog-dev + sudo apt-get install -y cmake ninja-build g++ clang pkg-config - - name: Install Dependencies (macOS) + - name: Install Build Tools (macOS) if: runner.os == 'macOS' run: | - brew install cmake ninja sqlite pkg-config grpc protobuf spdlog + brew install cmake ninja pkg-config - name: Cache Boost id: cache-boost @@ -43,19 +47,17 @@ jobs: key: boost-1.90.0-${{ runner.os }} - name: Setup ccache - if: runner.os != 'Windows' uses: hendrikmuhs/ccache-action@v1.2 with: - key: ${{ runner.os }}-ccache + key: ${{ runner.os }}-${{ matrix.toolchain }}-ccache - name: Cache vcpkg dependencies - if: runner.os == 'Windows' uses: actions/cache@v4 with: path: build/vcpkg_installed - key: vcpkg-${{ runner.os }}-${{ hashFiles('vcpkg.json') }} + key: vcpkg-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json') }} restore-keys: | - vcpkg-${{ runner.os }}- + vcpkg-${{ runner.os }}-${{ matrix.toolchain }}- - name: Clone Boost if: steps.cache-boost.outputs.cache-hit != 'true' @@ -68,15 +70,32 @@ jobs: - name: Configure CMake (Unix) if: runner.os != 'Windows' - run: cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DBOOST_SOURCE_DIR=${{ github.workspace }}/third_party/boost + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + TRIPLET="x64-linux" # vcpkg's x64-linux defaults to static libraries + else + TRIPLET="arm64-osx-static" # Force static linking on macOS + fi + + cmake -G Ninja -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \ + -DVCPKG_TARGET_TRIPLET=$TRIPLET \ + -DBOOST_SOURCE_DIR=${{ github.workspace }}/third_party/boost - name: Build (Unix) if: runner.os != 'Windows' run: cmake --build build --config Release - - name: Configure CMake (Windows) - if: runner.os == 'Windows' - run: cmake -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DRPS_MSVC_STATIC_RUNTIME=ON -DBOOST_SOURCE_DIR="${{ github.workspace }}/third_party/boost" + - name: Configure CMake (Windows MSVC) + if: runner.os == 'Windows' && matrix.toolchain == 'msvc' + run: cmake -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON -DBOOST_SOURCE_DIR="${{ github.workspace }}/third_party/boost" + + - name: Configure CMake (Windows Clang) + if: runner.os == 'Windows' && matrix.toolchain == 'clang' + run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DBOOST_SOURCE_DIR="${{ github.workspace }}/third_party/boost" - name: Build (Windows) if: runner.os == 'Windows' @@ -94,8 +113,8 @@ jobs: cd release_stage zip -r ../rps-binaries-${{ matrix.os }}.zip ./* - - name: Stage & Zip Binaries (Windows) - if: runner.os == 'Windows' + - name: Stage & Zip Binaries (Windows MSVC) + if: runner.os == 'Windows' && matrix.toolchain == 'msvc' run: | New-Item -ItemType Directory -Force -Path release_stage Copy-Item build\apps\rps-server\Release\rps-server.exe release_stage\ @@ -103,11 +122,22 @@ jobs: Copy-Item build\apps\rps-pluginscanner\Release\rps-pluginscanner.exe release_stage\ Copy-Item build\apps\rps-vstscannermaster\Release\vstscannermaster.exe release_stage\ Copy-Item build\examples\cpp\Release\rps-example-client.exe release_stage\ - Compress-Archive -Path release_stage\* -DestinationPath rps-binaries-${{ matrix.os }}.zip + Compress-Archive -Path release_stage\* -DestinationPath rps-binaries-${{ matrix.name }}.zip + + - name: Stage & Zip Binaries (Windows Clang) + if: runner.os == 'Windows' && matrix.toolchain == 'clang' + run: | + New-Item -ItemType Directory -Force -Path release_stage + Copy-Item build\apps\rps-server\rps-server.exe release_stage\ + Copy-Item build\apps\rps-standalone\rps-standalone.exe release_stage\ + Copy-Item build\apps\rps-pluginscanner\rps-pluginscanner.exe release_stage\ + Copy-Item build\apps\rps-vstscannermaster\vstscannermaster.exe release_stage\ + Copy-Item build\examples\cpp\rps-example-client.exe release_stage\ + Compress-Archive -Path release_stage\* -DestinationPath rps-binaries-${{ matrix.name }}.zip - name: Upload Artifacts uses: actions/upload-artifact@v4 with: - name: rps-binaries-${{ matrix.os }} - path: rps-binaries-${{ matrix.os }}.zip + name: rps-binaries-${{ matrix.name }} + path: rps-binaries-${{ matrix.name }}.zip retention-days: 7 From fb3bc88daccb332e5c4862cd3bdc3af57ebd3e30 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 14:36:09 +0100 Subject: [PATCH 02/27] Switched everything to vcpkg. --- .github/workflows/build.yml | 23 +----- CMakeLists.txt | 23 +----- DEVELOPMENT.md | 26 ++----- README.md | 140 ++++++++++++++---------------------- vcpkg.json | 57 ++++++++++++++- 5 files changed, 119 insertions(+), 150 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd5c3d9..80d64a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,13 +39,6 @@ jobs: run: | brew install cmake ninja pkg-config - - name: Cache Boost - id: cache-boost - uses: actions/cache@v4 - with: - path: third_party/boost - key: boost-1.90.0-${{ runner.os }} - - name: Setup ccache uses: hendrikmuhs/ccache-action@v1.2 with: @@ -59,15 +52,6 @@ jobs: restore-keys: | vcpkg-${{ runner.os }}-${{ matrix.toolchain }}- - - name: Clone Boost - if: steps.cache-boost.outputs.cache-hit != 'true' - shell: bash - run: | - mkdir -p third_party - git clone --branch boost-1.90.0 https://github.com/boostorg/boost.git third_party/boost - cd third_party/boost - git submodule update --init --recursive - - name: Configure CMake (Unix) if: runner.os != 'Windows' run: | @@ -82,8 +66,7 @@ jobs: -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \ - -DVCPKG_TARGET_TRIPLET=$TRIPLET \ - -DBOOST_SOURCE_DIR=${{ github.workspace }}/third_party/boost + -DVCPKG_TARGET_TRIPLET=$TRIPLET - name: Build (Unix) if: runner.os != 'Windows' @@ -91,11 +74,11 @@ jobs: - name: Configure CMake (Windows MSVC) if: runner.os == 'Windows' && matrix.toolchain == 'msvc' - run: cmake -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON -DBOOST_SOURCE_DIR="${{ github.workspace }}/third_party/boost" + run: cmake -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON - name: Configure CMake (Windows Clang) if: runner.os == 'Windows' && matrix.toolchain == 'clang' - run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DBOOST_SOURCE_DIR="${{ github.workspace }}/third_party/boost" + run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build (Windows) if: runner.os == 'Windows' diff --git a/CMakeLists.txt b/CMakeLists.txt index fdfc88a..800e4ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,27 +50,8 @@ if(WIN32 AND NOT MSVC) set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() -# Boost: build from local source tree via add_subdirectory -# Override with: cmake -DBOOST_SOURCE_DIR=/path/to/boost -# or: export BOOST_SOURCE_DIR=/path/to/boost && cmake -B build -if(DEFINED ENV{BOOST_SOURCE_DIR} AND NOT DEFINED BOOST_SOURCE_DIR) - set(BOOST_SOURCE_DIR "$ENV{BOOST_SOURCE_DIR}" CACHE PATH "Path to Boost source tree (requires Boost 1.90+)") -else() - set(BOOST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost" CACHE PATH "Path to Boost source tree (requires Boost 1.90+)") -endif() -if(NOT EXISTS "${BOOST_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR - "Boost CMake source tree not found at: ${BOOST_SOURCE_DIR}\n" - "Note: The official boost.org tarball does NOT include CMake support.\n" - "You must clone Boost from GitHub:\n" - " git clone https://github.com/boostorg/boost.git /path/to/boost\n" - " cd /path/to/boost && git checkout boost-1.90.0\n" - " git submodule update --init --recursive\n" - "Then pass: cmake -DBOOST_SOURCE_DIR=/path/to/boost\n" - ) -endif() -set(BOOST_INCLUDE_LIBRARIES json program_options filesystem process interprocess uuid dll date_time) -add_subdirectory(${BOOST_SOURCE_DIR} ${CMAKE_BINARY_DIR}/_deps/boost EXCLUDE_FROM_ALL) +# Boost: managed entirely by vcpkg +find_package(Boost REQUIRED COMPONENTS json program_options filesystem process interprocess uuid dll date_time) # Find SQLite3 # Prefer the static library; fall back to shared via find_package diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3f08f54..7a2e708 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -53,18 +53,13 @@ We adhere to a set of strict constraints to ensure maximum compatibility, adopti ### 2.1 Dependency Management -To avoid platform-specific packaging issues (missing static libs on Linux, DLL mismatches on Windows, version conflicts), **Boost is built from source** as part of the CMake build. The developer must provide a Boost 1.90 source tree (cloned separately) and point CMake at it via `BOOST_SOURCE_DIR`. +To guarantee binary portability (creating self-contained executables without DLL/shared object dependencies), RPS relies on **`vcpkg`** across all platforms for resolving its dependencies (`boost`, `grpc`, `protobuf`, `spdlog`, and `sqlite3`) as **static libraries**. -The CMake variable `BOOST_SOURCE_DIR` controls where Boost source is located: -- **Default fallback**: `${CMAKE_SOURCE_DIR}/third_party/boost` -- **Override via CMake flag**: `cmake -DBOOST_SOURCE_DIR=/path/to/boost -B build` -- **Override via environment variable**: `export BOOST_SOURCE_DIR=/path/to/boost && cmake -B build` +- **Windows**: Built with MSVC using the `x64-windows-static` vcpkg triplet and `-DRPS_MSVC_STATIC_RUNTIME=ON`. Can also be built with Clang. +- **macOS**: Built with Apple Clang using the `arm64-osx-static` vcpkg triplet. +- **Linux**: Built with GCC using the `x64-linux` vcpkg triplet (which defaults to static linking). -The `-D` flag takes priority over the environment variable. If neither is set, CMake falls back to `third_party/boost`. - -Only the required Boost libraries are compiled (via `BOOST_INCLUDE_LIBRARIES`), keeping build times reasonable. - -On **Windows** (non-MSVC), the C/C++ runtimes are statically linked (`-static -static-libgcc -static-libstdc++`) to produce standalone executables with no DLL dependencies beyond the Windows system libraries. +Because all dependencies are managed via `vcpkg.json` (Manifest Mode), the CMake configure step will automatically download and build the exact requested versions of Boost (1.90.0) and other libraries before compiling RPS. ### 2.2 Optional: VST2.4 SDK @@ -225,18 +220,9 @@ Client (Python/C++/etc.) ──gRPC──▶ RpsServiceImpl ──▶ ScanEn - **Shutdown**: `Shutdown` RPC triggers `grpc::Server::Shutdown()` asynchronously (after returning the response). Signal handlers (SIGINT/SIGTERM) also trigger graceful shutdown. - **Lifecycle**: Designed to be spawned and killed by a parent application. The Python example client demonstrates this pattern. -### 3.13 Build System: gRPC/Protobuf on MSYS2 - -The MSYS2 gRPC package has a known issue where `protobuf-targets.cmake` references missing UPB targets. To work around this, `apps/rps-server/CMakeLists.txt` uses **pkg-config** (`pkg_check_modules`) instead of `find_package(gRPC CONFIG)` for finding gRPC and protobuf. Proto codegen uses `protoc` and `grpc_cpp_plugin` found via `find_program()`. - -Required MSYS2 packages: -```bash -pacman -S mingw-w64-clang-x86_64-grpc mingw-w64-clang-x86_64-spdlog -``` - --- -### 3.14 Python TUI Client +### 3.13 Python TUI Client The example Python client (`examples/python/`) demonstrates the full gRPC workflow: diff --git a/README.md b/README.md index 487a3a7..0d1b436 100644 --- a/README.md +++ b/README.md @@ -27,18 +27,15 @@ RPS solves this by using a **multi-process architecture**: | Dependency | Minimum Version | Notes | |--------------|-----------------|----------------------------------------------------| -| CMake | 3.25 | Build system | +| CMake | 3.25 | Build system (4.2+ required for VS 2026) | | C++ Compiler | C++23 capable | Clang 16+ (macOS+Linux), MSVC 2022 17.5+ (Windows) | -| Ninja | 1.11+ | Build backend (macOS+Linux) | -| vcpkg | Latest | C/C++ package manager (Windows) | -| SQLite3 | 3.x | For the plugin database | -| gRPC | 1.60+ | For the `rps-server` gRPC API | -| spdlog | 1.12+ | Structured logging for `rps-server` | +| Ninja | 1.11+ | Build backend (Recommended) | +| vcpkg | Latest | C/C++ package manager (Used on ALL platforms) | | Git | 2.x | For version management | -**Boost 1.90** is built from source. You must provide a path to a Boost source tree (see Step 1). +All dependencies (`boost`, `grpc`, `protobuf`, `spdlog`, `sqlite3`) are managed automatically by `vcpkg` during the CMake configure step. -### Step 1: Clone and Set Up Dependencies +### Step 1: Clone the Repository ```bash git clone @@ -48,109 +45,76 @@ git submodule update --init --recursive The submodule init will download the plugin SDK headers (CLAP, VST3). -#### Boost Source Tree - -RPS requires the **Boost 1.90 source tree** cloned from GitHub. The official boost.org tarball does **not** include CMake support and will not work. - -```bash -git clone https://github.com/boostorg/boost.git /path/to/boost -cd /path/to/boost -git checkout boost-1.90.0 -git submodule update --init --recursive -``` - -This will take several minutes (~180 sub-repos). Once done, the directory will contain a `CMakeLists.txt` at the top level. - ### Step 2: Configure and Build -#### Windows (MSVC + vcpkg - Recommended for Static Builds) - -To avoid DLL dependencies and build a single, standalone executable on Windows, it is recommended to use Visual Studio (MSVC) with `vcpkg` for dependency management. - -1. **Install Visual Studio 2022** (or later) with the "Desktop development with C++" workload. -2. **Install vcpkg** and the required libraries: - ```cmd - git clone https://github.com/microsoft/vcpkg.git c:\vcpkg - cd c:\vcpkg - bootstrap-vcpkg.bat - vcpkg.exe install grpc protobuf spdlog sqlite3 --triplet x64-windows-static - ``` - -3. **Configure and Build** (using Developer Command Prompt for VS 2022): - ```cmd - # Set the Boost source directory (or use the cmake parameter like in the example below) - set BOOST_SOURCE_DIR=C:\develop\boost - - # Configure CMake to use vcpkg and static linking - # * leave out the VST2 parameters to build without it - # * leave out the boost source dir if you have the environment variable set - # * use -G "Visual Studio 18 2026" if you use the latest one - VS 18 requires at least cmake 4.2! - # * adapt the paths - - cmake -G "Visual Studio 17 2022" -A x64 -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static -DRPS_MSVC_STATIC_RUNTIME=ON -DBOOST_SOURCE_DIR=C:/dev/boost -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=c:/dev/vstsdk2.4 - - # Build the project - cmake --build build --config Release - ``` - *Note: Ensure your `C:\vcpkg` path matches where you cloned it.* - -> **Important Note regarding process termination on Windows:** -> When using the example Java or C++ clients on Windows, it is recommended to run them from PowerShell rather than MSYS2/MinTTY terminals. MSYS2 terminals do not always translate `Ctrl+C` into proper Windows console control events, which can cause the client to abruptly terminate without running shutdown hooks, leaving the `rps-server.exe` running in the background as an orphaned process. Running from PowerShell or standard Command Prompt ensures proper process termination. +RPS uses `vcpkg` across **all platforms** (Windows, macOS, and Linux) to ensure that the resulting executables are completely self-contained and statically linked. This prevents runtime crashes due to missing `.dll`, `.so`, or `.dylib` files. -#### Windows (MSYS2/Clang - Alternative) -The MSYS2 Clang64 environment can be used, but note that it produces dynamically linked executables. - -1. Install MSYS2 and launch the **MSYS2 Clang64** terminal. -2. Install the toolchain and dependencies: +1. **Install vcpkg** if you haven't already: ```bash - pacman -S mingw-w64-clang-x86_64-toolchain mingw-w64-clang-x86_64-cmake mingw-w64-clang-x86_64-ninja mingw-w64-clang-x86_64-sqlite3 mingw-w64-clang-x86_64-grpc mingw-w64-clang-x86_64-protobuf mingw-w64-clang-x86_64-spdlog + git clone https://github.com/microsoft/vcpkg.git /path/to/vcpkg + cd /path/to/vcpkg + # Windows: + bootstrap-vcpkg.bat + # Linux/macOS: + ./bootstrap-vcpkg.sh ``` -> **Note on running MSYS2 executables:** -> If you build with MSYS2, you must run the resulting executables from within the MSYS2 terminal, or add the MSYS2 `bin` folder (`C:\msys64\clang64\bin`) to your Windows PATH. Otherwise, Windows will fail to find the required MSYS2 DLLs (`libgrpc++`, `libprotobuf`, `libspdlog`, etc.) and the executable will exit with code `0xC0000135`. - -Configure and build: -```bash -# Set the Boost source directory (or use the cmake parameter like in the example below) -export BOOST_SOURCE_DIR=/c/develop/boost +#### Windows (MSVC) +```cmd +# Configure CMake to use vcpkg and static linking # * leave out the VST2 parameters to build without it -# * leave out the boost source dir if you have the environment variable set +# * use -G "Visual Studio 18 2026" if you use the latest one - VS 18 requires at least cmake 4.2! # * adapt the paths -cmake -G Ninja -DBOOST_SOURCE_DIR=/c/dev/boost -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/c/dev/vstsdk2.4 -B build -cmake --build build -``` - -#### macOS (Homebrew) -Install build tools via Homebrew: -```bash -brew install cmake ninja sqlite pkg-config grpc protobuf spdlog +cmake -G "Visual Studio 17 2022" -A x64 -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static -DRPS_MSVC_STATIC_RUNTIME=ON -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=c:/dev/vstsdk2.4 + +# Build the project +cmake --build build --config Release ``` -Configure and build: - -Same as Windows MSYS2. +> **Important Note regarding process termination on Windows:** +> When using the example Java or C++ clients on Windows, it is recommended to run them from PowerShell rather than MSYS2/MinTTY terminals. MSYS2 terminals do not always translate `Ctrl+C` into proper Windows console control events, which can cause the client to abruptly terminate without running shutdown hooks, leaving the `rps-server.exe` running in the background as an orphaned process. Running from PowerShell or standard Command Prompt ensures proper process termination. -#### Linux (Fedora) +#### Windows (Clang) -```bash -sudo dnf install cmake ninja-build gcc-c++ clang sqlite-devel git grpc-devel grpc-plugins spdlog-devel +```cmd +cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static +cmake --build build --config Release ``` -Configure and build: +#### macOS (Apple Silicon) -Same as Windows MSYS2. +```bash +# Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 +cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=arm64-osx-static +cmake --build build --config Release +``` -#### Linux (Ubuntu / Debian) +#### Linux (Ubuntu/Debian/Fedora) +*Note: The default `x64-linux` triplet in vcpkg automatically builds static libraries.* ```bash -sudo apt install cmake ninja-build g++ clang libsqlite3-dev git libgrpc++-dev protobuf-compiler-grpc libspdlog-dev +# Ensure you have build tools installed: +# Ubuntu: sudo apt install build-essential cmake ninja-build pkg-config curl zip unzip tar +# Fedora: sudo dnf install gcc-c++ cmake ninja-build pkgconf-pkg-config curl zip unzip tar + +cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux +cmake --build build --config Release ``` -Configure and build: +#### Vcpkg Triplet Reference + +| OS | Architecture | Compiler | Recommended `VCPKG_TARGET_TRIPLET` | CMake Flags Required | +|---|---|---|---|---| +| **Windows** | x64 | MSVC | `x64-windows-static` | `-DRPS_MSVC_STATIC_RUNTIME=ON` | +| **Windows** | x64 | Clang | `x64-windows-static` | `-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++` | +| **macOS** | arm64 (M1/M2) | Apple Clang | `arm64-osx-static` | | +| **macOS** | x64 (Intel) | Apple Clang | `x64-osx-static` | | +| **Linux** | x64 | GCC / Clang | `x64-linux` | | +| **Linux** | arm64 | GCC / Clang | `arm64-linux` | | -Same as Windows MSYS2. +--- ### Build Output diff --git a/vcpkg.json b/vcpkg.json index ff15ac7..3dae51e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,14 +1,69 @@ { "name": "rps-plugin-scanner", "version-string": "0.2.0", + "builtin-baseline": "383fc51480f2747372cf9009df34469ce00d604b", "dependencies": [ "boost-program-options", "boost-json", "boost-process", "boost-interprocess", + "boost-filesystem", + "boost-uuid", + "boost-dll", + "boost-date-time", "sqlite3", "grpc", "protobuf", - "spdlog" + "spdlog" + ], + "overrides": [ + { + "name": "boost-program-options", + "version": "1.90.0" + }, + { + "name": "boost-json", + "version": "1.90.0" + }, + { + "name": "boost-process", + "version": "1.90.0" + }, + { + "name": "boost-interprocess", + "version": "1.90.0" + }, + { + "name": "boost-filesystem", + "version": "1.90.0" + }, + { + "name": "boost-uuid", + "version": "1.90.0" + }, + { + "name": "boost-dll", + "version": "1.90.0" + }, + { + "name": "boost-date-time", + "version": "1.90.0" + }, + { + "name": "grpc", + "version": "1.76.0" + }, + { + "name": "protobuf", + "version": "6.33.4" + }, + { + "name": "spdlog", + "version": "1.17.0" + }, + { + "name": "sqlite3", + "version": "3.51.2" + } ] } From d8147d4c24c0e55aa9988bbd4dfab1f986578bf2 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 15:06:59 +0100 Subject: [PATCH 03/27] All builds running on vcpkg and ninja by default. --- .github/workflows/build.yml | 16 ++++++++++------ .gitignore | 5 ++++- DEVELOPMENT.md | 2 +- README.md | 11 +++++++++-- vcpkg.json | 2 +- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80d64a7..30304e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,10 @@ jobs: with: key: ${{ runner.os }}-${{ matrix.toolchain }}-ccache + - name: Setup MSVC Developer Command Prompt + if: runner.os == 'Windows' && matrix.toolchain == 'msvc' + uses: ilammy/msvc-dev-cmd@v1 + - name: Cache vcpkg dependencies uses: actions/cache@v4 with: @@ -74,7 +78,7 @@ jobs: - name: Configure CMake (Windows MSVC) if: runner.os == 'Windows' && matrix.toolchain == 'msvc' - run: cmake -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON + run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON - name: Configure CMake (Windows Clang) if: runner.os == 'Windows' && matrix.toolchain == 'clang' @@ -100,11 +104,11 @@ jobs: if: runner.os == 'Windows' && matrix.toolchain == 'msvc' run: | New-Item -ItemType Directory -Force -Path release_stage - Copy-Item build\apps\rps-server\Release\rps-server.exe release_stage\ - Copy-Item build\apps\rps-standalone\Release\rps-standalone.exe release_stage\ - Copy-Item build\apps\rps-pluginscanner\Release\rps-pluginscanner.exe release_stage\ - Copy-Item build\apps\rps-vstscannermaster\Release\vstscannermaster.exe release_stage\ - Copy-Item build\examples\cpp\Release\rps-example-client.exe release_stage\ + Copy-Item build\apps\rps-server\rps-server.exe release_stage\ + Copy-Item build\apps\rps-standalone\rps-standalone.exe release_stage\ + Copy-Item build\apps\rps-pluginscanner\rps-pluginscanner.exe release_stage\ + Copy-Item build\apps\rps-vstscannermaster\vstscannermaster.exe release_stage\ + Copy-Item build\examples\cpp\rps-example-client.exe release_stage\ Compress-Archive -Path release_stage\* -DestinationPath rps-binaries-${{ matrix.name }}.zip - name: Stage & Zip Binaries (Windows Clang) diff --git a/.gitignore b/.gitignore index 4bafc4d..1506207 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ kiloHearts.log .vscode -plans/ \ No newline at end of file +plans/ +build_ninja_msvc/vcpkg-manifest-install.log +build_ninja_msvc/CMakeCache.txt +build_ninja_msvc/CMakeFiles/cmake.check_cache diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7a2e708..95cc2f9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -55,7 +55,7 @@ We adhere to a set of strict constraints to ensure maximum compatibility, adopti To guarantee binary portability (creating self-contained executables without DLL/shared object dependencies), RPS relies on **`vcpkg`** across all platforms for resolving its dependencies (`boost`, `grpc`, `protobuf`, `spdlog`, and `sqlite3`) as **static libraries**. -- **Windows**: Built with MSVC using the `x64-windows-static` vcpkg triplet and `-DRPS_MSVC_STATIC_RUNTIME=ON`. Can also be built with Clang. +- **Windows**: Built with MSVC (`cl.exe`) or Clang via the Ninja generator, using the `x64-windows-static` vcpkg triplet and `-DRPS_MSVC_STATIC_RUNTIME=ON`. - **macOS**: Built with Apple Clang using the `arm64-osx-static` vcpkg triplet. - **Linux**: Built with GCC using the `x64-linux` vcpkg triplet (which defaults to static linking). diff --git a/README.md b/README.md index 0d1b436..7a3e2c7 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,22 @@ RPS uses `vcpkg` across **all platforms** (Windows, macOS, and Linux) to ensure ./bootstrap-vcpkg.sh ``` +2. **Install Ninja** (Windows): + The Windows builds are configured to use the Ninja generator for significantly faster and more consistent compilation. + ```cmd + winget install Ninja-build.Ninja + ``` + #### Windows (MSVC) ```cmd # Configure CMake to use vcpkg and static linking # * leave out the VST2 parameters to build without it -# * use -G "Visual Studio 18 2026" if you use the latest one - VS 18 requires at least cmake 4.2! +# * use Ninja for faster builds, but it requires running from Developer Command Prompt +# * if using the MSBuild generator (e.g. -G "Visual Studio 18 2026"), CMake 4.2+ is required # * adapt the paths -cmake -G "Visual Studio 17 2022" -A x64 -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static -DRPS_MSVC_STATIC_RUNTIME=ON -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=c:/dev/vstsdk2.4 +cmake -G Ninja -B build -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static -DRPS_MSVC_STATIC_RUNTIME=ON -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=c:/dev/vstsdk2.4 # Build the project cmake --build build --config Release diff --git a/vcpkg.json b/vcpkg.json index 3dae51e..1584b4f 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "name": "rps-plugin-scanner", "version-string": "0.2.0", - "builtin-baseline": "383fc51480f2747372cf9009df34469ce00d604b", + "builtin-baseline": "62159a45e18f3a9ac0548628dcaf74fcb60c6ff9", "dependencies": [ "boost-program-options", "boost-json", From 4c628953713253f5a25f1b8d842a10ef35cac103 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 15:47:21 +0100 Subject: [PATCH 04/27] updated readme. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7a3e2c7..06f573e 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,13 @@ The submodule init will download the plugin SDK headers (CLAP, VST3). RPS uses `vcpkg` across **all platforms** (Windows, macOS, and Linux) to ensure that the resulting executables are completely self-contained and statically linked. This prevents runtime crashes due to missing `.dll`, `.so`, or `.dylib` files. 1. **Install vcpkg** if you haven't already: + Because we use `vcpkg` in manifest mode with specific dependency version overrides, vcpkg requires its repository to be checked out at the exact `builtin-baseline` specified in `vcpkg.json`. ```bash git clone https://github.com/microsoft/vcpkg.git /path/to/vcpkg cd /path/to/vcpkg + # Checkout the specific baseline commit required by rps/vcpkg.json + git checkout 62159a45e18f3a9ac0548628dcaf74fcb60c6ff9 + # Windows: bootstrap-vcpkg.bat # Linux/macOS: From dc00382376cf1d55c928ab5b13cb7c8d9d83eee4 Mon Sep 17 00:00:00 2001 From: "B. Zeiss" Date: Sat, 28 Feb 2026 15:51:22 +0100 Subject: [PATCH 05/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06f573e..43bd2ec 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ All dependencies (`boost`, `grpc`, `protobuf`, `spdlog`, `sqlite3`) are managed ### Step 1: Clone the Repository ```bash -git clone +git clone https://github.com/bzeiss/rps.git cd rps git submodule update --init --recursive ``` From 1cc6ab708488ddedabf15be73153256b33acad28 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 16:04:25 +0100 Subject: [PATCH 06/27] cmakelists fixes adaptations to prevent using system installed packages. --- CMakeLists.txt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 800e4ce..71b0bff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,16 @@ cmake_minimum_required(VERSION 3.25) -cmake_policy(SET CMP0167 NEW) +# force cmake to ignore system packages +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) +set(CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY OFF) +set(CMAKE_FIND_USE_PACKAGE_REGISTRY OFF) +set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OFF) +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) +set(CMAKE_PREFIX_PATH "${CMAKE_TOOLCHAIN_FILE}" ${CMAKE_PREFIX_PATH}) project(rps-plugin-scanner - VERSION 0.2.0 - DESCRIPTION "Reliable Plugin Scanner" + VERSION 0.2.1 + DESCRIPTION "RPS" LANGUAGES C CXX ) From ec488f534dd548a1c3f58236bd369e523d7156b7 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 16:35:18 +0100 Subject: [PATCH 07/27] documentation fixes. --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 43bd2ec..8eea950 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ cmake --build build --config Release #### Windows (Clang) ```cmd +# Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static cmake --build build --config Release ``` @@ -98,7 +99,7 @@ cmake --build build --config Release ```bash # Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 -cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=arm64-osx-static +cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=arm64-osx-release cmake --build build --config Release ``` @@ -110,20 +111,21 @@ cmake --build build --config Release # Ubuntu: sudo apt install build-essential cmake ninja-build pkg-config curl zip unzip tar # Fedora: sudo dnf install gcc-c++ cmake ninja-build pkgconf-pkg-config curl zip unzip tar +# Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-linux cmake --build build --config Release ``` #### Vcpkg Triplet Reference -| OS | Architecture | Compiler | Recommended `VCPKG_TARGET_TRIPLET` | CMake Flags Required | -|---|---|---|---|---| -| **Windows** | x64 | MSVC | `x64-windows-static` | `-DRPS_MSVC_STATIC_RUNTIME=ON` | -| **Windows** | x64 | Clang | `x64-windows-static` | `-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++` | -| **macOS** | arm64 (M1/M2) | Apple Clang | `arm64-osx-static` | | -| **macOS** | x64 (Intel) | Apple Clang | `x64-osx-static` | | -| **Linux** | x64 | GCC / Clang | `x64-linux` | | -| **Linux** | arm64 | GCC / Clang | `arm64-linux` | | +| OS | Architecture | Compiler | Recommended `VCPKG_TARGET_TRIPLET` | CMake Flags Required | +|-------------|---------------|-------------|------------------------------------|---------------------------------------------------------| +| **Windows** | x64 | MSVC | `x64-windows-static` | `-DRPS_MSVC_STATIC_RUNTIME=ON` | +| **Windows** | x64 | Clang | `x64-windows-static` | `-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++` | +| **macOS** | arm64 (M1/M2) | Apple Clang | `arm64-osx-release` | | +| **macOS** | x64 (Intel) | Apple Clang | `x64-osx-release` | | +| **Linux** | x64 | GCC / Clang | `x64-linux` | | +| **Linux** | arm64 | GCC / Clang | `arm64-linux` | | --- From 1a3104e7c8fb44b5a4ae97521f35368cb94d0208 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 17:31:40 +0100 Subject: [PATCH 08/27] build fix. --- CMakeLists.txt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 71b0bff..071cf28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,6 @@ cmake_minimum_required(VERSION 3.25) -# force cmake to ignore system packages -set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) -set(CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY OFF) -set(CMAKE_FIND_USE_PACKAGE_REGISTRY OFF) -set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OFF) -set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) -set(CMAKE_PREFIX_PATH "${CMAKE_TOOLCHAIN_FILE}" ${CMAKE_PREFIX_PATH}) +cmake_policy(SET CMP0167 NEW) project(rps-plugin-scanner VERSION 0.2.1 From fa950794b2340b316b1336351bb67ba5ce9b41da Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 18:23:55 +0100 Subject: [PATCH 09/27] build fixes. --- CMakeLists.txt | 19 ++++++++++++++++++- README.md | 2 +- libs/rps-engine/CMakeLists.txt | 2 +- vcpkg.json | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 071cf28..22044ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,23 @@ project(rps-plugin-scanner LANGUAGES C CXX ) +# x64-windows* vcpkg triplets produce MSVC-ABI binaries. Using GNU-targeting +# clang++ (e.g. MSYS2 clang64) with those libraries causes linker failures +# such as missing LIBCMTD/OLDNAMES import libs. +if(WIN32 + AND VCPKG_TARGET_TRIPLET MATCHES "^x64-windows" + AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND NOT CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC") + message(FATAL_ERROR + "Incompatible toolchain combination detected:\n" + " compiler: ${CMAKE_CXX_COMPILER}\n" + " triplet: ${VCPKG_TARGET_TRIPLET}\n\n" + "The selected vcpkg triplet uses MSVC ABI libraries, but this clang++ " + "targets GNU/MinGW. Use one of:\n" + " 1) clang-cl or cl.exe with -DVCPKG_TARGET_TRIPLET=x64-windows-static\n" + " 2) clang++ with a MinGW triplet (for example x64-mingw-static).") +endif() + # Enforce strictly C++23 set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -51,7 +68,7 @@ if(WIN32 AND NOT MSVC) endif() # Boost: managed entirely by vcpkg -find_package(Boost REQUIRED COMPONENTS json program_options filesystem process interprocess uuid dll date_time) +find_package(Boost REQUIRED COMPONENTS json program_options filesystem process interprocess uuid dll date_time crc) # Find SQLite3 # Prefer the static library; fall back to shared via find_package diff --git a/README.md b/README.md index 8eea950..f3b7e84 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ cmake --build build --config Release ```cmd # Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 -cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static +cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static cmake --build build --config Release ``` diff --git a/libs/rps-engine/CMakeLists.txt b/libs/rps-engine/CMakeLists.txt index e902a2a..4a6c055 100644 --- a/libs/rps-engine/CMakeLists.txt +++ b/libs/rps-engine/CMakeLists.txt @@ -7,7 +7,6 @@ add_library(rps-engine STATIC target_include_directories(rps-engine PUBLIC include - PRIVATE ${BOOST_SOURCE_DIR}/libs/crc/include ) target_link_libraries(rps-engine @@ -19,6 +18,7 @@ target_link_libraries(rps-engine SQLite::SQLite3 PRIVATE rps_warnings + Boost::crc Boost::process Boost::interprocess Boost::uuid diff --git a/vcpkg.json b/vcpkg.json index 1584b4f..fb67a86 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -11,6 +11,7 @@ "boost-uuid", "boost-dll", "boost-date-time", + "boost-crc", "sqlite3", "grpc", "protobuf", @@ -49,6 +50,10 @@ "name": "boost-date-time", "version": "1.90.0" }, + { + "name": "boost-crc", + "version": "1.90.0" + }, { "name": "grpc", "version": "1.76.0" From 5b5d5d1c7332d1e9bee9652406de7fa6d623b873 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 18:39:53 +0100 Subject: [PATCH 10/27] readme fixes --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f3b7e84..521978a 100644 --- a/README.md +++ b/README.md @@ -87,13 +87,21 @@ cmake --build build --config Release > **Important Note regarding process termination on Windows:** > When using the example Java or C++ clients on Windows, it is recommended to run them from PowerShell rather than MSYS2/MinTTY terminals. MSYS2 terminals do not always translate `Ctrl+C` into proper Windows console control events, which can cause the client to abruptly terminate without running shutdown hooks, leaving the `rps-server.exe` running in the background as an orphaned process. Running from PowerShell or standard Command Prompt ensures proper process termination. -#### Windows (Clang) +#### Windows (Clang with MSVC ABI) ```cmd # Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static cmake --build build --config Release ``` +#### Windows (Clang with GNU/MinGW ABI) + +```cmd +# Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 +cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-mingw-static +cmake --build build --config Release +``` + #### macOS (Apple Silicon) @@ -118,14 +126,16 @@ cmake --build build --config Release #### Vcpkg Triplet Reference -| OS | Architecture | Compiler | Recommended `VCPKG_TARGET_TRIPLET` | CMake Flags Required | -|-------------|---------------|-------------|------------------------------------|---------------------------------------------------------| -| **Windows** | x64 | MSVC | `x64-windows-static` | `-DRPS_MSVC_STATIC_RUNTIME=ON` | -| **Windows** | x64 | Clang | `x64-windows-static` | `-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++` | -| **macOS** | arm64 (M1/M2) | Apple Clang | `arm64-osx-release` | | -| **macOS** | x64 (Intel) | Apple Clang | `x64-osx-release` | | -| **Linux** | x64 | GCC / Clang | `x64-linux` | | -| **Linux** | arm64 | GCC / Clang | `arm64-linux` | | +| OS | Architecture | Compiler | Recommended `VCPKG_TARGET_TRIPLET` | CMake Flags Required | +|-------------|---------------|---------------------|------------------------------------|-------------------------------------------------------------| +| **Windows** | x64 | MSVC | `x64-windows-static` | `-DRPS_MSVC_STATIC_RUNTIME=ON` | +| **Windows** | x64 | Clang/MSVC ABI | `x64-windows-static` | `-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl` | +| **Windows** | x64 | Clang/GNU/MinGW ABI | `x64-mingw-static` | `-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++` | +| **Windows** | arm64 | MSVC ABI | `x64-windows-static` | `-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl` | +| **macOS** | arm64 (M1/M2) | Apple Clang | `arm64-osx-release` | | +| **macOS** | x64 (Intel) | Apple Clang | `x64-osx-release` | | +| **Linux** | x64 | GCC / Clang | `x64-linux` | | +| **Linux** | arm64 | GCC / Clang | `arm64-linux` | | --- From ab1af5789ed59ae403e04e231ad5438c2ae54652 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 19:08:24 +0100 Subject: [PATCH 11/27] workflow fixes --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30304e3..60834b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: key: ${{ runner.os }}-${{ matrix.toolchain }}-ccache - name: Setup MSVC Developer Command Prompt - if: runner.os == 'Windows' && matrix.toolchain == 'msvc' + if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - name: Cache vcpkg dependencies @@ -62,7 +62,7 @@ jobs: if [ "$RUNNER_OS" == "Linux" ]; then TRIPLET="x64-linux" # vcpkg's x64-linux defaults to static libraries else - TRIPLET="arm64-osx-static" # Force static linking on macOS + TRIPLET="arm64-osx-release" # Force static linking on macOS fi cmake -G Ninja -B build \ @@ -82,7 +82,7 @@ jobs: - name: Configure CMake (Windows Clang) if: runner.os == 'Windows' && matrix.toolchain == 'clang' - run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON - name: Build (Windows) if: runner.os == 'Windows' @@ -98,7 +98,7 @@ jobs: cp build/apps/rps-vstscannermaster/vstscannermaster release_stage/ cp build/examples/cpp/rps-example-client release_stage/ cd release_stage - zip -r ../rps-binaries-${{ matrix.os }}.zip ./* + zip -r ../rps-binaries-${{ matrix.name }}.zip ./* - name: Stage & Zip Binaries (Windows MSVC) if: runner.os == 'Windows' && matrix.toolchain == 'msvc' From 2af2337700f845fd189d469b40464d5d3573b02c Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 19:16:49 +0100 Subject: [PATCH 12/27] cmake fixes. --- apps/rps-server/CMakeLists.txt | 34 +++++++++++++------------------ examples/cpp/CMakeLists.txt | 37 ++++++++++++++-------------------- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/apps/rps-server/CMakeLists.txt b/apps/rps-server/CMakeLists.txt index 7042a0c..b735508 100644 --- a/apps/rps-server/CMakeLists.txt +++ b/apps/rps-server/CMakeLists.txt @@ -1,28 +1,13 @@ # --- Cross-platform gRPC / Protobuf resolution --- -if(MINGW OR MSYS) - # 1. Find Libraries (MSYS2 Workaround) - find_package(PkgConfig REQUIRED) - pkg_check_modules(GRPCPP REQUIRED IMPORTED_TARGET grpc++) - pkg_check_modules(PROTOBUF REQUIRED IMPORTED_TARGET protobuf) - set(RPS_GRPC_LIB PkgConfig::GRPCPP) - set(RPS_PROTO_LIB PkgConfig::PROTOBUF) +# Prefer CMake config packages (works with vcpkg on all platforms, including MinGW). +# Fall back to pkg-config only when config packages are unavailable. +find_package(gRPC CONFIG QUIET) +find_package(protobuf CONFIG QUIET) - # 2. Find Tools (MSYS2 Standard Path) - find_program(PROTOC_EXECUTABLE protoc REQUIRED) - find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) -else() - # 1. Find Libraries (vcpkg / MSVC / Native CMake) - find_package(gRPC CONFIG REQUIRED) +if(gRPC_FOUND AND protobuf_FOUND) set(RPS_GRPC_LIB gRPC::grpc++) - - # Try protobuf config (vcpkg) first, then fallback to FindProtobuf module - find_package(protobuf CONFIG QUIET) - if(NOT protobuf_FOUND) - find_package(Protobuf REQUIRED) - endif() set(RPS_PROTO_LIB protobuf::libprotobuf) - # 2. Find Tools if(TARGET protobuf::protoc) set(PROTOC_EXECUTABLE protobuf::protoc) else() @@ -34,6 +19,15 @@ else() else() find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) endif() +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(GRPCPP REQUIRED IMPORTED_TARGET grpc++) + pkg_check_modules(PROTOBUF REQUIRED IMPORTED_TARGET protobuf) + set(RPS_GRPC_LIB PkgConfig::GRPCPP) + set(RPS_PROTO_LIB PkgConfig::PROTOBUF) + + find_program(PROTOC_EXECUTABLE protoc REQUIRED) + find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) endif() find_package(spdlog REQUIRED) diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt index 6f3770b..e1d6465 100644 --- a/examples/cpp/CMakeLists.txt +++ b/examples/cpp/CMakeLists.txt @@ -1,31 +1,15 @@ -# C++ gRPC example client for RPS -# Reuses the same pkg-config / proto codegen approach as rps-server. +# C++ gRPC example client for RPS. # --- Cross-platform gRPC / Protobuf resolution --- -if(MINGW OR MSYS) - # 1. Find Libraries (MSYS2 Workaround) - find_package(PkgConfig REQUIRED) - pkg_check_modules(GRPCPP REQUIRED IMPORTED_TARGET grpc++) - pkg_check_modules(PROTOBUF REQUIRED IMPORTED_TARGET protobuf) - set(RPS_GRPC_LIB PkgConfig::GRPCPP) - set(RPS_PROTO_LIB PkgConfig::PROTOBUF) +# Prefer CMake config packages (works with vcpkg on all platforms, including MinGW). +# Fall back to pkg-config only when config packages are unavailable. +find_package(gRPC CONFIG QUIET) +find_package(protobuf CONFIG QUIET) - # 2. Find Tools (MSYS2 Standard Path) - find_program(PROTOC_EXECUTABLE protoc REQUIRED) - find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) -else() - # 1. Find Libraries (vcpkg / MSVC / Native CMake) - find_package(gRPC CONFIG REQUIRED) +if(gRPC_FOUND AND protobuf_FOUND) set(RPS_GRPC_LIB gRPC::grpc++) - - # Try protobuf config (vcpkg) first, then fallback to FindProtobuf module - find_package(protobuf CONFIG QUIET) - if(NOT protobuf_FOUND) - find_package(Protobuf REQUIRED) - endif() set(RPS_PROTO_LIB protobuf::libprotobuf) - # 2. Find Tools if(TARGET protobuf::protoc) set(PROTOC_EXECUTABLE protobuf::protoc) else() @@ -37,6 +21,15 @@ else() else() find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) endif() +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(GRPCPP REQUIRED IMPORTED_TARGET grpc++) + pkg_check_modules(PROTOBUF REQUIRED IMPORTED_TARGET protobuf) + set(RPS_GRPC_LIB PkgConfig::GRPCPP) + set(RPS_PROTO_LIB PkgConfig::PROTOBUF) + + find_program(PROTOC_EXECUTABLE protoc REQUIRED) + find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) endif() # Proto codegen — generate into our own build directory From bd205ed37310d89c4eaf6a264cef7216bc901f5e Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 19:20:09 +0100 Subject: [PATCH 13/27] correct vcpkg baseline fetch --- .github/workflows/build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60834b0..a10b475 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,6 +56,24 @@ jobs: restore-keys: | vcpkg-${{ runner.os }}-${{ matrix.toolchain }}- + - name: Ensure vcpkg baseline commit is available (Unix) + if: runner.os != 'Windows' + run: | + BASELINE=$(python3 -c "import json; print(json.load(open('vcpkg.json', encoding='utf-8'))['builtin-baseline'])") + git -C "$VCPKG_INSTALLATION_ROOT" fetch --no-tags origin "$BASELINE" || git -C "$VCPKG_INSTALLATION_ROOT" fetch --tags --prune origin + git -C "$VCPKG_INSTALLATION_ROOT" cat-file -e "${BASELINE}^{commit}" + + - name: Ensure vcpkg baseline commit is available (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $baseline = (Get-Content vcpkg.json | ConvertFrom-Json).'builtin-baseline' + git -C "$env:VCPKG_INSTALLATION_ROOT" fetch --no-tags origin $baseline + if ($LASTEXITCODE -ne 0) { + git -C "$env:VCPKG_INSTALLATION_ROOT" fetch --tags --prune origin + } + git -C "$env:VCPKG_INSTALLATION_ROOT" cat-file -e "$baseline`^{commit}" + - name: Configure CMake (Unix) if: runner.os != 'Windows' run: | From ee9cd26f269ff4fae2fa8ee9d31ff885a30511ed Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 19:48:32 +0100 Subject: [PATCH 14/27] build fixes. --- CMakeLists.txt | 25 +++++++++++++++++++------ apps/rps-server/CMakeLists.txt | 5 +++++ apps/rps-server/src/ProtobufCompat.cpp | 13 +++++++++++++ examples/cpp/CMakeLists.txt | 5 +++++ examples/cpp/src/ProtobufCompat.cpp | 13 +++++++++++++ 5 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 apps/rps-server/src/ProtobufCompat.cpp create mode 100644 examples/cpp/src/ProtobufCompat.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 22044ac..b67c903 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,11 +38,17 @@ else() option(RPS_MSVC_STATIC_RUNTIME "Use static MSVC runtime (/MT)" OFF) endif() -if(MSVC AND RPS_MSVC_STATIC_RUNTIME) +if(WIN32 + AND RPS_MSVC_STATIC_RUNTIME + AND (MSVC OR CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") - # Resolve LNK4098 by ignoring the runtime we're not using for each config - set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /NODEFAULTLIB:LIBCMTD") - set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} /NODEFAULTLIB:LIBCMT") + # Resolve LNK4098 by ignoring the runtime we're not using for each config. + # Keep this only for cl.exe-style drivers; clang++ (MSVC target) does not + # accept raw '/NODEFAULTLIB:...' in CMAKE_EXE_LINKER_FLAGS. + if(MSVC) + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /NODEFAULTLIB:LIBCMTD") + set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} /NODEFAULTLIB:LIBCMT") + endif() endif() # Disable Boost Auto-Linking on Windows (CMake handles libraries explicitly) @@ -52,7 +58,6 @@ add_definitions(-DBOOST_ALL_NO_LIB) add_library(rps_warnings INTERFACE) if(MSVC) target_compile_options(rps_warnings INTERFACE /W4 /WX /permissive-) - target_compile_definitions(rps_warnings INTERFACE NOMINMAX WIN32_LEAN_AND_MEAN) else() target_compile_options(rps_warnings INTERFACE -Wall -Wextra -Wpedantic -Werror) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -61,8 +66,16 @@ else() endif() endif() +if(WIN32) + target_compile_definitions(rps_warnings INTERFACE + NOMINMAX + WIN32_LEAN_AND_MEAN + _CRT_SECURE_NO_WARNINGS + ) +endif() + # Force static linking -if(WIN32 AND NOT MSVC) +if(WIN32 AND NOT MSVC AND NOT CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() diff --git a/apps/rps-server/CMakeLists.txt b/apps/rps-server/CMakeLists.txt index b735508..0536493 100644 --- a/apps/rps-server/CMakeLists.txt +++ b/apps/rps-server/CMakeLists.txt @@ -7,6 +7,9 @@ find_package(protobuf CONFIG QUIET) if(gRPC_FOUND AND protobuf_FOUND) set(RPS_GRPC_LIB gRPC::grpc++) set(RPS_PROTO_LIB protobuf::libprotobuf) + if(TARGET protobuf::libprotobuf-lite) + list(APPEND RPS_PROTO_LIB protobuf::libprotobuf-lite) + endif() if(TARGET protobuf::protoc) set(PROTOC_EXECUTABLE protobuf::protoc) @@ -67,6 +70,7 @@ add_executable(rps-server src/main.cpp src/RpsServiceImpl.cpp src/GrpcScanObserver.cpp + src/ProtobufCompat.cpp ${PROTO_SRCS} ${GRPC_SRCS} ) @@ -77,6 +81,7 @@ target_include_directories(rps-server ${PROTO_OUT_DIR} ) + target_link_libraries(rps-server PRIVATE rps-engine diff --git a/apps/rps-server/src/ProtobufCompat.cpp b/apps/rps-server/src/ProtobufCompat.cpp new file mode 100644 index 0000000..a791c1c --- /dev/null +++ b/apps/rps-server/src/ProtobufCompat.cpp @@ -0,0 +1,13 @@ +#include + +#if defined(_MSC_VER) && defined(__clang__) +namespace google::protobuf::internal { + +// clang-cl (MSVC ABI) can emit a restrict-qualified mangling for this symbol +// that differs from protobuf binaries built with cl.exe. Emit the clang-side +// instantiation locally so links remain stable across compiler choices. +template void memswap::value>( + char* __restrict a, char* __restrict b); + +} // namespace google::protobuf::internal +#endif diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt index e1d6465..6cff381 100644 --- a/examples/cpp/CMakeLists.txt +++ b/examples/cpp/CMakeLists.txt @@ -9,6 +9,9 @@ find_package(protobuf CONFIG QUIET) if(gRPC_FOUND AND protobuf_FOUND) set(RPS_GRPC_LIB gRPC::grpc++) set(RPS_PROTO_LIB protobuf::libprotobuf) + if(TARGET protobuf::libprotobuf-lite) + list(APPEND RPS_PROTO_LIB protobuf::libprotobuf-lite) + endif() if(TARGET protobuf::protoc) set(PROTOC_EXECUTABLE protobuf::protoc) @@ -67,6 +70,7 @@ endif() add_executable(rps-example-client src/main.cpp + src/ProtobufCompat.cpp ${PROTO_SRCS} ${GRPC_SRCS} ) @@ -76,6 +80,7 @@ target_include_directories(rps-example-client ${PROTO_OUT_DIR} ) + target_link_libraries(rps-example-client PRIVATE rps_warnings diff --git a/examples/cpp/src/ProtobufCompat.cpp b/examples/cpp/src/ProtobufCompat.cpp new file mode 100644 index 0000000..a791c1c --- /dev/null +++ b/examples/cpp/src/ProtobufCompat.cpp @@ -0,0 +1,13 @@ +#include + +#if defined(_MSC_VER) && defined(__clang__) +namespace google::protobuf::internal { + +// clang-cl (MSVC ABI) can emit a restrict-qualified mangling for this symbol +// that differs from protobuf binaries built with cl.exe. Emit the clang-side +// instantiation locally so links remain stable across compiler choices. +template void memswap::value>( + char* __restrict a, char* __restrict b); + +} // namespace google::protobuf::internal +#endif From 953d210559f9133b13d29aae58cfefe3f9547841 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 20:13:03 +0100 Subject: [PATCH 15/27] caching a bit more conservative. --- .github/workflows/build.yml | 18 ++++++++++++++---- README.md | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a10b475..1680ca5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,13 +48,23 @@ jobs: if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - - name: Cache vcpkg dependencies + - name: Cache vcpkg binary archives (Unix) + if: runner.os != 'Windows' + uses: actions/cache@v4 + with: + path: ~/.cache/vcpkg/archives + key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} + restore-keys: | + vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}- + + - name: Cache vcpkg binary archives (Windows) + if: runner.os == 'Windows' uses: actions/cache@v4 with: - path: build/vcpkg_installed - key: vcpkg-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json') }} + path: ${{ env.LOCALAPPDATA }}\vcpkg\archives + key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} restore-keys: | - vcpkg-${{ runner.os }}-${{ matrix.toolchain }}- + vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}- - name: Ensure vcpkg baseline commit is available (Unix) if: runner.os != 'Windows' diff --git a/README.md b/README.md index 521978a..f005517 100644 --- a/README.md +++ b/README.md @@ -87,14 +87,14 @@ cmake --build build --config Release > **Important Note regarding process termination on Windows:** > When using the example Java or C++ clients on Windows, it is recommended to run them from PowerShell rather than MSYS2/MinTTY terminals. MSYS2 terminals do not always translate `Ctrl+C` into proper Windows console control events, which can cause the client to abruptly terminate without running shutdown hooks, leaving the `rps-server.exe` running in the background as an orphaned process. Running from PowerShell or standard Command Prompt ensures proper process termination. -#### Windows (Clang with MSVC ABI) +#### Windows (Clang MSVC ABI) ```cmd # Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static cmake --build build --config Release ``` -#### Windows (Clang with GNU/MinGW ABI) +#### Windows (Clang GNU/MinGW ABI in MSYS2 CLANG64) ```cmd # Enable VST2 with custom SDK path if needed: -DRPS_ENABLE_VST2=ON -DRPS_VST2_SDK_PATH=/path/to/vstsdk2.4 From 096e79bb3014cae6a7e3e8682ee816a551da1065 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 21:56:11 +0100 Subject: [PATCH 16/27] switched python example to uv. --- examples/python/.python-version | 1 + examples/python/README.md | 34 ++--- examples/python/generate_proto.sh | 15 -- examples/python/pyproject.toml | 2 +- examples/python/uv.lock | 223 ++++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 examples/python/.python-version delete mode 100644 examples/python/generate_proto.sh create mode 100644 examples/python/uv.lock diff --git a/examples/python/.python-version b/examples/python/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/examples/python/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/examples/python/README.md b/examples/python/README.md index 04b1f21..baf6840 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -7,17 +7,17 @@ A Python command-line client for the RPS Plugin Scanner gRPC server, featuring a ```bash cd examples/python -# Create virtual environment -python -m venv .venv -source .venv/bin/activate # or .venv\Scripts\activate on Windows - -# Install dependencies -pip install -e . +# Install dependencies and create .venv from uv.lock +uv sync # Generate gRPC stubs (generates and patches imports) -python generate_proto.py +uv run python generate_proto.py ``` +Notes: +- This project uses `uv` as the package/environment manager. +- Recommended Python version is `3.13` (see `.python-version`). + ## Usage The client auto-spawns and kills `rps-server` unless `--server` is provided. @@ -26,34 +26,34 @@ The client auto-spawns and kills `rps-server` unless `--server` is provided. ```bash # Incremental scan of all formats (default) -python -m rps_client scan +uv run python -m rps_client scan # Full scan, specific formats, limited -python -m rps_client scan --mode full --formats vst3,clap --limit 50 +uv run python -m rps_client scan --mode full --formats vst3,clap --limit 50 # Single plugin scan -python -m rps_client scan --scan /path/to/plugin.vst3 +uv run python -m rps_client scan --scan /path/to/plugin.vst3 # Custom scan directories -python -m rps_client scan --scan-dir /path/to/plugins --jobs 8 +uv run python -m rps_client scan --scan-dir /path/to/plugins --jobs 8 # Verbose debug output -python -m rps_client scan --verbose +uv run python -m rps_client scan --verbose ``` ### Connect to an existing server ```bash -python -m rps_client --server localhost:50051 scan --formats vst3 -python -m rps_client --server localhost:50051 status -python -m rps_client --server localhost:50051 shutdown +uv run python -m rps_client --server localhost:50051 scan --formats vst3 +uv run python -m rps_client --server localhost:50051 status +uv run python -m rps_client --server localhost:50051 shutdown ``` ### Options ``` -python -m rps_client --help -python -m rps_client scan --help +uv run python -m rps_client --help +uv run python -m rps_client scan --help ``` | Option | Description | diff --git a/examples/python/generate_proto.sh b/examples/python/generate_proto.sh deleted file mode 100644 index 3fc94df..0000000 --- a/examples/python/generate_proto.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Generate Python gRPC stubs from rps.proto -# Run from the examples/python/ directory - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROTO_DIR="$SCRIPT_DIR/../../proto" -OUT_DIR="$SCRIPT_DIR/rps_client/proto" - -python -m grpc_tools.protoc \ - -I "$PROTO_DIR" \ - --python_out="$OUT_DIR" \ - --grpc_python_out="$OUT_DIR" \ - "$PROTO_DIR/rps.proto" - -echo "Generated Python proto stubs in $OUT_DIR" diff --git a/examples/python/pyproject.toml b/examples/python/pyproject.toml index 1ccc42e..012aad2 100644 --- a/examples/python/pyproject.toml +++ b/examples/python/pyproject.toml @@ -2,7 +2,7 @@ name = "rps-client" version = "0.1.0" description = "Python TUI client for RPS Plugin Scanner" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.14" dependencies = [ "grpcio>=1.60.0", "grpcio-tools>=1.60.0", diff --git a/examples/python/uv.lock b/examples/python/uv.lock new file mode 100644 index 0000000..003f0a5 --- /dev/null +++ b/examples/python/uv.lock @@ -0,0 +1,223 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.14" + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/d1/cbefe328653f746fd319c4377836a25ba64226e41c6a1d7d5cdbc87a459f/grpcio_tools-1.78.0.tar.gz", hash = "sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b", size = 5393026, upload-time = "2026-02-06T09:59:59.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/70/2118a814a62ab205c905d221064bc09021db83fceeb84764d35c00f0f633/grpcio_tools-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:ea64e38d1caa2b8468b08cb193f5a091d169b6dbfe1c7dac37d746651ab9d84e", size = 2545568, upload-time = "2026-02-06T09:57:30.308Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68134839dd1a00f964185ead103646d6dd6a396b92ed264eaf521431b793/grpcio_tools-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:4003fcd5cbb5d578b06176fd45883a72a8f9203152149b7c680ce28653ad9e3a", size = 5708704, upload-time = "2026-02-06T09:57:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/b6135aa9534e22051c53e5b9c0853d18024a41c50aaff464b7b47c1ed379/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe6b0081775394c61ec633c9ff5dbc18337100eabb2e946b5c83967fe43b2748", size = 2591905, upload-time = "2026-02-06T09:57:35.338Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/6380df1390d62b1d18ae18d4d790115abf4997fa29498aa50ba644ecb9d8/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7e989ad2cd93db52d7f1a643ecaa156ac55bf0484f1007b485979ce8aef62022", size = 2905271, upload-time = "2026-02-06T09:57:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/3a/07/9b369f37c8f4956b68778c044d57390a8f0f3b1cca590018809e75a4fce2/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b874991797e96c41a37e563236c3317ed41b915eff25b292b202d6277d30da85", size = 2656234, upload-time = "2026-02-06T09:57:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/61/40eee40e7a54f775a0d4117536532713606b6b177fff5e327f33ad18746e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8c288b728228377aaf758925692fc6068939d9fa32f92ca13dedcbeb41f33", size = 3105770, upload-time = "2026-02-06T09:57:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/81ee4b728e70e8ba66a589f86469925ead02ed6f8973434e4a52e3576148/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:87e648759b06133199f4bc0c0053e3819f4ec3b900dc399e1097b6065db998b5", size = 3654896, upload-time = "2026-02-06T09:57:45.402Z" }, + { url = "https://files.pythonhosted.org/packages/be/b9/facb3430ee427c800bb1e39588c85685677ea649491d6e0874bd9f3a1c0e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f3d3ced52bfe39eba3d24f5a8fab4e12d071959384861b41f0c52ca5399d6920", size = 3322529, upload-time = "2026-02-06T09:57:47.292Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/d7a011df9abfed8c30f0d2077b0562a6e3edc57cb3e5514718e2a81f370a/grpcio_tools-1.78.0-cp310-cp310-win32.whl", hash = "sha256:4bb6ed690d417b821808796221bde079377dff98fdc850ac157ad2f26cda7a36", size = 993518, upload-time = "2026-02-06T09:57:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/f7f60c3ae2281c6b438c3a8455f4a5d5d2e677cf20207864cbee3763da22/grpcio_tools-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c676d8342fd53bd85a5d5f0d070cd785f93bc040510014708ede6fcb32fada1", size = 1158505, upload-time = "2026-02-06T09:57:50.633Z" }, + { url = "https://files.pythonhosted.org/packages/75/78/280184d19242ed6762bf453c47a70b869b3c5c72a24dc5bf2bf43909faa3/grpcio_tools-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:6a8b8b7b49f319d29dbcf507f62984fa382d1d10437d75c3f26db5f09c4ac0af", size = 2545904, upload-time = "2026-02-06T09:57:52.769Z" }, + { url = "https://files.pythonhosted.org/packages/5b/51/3c46dea5113f68fe879961cae62d34bb7a3c308a774301b45d614952ee98/grpcio_tools-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d62cf3b68372b0c6d722a6165db41b976869811abeabc19c8522182978d8db10", size = 5709078, upload-time = "2026-02-06T09:57:56.389Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/dc1ae9ec53182c96d56dfcbf3bcd3e55a8952ad508b188c75bf5fc8993d4/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa9056742efeaf89d5fe14198af71e5cbc4fbf155d547b89507e19d6025906c6", size = 2591744, upload-time = "2026-02-06T09:57:58.341Z" }, + { url = "https://files.pythonhosted.org/packages/04/63/9b53fc9a9151dd24386785171a4191ee7cb5afb4d983b6a6a87408f41b28/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3191af125dcb705aa6bc3856ba81ba99b94121c1b6ebee152e66ea084672831", size = 2905113, upload-time = "2026-02-06T09:58:00.38Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/0ad8d789f3a2a00893131c140865605fa91671a6e6fcf9da659e1fabba10/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:283239ddbb67ae83fac111c61b25d8527a1dbd355b377cbc8383b79f1329944d", size = 2656436, upload-time = "2026-02-06T09:58:03.038Z" }, + { url = "https://files.pythonhosted.org/packages/09/4d/580f47ce2fc61b093ade747b378595f51b4f59972dd39949f7444b464a03/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac977508c0db15301ef36d6c79769ec1a6cc4e3bc75735afca7fe7e360cead3a", size = 3106128, upload-time = "2026-02-06T09:58:05.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/d83b2d89f8d10e438bad36b1eb29356510fb97e81e6a608b22ae1890e8e6/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4ff605e25652a0bd13aa8a73a09bc48669c68170902f5d2bf1468a57d5e78771", size = 3654953, upload-time = "2026-02-06T09:58:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/917ce85633311e54fefd7e6eb1224fb780ef317a4d092766f5630c3fc419/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0197d7b561c79be78ab93d0fe2836c8def470683df594bae3ac89dd8e5c821b2", size = 3322630, upload-time = "2026-02-06T09:58:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/b2/55/3fbf6b26ab46fc79e1e6f7f4e0993cf540263dad639290299fad374a0829/grpcio_tools-1.78.0-cp311-cp311-win32.whl", hash = "sha256:28f71f591f7f39555863ced84fcc209cbf4454e85ef957232f43271ee99af577", size = 993804, upload-time = "2026-02-06T09:58:13.698Z" }, + { url = "https://files.pythonhosted.org/packages/73/86/4affe006d9e1e9e1c6653d6aafe2f8b9188acb2b563cd8ed3a2c7c0e8aec/grpcio_tools-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a6de495dabf86a3b40b9a7492994e1232b077af9d63080811838b781abbe4e8", size = 1158566, upload-time = "2026-02-06T09:58:15.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ae/5b1fa5dd8d560a6925aa52de0de8731d319f121c276e35b9b2af7cc220a2/grpcio_tools-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:9eb122da57d4cad7d339fc75483116f0113af99e8d2c67f3ef9cae7501d806e4", size = 2546823, upload-time = "2026-02-06T09:58:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ed/d33ccf7fa701512efea7e7e23333b748848a123e9d3bbafde4e126784546/grpcio_tools-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d0c501b8249940b886420e6935045c44cb818fa6f265f4c2b97d5cff9cb5e796", size = 5706776, upload-time = "2026-02-06T09:58:20.944Z" }, + { url = "https://files.pythonhosted.org/packages/c6/69/4285583f40b37af28277fc6b867d636e3b10e1b6a7ebd29391a856e1279b/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:77e5aa2d2a7268d55b1b113f958264681ef1994c970f69d48db7d4683d040f57", size = 2593972, upload-time = "2026-02-06T09:58:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/d7/eb/ecc1885bd6b3147f0a1b7dff5565cab72f01c8f8aa458f682a1c77a9fb08/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e3c0b0e6ba5275322ba29a97bf890565a55f129f99a21b121145e9e93a22525", size = 2905531, upload-time = "2026-02-06T09:58:25.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a9/511d0040ced66960ca10ba0f082d6b2d2ee6dd61837b1709636fdd8e23b4/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975d4cb48694e20ebd78e1643e5f1cd94cdb6a3d38e677a8e84ae43665aa4790", size = 2656909, upload-time = "2026-02-06T09:58:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/06/a3/3d2c707e7dee8df842c96fbb24feb2747e506e39f4a81b661def7fed107c/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:553ff18c5d52807dedecf25045ae70bad7a3dbba0b27a9a3cdd9bcf0a1b7baec", size = 3109778, upload-time = "2026-02-06T09:58:30.091Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4b/646811ba241bf05da1f0dc6f25764f1c837f78f75b4485a4210c84b79eae/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8c7f5e4af5a84d2e96c862b1a65e958a538237e268d5f8203a3a784340975b51", size = 3658763, upload-time = "2026-02-06T09:58:32.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/de/0a5ef3b3e79d1011375f5580dfee3a9c1ccb96c5f5d1c74c8cee777a2483/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96183e2b44afc3f9a761e9d0f985c3b44e03e8bb98e626241a6cbfb3b6f7e88f", size = 3325116, upload-time = "2026-02-06T09:58:34.894Z" }, + { url = "https://files.pythonhosted.org/packages/95/d2/6391b241ad571bc3e71d63f957c0b1860f0c47932d03c7f300028880f9b8/grpcio_tools-1.78.0-cp312-cp312-win32.whl", hash = "sha256:2250e8424c565a88573f7dc10659a0b92802e68c2a1d57e41872c9b88ccea7a6", size = 993493, upload-time = "2026-02-06T09:58:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/7d0d3a39ecad76ccc136be28274daa660569b244fa7d7d0bbb24d68e5ece/grpcio_tools-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:217d1fa29de14d9c567d616ead7cb0fef33cde36010edff5a9390b00d52e5094", size = 1158423, upload-time = "2026-02-06T09:58:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/ce/17311fb77530420e2f441e916b347515133e83d21cd6cc77be04ce093d5b/grpcio_tools-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb", size = 2546284, upload-time = "2026-02-06T09:58:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/79e101483115f0e78223397daef71751b75eba7e92a32060c10aae11ca64/grpcio_tools-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456", size = 5705653, upload-time = "2026-02-06T09:58:45.533Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/52fa3ccb39ceeee6adc010056eadfbca8198651c113e418dafebbdf2b306/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72", size = 2592788, upload-time = "2026-02-06T09:58:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/682ff6bb548225513d73dc9403742d8975439d7469c673bc534b9bbc83a7/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812", size = 2905157, upload-time = "2026-02-06T09:58:51.478Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/264f3836a96423b7018e5ada79d62576a6401f6da4e1f4975b18b2be1265/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45", size = 2656166, upload-time = "2026-02-06T09:58:54.117Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/f108276611522e03e98386b668cc7e575eff6952f2db9caa15b2a3b3e883/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822", size = 3109110, upload-time = "2026-02-06T09:58:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/cf048dbcd64b3396b3c860a2ffbcc67a8f8c87e736aaa74c2e505a7eee4c/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d", size = 3657863, upload-time = "2026-02-06T09:58:59.176Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e2736912c8fda57e2e57a66ea5e0bc8eb9a5fb7ded00e866ad22d50afb08/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63", size = 3324748, upload-time = "2026-02-06T09:59:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/726abc75bb5bfc2841e88ea05896e42f51ca7c30cb56da5c5b63058b3867/grpcio_tools-1.78.0-cp313-cp313-win32.whl", hash = "sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585", size = 993074, upload-time = "2026-02-06T09:59:03.085Z" }, + { url = "https://files.pythonhosted.org/packages/c5/68/91b400bb360faf9b177ffb5540ec1c4d06ca923691ddf0f79e2c9683f4da/grpcio_tools-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5", size = 1158185, upload-time = "2026-02-06T09:59:05.036Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rps-client" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "click" }, + { name = "grpcio" }, + { name = "grpcio-tools" }, + { name = "rich" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.0.0" }, + { name = "grpcio", specifier = ">=1.60.0" }, + { name = "grpcio-tools", specifier = ">=1.60.0" }, + { name = "rich", specifier = ">=13.0.0" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 5e6de6d5de22c47999403b2c314a6a72c6183117 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 22:44:19 +0100 Subject: [PATCH 17/27] github workflow fixes, java client windows job fixes. --- .github/workflows/build.yml | 6 +- examples/java/README.md | 24 ++- examples/java/run-client.ps1 | 34 +++ .../main/java/rps/client/RpsClientMain.java | 11 +- .../main/java/rps/client/ServerManager.java | 39 +++- .../java/rps/client/WindowsJobObject.java | 199 ++++++++++++++++++ 6 files changed, 300 insertions(+), 13 deletions(-) create mode 100644 examples/java/run-client.ps1 create mode 100644 examples/java/src/main/java/rps/client/WindowsJobObject.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1680ca5..77a38b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,9 @@ jobs: name: Linux x64 - os: macos-15 name: macOS Apple Silicon - - os: windows-latest - name: Windows MSVC - toolchain: msvc +# - os: windows-latest +# name: Windows MSVC +# toolchain: msvc - os: windows-latest name: Windows Clang toolchain: clang diff --git a/examples/java/README.md b/examples/java/README.md index 7ddf938..f05766a 100644 --- a/examples/java/README.md +++ b/examples/java/README.md @@ -27,12 +27,34 @@ The `rps.proto` file is automatically copied from the project root into the Java ## Running the Client -To run the client, use the `exec:exec` goal. This ensures the correct JVM flags are passed to provide a clean, warning-free output: +### Recommended on Windows + +Run the Java process directly (not via `mvn exec:*`): + +```powershell +cd examples/java +.\run-client.ps1 +``` + +Why: on Windows, `mvn exec:*` runs under `cmd`/batch mediation, and `Ctrl+C` handling is less reliable for child-process teardown. Running `java` directly gives the JVM full signal handling. +On Windows, spawned server processes are additionally bound to a native Job Object, so they are torn down when the Java parent exits. + +You can skip rebuilding after the first run: + +```powershell +.\run-client.ps1 -SkipBuild +``` + +### Optional (all platforms) + +You can still use Maven exec: ```bash mvn exec:exec ``` +But on Windows this is not recommended for interactive runs because `Ctrl+C` may be intercepted by the batch layer before normal JVM shutdown flow. + The client will: 1. Check if an `rps-server` is running (and start one if needed). 2. Connect to the server at `127.0.0.1:50051`. diff --git a/examples/java/run-client.ps1 b/examples/java/run-client.ps1 new file mode 100644 index 0000000..cf1b91a --- /dev/null +++ b/examples/java/run-client.ps1 @@ -0,0 +1,34 @@ +param( + [switch]$SkipBuild +) + +$ErrorActionPreference = "Stop" + +if (-not $SkipBuild) { + mvn -q -DskipTests compile dependency:build-classpath "-Dmdep.outputFile=target/classpath.txt" "-Dmdep.pathSeparator=;" +} + +if (-not (Test-Path "target/classes")) { + throw "Missing target/classes. Run without -SkipBuild first." +} + +if (-not (Test-Path "target/classpath.txt")) { + throw "Missing target/classpath.txt. Run without -SkipBuild first." +} + +$depCp = (Get-Content "target/classpath.txt" -Raw).Trim() +$cp = "target/classes;$depCp" + +$javaArgs = @( + "-Dsun.misc.Unsafe.SCAN_FOR_DEPRECATED_METHODS=false" + "--add-opens=java.base/java.nio=ALL-UNNAMED" + "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED" + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" + "--add-opens=java.base/java.lang=ALL-UNNAMED" + "--enable-native-access=ALL-UNNAMED" + "-classpath" + $cp + "rps.client.RpsClientMain" +) + +& java @javaArgs diff --git a/examples/java/src/main/java/rps/client/RpsClientMain.java b/examples/java/src/main/java/rps/client/RpsClientMain.java index 4163a1e..bff7b58 100644 --- a/examples/java/src/main/java/rps/client/RpsClientMain.java +++ b/examples/java/src/main/java/rps/client/RpsClientMain.java @@ -53,10 +53,11 @@ public static void main(String[] args) { terminal.writer().println("RPS Java Client starting..."); String serverBin = ServerManager.findServerBinary(); + final boolean ownServer = serverBin != null; try (ServerManager server = (serverBin != null) ? new ServerManager(serverBin, port, dbPath, "info") : null) { if (server != null) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { server.close(); } catch (Exception e) {} + try { server.close(); } catch (Exception ignored) {} })); server.start(); } @@ -76,6 +77,14 @@ public static void main(String[] args) { } terminal.writer().println("\nScan complete."); + + // Graceful shutdown is only attempted in the normal path. + if (ownServer) { + try { + client.shutdown(); + } catch (Exception ignored) { + } + } } } catch (io.grpc.StatusRuntimeException e) { terminal.writer().println("gRPC Error: " + e.getStatus().getCode()); diff --git a/examples/java/src/main/java/rps/client/ServerManager.java b/examples/java/src/main/java/rps/client/ServerManager.java index f85b3bf..5f5bf00 100644 --- a/examples/java/src/main/java/rps/client/ServerManager.java +++ b/examples/java/src/main/java/rps/client/ServerManager.java @@ -15,6 +15,7 @@ public class ServerManager implements AutoCloseable { private final String dbPath; private final String logLevel; private Process process; + private WindowsJobObject windowsJob; public ServerManager(String binPath, int port, String dbPath, String logLevel) { this.binPath = binPath; @@ -51,6 +52,9 @@ public void start() throws IOException, InterruptedException { System.out.println("Spawning server: " + String.join(" ", cmd)); this.process = pb.start(); + if (isWindows()) { + this.windowsJob = WindowsJobObject.createAndAssign(process.pid()); + } // Wait for server to be ready long deadline = System.currentTimeMillis() + 10000; @@ -106,15 +110,34 @@ public static String findServerBinary() { @Override public void close() { - if (process != null && process.isAlive()) { - // Kill the entire process tree (server + any scanner workers) - process.descendants().forEach(ProcessHandle::destroyForcibly); - process.destroyForcibly(); - try { - process.waitFor(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - // already force-killed + try { + if (process != null) { + process.destroy(); + try { + process.waitFor(2, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + + // If graceful stop didn't finish quickly, force-kill child tree. + if (process.isAlive()) { + process.descendants().forEach(ProcessHandle::destroyForcibly); + process.destroyForcibly(); + try { + process.waitFor(5, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + } + } + } + } finally { + if (windowsJob != null) { + windowsJob.close(); + windowsJob = null; } + process = null; } } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("win"); + } } diff --git a/examples/java/src/main/java/rps/client/WindowsJobObject.java b/examples/java/src/main/java/rps/client/WindowsJobObject.java new file mode 100644 index 0000000..8d7030f --- /dev/null +++ b/examples/java/src/main/java/rps/client/WindowsJobObject.java @@ -0,0 +1,199 @@ +package rps.client; + +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.invoke.MethodHandle; + +final class WindowsJobObject implements AutoCloseable { + private static final int JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + private static final int JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9; + private static final int LIMIT_FLAGS_OFFSET = 16; // DWORD LimitFlags + private static final int PROCESS_TERMINATE = 0x0001; + private static final int PROCESS_SET_QUOTA = 0x0100; + private static final int POINTER_SIZE = (int) java.lang.foreign.ValueLayout.ADDRESS.byteSize(); + private static final int JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_SIZE; + + private static final MethodHandle CREATE_JOB_OBJECT_A; + private static final MethodHandle SET_INFORMATION_JOB_OBJECT; + private static final MethodHandle OPEN_PROCESS; + private static final MethodHandle ASSIGN_PROCESS_TO_JOB_OBJECT; + private static final MethodHandle CLOSE_HANDLE; + private static final MethodHandle GET_LAST_ERROR; + + static { + int minimumWorkingSetOffset = alignUp(20, POINTER_SIZE); + int maximumWorkingSetOffset = minimumWorkingSetOffset + POINTER_SIZE; + int activeProcessLimitOffset = maximumWorkingSetOffset + POINTER_SIZE; + int affinityOffset = alignUp(activeProcessLimitOffset + 4, POINTER_SIZE); + int priorityClassOffset = affinityOffset + POINTER_SIZE; + int schedulingClassOffset = priorityClassOffset + 4; + int basicLimitSize = schedulingClassOffset + 4; + int ioCountersSize = 48; // 6x ULONGLONG + int processMemoryLimitSize = POINTER_SIZE; + int jobMemoryLimitSize = POINTER_SIZE; + int peakProcessMemoryUsedSize = POINTER_SIZE; + int peakJobMemoryUsedSize = POINTER_SIZE; + JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_SIZE = + basicLimitSize + ioCountersSize + processMemoryLimitSize + jobMemoryLimitSize + + peakProcessMemoryUsedSize + peakJobMemoryUsedSize; + } + + static { + try { + Linker linker = Linker.nativeLinker(); + SymbolLookup kernel32 = SymbolLookup.libraryLookup("kernel32", Arena.global()); + + CREATE_JOB_OBJECT_A = linker.downcallHandle( + kernel32.find("CreateJobObjectA").orElseThrow(), + FunctionDescriptor.of( + java.lang.foreign.ValueLayout.ADDRESS, + java.lang.foreign.ValueLayout.ADDRESS, + java.lang.foreign.ValueLayout.ADDRESS + ) + ); + SET_INFORMATION_JOB_OBJECT = linker.downcallHandle( + kernel32.find("SetInformationJobObject").orElseThrow(), + FunctionDescriptor.of( + java.lang.foreign.ValueLayout.JAVA_INT, + java.lang.foreign.ValueLayout.ADDRESS, + java.lang.foreign.ValueLayout.JAVA_INT, + java.lang.foreign.ValueLayout.ADDRESS, + java.lang.foreign.ValueLayout.JAVA_INT + ) + ); + OPEN_PROCESS = linker.downcallHandle( + kernel32.find("OpenProcess").orElseThrow(), + FunctionDescriptor.of( + java.lang.foreign.ValueLayout.ADDRESS, + java.lang.foreign.ValueLayout.JAVA_INT, + java.lang.foreign.ValueLayout.JAVA_INT, + java.lang.foreign.ValueLayout.JAVA_INT + ) + ); + ASSIGN_PROCESS_TO_JOB_OBJECT = linker.downcallHandle( + kernel32.find("AssignProcessToJobObject").orElseThrow(), + FunctionDescriptor.of( + java.lang.foreign.ValueLayout.JAVA_INT, + java.lang.foreign.ValueLayout.ADDRESS, + java.lang.foreign.ValueLayout.ADDRESS + ) + ); + CLOSE_HANDLE = linker.downcallHandle( + kernel32.find("CloseHandle").orElseThrow(), + FunctionDescriptor.of( + java.lang.foreign.ValueLayout.JAVA_INT, + java.lang.foreign.ValueLayout.ADDRESS + ) + ); + GET_LAST_ERROR = linker.downcallHandle( + kernel32.find("GetLastError").orElseThrow(), + FunctionDescriptor.of(java.lang.foreign.ValueLayout.JAVA_INT) + ); + } catch (Throwable t) { + throw new ExceptionInInitializerError(t); + } + } + + private final MemorySegment jobHandle; + + private WindowsJobObject(MemorySegment jobHandle) { + this.jobHandle = jobHandle; + } + + static WindowsJobObject createAndAssign(long pid) throws IOException { + if (pid <= 0 || pid > Integer.MAX_VALUE) { + throw new IOException("Unsupported process id: " + pid); + } + + try { + MemorySegment jobHandle = (MemorySegment) CREATE_JOB_OBJECT_A.invokeExact( + MemorySegment.NULL, + MemorySegment.NULL + ); + if (MemorySegment.NULL.equals(jobHandle)) { + throw new IOException("CreateJobObjectA failed (GetLastError=" + getLastError() + ")"); + } + + try (Arena arena = Arena.ofConfined()) { + MemorySegment info = arena.allocate(JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_SIZE); + info.set(java.lang.foreign.ValueLayout.JAVA_INT, LIMIT_FLAGS_OFFSET, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE); + + int setRes = (int) SET_INFORMATION_JOB_OBJECT.invokeExact( + jobHandle, + JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, + info, + JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_SIZE + ); + if (setRes == 0) { + throw new IOException( + "SetInformationJobObject failed (GetLastError=" + getLastError() + + ", infoSize=" + JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_SIZE + + ", ptrSize=" + POINTER_SIZE + ")" + ); + } + + MemorySegment processHandle = (MemorySegment) OPEN_PROCESS.invokeExact( + PROCESS_TERMINATE | PROCESS_SET_QUOTA, + 0, + (int) pid + ); + if (MemorySegment.NULL.equals(processHandle)) { + throw new IOException("OpenProcess failed for pid " + pid + " (GetLastError=" + getLastError() + ")"); + } + + try { + int assignRes = (int) ASSIGN_PROCESS_TO_JOB_OBJECT.invokeExact(jobHandle, processHandle); + if (assignRes == 0) { + throw new IOException("AssignProcessToJobObject failed for pid " + pid + " (GetLastError=" + getLastError() + ")"); + } + } finally { + closeHandleQuiet(processHandle); + } + } catch (Throwable t) { + closeHandleQuiet(jobHandle); + if (t instanceof IOException ioe) { + throw ioe; + } + throw new IOException("Failed to configure Windows Job Object", t); + } + + return new WindowsJobObject(jobHandle); + } catch (IOException e) { + throw e; + } catch (Throwable t) { + throw new IOException("Failed to initialize Windows Job Object", t); + } + } + + @Override + public void close() { + closeHandleQuiet(jobHandle); + } + + private static void closeHandleQuiet(MemorySegment handle) { + if (handle == null || MemorySegment.NULL.equals(handle)) { + return; + } + try { + CLOSE_HANDLE.invoke(handle); + } catch (Throwable ignored) { + } + } + + private static int getLastError() { + try { + return (int) GET_LAST_ERROR.invokeExact(); + } catch (Throwable ignored) { + return -1; + } + } + + private static int alignUp(int value, int alignment) { + int mask = alignment - 1; + return (value + mask) & ~mask; + } +} From adbe36fe982bf7bb4c41f8f20a596c009a5b63ac Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 22:51:55 +0100 Subject: [PATCH 18/27] windows job objects cleanup for python --- examples/python/README.md | 1 + examples/python/rps_client/server_manager.py | 99 ++++++++------- examples/python/rps_client/windows_job.py | 124 +++++++++++++++++++ 3 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 examples/python/rps_client/windows_job.py diff --git a/examples/python/README.md b/examples/python/README.md index baf6840..24e2640 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -21,6 +21,7 @@ Notes: ## Usage The client auto-spawns and kills `rps-server` unless `--server` is provided. +On Windows, auto-spawned server processes are attached to a Job Object so they are reliably cleaned up when the Python client exits. ### Scan (auto-managed server) diff --git a/examples/python/rps_client/server_manager.py b/examples/python/rps_client/server_manager.py index cb9b4af..19d13b0 100644 --- a/examples/python/rps_client/server_manager.py +++ b/examples/python/rps_client/server_manager.py @@ -1,11 +1,11 @@ """Manages the rps-server subprocess lifecycle.""" -import subprocess -import time -import socket import os -import sys import signal +import socket +import subprocess +import sys +import time from pathlib import Path import grpc @@ -28,6 +28,7 @@ def __init__( self.log_file = log_file self.log_level = log_level self._process: subprocess.Popen | None = None + self._windows_job = None self._old_sigint_handler = None self._old_sigterm_handler = None @@ -43,7 +44,6 @@ def _handle_signal(self, signum, frame): def start(self, timeout: float = 10.0) -> None: """Start the server subprocess and wait for it to accept connections.""" - # Precondition checks for binaries server_path = Path(self.server_bin) if not server_path.exists() or not server_path.is_file(): raise FileNotFoundError(f"Cannot find rps-server binary at: {self.server_bin}") @@ -53,7 +53,6 @@ def start(self, timeout: float = 10.0) -> None: if not scanner_path.exists() or not scanner_path.is_file(): raise FileNotFoundError(f"Cannot find {scanner_name} alongside rps-server at: {scanner_path}") - # Register signal handlers for cleanup self._old_sigint_handler = signal.getsignal(signal.SIGINT) self._old_sigterm_handler = signal.getsignal(signal.SIGTERM) signal.signal(signal.SIGINT, self._handle_signal) @@ -67,38 +66,45 @@ def start(self, timeout: float = 10.0) -> None: "--log-level", self.log_level, ] env = self._build_env() - # On Unix, put the process in its own group so Ctrl+C to parent doesn't - # kill the child immediately before we can call Shutdown() or cleanup. - # Wait, actually if we WANT it killed, we should leave it. - # But for reliability, we want the PARENT to control the death. self._process = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env, - start_new_session=(sys.platform != "win32") + start_new_session=(sys.platform != "win32"), ) - # Wait for the server to start accepting connections + if sys.platform == "win32": + from rps_client.windows_job import WindowsJobObject + + try: + self._windows_job = WindowsJobObject.create_and_assign(self._process.pid) + except Exception: + self._process.terminate() + self._process.wait(timeout=2.0) + self._process = None + raise + deadline = time.monotonic() + timeout while time.monotonic() < deadline: if self._process.poll() is not None: + self._close_windows_job() raise RuntimeError( f"rps-server exited immediately with code {self._process.returncode}" ) try: with socket.create_connection(("localhost", self.port), timeout=0.5): - return # Server is ready + return except (ConnectionRefusedError, OSError, socket.timeout): time.sleep(0.2) + self.stop(in_hurry=True) raise TimeoutError( f"rps-server did not start within {timeout}s on port {self.port}" ) def stop(self, in_hurry: bool = False) -> None: """Stop the server subprocess gracefully, then forcefully if needed.""" - # Restore signal handlers if self._old_sigint_handler: signal.signal(signal.SIGINT, self._old_sigint_handler) self._old_sigint_handler = None @@ -107,43 +113,48 @@ def stop(self, in_hurry: bool = False) -> None: self._old_sigterm_handler = None if self._process is None: + self._close_windows_job() return - if in_hurry: - # Kill immediately — server's signal handler will clean up children - self._process.terminate() # SIGTERM - try: - self._process.wait(timeout=3.0) - except subprocess.TimeoutExpired: - self._process.kill() # SIGKILL - self._process.wait() - self._process = None - return - - # Try graceful shutdown via gRPC first try: - from rps_client.proto import rps_pb2, rps_pb2_grpc + if in_hurry: + self._process.terminate() + try: + self._process.wait(timeout=3.0) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait() + return - channel = grpc.insecure_channel(f"localhost:{self.port}") - stub = rps_pb2_grpc.RpsServiceStub(channel) - stub.Shutdown(rps_pb2.ShutdownRequest(), timeout=2) - channel.close() - except Exception: - pass + try: + from rps_client.proto import rps_pb2, rps_pb2_grpc + + channel = grpc.insecure_channel(f"localhost:{self.port}") + stub = rps_pb2_grpc.RpsServiceStub(channel) + stub.Shutdown(rps_pb2.ShutdownRequest(), timeout=2) + channel.close() + except Exception: + pass - # Wait for graceful exit - try: - self._process.wait(timeout=5.0) - except subprocess.TimeoutExpired: - # Force kill - self._process.terminate() try: - self._process.wait(timeout=2) + self._process.wait(timeout=5.0) except subprocess.TimeoutExpired: - self._process.kill() - self._process.wait() + self._process.terminate() + try: + self._process.wait(timeout=2) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait() + finally: + self._process = None + self._close_windows_job() - self._process = None + def _close_windows_job(self) -> None: + if self._windows_job is not None: + try: + self._windows_job.close() + finally: + self._windows_job = None @staticmethod def _build_env() -> dict[str, str]: @@ -180,4 +191,4 @@ def __enter__(self): return self def __exit__(self, *exc): - self.stop() + self.stop() \ No newline at end of file diff --git a/examples/python/rps_client/windows_job.py b/examples/python/rps_client/windows_job.py new file mode 100644 index 0000000..02cbbb1 --- /dev/null +++ b/examples/python/rps_client/windows_job.py @@ -0,0 +1,124 @@ +"""Windows Job Object helper for deterministic child-process cleanup.""" + +from __future__ import annotations + +import ctypes +from ctypes import wintypes + + +_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + + +JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000 +JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9 + +PROCESS_TERMINATE = 0x0001 +PROCESS_SET_QUOTA = 0x0100 + + +class IO_COUNTERS(ctypes.Structure): + _fields_ = [ + ("ReadOperationCount", ctypes.c_ulonglong), + ("WriteOperationCount", ctypes.c_ulonglong), + ("OtherOperationCount", ctypes.c_ulonglong), + ("ReadTransferCount", ctypes.c_ulonglong), + ("WriteTransferCount", ctypes.c_ulonglong), + ("OtherTransferCount", ctypes.c_ulonglong), + ] + + +class JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure): + _fields_ = [ + ("PerProcessUserTimeLimit", ctypes.c_longlong), + ("PerJobUserTimeLimit", ctypes.c_longlong), + ("LimitFlags", wintypes.DWORD), + ("MinimumWorkingSetSize", ctypes.c_size_t), + ("MaximumWorkingSetSize", ctypes.c_size_t), + ("ActiveProcessLimit", wintypes.DWORD), + ("Affinity", ctypes.c_size_t), + ("PriorityClass", wintypes.DWORD), + ("SchedulingClass", wintypes.DWORD), + ] + + +class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure): + _fields_ = [ + ("BasicLimitInformation", JOBOBJECT_BASIC_LIMIT_INFORMATION), + ("IoInfo", IO_COUNTERS), + ("ProcessMemoryLimit", ctypes.c_size_t), + ("JobMemoryLimit", ctypes.c_size_t), + ("PeakProcessMemoryUsed", ctypes.c_size_t), + ("PeakJobMemoryUsed", ctypes.c_size_t), + ] + + +_kernel32.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR] +_kernel32.CreateJobObjectW.restype = wintypes.HANDLE + +_kernel32.SetInformationJobObject.argtypes = [ + wintypes.HANDLE, + wintypes.INT, + ctypes.c_void_p, + wintypes.DWORD, +] +_kernel32.SetInformationJobObject.restype = wintypes.BOOL + +_kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] +_kernel32.OpenProcess.restype = wintypes.HANDLE + +_kernel32.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE] +_kernel32.AssignProcessToJobObject.restype = wintypes.BOOL + +_kernel32.CloseHandle.argtypes = [wintypes.HANDLE] +_kernel32.CloseHandle.restype = wintypes.BOOL + + +class WindowsJobObject: + """Owns a Windows Job Object handle and can assign a process to it.""" + + def __init__(self, handle: wintypes.HANDLE): + self._handle = handle + + @classmethod + def create_and_assign(cls, pid: int) -> "WindowsJobObject": + if pid <= 0: + raise OSError(f"invalid pid: {pid}") + + job = _kernel32.CreateJobObjectW(None, None) + if not job: + raise ctypes.WinError(ctypes.get_last_error()) + + try: + info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + + ok = _kernel32.SetInformationJobObject( + job, + JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, + ctypes.byref(info), + ctypes.sizeof(info), + ) + if not ok: + raise ctypes.WinError(ctypes.get_last_error()) + + proc = _kernel32.OpenProcess(PROCESS_TERMINATE | PROCESS_SET_QUOTA, False, int(pid)) + if not proc: + raise ctypes.WinError(ctypes.get_last_error()) + + try: + ok = _kernel32.AssignProcessToJobObject(job, proc) + if not ok: + raise ctypes.WinError(ctypes.get_last_error()) + finally: + _kernel32.CloseHandle(proc) + + return cls(job) + except Exception: + _kernel32.CloseHandle(job) + raise + + def close(self) -> None: + if self._handle: + _kernel32.CloseHandle(self._handle) + self._handle = None + From b0d8032feba3c5e7e6a32fc51ba252f263df9a8f Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 22:54:28 +0100 Subject: [PATCH 19/27] cpp example windows job object cleanup --- examples/cpp/src/main.cpp | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/examples/cpp/src/main.cpp b/examples/cpp/src/main.cpp index 75a866b..e536e4f 100644 --- a/examples/cpp/src/main.cpp +++ b/examples/cpp/src/main.cpp @@ -114,6 +114,13 @@ class ServerManager { } m_processHandle = pi.hProcess; CloseHandle(pi.hThread); + if (!setupWindowsJobObject(m_processHandle)) { + TerminateProcess(m_processHandle, 1); + CloseHandle(m_processHandle); + m_processHandle = nullptr; + cleanupWindowsJobObject(); + return false; + } #else // Fork + exec on POSIX m_pid = fork(); @@ -177,6 +184,7 @@ class ServerManager { CloseHandle(m_processHandle); m_processHandle = nullptr; } + cleanupWindowsJobObject(); #else if (m_pid > 0) { int status; @@ -199,6 +207,42 @@ class ServerManager { } private: +#ifdef _WIN32 + bool setupWindowsJobObject(HANDLE processHandle) { + cleanupWindowsJobObject(); + + m_jobHandle = CreateJobObjectA(nullptr, nullptr); + if (!m_jobHandle) { + return false; + } + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!SetInformationJobObject( + m_jobHandle, + JobObjectExtendedLimitInformation, + &info, + sizeof(info))) { + cleanupWindowsJobObject(); + return false; + } + + if (!AssignProcessToJobObject(m_jobHandle, processHandle)) { + cleanupWindowsJobObject(); + return false; + } + + return true; + } + + void cleanupWindowsJobObject() { + if (m_jobHandle) { + CloseHandle(m_jobHandle); + m_jobHandle = nullptr; + } + } +#endif + std::string m_bin; int m_port; std::string m_db; @@ -206,6 +250,7 @@ class ServerManager { bool m_running = false; #ifdef _WIN32 HANDLE m_processHandle = nullptr; + HANDLE m_jobHandle = nullptr; #else pid_t m_pid = -1; #endif From 9acbc4eee5d467d88f38953f51c5f3b90d9f114c Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sat, 28 Feb 2026 23:12:54 +0100 Subject: [PATCH 20/27] windows job object cleanups. --- examples/cpp/CMakeLists.txt | 1 + examples/cpp/README.md | 7 ++ examples/cpp/src/WindowsProcessJob.cpp | 59 +++++++++++++++++ examples/cpp/src/WindowsProcessJob.hpp | 28 ++++++++ examples/cpp/src/main.cpp | 66 +++++++------------ examples/java/README.md | 4 ++ .../main/java/rps/client/ServerManager.java | 15 +++++ .../java/rps/client/WindowsJobObject.java | 19 ++++-- examples/python/README.md | 1 + examples/python/rps_client/server_manager.py | 46 ++++++------- examples/python/rps_client/windows_job.py | 19 ++++-- 11 files changed, 186 insertions(+), 79 deletions(-) create mode 100644 examples/cpp/README.md create mode 100644 examples/cpp/src/WindowsProcessJob.cpp create mode 100644 examples/cpp/src/WindowsProcessJob.hpp diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt index 6cff381..c5e46da 100644 --- a/examples/cpp/CMakeLists.txt +++ b/examples/cpp/CMakeLists.txt @@ -71,6 +71,7 @@ endif() add_executable(rps-example-client src/main.cpp src/ProtobufCompat.cpp + src/WindowsProcessJob.cpp ${PROTO_SRCS} ${GRPC_SRCS} ) diff --git a/examples/cpp/README.md b/examples/cpp/README.md new file mode 100644 index 0000000..3257d38 --- /dev/null +++ b/examples/cpp/README.md @@ -0,0 +1,7 @@ +# RPS C++ gRPC Example + +This example auto-spawns `rps-server` unless `--server` is provided. + +On Windows, auto-spawned server processes are attached to a Job Object so they are reliably cleaned up when the C++ client exits. + +Set `RPS_DEBUG_PROCESS_LIFECYCLE=1` to print spawn/attach/stop lifecycle logs. diff --git a/examples/cpp/src/WindowsProcessJob.cpp b/examples/cpp/src/WindowsProcessJob.cpp new file mode 100644 index 0000000..df7cd6b --- /dev/null +++ b/examples/cpp/src/WindowsProcessJob.cpp @@ -0,0 +1,59 @@ +#ifdef _WIN32 + +#include "WindowsProcessJob.hpp" + +#include +#include + +namespace { +bool processDebugEnabled() { + const char* v = std::getenv("RPS_DEBUG_PROCESS_LIFECYCLE"); + return v && std::string(v) == "1"; +} +} + +WindowsProcessJob::~WindowsProcessJob() { + close(); +} + +bool WindowsProcessJob::attach(HANDLE processHandle, std::string* err) { + close(); + + m_job = CreateJobObjectA(nullptr, nullptr); + if (!m_job) { + if (err) *err = "failed to create Windows Job Object (GetLastError=" + std::to_string(GetLastError()) + ")"; + return false; + } + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!SetInformationJobObject(m_job, JobObjectExtendedLimitInformation, &info, sizeof(info))) { + if (err) *err = "failed to configure Windows Job Object (GetLastError=" + std::to_string(GetLastError()) + ")"; + close(); + return false; + } + + if (!AssignProcessToJobObject(m_job, processHandle)) { + if (err) *err = "failed to assign process to Windows Job Object (GetLastError=" + std::to_string(GetLastError()) + ")"; + close(); + return false; + } + + if (processDebugEnabled()) { + std::cerr << "[rps] attached Windows Job Object\n"; + } + return true; +} + +void WindowsProcessJob::close() { + if (m_job) { + CloseHandle(m_job); + m_job = nullptr; + if (processDebugEnabled()) { + std::cerr << "[rps] closed Windows Job Object\n"; + } + } +} + +#endif + diff --git a/examples/cpp/src/WindowsProcessJob.hpp b/examples/cpp/src/WindowsProcessJob.hpp new file mode 100644 index 0000000..3ab4ef2 --- /dev/null +++ b/examples/cpp/src/WindowsProcessJob.hpp @@ -0,0 +1,28 @@ +#pragma once + +#ifdef _WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include + +class WindowsProcessJob { +public: + WindowsProcessJob() = default; + ~WindowsProcessJob(); + + WindowsProcessJob(const WindowsProcessJob&) = delete; + WindowsProcessJob& operator=(const WindowsProcessJob&) = delete; + + bool attach(HANDLE processHandle, std::string* err = nullptr); + void close(); + +private: + HANDLE m_job = nullptr; +}; + +#endif + diff --git a/examples/cpp/src/main.cpp b/examples/cpp/src/main.cpp index e536e4f..48c01ee 100644 --- a/examples/cpp/src/main.cpp +++ b/examples/cpp/src/main.cpp @@ -3,6 +3,7 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include "WindowsProcessJob.hpp" #else #include #include @@ -81,8 +82,12 @@ namespace ansi { // --------------------------------------------------------------------------- static std::atomic g_interrupted{false}; -// Forward-declare — defined after ServerManager +// Forward-declare signal setup. static void installSignalHandlers(); +static bool processDebugEnabled() { + const char* v = std::getenv("RPS_DEBUG_PROCESS_LIFECYCLE"); + return v && std::string(v) == "1"; +} // --------------------------------------------------------------------------- // Server process manager @@ -114,11 +119,20 @@ class ServerManager { } m_processHandle = pi.hProcess; CloseHandle(pi.hThread); - if (!setupWindowsJobObject(m_processHandle)) { + if (processDebugEnabled()) { + std::cerr << "[rps] spawned server process\n"; + } + + // Windows process ownership model: + // parent/child does not imply lifetime ownership. Attach rps-server + // to a Job Object so parent exit tears down the process tree. + std::string jobErr; + if (!m_windowsJob.attach(m_processHandle, &jobErr)) { TerminateProcess(m_processHandle, 1); CloseHandle(m_processHandle); m_processHandle = nullptr; - cleanupWindowsJobObject(); + m_windowsJob.close(); + std::cerr << "Error: " << jobErr << "\n"; return false; } #else @@ -175,6 +189,9 @@ class ServerManager { #ifdef _WIN32 if (m_processHandle) { + if (processDebugEnabled()) { + std::cerr << "[rps] stopping server process\n"; + } if (inHurry) { TerminateProcess(m_processHandle, 0); } else { @@ -184,7 +201,7 @@ class ServerManager { CloseHandle(m_processHandle); m_processHandle = nullptr; } - cleanupWindowsJobObject(); + m_windowsJob.close(); #else if (m_pid > 0) { int status; @@ -207,42 +224,6 @@ class ServerManager { } private: -#ifdef _WIN32 - bool setupWindowsJobObject(HANDLE processHandle) { - cleanupWindowsJobObject(); - - m_jobHandle = CreateJobObjectA(nullptr, nullptr); - if (!m_jobHandle) { - return false; - } - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; - info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; - if (!SetInformationJobObject( - m_jobHandle, - JobObjectExtendedLimitInformation, - &info, - sizeof(info))) { - cleanupWindowsJobObject(); - return false; - } - - if (!AssignProcessToJobObject(m_jobHandle, processHandle)) { - cleanupWindowsJobObject(); - return false; - } - - return true; - } - - void cleanupWindowsJobObject() { - if (m_jobHandle) { - CloseHandle(m_jobHandle); - m_jobHandle = nullptr; - } - } -#endif - std::string m_bin; int m_port; std::string m_db; @@ -250,7 +231,7 @@ class ServerManager { bool m_running = false; #ifdef _WIN32 HANDLE m_processHandle = nullptr; - HANDLE m_jobHandle = nullptr; + WindowsProcessJob m_windowsJob; #else pid_t m_pid = -1; #endif @@ -790,3 +771,6 @@ int main(int argc, char* argv[]) { // ServerManager destructor handles shutdown return rc; } + + + diff --git a/examples/java/README.md b/examples/java/README.md index f05766a..0057649 100644 --- a/examples/java/README.md +++ b/examples/java/README.md @@ -55,6 +55,10 @@ mvn exec:exec But on Windows this is not recommended for interactive runs because `Ctrl+C` may be intercepted by the batch layer before normal JVM shutdown flow. +### Process lifecycle debug + +Set `RPS_DEBUG_PROCESS_LIFECYCLE=1` to print spawn/attach/stop lifecycle logs. + The client will: 1. Check if an `rps-server` is running (and start one if needed). 2. Connect to the server at `127.0.0.1:50051`. diff --git a/examples/java/src/main/java/rps/client/ServerManager.java b/examples/java/src/main/java/rps/client/ServerManager.java index 5f5bf00..0f60cb2 100644 --- a/examples/java/src/main/java/rps/client/ServerManager.java +++ b/examples/java/src/main/java/rps/client/ServerManager.java @@ -10,6 +10,9 @@ import java.util.concurrent.TimeUnit; public class ServerManager implements AutoCloseable { + private static final boolean PROCESS_DEBUG = + "1".equals(System.getenv("RPS_DEBUG_PROCESS_LIFECYCLE")); + private final String binPath; private final int port; private final String dbPath; @@ -53,7 +56,13 @@ public void start() throws IOException, InterruptedException { System.out.println("Spawning server: " + String.join(" ", cmd)); this.process = pb.start(); if (isWindows()) { + // Windows process ownership model: + // parent/child does not imply lifetime ownership. We explicitly attach + // rps-server to a Job Object so parent exit tears down the process tree. this.windowsJob = WindowsJobObject.createAndAssign(process.pid()); + if (PROCESS_DEBUG) { + System.out.println("[rps] attached Windows Job Object to pid " + process.pid()); + } } // Wait for server to be ready @@ -112,6 +121,9 @@ public static String findServerBinary() { public void close() { try { if (process != null) { + if (PROCESS_DEBUG) { + System.out.println("[rps] stopping server pid " + process.pid()); + } process.destroy(); try { process.waitFor(2, TimeUnit.SECONDS); @@ -132,6 +144,9 @@ public void close() { if (windowsJob != null) { windowsJob.close(); windowsJob = null; + if (PROCESS_DEBUG) { + System.out.println("[rps] closed Windows Job Object"); + } } process = null; } diff --git a/examples/java/src/main/java/rps/client/WindowsJobObject.java b/examples/java/src/main/java/rps/client/WindowsJobObject.java index 8d7030f..56d9b01 100644 --- a/examples/java/src/main/java/rps/client/WindowsJobObject.java +++ b/examples/java/src/main/java/rps/client/WindowsJobObject.java @@ -23,6 +23,8 @@ final class WindowsJobObject implements AutoCloseable { private static final MethodHandle ASSIGN_PROCESS_TO_JOB_OBJECT; private static final MethodHandle CLOSE_HANDLE; private static final MethodHandle GET_LAST_ERROR; + private static final boolean PROCESS_DEBUG = + "1".equals(System.getenv("RPS_DEBUG_PROCESS_LIFECYCLE")); static { int minimumWorkingSetOffset = alignUp(20, POINTER_SIZE); @@ -115,7 +117,7 @@ static WindowsJobObject createAndAssign(long pid) throws IOException { MemorySegment.NULL ); if (MemorySegment.NULL.equals(jobHandle)) { - throw new IOException("CreateJobObjectA failed (GetLastError=" + getLastError() + ")"); + throw new IOException("failed to create Windows Job Object (GetLastError=" + getLastError() + ")"); } try (Arena arena = Arena.ofConfined()) { @@ -130,7 +132,7 @@ static WindowsJobObject createAndAssign(long pid) throws IOException { ); if (setRes == 0) { throw new IOException( - "SetInformationJobObject failed (GetLastError=" + getLastError() + "failed to configure Windows Job Object (GetLastError=" + getLastError() + ", infoSize=" + JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_SIZE + ", ptrSize=" + POINTER_SIZE + ")" ); @@ -142,13 +144,15 @@ static WindowsJobObject createAndAssign(long pid) throws IOException { (int) pid ); if (MemorySegment.NULL.equals(processHandle)) { - throw new IOException("OpenProcess failed for pid " + pid + " (GetLastError=" + getLastError() + ")"); + throw new IOException("failed to open process for Windows Job assignment, pid=" + pid + + " (GetLastError=" + getLastError() + ")"); } try { int assignRes = (int) ASSIGN_PROCESS_TO_JOB_OBJECT.invokeExact(jobHandle, processHandle); if (assignRes == 0) { - throw new IOException("AssignProcessToJobObject failed for pid " + pid + " (GetLastError=" + getLastError() + ")"); + throw new IOException("failed to assign process to Windows Job Object, pid=" + pid + + " (GetLastError=" + getLastError() + ")"); } } finally { closeHandleQuiet(processHandle); @@ -158,14 +162,17 @@ static WindowsJobObject createAndAssign(long pid) throws IOException { if (t instanceof IOException ioe) { throw ioe; } - throw new IOException("Failed to configure Windows Job Object", t); + throw new IOException("failed to configure Windows Job Object", t); } + if (PROCESS_DEBUG) { + System.out.println("[rps] created Windows Job Object for pid " + pid); + } return new WindowsJobObject(jobHandle); } catch (IOException e) { throw e; } catch (Throwable t) { - throw new IOException("Failed to initialize Windows Job Object", t); + throw new IOException("failed to initialize Windows Job Object", t); } } diff --git a/examples/python/README.md b/examples/python/README.md index 24e2640..c140bc9 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -22,6 +22,7 @@ Notes: The client auto-spawns and kills `rps-server` unless `--server` is provided. On Windows, auto-spawned server processes are attached to a Job Object so they are reliably cleaned up when the Python client exits. +Set `RPS_DEBUG_PROCESS_LIFECYCLE=1` to print spawn/attach/stop lifecycle logs. ### Scan (auto-managed server) diff --git a/examples/python/rps_client/server_manager.py b/examples/python/rps_client/server_manager.py index 19d13b0..a9e14bc 100644 --- a/examples/python/rps_client/server_manager.py +++ b/examples/python/rps_client/server_manager.py @@ -6,10 +6,14 @@ import subprocess import sys import time +from typing import TYPE_CHECKING from pathlib import Path import grpc +if TYPE_CHECKING: + from rps_client.windows_job import WindowsJobObject + class ServerManager: """Spawns and kills the rps-server binary as a subprocess.""" @@ -28,9 +32,10 @@ def __init__( self.log_file = log_file self.log_level = log_level self._process: subprocess.Popen | None = None - self._windows_job = None + self._windows_job: WindowsJobObject | None = None self._old_sigint_handler = None self._old_sigterm_handler = None + self._debug = os.getenv("RPS_DEBUG_PROCESS_LIFECYCLE") == "1" def _handle_signal(self, signum, frame): """Signal handler to ensure cleanup on SIGINT/SIGTERM.""" @@ -65,20 +70,25 @@ def start(self, timeout: float = 10.0) -> None: "--log", self.log_file, "--log-level", self.log_level, ] - env = self._build_env() self._process = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - env=env, start_new_session=(sys.platform != "win32"), ) + if self._debug: + print(f"[rps] spawned server pid {self._process.pid}") if sys.platform == "win32": + # Windows process ownership model: + # parent/child does not imply lifetime ownership. Attach rps-server + # to a Job Object so parent exit tears down the process tree. from rps_client.windows_job import WindowsJobObject try: self._windows_job = WindowsJobObject.create_and_assign(self._process.pid) + if self._debug: + print(f"[rps] attached Windows Job Object to pid {self._process.pid}") except Exception: self._process.terminate() self._process.wait(timeout=2.0) @@ -118,6 +128,8 @@ def stop(self, in_hurry: bool = False) -> None: try: if in_hurry: + if self._debug: + print("[rps] stop(in_hurry=True)") self._process.terminate() try: self._process.wait(timeout=3.0) @@ -146,6 +158,8 @@ def stop(self, in_hurry: bool = False) -> None: self._process.kill() self._process.wait() finally: + if self._debug: + print("[rps] server process stopped") self._process = None self._close_windows_job() @@ -153,31 +167,11 @@ def _close_windows_job(self) -> None: if self._windows_job is not None: try: self._windows_job.close() + if self._debug: + print("[rps] closed Windows Job Object") finally: self._windows_job = None - @staticmethod - def _build_env() -> dict[str, str]: - """Build subprocess environment with MSYS2 DLL directories on PATH. - - On Windows, rps-server links shared gRPC/protobuf/spdlog DLLs from - MSYS2. If the user runs from PowerShell or VS Code (rather than the - MSYS2 shell), those DLLs won't be on PATH. We auto-detect and add - the MSYS2 bin directory so the server can find them. - """ - env = os.environ.copy() - if sys.platform == "win32": - msys2_dirs = [ - r"C:\msys64\clang64\bin", - r"C:\msys64\mingw64\bin", - r"C:\msys64\ucrt64\bin", - ] - path = env.get("PATH", "") - additions = [d for d in msys2_dirs if os.path.isdir(d) and d not in path] - if additions: - env["PATH"] = ";".join(additions) + ";" + path - return env - @property def address(self) -> str: return f"localhost:{self.port}" @@ -191,4 +185,4 @@ def __enter__(self): return self def __exit__(self, *exc): - self.stop() \ No newline at end of file + self.stop() diff --git a/examples/python/rps_client/windows_job.py b/examples/python/rps_client/windows_job.py index 02cbbb1..b956ab4 100644 --- a/examples/python/rps_client/windows_job.py +++ b/examples/python/rps_client/windows_job.py @@ -82,11 +82,11 @@ def __init__(self, handle: wintypes.HANDLE): @classmethod def create_and_assign(cls, pid: int) -> "WindowsJobObject": if pid <= 0: - raise OSError(f"invalid pid: {pid}") + raise OSError(f"failed to assign Windows Job Object: invalid pid {pid}") job = _kernel32.CreateJobObjectW(None, None) if not job: - raise ctypes.WinError(ctypes.get_last_error()) + raise OSError(f"failed to create Windows Job Object: {ctypes.WinError(ctypes.get_last_error())}") try: info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() @@ -99,16 +99,24 @@ def create_and_assign(cls, pid: int) -> "WindowsJobObject": ctypes.sizeof(info), ) if not ok: - raise ctypes.WinError(ctypes.get_last_error()) + raise OSError( + f"failed to configure Windows Job Object: {ctypes.WinError(ctypes.get_last_error())}" + ) proc = _kernel32.OpenProcess(PROCESS_TERMINATE | PROCESS_SET_QUOTA, False, int(pid)) if not proc: - raise ctypes.WinError(ctypes.get_last_error()) + raise OSError( + f"failed to open process for Windows Job assignment, pid={pid}: " + f"{ctypes.WinError(ctypes.get_last_error())}" + ) try: ok = _kernel32.AssignProcessToJobObject(job, proc) if not ok: - raise ctypes.WinError(ctypes.get_last_error()) + raise OSError( + f"failed to assign process to Windows Job Object, pid={pid}: " + f"{ctypes.WinError(ctypes.get_last_error())}" + ) finally: _kernel32.CloseHandle(proc) @@ -121,4 +129,3 @@ def close(self) -> None: if self._handle: _kernel32.CloseHandle(self._handle) self._handle = None - From 8ac4152ef61a516ae935fbfc01509512caa6736f Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sun, 1 Mar 2026 00:24:15 +0100 Subject: [PATCH 21/27] linux/macos fix. --- examples/cpp/src/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/cpp/src/main.cpp b/examples/cpp/src/main.cpp index 48c01ee..ea94315 100644 --- a/examples/cpp/src/main.cpp +++ b/examples/cpp/src/main.cpp @@ -84,10 +84,12 @@ static std::atomic g_interrupted{false}; // Forward-declare signal setup. static void installSignalHandlers(); +#ifdef _WIN32 static bool processDebugEnabled() { const char* v = std::getenv("RPS_DEBUG_PROCESS_LIFECYCLE"); return v && std::string(v) == "1"; } +#endif // --------------------------------------------------------------------------- // Server process manager From 730c41534c46334cdf2e8d112e4829cb17477352 Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sun, 1 Mar 2026 00:39:18 +0100 Subject: [PATCH 22/27] workflow improvements. --- .github/workflows/build.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77a38b5..434b55a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,8 +13,10 @@ jobs: include: - os: ubuntu-latest name: Linux x64 + toolchain: clang - os: macos-15 name: macOS Apple Silicon + toolchain: clang # - os: windows-latest # name: Windows MSVC # toolchain: msvc @@ -48,18 +50,20 @@ jobs: if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - - name: Cache vcpkg binary archives (Unix) + - name: Restore vcpkg binary archives (Unix) if: runner.os != 'Windows' - uses: actions/cache@v4 + id: vcpkg_cache_restore_unix + uses: actions/cache/restore@v4 with: path: ~/.cache/vcpkg/archives key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} restore-keys: | vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}- - - name: Cache vcpkg binary archives (Windows) + - name: Restore vcpkg binary archives (Windows) if: runner.os == 'Windows' - uses: actions/cache@v4 + id: vcpkg_cache_restore_windows + uses: actions/cache/restore@v4 with: path: ${{ env.LOCALAPPDATA }}\vcpkg\archives key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} @@ -95,11 +99,20 @@ jobs: cmake -G Ninja -B build \ -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \ -DVCPKG_TARGET_TRIPLET=$TRIPLET + - name: Save vcpkg binary archives (Unix) + if: ${{ runner.os != 'Windows' && always() && steps.vcpkg_cache_restore_unix.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: ~/.cache/vcpkg/archives + key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} + - name: Build (Unix) if: runner.os != 'Windows' run: cmake --build build --config Release @@ -112,6 +125,13 @@ jobs: if: runner.os == 'Windows' && matrix.toolchain == 'clang' run: cmake -G Ninja -B build -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DRPS_MSVC_STATIC_RUNTIME=ON + - name: Save vcpkg binary archives (Windows) + if: ${{ runner.os == 'Windows' && always() && steps.vcpkg_cache_restore_windows.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: ${{ env.LOCALAPPDATA }}\vcpkg\archives + key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} + - name: Build (Windows) if: runner.os == 'Windows' run: cmake --build build --config Release From 6cc4e22dc69d844da2e5199241d55be441aab32e Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sun, 1 Mar 2026 01:03:36 +0100 Subject: [PATCH 23/27] github runner fixes --- .github/workflows/build.yml | 52 +++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 434b55a..45f8e36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,54 @@ jobs: if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 + - name: Select Windows storage for vcpkg + if: runner.os == 'Windows' + id: win_storage + shell: pwsh + run: | + $candidates = @() + foreach ($p in @($env:RUNNER_TEMP, $env:GITHUB_WORKSPACE, 'D:\', 'C:\')) { + if (-not $p) { continue } + if (-not (Test-Path $p)) { continue } + $resolved = (Resolve-Path $p).Path + $qualifier = Split-Path -Qualifier $resolved + $driveName = $qualifier.TrimEnd('\').TrimEnd(':') + $drive = Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue + if ($drive) { + $candidates += [pscustomobject]@{ + Root = $qualifier + Free = [int64]$drive.Free + } + } + } + + if (-not $candidates) { + throw "Could not determine a writable storage location for vcpkg." + } + + $best = $candidates | Sort-Object Free -Descending | Select-Object -First 1 + $base = Join-Path $best.Root "vcpkg-cache" + $archives = Join-Path $base "archives" + $downloads = Join-Path $base "downloads" + + New-Item -ItemType Directory -Force -Path $archives, $downloads | Out-Null + + "VCPKG_DEFAULT_BINARY_CACHE=$archives" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "VCPKG_DOWNLOADS=$downloads" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + "vcpkg_cache_dir=$archives" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "vcpkg_downloads_dir=$downloads" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + $bestGb = [math]::Round($best.Free / 1GB, 2) + $all = $candidates | Sort-Object Free -Descending | ForEach-Object { + "$($_.Root)=$([math]::Round($_.Free / 1GB, 2))GB" + } + Write-Host "Storage candidates: $($all -join ', ')" + Write-Host "Selected vcpkg storage root: $($best.Root)" + Write-Host "Selected drive free space: $bestGb GB" + Write-Host "vcpkg archives: $archives" + Write-Host "vcpkg downloads: $downloads" + - name: Restore vcpkg binary archives (Unix) if: runner.os != 'Windows' id: vcpkg_cache_restore_unix @@ -65,7 +113,7 @@ jobs: id: vcpkg_cache_restore_windows uses: actions/cache/restore@v4 with: - path: ${{ env.LOCALAPPDATA }}\vcpkg\archives + path: ${{ steps.win_storage.outputs.vcpkg_cache_dir }} key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} restore-keys: | vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}- @@ -129,7 +177,7 @@ jobs: if: ${{ runner.os == 'Windows' && always() && steps.vcpkg_cache_restore_windows.outputs.cache-hit != 'true' }} uses: actions/cache/save@v4 with: - path: ${{ env.LOCALAPPDATA }}\vcpkg\archives + path: ${{ steps.win_storage.outputs.vcpkg_cache_dir }} key: vcpkg-archives-${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }} - name: Build (Windows) From 27685c2a1207b031a9353bd54ae36ac889c717bc Mon Sep 17 00:00:00 2001 From: "B. Zeiss" Date: Sun, 1 Mar 2026 08:21:50 +0100 Subject: [PATCH 24/27] Update build.yml --- .github/workflows/build.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45f8e36..db89a55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,26 +77,28 @@ jobs: $best = $candidates | Sort-Object Free -Descending | Select-Object -First 1 $base = Join-Path $best.Root "vcpkg-cache" + + # Add buildtrees and packages to the D: drive paths $archives = Join-Path $base "archives" $downloads = Join-Path $base "downloads" + $buildtrees = Join-Path $base "buildtrees" + $packages = Join-Path $base "packages" - New-Item -ItemType Directory -Force -Path $archives, $downloads | Out-Null + New-Item -ItemType Directory -Force -Path $archives, $downloads, $buildtrees, $packages | Out-Null "VCPKG_DEFAULT_BINARY_CACHE=$archives" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "VCPKG_DOWNLOADS=$downloads" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + # Force vcpkg to compile on the D: drive + "VCPKG_BUILDTREES_DIR=$buildtrees" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "VCPKG_PACKAGES_DIR=$packages" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "vcpkg_cache_dir=$archives" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "vcpkg_downloads_dir=$downloads" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append $bestGb = [math]::Round($best.Free / 1GB, 2) - $all = $candidates | Sort-Object Free -Descending | ForEach-Object { - "$($_.Root)=$([math]::Round($_.Free / 1GB, 2))GB" - } - Write-Host "Storage candidates: $($all -join ', ')" Write-Host "Selected vcpkg storage root: $($best.Root)" Write-Host "Selected drive free space: $bestGb GB" - Write-Host "vcpkg archives: $archives" - Write-Host "vcpkg downloads: $downloads" - name: Restore vcpkg binary archives (Unix) if: runner.os != 'Windows' From dcb5aa9d1ea646e04564550760a12779ef17d2c4 Mon Sep 17 00:00:00 2001 From: "B. Zeiss" Date: Sun, 1 Mar 2026 08:29:05 +0100 Subject: [PATCH 25/27] Update build.yml --- .github/workflows/build.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db89a55..037a3b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - name: Setup MSVC Developer Command Prompt if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - + - name: Select Windows storage for vcpkg if: runner.os == 'Windows' id: win_storage @@ -78,7 +78,6 @@ jobs: $best = $candidates | Sort-Object Free -Descending | Select-Object -First 1 $base = Join-Path $best.Root "vcpkg-cache" - # Add buildtrees and packages to the D: drive paths $archives = Join-Path $base "archives" $downloads = Join-Path $base "downloads" $buildtrees = Join-Path $base "buildtrees" @@ -88,14 +87,19 @@ jobs: "VCPKG_DEFAULT_BINARY_CACHE=$archives" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "VCPKG_DOWNLOADS=$downloads" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - # Force vcpkg to compile on the D: drive - "VCPKG_BUILDTREES_DIR=$buildtrees" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "VCPKG_PACKAGES_DIR=$packages" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "vcpkg_cache_dir=$archives" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "vcpkg_downloads_dir=$downloads" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + # --- THE FIX: Create Junctions to redirect C: to D: --- + # 1. Remove existing directories if GitHub pre-created them + Remove-Item -Path "C:\vcpkg\buildtrees" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path "C:\vcpkg\packages" -Recurse -Force -ErrorAction SilentlyContinue + + # 2. Create the junctions + New-Item -ItemType Junction -Path "C:\vcpkg\buildtrees" -Target $buildtrees | Out-Null + New-Item -ItemType Junction -Path "C:\vcpkg\packages" -Target $packages | Out-Null + $bestGb = [math]::Round($best.Free / 1GB, 2) Write-Host "Selected vcpkg storage root: $($best.Root)" Write-Host "Selected drive free space: $bestGb GB" From 4bd4da1a53a9063e86acb56478a1aeac4af1a5c4 Mon Sep 17 00:00:00 2001 From: "B. Zeiss" Date: Sun, 1 Mar 2026 08:32:06 +0100 Subject: [PATCH 26/27] Update build.yml --- .github/workflows/build.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 037a3b7..73408ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,6 +99,15 @@ jobs: # 2. Create the junctions New-Item -ItemType Junction -Path "C:\vcpkg\buildtrees" -Target $buildtrees | Out-Null New-Item -ItemType Junction -Path "C:\vcpkg\packages" -Target $packages | Out-Null + + # --- PROOF OF REDIRECTION --- + Write-Host "Testing the Junctions..." + Set-Content -Path "C:\vcpkg\buildtrees\proof.txt" -Value "This is actually on the D drive!" + if (Test-Path "$buildtrees\proof.txt") { + Write-Host "SUCCESS: C:\vcpkg\buildtrees is successfully routing to the D: drive!" + } else { + Write-Error "FAILURE: Junction didn't work." + } $bestGb = [math]::Round($best.Free / 1GB, 2) Write-Host "Selected vcpkg storage root: $($best.Root)" From 04e7862dd706a26c2163f2902e7b2bebb020fa2a Mon Sep 17 00:00:00 2001 From: Benjamin Zeiss Date: Sun, 1 Mar 2026 13:22:40 +0100 Subject: [PATCH 27/27] windows workflow fixes. --- .github/workflows/build.yml | 6 +++--- .gitignore | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73408ae..b32b334 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -209,7 +209,7 @@ jobs: cp build/apps/rps-vstscannermaster/vstscannermaster release_stage/ cp build/examples/cpp/rps-example-client release_stage/ cd release_stage - zip -r ../rps-binaries-${{ matrix.name }}.zip ./* + zip -r "../rps-binaries-${{ matrix.name }}.zip" ./* - name: Stage & Zip Binaries (Windows MSVC) if: runner.os == 'Windows' && matrix.toolchain == 'msvc' @@ -220,7 +220,7 @@ jobs: Copy-Item build\apps\rps-pluginscanner\rps-pluginscanner.exe release_stage\ Copy-Item build\apps\rps-vstscannermaster\vstscannermaster.exe release_stage\ Copy-Item build\examples\cpp\rps-example-client.exe release_stage\ - Compress-Archive -Path release_stage\* -DestinationPath rps-binaries-${{ matrix.name }}.zip + Compress-Archive -Path release_stage\* -DestinationPath "rps-binaries-${{ matrix.name }}.zip" - name: Stage & Zip Binaries (Windows Clang) if: runner.os == 'Windows' && matrix.toolchain == 'clang' @@ -231,7 +231,7 @@ jobs: Copy-Item build\apps\rps-pluginscanner\rps-pluginscanner.exe release_stage\ Copy-Item build\apps\rps-vstscannermaster\vstscannermaster.exe release_stage\ Copy-Item build\examples\cpp\rps-example-client.exe release_stage\ - Compress-Archive -Path release_stage\* -DestinationPath rps-binaries-${{ matrix.name }}.zip + Compress-Archive -Path release_stage\* -DestinationPath "rps-binaries-${{ matrix.name }}.zip" - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 1506207..d01c978 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ plans/ build_ninja_msvc/vcpkg-manifest-install.log build_ninja_msvc/CMakeCache.txt build_ninja_msvc/CMakeFiles/cmake.check_cache + +_private