From ae333a3802094eb7cbe13d6a4b79f50b404b3832 Mon Sep 17 00:00:00 2001 From: Prachi-Gupta2808 Date: Thu, 16 Jul 2026 12:22:35 +0530 Subject: [PATCH 01/29] chore(core-debug): add spdlog debug logging and ban raw std::cout --- .github/workflows/no-raw-debug-output.yml | 38 +++++++++++++ .gitmodules | 3 + CMakeLists.txt | 1 + core/CMakeLists.txt | 11 ++++ core/include/internal/debug.h | 22 ++++++++ core/include/internal/gpu.h | 65 +++++++++++----------- core/src/internal/bilateral_filter_gpu.cpp | 22 ++++---- core/src/internal/kmeans_gpu.cpp | 22 ++++---- docs/docs/contributing/debug-logging.md | 50 +++++++++++++++++ third_party/dawn | 2 +- third_party/spdlog | 1 + 11 files changed, 181 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/no-raw-debug-output.yml create mode 100644 core/include/internal/debug.h create mode 100644 docs/docs/contributing/debug-logging.md create mode 160000 third_party/spdlog diff --git a/.github/workflows/no-raw-debug-output.yml b/.github/workflows/no-raw-debug-output.yml new file mode 100644 index 000000000..55f1803f1 --- /dev/null +++ b/.github/workflows/no-raw-debug-output.yml @@ -0,0 +1,38 @@ +name: CI / No Raw Debug Output + +on: + pull_request: + push: + branches: [dev, main] + +jobs: + check-debug-output: + name: Check for raw debug output + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install ripgrep + run: sudo apt-get install -y ripgrep + + - name: Check for std::cout + run: | + if rg "std::cout" \ + --type cpp \ + -g '!third_party/**' \ + -g '!example-apps/**' \ + -l; then + echo "ERROR: Raw std::cout found. Use IMG2NUM_LOG_* macros instead." + exit 1 + fi + + - name: Check for std::cerr + run: | + if rg "std::cerr" \ + --type cpp \ + -g '!third_party/**' \ + -g '!example-apps/**' \ + -l; then + echo "ERROR: Raw std::cerr found. Use IMG2NUM_LOG_* macros instead." + exit 1 + fi \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 2f693d45c..f6cdc3d3e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "third_party/stb"] path = third_party/stb url = https://github.com/nothings/stb.git +[submodule "third_party/spdlog"] + path = third_party/spdlog + url = https://github.com/gabime/spdlog.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b319f573..5b2a9b57e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ option(IMG2NUM_BUILD_C "Build C bindings (required for WASM builds)" ON) option(IMG2NUM_BUILD_PYTHON "Build Python bindings" OFF) option(IMG2NUM_BUILD_EXAMPLES "Build example applications" ON) option(IMG2NUM_DEBUG_CACHE_VARIABLES_DUMP "Dump CMake environment variables in Debug mode" ON) +option(IMG2NUM_ENABLE_DEBUG_LOGGING "Compile with debug logs allowed to show in the console" ON) # Force a safe default to avoid the weirdness of CMake's `None` if(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "") diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index ad4b090f8..25e54a69d 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -38,6 +38,17 @@ add_custom_target(Img2Num_shaders ALL add_library(Img2Num ${CORE_SRC}) +# spdlog (debug logging) +if(IMG2NUM_ENABLE_DEBUG_LOGGING AND NOT EMSCRIPTEN) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" + EXCLUDE_FROM_ALL) + target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) + target_compile_definitions(Img2Num PRIVATE IMG2NUM_ENABLE_DEBUG_LOGGING) + set_target_properties(spdlog_header_only PROPERTIES EXPORT_NAME spdlog_header_only) + install(TARGETS spdlog_header_only EXPORT Img2NumTargets) +endif() + target_compile_options(Img2Num PRIVATE ${IMG2NUM_STRICT_CXX_FLAGS}) set_target_properties(Img2Num PROPERTIES diff --git a/core/include/internal/debug.h b/core/include/internal/debug.h new file mode 100644 index 000000000..722925523 --- /dev/null +++ b/core/include/internal/debug.h @@ -0,0 +1,22 @@ +#pragma once + +#ifdef IMG2NUM_ENABLE_DEBUG_LOGGING +#include +#define IMG2NUM_LOG_INFO(...) spdlog::info(__VA_ARGS__) +#define IMG2NUM_LOG_WARN(...) spdlog::warn(__VA_ARGS__) +#define IMG2NUM_LOG_ERROR(...) spdlog::error(__VA_ARGS__) +#define IMG2NUM_LOG_DEBUG(...) spdlog::debug(__VA_ARGS__) +#else +#define IMG2NUM_LOG_INFO(...) \ + do { \ + } while (0) +#define IMG2NUM_LOG_WARN(...) \ + do { \ + } while (0) +#define IMG2NUM_LOG_ERROR(...) \ + do { \ + } while (0) +#define IMG2NUM_LOG_DEBUG(...) \ + do { \ + } while (0) +#endif \ No newline at end of file diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index 43dae3c44..d9c63f3e5 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -6,8 +6,9 @@ #include #endif +#include "internal/debug.h" + #include -#include #include #include #include @@ -117,12 +118,11 @@ class GPU { for (uint32_t i = 0; i < info->messageCount; ++i) { const auto& msg = info->messages[i]; - std::cerr << "Shader Error [" - << (msg.type == wgpu::CompilationMessageType::Error ? "ERR" : "WARN") - << "]" - << " Line " << msg.lineNum << ":" << msg.linePos << " - " - << msg.message.data // .data for StringView - << std::endl; + IMG2NUM_LOG_ERROR( + "Shader Error [{}] Line {}:{} - {}", + msg.type == wgpu::CompilationMessageType::Error ? "ERR" : "WARN", + msg.lineNum, msg.linePos, msg.message.data + ); } } ); @@ -136,14 +136,14 @@ class GPU { instance = wgpu::CreateInstance(&instanceDesc); if (!instance) { - std::cerr << "Fatal: WebGPU instance creation failed." << std::endl; + IMG2NUM_LOG_ERROR("Fatal: WebGPU instance creation failed."); return; } // --------------------------------------------------------- // 1. Get Adapter // --------------------------------------------------------- - std::cout << "Requesting Adapter..." << std::endl; + IMG2NUM_LOG_INFO("Requesting Adapter..."); adapter_ready = false; instance.RequestAdapter( @@ -152,11 +152,11 @@ class GPU { [this](wgpu::RequestAdapterStatus status, wgpu::Adapter a, wgpu::StringView msg) { if (status == wgpu::RequestAdapterStatus::Success) { adapter = std::move(a); - std::cout << "Adapter Acquired" << std::endl; + IMG2NUM_LOG_INFO("Adapter Acquired"); } else { - std::cerr << "Adapter Failed: " - << std::string_view(msg.data ? msg.data : "", msg.length) - << std::endl; + IMG2NUM_LOG_ERROR( + "Adapter Failed: {}", std::string_view(msg.data ? msg.data : "", msg.length) + ); } adapter_ready = true; // Unblock the loop } @@ -171,14 +171,14 @@ class GPU { } if (!adapter) { - std::cerr << "Fatal: Could not get WebGPU Adapter." << std::endl; + IMG2NUM_LOG_ERROR("Fatal: Could not get WebGPU Adapter."); return; } // --------------------------------------------------------- // 2. Get Device // --------------------------------------------------------- - std::cout << "Requesting Device..." << std::endl; + IMG2NUM_LOG_INFO("Requesting Device..."); device_ready = false; wgpu::DeviceDescriptor deviceDesc = {}; @@ -189,9 +189,9 @@ class GPU { : "Unknown Error (Null message)"; // 2. Print it safely - std::cerr << "\n[WEBGPU FATAL ERROR] Type: " << static_cast(type) - << " | Msg: " << err_str << "\n" - << std::endl; + IMG2NUM_LOG_ERROR( + "\n[WEBGPU FATAL ERROR] Type: {} | Msg: {}\n", static_cast(type), err_str + ); }); deviceDesc.SetDeviceLostCallback( wgpu::CallbackMode::AllowProcessEvents, @@ -199,8 +199,9 @@ class GPU { std::string err_msg = (msg.data && msg.length > 0) ? std::string(msg.data, msg.length) : "Unknown device lost reason"; - std::cerr << "[DEVICE LOST] Reason: " << static_cast(reason) - << " Msg: " << err_msg << std::endl; + IMG2NUM_LOG_ERROR( + "[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg + ); } ); @@ -213,9 +214,10 @@ class GPU { // Copy the adapter's physical limits over to your requested limits requiredLimits = supportedLimits; - std::cout << "maxBufferSize: " << requiredLimits.maxBufferSize << std::endl; - std::cout << "maxStorageBufferBindingSize: " - << requiredLimits.maxStorageBufferBindingSize << std::endl; + IMG2NUM_LOG_INFO("maxBufferSize: {}", requiredLimits.maxBufferSize); + IMG2NUM_LOG_INFO( + "maxStorageBufferBindingSize: {}", requiredLimits.maxStorageBufferBindingSize + ); deviceDesc.requiredLimits = &requiredLimits; } @@ -226,12 +228,13 @@ class GPU { [this](wgpu::RequestDeviceStatus status, wgpu::Device d, wgpu::StringView msg) { if (status == wgpu::RequestDeviceStatus::Success) { device = std::move(d); - std::cout << "Device Acquired" << std::endl; + IMG2NUM_LOG_INFO("Device Acquired"); } else { - std::cerr << "Device Failed: " - << (msg.data && msg.length > 0 ? std::string(msg.data, msg.length) - : "Unknown error") - << std::endl; + IMG2NUM_LOG_ERROR( + "Device Failed: {}", msg.data && msg.length > 0 + ? std::string(msg.data, msg.length) + : "Unknown error" + ); } device_ready = true; // Unblock the loop } @@ -246,18 +249,18 @@ class GPU { } if (!device) { - std::cerr << "Fatal: Could not get WebGPU Device." << std::endl; + IMG2NUM_LOG_ERROR("Fatal: Could not get WebGPU Device."); return; } if (!validate_device()) { - std::cerr << "Fatal: Could not get WebGPU Device." << std::endl; + IMG2NUM_LOG_ERROR("Fatal: Could not get WebGPU Device."); return; } queue = device.GetQueue(); gpu_initialized = true; - std::cout << "GPU Fully Initialized." << std::endl; + IMG2NUM_LOG_INFO("GPU Fully Initialized."); }; ~GPU() { diff --git a/core/src/internal/bilateral_filter_gpu.cpp b/core/src/internal/bilateral_filter_gpu.cpp index 58dfc9f12..3ccfd717f 100644 --- a/core/src/internal/bilateral_filter_gpu.cpp +++ b/core/src/internal/bilateral_filter_gpu.cpp @@ -2,6 +2,7 @@ #include "img2num.h" #include "internal/cielab.h" +#include "internal/debug.h" #include "internal/gpu.h" #include @@ -10,7 +11,6 @@ #include #include #include -#include #include static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB {0}; @@ -52,7 +52,7 @@ void bilateral_filter_gpu( std::memcpy(result.data(), image, width * height * 4); // CIELAB conversion will run as shader - std::cout << "begin wgpu portion" << std::endl; + IMG2NUM_LOG_DEBUG("begin wgpu portion"); // 1. Create Input Texture wgpu::TextureDescriptor texDesc = {}; texDesc.size = {static_cast(width), static_cast(height), 1}; @@ -61,7 +61,7 @@ void bilateral_filter_gpu( texDesc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst; wgpu::Texture inputTexture = GPU::getClassInstance().get_device().CreateTexture(&texDesc); - std::cout << "upload texture" << std::endl; + IMG2NUM_LOG_DEBUG("upload texture"); // Upload data to Input Texture wgpu::TexelCopyTextureInfo dst = {}; dst.texture = inputTexture; @@ -74,7 +74,7 @@ void bilateral_filter_gpu( &dst, image, bytesPerPixel * width * height, &layout, &texDesc.size ); - std::cout << "create output texture" << std::endl; + IMG2NUM_LOG_DEBUG("create output texture"); // 2. Create Output Texture (Storage) wgpu::TextureDescriptor outDesc = texDesc; outDesc.usage = wgpu::TextureUsage::StorageBinding | wgpu::TextureUsage::CopySrc; @@ -89,7 +89,7 @@ void bilateral_filter_gpu( // filtered lab wgpu::Texture texLabFiltered = GPU::getClassInstance().get_device().CreateTexture(&descLab); - std::cout << "create buffer" << std::endl; + IMG2NUM_LOG_DEBUG("create buffer"); // 3. Create Uniform Buffer float sr = static_cast(sigma_range); FilterParams params = {static_cast(sigma_spatial), sr, 0.0f, 0.0f}; @@ -230,7 +230,7 @@ void bilateral_filter_gpu( wgpu::CommandBuffer commands = encoder.Finish(); GPU::getClassInstance().get_queue().Submit(1, &commands); - std::cout << "queue submit" << std::endl; + IMG2NUM_LOG_DEBUG("queue submit"); // static volatile bool waiting = true; @@ -250,23 +250,21 @@ void bilateral_filter_gpu( readBuffer.MapAsync( wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowProcessEvents, [](wgpu::MapAsyncStatus status, wgpu::StringView message, void* userdata) { - std::cout << "In callback" << std::endl; + IMG2NUM_LOG_DEBUG("In callback"); bool* flag = static_cast(userdata); bool success = false; if (status == wgpu::MapAsyncStatus::Success) { success = true; - // std::cout << "Map success: " << message.data << std::endl; } else { // Handle error success = false; - // std::cerr << "Map failed: " << message.data << std::endl; } *flag = false; }, (void*)waiting ); - std::cout << "waiting " << *waiting << std::endl; + IMG2NUM_LOG_DEBUG("waiting {}", *waiting); while (*waiting) { GPU::getClassInstance().get_instance().ProcessEvents(); @@ -274,7 +272,7 @@ void bilateral_filter_gpu( emscripten_sleep(10); #endif } - std::cout << "done wgpu" << std::endl; + IMG2NUM_LOG_DEBUG("done wgpu"); const uint8_t* mappedData = (const uint8_t*)readBuffer.GetConstMappedRange(0, bufferSize); // copy to cpu buffer for (size_t y = 0; y < height; ++y) { @@ -291,7 +289,7 @@ void bilateral_filter_gpu( } readBuffer.Unmap(); std::memcpy(image, result.data(), result.size()); - std::cout << "done memcpy" << std::endl; + IMG2NUM_LOG_DEBUG("done memcpy"); // explicit clean up diff --git a/core/src/internal/kmeans_gpu.cpp b/core/src/internal/kmeans_gpu.cpp index 6cf39f740..f0242e0a0 100644 --- a/core/src/internal/kmeans_gpu.cpp +++ b/core/src/internal/kmeans_gpu.cpp @@ -3,6 +3,7 @@ #include "img2num.h" #include "internal/cielab.h" +#include "internal/debug.h" #include "internal/gpu.h" #include "internal/Image.h" #include "internal/LABAPixel.h" @@ -228,11 +229,9 @@ void kMeansPlusPlusInitGpu( bool* flag = static_cast(userdata); bool success = false; if (status == wgpu::MapAsyncStatus::Success) { - // std::cout << "Map success: " << msg.data << std::endl; success = true; } else { // Handle error - // std::cerr << "Map failed: " << msg.data << std::endl; success = false; } *flag = true; @@ -488,7 +487,7 @@ void kmeans_gpu( } } - std::cout << "starting" << std::endl; + IMG2NUM_LOG_DEBUG("starting"); // Step 2: Initialize centroids switch (color_space) { @@ -501,7 +500,8 @@ void kmeans_gpu( break; } } - std::cout << "kmeans++ init done" << std::endl; + + IMG2NUM_LOG_DEBUG("kmeans++ init done"); // Step 3: Run k-means iterations int bytesPerPixel {16}; // float pixels @@ -548,7 +548,7 @@ void kmeans_gpu( GPU::getClassInstance().get_device().CreateBuffer(&readCentroidsDesc); // This is the actual KMeans loop - std::cout << "start iterations" << std::endl; + IMG2NUM_LOG_DEBUG("start iterations"); wgpu::CommandEncoder encoder = GPU::getClassInstance().get_device().CreateCommandEncoder(); for (int32_t iter {0}; iter < max_iter; ++iter) { wgpu::ComputePassEncoder pass1 = encoder.BeginComputePass(); @@ -586,7 +586,7 @@ void kmeans_gpu( wgpu::CommandBuffer commands = encoder.Finish(); GPU::getClassInstance().get_queue().Submit(1, &commands); - std::cout << "done iterations" << std::endl; + IMG2NUM_LOG_DEBUG("done iterations"); // 4. Map Async & Wait bool* done1 = new bool(false); @@ -599,7 +599,6 @@ void kmeans_gpu( bool* flag = static_cast(userdata); bool success = false; if (status == wgpu::MapAsyncStatus::Success) { - // std::cout << "Map success" << std::endl; success = true; } *flag = true; @@ -607,7 +606,7 @@ void kmeans_gpu( (void*)done1 ); - std::cout << "read out" << std::endl; + IMG2NUM_LOG_DEBUG("read out"); while (!*done1) { GPU::getClassInstance().get_instance().ProcessEvents(); @@ -616,7 +615,7 @@ void kmeans_gpu( #endif } - std::cout << "mapping labels" << std::endl; + IMG2NUM_LOG_DEBUG("mapping labels"); const uint8_t* mappedData = (const uint8_t*)readLabelsBuffer.GetConstMappedRange(); // ... Copy data to your C++ vector ... // Copy row by row to remove padding and put data into 'result' @@ -641,7 +640,6 @@ void kmeans_gpu( bool* flag = static_cast(userdata); bool success = false; if (status == wgpu::MapAsyncStatus::Success) { - // std::cout << "Map success" << std::endl; success = true; } *flag = true; // Signal completion @@ -656,7 +654,7 @@ void kmeans_gpu( #endif } - std::cout << "mapping centroids" << std::endl; + IMG2NUM_LOG_DEBUG("mapping centroids"); const float* mappedDataFloat = (const float*)readCentroidsBuffer.GetConstMappedRange(); // ... Copy data to your C++ vector ... @@ -699,7 +697,7 @@ void kmeans_gpu( } // Write labels to out_labels - std::cout << "copying labels out" << std::endl; + IMG2NUM_LOG_DEBUG("copying labels out"); std::memcpy(out_labels, labels.data(), labels.size() * sizeof(int32_t)); if (inputTexture) diff --git a/docs/docs/contributing/debug-logging.md b/docs/docs/contributing/debug-logging.md new file mode 100644 index 000000000..82f0d3aeb --- /dev/null +++ b/docs/docs/contributing/debug-logging.md @@ -0,0 +1,50 @@ +--- +id: debug-logging +title: Debug Logging +sidebar_label: 🪲 Debug Logging +--- + +## Overview + +Img2Num uses [spdlog](https://github.com/gabime/spdlog) for compile-time gated debug logging. +Raw `std::cout` and `std::cerr` are banned from production code — CI will fail if they are detected. + +## Using the Debug Macros + +Include the debug header in your file: + +```cpp +#include "internal/debug.h" +``` + +Then use the macros: + +```cpp +IMG2NUM_LOG_INFO("GPU initialized"); +IMG2NUM_LOG_WARN("Falling back to CPU"); +IMG2NUM_LOG_ERROR("Fatal: {}", error_message); +IMG2NUM_LOG_DEBUG("Processing pixel {}", index); +``` + +## Enabling Debug Logging + +Debug logging is enabled by default. To disable it, pass the CMake flag: + +```bash +cmake -DIMG2NUM_ENABLE_DEBUG_LOGGING=OFF -B build-release/ . +``` + +> Note: `std::flush` should be used when you need to ensure output is flushed immediately, +> unless you design a specific Emscripten macro branch that handles flushing automatically. + +## Why spdlog? + +- Compile-time gated — zero overhead in production +- Cannot accidentally ship debug output +- Works across C++ core, C bindings, and Python bindings +- Not linked against in WASM builds (Emscripten uses its own console functions) + +## CI Enforcement + +A GitHub Actions workflow checks all C++ files (excluding `third_party/` and `example-apps/`) +for raw `std::cout` and `std::cerr` usage. PRs will fail if violations are found. \ No newline at end of file diff --git a/third_party/dawn b/third_party/dawn index 37a53b346..2a49ad12f 160000 --- a/third_party/dawn +++ b/third_party/dawn @@ -1 +1 @@ -Subproject commit 37a53b346d181a77a1b2469defbf51ab96f4168c +Subproject commit 2a49ad12f8a9bf7451cb71983647d662fbe70224 diff --git a/third_party/spdlog b/third_party/spdlog new file mode 160000 index 000000000..2ee3cf820 --- /dev/null +++ b/third_party/spdlog @@ -0,0 +1 @@ +Subproject commit 2ee3cf8204ed5048627644e00a51a7d93fbc4786 From db35d9e4c8b89bbb16df6318221344089c6de4c5 Mon Sep 17 00:00:00 2001 From: Prachi Gupta Date: Thu, 16 Jul 2026 12:47:39 +0530 Subject: [PATCH 02/29] Update core/include/internal/gpu.h Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- core/include/internal/gpu.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index d9c63f3e5..15792aeb1 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -121,7 +121,8 @@ class GPU { IMG2NUM_LOG_ERROR( "Shader Error [{}] Line {}:{} - {}", msg.type == wgpu::CompilationMessageType::Error ? "ERR" : "WARN", - msg.lineNum, msg.linePos, msg.message.data + msg.lineNum, msg.linePos, + std::string_view(msg.message.data ? msg.message.data : "", msg.message.length) ); } } From 45e1a0fd8003c4366ddff146abbb16448a150ace Mon Sep 17 00:00:00 2001 From: Prachi Gupta Date: Thu, 16 Jul 2026 12:48:22 +0530 Subject: [PATCH 03/29] Update .github/workflows/no-raw-debug-output.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/no-raw-debug-output.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/no-raw-debug-output.yml b/.github/workflows/no-raw-debug-output.yml index 55f1803f1..9510cb4ce 100644 --- a/.github/workflows/no-raw-debug-output.yml +++ b/.github/workflows/no-raw-debug-output.yml @@ -5,13 +5,15 @@ on: push: branches: [dev, main] +permissions: + contents: read + jobs: check-debug-output: name: Check for raw debug output runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install ripgrep run: sudo apt-get install -y ripgrep From 018bdccd8de9d29107975579701416a7cef58013 Mon Sep 17 00:00:00 2001 From: Prachi-Gupta2808 Date: Thu, 23 Jul 2026 14:06:12 +0530 Subject: [PATCH 04/29] chore(core): add spdlog debug logging and ban raw std::cout --- .clang-tidy | 11 +++++++++++ CMakeLists.txt | 1 - core/CMakeLists.txt | 14 +++++++++----- core/include/internal/debug.h | 4 ++-- third_party/spdlog | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..41640a96d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,11 @@ +Checks: > + -*, + readability-*, + cppcoreguidelines-avoid-non-const-global-variables +WarningsAsErrors: '' +HeaderFilterRegex: '(core|bindings)/.*\.(h|hpp)$' +CheckOptions: + - key: readability-identifier-naming.FunctionCase + value: camelCase + +Checks: 'cppcoreguidelines-avoid-non-const-global-variables' diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b2a9b57e..4b319f573 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,6 @@ option(IMG2NUM_BUILD_C "Build C bindings (required for WASM builds)" ON) option(IMG2NUM_BUILD_PYTHON "Build Python bindings" OFF) option(IMG2NUM_BUILD_EXAMPLES "Build example applications" ON) option(IMG2NUM_DEBUG_CACHE_VARIABLES_DUMP "Dump CMake environment variables in Debug mode" ON) -option(IMG2NUM_ENABLE_DEBUG_LOGGING "Compile with debug logs allowed to show in the console" ON) # Force a safe default to avoid the weirdness of CMake's `None` if(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "") diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 25e54a69d..7b493f6ff 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -39,14 +39,18 @@ add_custom_target(Img2Num_shaders ALL add_library(Img2Num ${CORE_SRC}) # spdlog (debug logging) -if(IMG2NUM_ENABLE_DEBUG_LOGGING AND NOT EMSCRIPTEN) - add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" +if(NOT EMSCRIPTEN) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" EXCLUDE_FROM_ALL) target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) - target_compile_definitions(Img2Num PRIVATE IMG2NUM_ENABLE_DEBUG_LOGGING) - set_target_properties(spdlog_header_only PROPERTIES EXPORT_NAME spdlog_header_only) install(TARGETS spdlog_header_only EXPORT Img2NumTargets) + + add_compile_definitions(SPDLOG_USE_STD_FORMAT) + add_compile_definitions( + $<$:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_OFF> + $<$>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG> + ) endif() target_compile_options(Img2Num PRIVATE ${IMG2NUM_STRICT_CXX_FLAGS}) diff --git a/core/include/internal/debug.h b/core/include/internal/debug.h index 722925523..ef3e19c64 100644 --- a/core/include/internal/debug.h +++ b/core/include/internal/debug.h @@ -1,6 +1,6 @@ #pragma once -#ifdef IMG2NUM_ENABLE_DEBUG_LOGGING +#ifndef EMSCRIPTEN #include #define IMG2NUM_LOG_INFO(...) spdlog::info(__VA_ARGS__) #define IMG2NUM_LOG_WARN(...) spdlog::warn(__VA_ARGS__) @@ -19,4 +19,4 @@ #define IMG2NUM_LOG_DEBUG(...) \ do { \ } while (0) -#endif \ No newline at end of file +#endif diff --git a/third_party/spdlog b/third_party/spdlog index 2ee3cf820..79524ddd0 160000 --- a/third_party/spdlog +++ b/third_party/spdlog @@ -1 +1 @@ -Subproject commit 2ee3cf8204ed5048627644e00a51a7d93fbc4786 +Subproject commit 79524ddd08a4ec981b7fea76afd08ee05f83755d From 63e2eab89428aaaac685b8159c262a0a15231080 Mon Sep 17 00:00:00 2001 From: Krasner Date: Fri, 24 Jul 2026 13:59:47 +0000 Subject: [PATCH 05/29] fix spdlog --- Justfile | 6 ++-- core/CMakeLists.txt | 24 ++++++------- core/include/internal/debug.h | 22 ------------ core/include/internal/gpu.h | 39 ++++++++++++---------- core/src/internal/bilateral_filter_gpu.cpp | 21 ++++++------ core/src/internal/kmeans_gpu.cpp | 18 +++++----- 6 files changed, 55 insertions(+), 75 deletions(-) delete mode 100644 core/include/internal/debug.h diff --git a/Justfile b/Justfile index dc3214db1..13b77a5c9 100644 --- a/Justfile +++ b/Justfile @@ -39,9 +39,9 @@ format: @echo "Format all files" pnpm format -build-c-cpp: - @echo "Build C++ core and C bindings" - cmake -DCMAKE_BUILD_TYPE=Release -B build-c-cpp/ . +build-c-cpp build_type="Release": + @echo "Build C++ core and C bindings with CMAKE_BUILD_TYPE {{ build_type}}" + cmake -DCMAKE_BUILD_TYPE={{ build_type }} -B build-c-cpp/ . cmake --build build-c-cpp/ --parallel build-wasm: diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7b493f6ff..30e052105 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -39,19 +39,17 @@ add_custom_target(Img2Num_shaders ALL add_library(Img2Num ${CORE_SRC}) # spdlog (debug logging) -if(NOT EMSCRIPTEN) - add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" - EXCLUDE_FROM_ALL) - target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) - install(TARGETS spdlog_header_only EXPORT Img2NumTargets) - - add_compile_definitions(SPDLOG_USE_STD_FORMAT) - add_compile_definitions( - $<$:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_OFF> - $<$>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG> - ) -endif() +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" + EXCLUDE_FROM_ALL) +target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) + +add_compile_definitions( + $<$:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_OFF> + $<$>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO> +) + +install(TARGETS spdlog_header_only EXPORT Img2NumTargets) target_compile_options(Img2Num PRIVATE ${IMG2NUM_STRICT_CXX_FLAGS}) diff --git a/core/include/internal/debug.h b/core/include/internal/debug.h deleted file mode 100644 index ef3e19c64..000000000 --- a/core/include/internal/debug.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#ifndef EMSCRIPTEN -#include -#define IMG2NUM_LOG_INFO(...) spdlog::info(__VA_ARGS__) -#define IMG2NUM_LOG_WARN(...) spdlog::warn(__VA_ARGS__) -#define IMG2NUM_LOG_ERROR(...) spdlog::error(__VA_ARGS__) -#define IMG2NUM_LOG_DEBUG(...) spdlog::debug(__VA_ARGS__) -#else -#define IMG2NUM_LOG_INFO(...) \ - do { \ - } while (0) -#define IMG2NUM_LOG_WARN(...) \ - do { \ - } while (0) -#define IMG2NUM_LOG_ERROR(...) \ - do { \ - } while (0) -#define IMG2NUM_LOG_DEBUG(...) \ - do { \ - } while (0) -#endif diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index 15792aeb1..1f1cd1148 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -6,14 +6,13 @@ #include #endif -#include "internal/debug.h" - #include #include #include #include #include #include +#include // auto generated by tools/embed_shaders.py #include @@ -118,7 +117,7 @@ class GPU { for (uint32_t i = 0; i < info->messageCount; ++i) { const auto& msg = info->messages[i]; - IMG2NUM_LOG_ERROR( + SPDLOG_INFO( "Shader Error [{}] Line {}:{} - {}", msg.type == wgpu::CompilationMessageType::Error ? "ERR" : "WARN", msg.lineNum, msg.linePos, @@ -137,14 +136,14 @@ class GPU { instance = wgpu::CreateInstance(&instanceDesc); if (!instance) { - IMG2NUM_LOG_ERROR("Fatal: WebGPU instance creation failed."); + SPDLOG_INFO("Fatal: WebGPU instance creation failed."); return; } // --------------------------------------------------------- // 1. Get Adapter // --------------------------------------------------------- - IMG2NUM_LOG_INFO("Requesting Adapter..."); + SPDLOG_INFO("Requesting Adapter..."); adapter_ready = false; instance.RequestAdapter( @@ -153,9 +152,9 @@ class GPU { [this](wgpu::RequestAdapterStatus status, wgpu::Adapter a, wgpu::StringView msg) { if (status == wgpu::RequestAdapterStatus::Success) { adapter = std::move(a); - IMG2NUM_LOG_INFO("Adapter Acquired"); + SPDLOG_INFO("Adapter Acquired"); } else { - IMG2NUM_LOG_ERROR( + SPDLOG_INFO( "Adapter Failed: {}", std::string_view(msg.data ? msg.data : "", msg.length) ); } @@ -172,14 +171,14 @@ class GPU { } if (!adapter) { - IMG2NUM_LOG_ERROR("Fatal: Could not get WebGPU Adapter."); + SPDLOG_INFO("Fatal: Could not get WebGPU Adapter."); return; } // --------------------------------------------------------- // 2. Get Device // --------------------------------------------------------- - IMG2NUM_LOG_INFO("Requesting Device..."); + SPDLOG_INFO("Requesting Device..."); device_ready = false; wgpu::DeviceDescriptor deviceDesc = {}; @@ -190,7 +189,7 @@ class GPU { : "Unknown Error (Null message)"; // 2. Print it safely - IMG2NUM_LOG_ERROR( + SPDLOG_INFO( "\n[WEBGPU FATAL ERROR] Type: {} | Msg: {}\n", static_cast(type), err_str ); }); @@ -200,7 +199,11 @@ class GPU { std::string err_msg = (msg.data && msg.length > 0) ? std::string(msg.data, msg.length) : "Unknown device lost reason"; - IMG2NUM_LOG_ERROR( + // 1. Ignore teardown cancellations so we don't log during static destruction + if (reason == wgpu::DeviceLostReason::CallbackCancelled) { + return; + } + SPDLOG_INFO( "[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg ); } @@ -215,8 +218,8 @@ class GPU { // Copy the adapter's physical limits over to your requested limits requiredLimits = supportedLimits; - IMG2NUM_LOG_INFO("maxBufferSize: {}", requiredLimits.maxBufferSize); - IMG2NUM_LOG_INFO( + SPDLOG_INFO("maxBufferSize: {}", requiredLimits.maxBufferSize); + SPDLOG_INFO( "maxStorageBufferBindingSize: {}", requiredLimits.maxStorageBufferBindingSize ); @@ -229,9 +232,9 @@ class GPU { [this](wgpu::RequestDeviceStatus status, wgpu::Device d, wgpu::StringView msg) { if (status == wgpu::RequestDeviceStatus::Success) { device = std::move(d); - IMG2NUM_LOG_INFO("Device Acquired"); + SPDLOG_INFO("Device Acquired"); } else { - IMG2NUM_LOG_ERROR( + SPDLOG_INFO( "Device Failed: {}", msg.data && msg.length > 0 ? std::string(msg.data, msg.length) : "Unknown error" @@ -250,18 +253,18 @@ class GPU { } if (!device) { - IMG2NUM_LOG_ERROR("Fatal: Could not get WebGPU Device."); + SPDLOG_INFO("Fatal: Could not get WebGPU Device."); return; } if (!validate_device()) { - IMG2NUM_LOG_ERROR("Fatal: Could not get WebGPU Device."); + SPDLOG_INFO("Fatal: Could not get WebGPU Device."); return; } queue = device.GetQueue(); gpu_initialized = true; - IMG2NUM_LOG_INFO("GPU Fully Initialized."); + SPDLOG_INFO("GPU Fully Initialized."); }; ~GPU() { diff --git a/core/src/internal/bilateral_filter_gpu.cpp b/core/src/internal/bilateral_filter_gpu.cpp index 3ccfd717f..b81dc7b66 100644 --- a/core/src/internal/bilateral_filter_gpu.cpp +++ b/core/src/internal/bilateral_filter_gpu.cpp @@ -2,7 +2,6 @@ #include "img2num.h" #include "internal/cielab.h" -#include "internal/debug.h" #include "internal/gpu.h" #include @@ -12,6 +11,8 @@ #include #include #include +// for debug printing +#include static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB {0}; static constexpr uint8_t COLOR_SPACE_OPTION_RGB {1}; @@ -52,7 +53,7 @@ void bilateral_filter_gpu( std::memcpy(result.data(), image, width * height * 4); // CIELAB conversion will run as shader - IMG2NUM_LOG_DEBUG("begin wgpu portion"); + SPDLOG_INFO("begin wgpu portion"); // 1. Create Input Texture wgpu::TextureDescriptor texDesc = {}; texDesc.size = {static_cast(width), static_cast(height), 1}; @@ -61,7 +62,7 @@ void bilateral_filter_gpu( texDesc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst; wgpu::Texture inputTexture = GPU::getClassInstance().get_device().CreateTexture(&texDesc); - IMG2NUM_LOG_DEBUG("upload texture"); + SPDLOG_INFO("upload texture"); // Upload data to Input Texture wgpu::TexelCopyTextureInfo dst = {}; dst.texture = inputTexture; @@ -74,7 +75,7 @@ void bilateral_filter_gpu( &dst, image, bytesPerPixel * width * height, &layout, &texDesc.size ); - IMG2NUM_LOG_DEBUG("create output texture"); + SPDLOG_INFO("create output texture"); // 2. Create Output Texture (Storage) wgpu::TextureDescriptor outDesc = texDesc; outDesc.usage = wgpu::TextureUsage::StorageBinding | wgpu::TextureUsage::CopySrc; @@ -89,7 +90,7 @@ void bilateral_filter_gpu( // filtered lab wgpu::Texture texLabFiltered = GPU::getClassInstance().get_device().CreateTexture(&descLab); - IMG2NUM_LOG_DEBUG("create buffer"); + SPDLOG_INFO("create buffer"); // 3. Create Uniform Buffer float sr = static_cast(sigma_range); FilterParams params = {static_cast(sigma_spatial), sr, 0.0f, 0.0f}; @@ -230,7 +231,7 @@ void bilateral_filter_gpu( wgpu::CommandBuffer commands = encoder.Finish(); GPU::getClassInstance().get_queue().Submit(1, &commands); - IMG2NUM_LOG_DEBUG("queue submit"); + SPDLOG_INFO("queue submit"); // static volatile bool waiting = true; @@ -250,7 +251,7 @@ void bilateral_filter_gpu( readBuffer.MapAsync( wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowProcessEvents, [](wgpu::MapAsyncStatus status, wgpu::StringView message, void* userdata) { - IMG2NUM_LOG_DEBUG("In callback"); + SPDLOG_INFO("In callback"); bool* flag = static_cast(userdata); bool success = false; if (status == wgpu::MapAsyncStatus::Success) { @@ -264,7 +265,7 @@ void bilateral_filter_gpu( (void*)waiting ); - IMG2NUM_LOG_DEBUG("waiting {}", *waiting); + SPDLOG_INFO("waiting {}", *waiting); while (*waiting) { GPU::getClassInstance().get_instance().ProcessEvents(); @@ -272,7 +273,7 @@ void bilateral_filter_gpu( emscripten_sleep(10); #endif } - IMG2NUM_LOG_DEBUG("done wgpu"); + SPDLOG_INFO("done wgpu"); const uint8_t* mappedData = (const uint8_t*)readBuffer.GetConstMappedRange(0, bufferSize); // copy to cpu buffer for (size_t y = 0; y < height; ++y) { @@ -289,7 +290,7 @@ void bilateral_filter_gpu( } readBuffer.Unmap(); std::memcpy(image, result.data(), result.size()); - IMG2NUM_LOG_DEBUG("done memcpy"); + SPDLOG_INFO("done memcpy"); // explicit clean up diff --git a/core/src/internal/kmeans_gpu.cpp b/core/src/internal/kmeans_gpu.cpp index f0242e0a0..8fe362db2 100644 --- a/core/src/internal/kmeans_gpu.cpp +++ b/core/src/internal/kmeans_gpu.cpp @@ -3,7 +3,6 @@ #include "img2num.h" #include "internal/cielab.h" -#include "internal/debug.h" #include "internal/gpu.h" #include "internal/Image.h" #include "internal/LABAPixel.h" @@ -23,6 +22,7 @@ #include #include // Required for std::is_same_v #include +#include static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB {0}; static constexpr uint8_t COLOR_SPACE_OPTION_RGB {1}; @@ -487,7 +487,7 @@ void kmeans_gpu( } } - IMG2NUM_LOG_DEBUG("starting"); + SPDLOG_INFO("starting"); // Step 2: Initialize centroids switch (color_space) { @@ -501,7 +501,7 @@ void kmeans_gpu( } } - IMG2NUM_LOG_DEBUG("kmeans++ init done"); + SPDLOG_INFO("kmeans++ init done"); // Step 3: Run k-means iterations int bytesPerPixel {16}; // float pixels @@ -548,7 +548,7 @@ void kmeans_gpu( GPU::getClassInstance().get_device().CreateBuffer(&readCentroidsDesc); // This is the actual KMeans loop - IMG2NUM_LOG_DEBUG("start iterations"); + SPDLOG_INFO("start iterations"); wgpu::CommandEncoder encoder = GPU::getClassInstance().get_device().CreateCommandEncoder(); for (int32_t iter {0}; iter < max_iter; ++iter) { wgpu::ComputePassEncoder pass1 = encoder.BeginComputePass(); @@ -586,7 +586,7 @@ void kmeans_gpu( wgpu::CommandBuffer commands = encoder.Finish(); GPU::getClassInstance().get_queue().Submit(1, &commands); - IMG2NUM_LOG_DEBUG("done iterations"); + SPDLOG_INFO("done iterations"); // 4. Map Async & Wait bool* done1 = new bool(false); @@ -606,7 +606,7 @@ void kmeans_gpu( (void*)done1 ); - IMG2NUM_LOG_DEBUG("read out"); + SPDLOG_INFO("read out"); while (!*done1) { GPU::getClassInstance().get_instance().ProcessEvents(); @@ -615,7 +615,7 @@ void kmeans_gpu( #endif } - IMG2NUM_LOG_DEBUG("mapping labels"); + SPDLOG_INFO("mapping labels"); const uint8_t* mappedData = (const uint8_t*)readLabelsBuffer.GetConstMappedRange(); // ... Copy data to your C++ vector ... // Copy row by row to remove padding and put data into 'result' @@ -654,7 +654,7 @@ void kmeans_gpu( #endif } - IMG2NUM_LOG_DEBUG("mapping centroids"); + SPDLOG_INFO("mapping centroids"); const float* mappedDataFloat = (const float*)readCentroidsBuffer.GetConstMappedRange(); // ... Copy data to your C++ vector ... @@ -697,7 +697,7 @@ void kmeans_gpu( } // Write labels to out_labels - IMG2NUM_LOG_DEBUG("copying labels out"); + SPDLOG_INFO("copying labels out"); std::memcpy(out_labels, labels.data(), labels.size() * sizeof(int32_t)); if (inputTexture) From 520e2507a84d3a5d0c6a4d7ef23a1a11a897bc17 Mon Sep 17 00:00:00 2001 From: Krasner Date: Fri, 24 Jul 2026 14:13:00 +0000 Subject: [PATCH 06/29] restore dawn --- third_party/dawn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/dawn b/third_party/dawn index 2a49ad12f..37a53b346 160000 --- a/third_party/dawn +++ b/third_party/dawn @@ -1 +1 @@ -Subproject commit 2a49ad12f8a9bf7451cb71983647d662fbe70224 +Subproject commit 37a53b346d181a77a1b2469defbf51ab96f4168c From 40473ddfad3db43fa7f62f68ad5b83e53abf180c Mon Sep 17 00:00:00 2001 From: Krasner Date: Fri, 24 Jul 2026 14:19:58 +0000 Subject: [PATCH 07/29] format --- .clang-tidy | 4 ++-- core/include/internal/gpu.h | 10 +++++----- core/src/internal/kmeans_gpu.cpp | 2 +- docs/docs/contributing/debug-logging.md | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 41640a96d..853e4d9aa 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -2,10 +2,10 @@ Checks: > -*, readability-*, cppcoreguidelines-avoid-non-const-global-variables -WarningsAsErrors: '' +WarningsAsErrors: "" HeaderFilterRegex: '(core|bindings)/.*\.(h|hpp)$' CheckOptions: - key: readability-identifier-naming.FunctionCase value: camelCase -Checks: 'cppcoreguidelines-avoid-non-const-global-variables' +Checks: "cppcoreguidelines-avoid-non-const-global-variables" diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index 1f1cd1148..7c80a8eaf 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -9,10 +9,10 @@ #include #include #include +#include #include #include #include -#include // auto generated by tools/embed_shaders.py #include @@ -121,7 +121,9 @@ class GPU { "Shader Error [{}] Line {}:{} - {}", msg.type == wgpu::CompilationMessageType::Error ? "ERR" : "WARN", msg.lineNum, msg.linePos, - std::string_view(msg.message.data ? msg.message.data : "", msg.message.length) + std::string_view( + msg.message.data ? msg.message.data : "", msg.message.length + ) ); } } @@ -203,9 +205,7 @@ class GPU { if (reason == wgpu::DeviceLostReason::CallbackCancelled) { return; } - SPDLOG_INFO( - "[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg - ); + SPDLOG_INFO("[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg); } ); diff --git a/core/src/internal/kmeans_gpu.cpp b/core/src/internal/kmeans_gpu.cpp index 8fe362db2..e55a669fb 100644 --- a/core/src/internal/kmeans_gpu.cpp +++ b/core/src/internal/kmeans_gpu.cpp @@ -20,9 +20,9 @@ #include #include #include +#include #include // Required for std::is_same_v #include -#include static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB {0}; static constexpr uint8_t COLOR_SPACE_OPTION_RGB {1}; diff --git a/docs/docs/contributing/debug-logging.md b/docs/docs/contributing/debug-logging.md index 82f0d3aeb..64a1faa2d 100644 --- a/docs/docs/contributing/debug-logging.md +++ b/docs/docs/contributing/debug-logging.md @@ -47,4 +47,4 @@ cmake -DIMG2NUM_ENABLE_DEBUG_LOGGING=OFF -B build-release/ . ## CI Enforcement A GitHub Actions workflow checks all C++ files (excluding `third_party/` and `example-apps/`) -for raw `std::cout` and `std::cerr` usage. PRs will fail if violations are found. \ No newline at end of file +for raw `std::cout` and `std::cerr` usage. PRs will fail if violations are found. From 82063ba3e3b56987429fd0edb06af5076fc563ac Mon Sep 17 00:00:00 2001 From: Krasner Date: Sat, 25 Jul 2026 22:00:55 -0400 Subject: [PATCH 08/29] Update .github/workflows/no-raw-debug-output.yml Co-authored-by: Ryan Millard <142347829+Ryan-Millard@users.noreply.github.com> --- .github/workflows/no-raw-debug-output.yml | 40 +++++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/workflows/no-raw-debug-output.yml b/.github/workflows/no-raw-debug-output.yml index 9510cb4ce..4dc147cc0 100644 --- a/.github/workflows/no-raw-debug-output.yml +++ b/.github/workflows/no-raw-debug-output.yml @@ -17,24 +17,28 @@ jobs: - name: Install ripgrep run: sudo apt-get install -y ripgrep - - name: Check for std::cout + - name: Check for iostream usage run: | - if rg "std::cout" \ + streams=( + cin + cout + cerr + clog + wcin + wcout + wcerr + wclog + ) + + patterns=('#include\s*') + + for stream in "${streams[@]}"; do + patterns+=("std::$stream") + done + + regex=$(IFS='|'; echo "${patterns[*]}") + + rg -e "$regex" \ --type cpp \ -g '!third_party/**' \ - -g '!example-apps/**' \ - -l; then - echo "ERROR: Raw std::cout found. Use IMG2NUM_LOG_* macros instead." - exit 1 - fi - - - name: Check for std::cerr - run: | - if rg "std::cerr" \ - --type cpp \ - -g '!third_party/**' \ - -g '!example-apps/**' \ - -l; then - echo "ERROR: Raw std::cerr found. Use IMG2NUM_LOG_* macros instead." - exit 1 - fi \ No newline at end of file + -g '!example-apps/**' \ No newline at end of file From 45884e6899b22fc25dcf67db3618f00dc99b95c8 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Thu, 20 Aug 2026 22:21:41 +0200 Subject: [PATCH 09/29] feat(core): add IMG2NUM_LOG_LEVEL for configurable compile-time logging Replace the hardcoded Release/non-Release SPDLOG_ACTIVE_LEVEL split with a cache variable (TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF, default AUTO: Debug->TRACE, Release/MinSizeRel->OFF, else INFO). The definition is now target-scoped to Img2Num instead of directory-scoped, so it no longer leaks into third-party subtrees. Core logging goes through a dedicated "img2num" spdlog logger (lazily initialized, runtime level synced to the compiled level) via new IMG2NUM_LOG_* macros in internal/log.h, replacing bare SPDLOG_* calls. This keeps the library from touching the consumer's default logger. Supporting changes: - Justfile: all build recipes accept `build_type` and `log_level` positional args (`just build-c-cpp Release TRACE`); python builds thread them through SKBUILD_CMAKE_BUILD_TYPE / SKBUILD_CMAKE_DEFINE. Fix `build all` passing recipe names as arguments to build-c-cpp. - bindings/js: Debug wasm uses -g (embedded DWARF) instead of -gsource-map; the Debug glue fetched img2num.wasm.map at runtime, got the dev server's SPA-fallback HTML, and failed module init with a JSON parse error. - packages/js: drop the `prebuild` hook, which re-ran a default Release wasm build and clobbered non-default builds; remove the matching react-js recipe dependency. - ci: fix broken line continuation in the native Configure step; pass -DIMG2NUM_LOG_LEVEL=OFF explicitly for release artifacts. - cmake: include CImg2Num* variables in the debug cache dump matchers. - example-apps/html-js: fix `-F html-js` filter name; add combined `start` script. --- .github/workflows/release.yml | 14 +++--- CMakeLists.txt | 11 +++-- Justfile | 51 ++++++++++++++-------- bindings/js/CMakeLists.txt | 4 +- core/CMakeLists.txt | 40 +++++++++++++++-- core/include/internal/gpu.h | 35 +++++++-------- core/include/internal/log.h | 29 ++++++++++++ core/src/internal/bilateral_filter_gpu.cpp | 20 ++++----- core/src/internal/kmeans_gpu.cpp | 18 ++++---- example-apps/html-js/package.json | 3 +- packages/js/package.json | 1 - 11 files changed, 153 insertions(+), 73 deletions(-) create mode 100644 core/include/internal/log.h diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49b0401ef..ab31abfa7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -187,7 +187,9 @@ jobs: -DIMG2NUM_BUILD_C=${{ steps.component.outputs.IMG2NUM_BUILD_C_VAL }} \ -DIMG2NUM_BUILD_PYTHON=OFF \ -DIMG2NUM_BUILD_EXAMPLES=OFF \ - -DIMG2NUM_DEBUG_CACHE_VARIABLES_DUMP=ON + -DIMG2NUM_DEBUG_CACHE_VARIABLES_DUMP=ON \ + -DIMG2NUM_LOG_LEVEL=OFF \ + . - name: Build run: cmake --build build --config Release @@ -254,14 +256,10 @@ jobs: with: version: 11.4.0 - - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0 - with: - just-version: '1.46.0' - - name: Install dependencies run: pnpm install - - name: Build WASM + - name: Configure run: | emcmake cmake -B build-wasm \ -DCMAKE_BUILD_TYPE=Release \ @@ -269,8 +267,10 @@ jobs: -DIMG2NUM_BUILD_PYTHON=OFF \ -DIMG2NUM_BUILD_EXAMPLES=OFF \ -DIMG2NUM_DEBUG_CACHE_VARIABLES_DUMP=ON \ + -DIMG2NUM_LOG_LEVEL=OFF \ . - cmake --build build-wasm + - name: Build + run: cmake --build build-wasm --config Release - name: Build JS run: pnpm build diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b319f573..e09b27acf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,9 +45,12 @@ if(IMG2NUM_DEBUG_CACHE_VARIABLES_DUMP) foreach(_var ${_vars}) string(TOUPPER "${_var}" _var_upper) - if(_var MATCHES "^IMG2NUM") + # Exact upper case matches (User flags like IMG2NUM_BUILD_C) + if(_var MATCHES "^(C)?IMG2NUM") message(STATUS "[User-Defined] ${COLOR_MAGENTA}${_var}=${${_var}}${COLOR_RESET}") - elseif(_var_upper MATCHES "^IMG2NUM") + + # Case-insensitive matches via _var_upper (Auto variables like CImg2Num_BINARY_DIR) + elseif(_var_upper MATCHES "^(C)?IMG2NUM") message(STATUS "[Auto] ${COLOR_CYAN}${_var}=${${_var}}${COLOR_RESET}") endif() endforeach() @@ -55,7 +58,8 @@ if(IMG2NUM_DEBUG_CACHE_VARIABLES_DUMP) foreach(_var ${_vars}) string(TOUPPER "${_var}" _var_upper) - if(NOT _var_upper MATCHES "^IMG2NUM") + # Adding (C)? here stops CImg2Num variables from sneaking into the third-party list + if(NOT _var_upper MATCHES "^(C)?IMG2NUM") message(STATUS "${_var}=${${_var}}") endif() endforeach() @@ -64,6 +68,7 @@ if(IMG2NUM_DEBUG_CACHE_VARIABLES_DUMP) endif() endblock() + # ================================================ # Library setup diff --git a/Justfile b/Justfile index 91332d47b..ef2f4cb9d 100644 --- a/Justfile +++ b/Justfile @@ -41,33 +41,46 @@ format: @echo "Format all files" pnpm format -build-c-cpp build_type="Release": - @echo "Build C++ core and C bindings with CMAKE_BUILD_TYPE {{ build_type}}" - cmake -DCMAKE_BUILD_TYPE={{ build_type }} -B build-c-cpp/ . +build-c-cpp build_type="Release" log_level="AUTO": + @echo "Build C++ core and C bindings ({{ build_type }}, log={{ log_level }})" + cmake -DCMAKE_BUILD_TYPE={{ build_type }} \ + -DIMG2NUM_LOG_LEVEL={{ log_level }} \ + -B build-c-cpp/ . cmake --build build-c-cpp/ --parallel -build-wasm: - @echo "Build JS bindings" - emcmake cmake -DCMAKE_BUILD_TYPE=Release -B build-wasm/ . +build-wasm build_type="Release" log_level="AUTO": + @echo "Build JS bindings ({{ build_type }}, log={{ log_level }})" + emcmake cmake -DCMAKE_BUILD_TYPE={{ build_type }} \ + -DIMG2NUM_LOG_LEVEL={{ log_level }} \ + -B build-wasm/ . cmake --build build-wasm/ --parallel -build-py: - @echo "Build python bindings and py package" - uv sync --reinstall +build-py build_type="Release" log_level="AUTO": + @echo "Build python bindings and py package ({{ build_type }}, log={{ log_level }})" + SKBUILD_CMAKE_BUILD_TYPE={{ build_type }} \ + SKBUILD_CMAKE_DEFINE="IMG2NUM_LOG_LEVEL={{ log_level }}" \ + uv sync --reinstall-package img2num + SKBUILD_CMAKE_BUILD_TYPE={{ build_type }} \ + SKBUILD_CMAKE_DEFINE="IMG2NUM_LOG_LEVEL={{ log_level }}" \ uv build --wheel -build-packages-js: +build-packages-js build_type="Release" log_level="AUTO": @echo "Build js packages" - just build-wasm + just build-wasm {{ build_type }} {{ log_level }} pnpm -F img2num build -build target: +build target build_type="Release" log_level="AUTO": case "{{ target }}" in \ - cpp) just build-c-cpp ;; \ - js) just build-wasm ;; \ - py) just build-py ;; \ - packages-js) just build-packages-js ;; \ - all) just build-c-cpp build-wasm build-py build-packages-js react-js build docs build ;; \ + cpp) just build-c-cpp {{ build_type }} {{ log_level }} ;; \ + js) just build-wasm {{ build_type }} {{ log_level }} ;; \ + py) just build-py {{ build_type }} {{ log_level }} ;; \ + packages-js) just build-packages-js {{ build_type }} {{ log_level }} ;; \ + all) just build-c-cpp {{ build_type }} {{ log_level }} && \ + just build-wasm {{ build_type }} {{ log_level }} && \ + just build-py {{ build_type }} {{ log_level }} && \ + just build-packages-js {{ build_type }} {{ log_level }} && \ + just react-js build && \ + just docs build ;; \ esac clean target: @@ -86,7 +99,7 @@ docs action: start) cd docs/ && pnpm run serve ;; \ esac -react-js action: build-packages-js +react-js action: @echo "Run react sample app" case "{{ action }}" in \ build) pnpm -F react-example run build ;; \ @@ -115,4 +128,4 @@ console-js-esm input: node example-apps/console-js-esm/index.mjs "{{ input }}" html-js script: - pnpm -F html-js-iife "{{script}}" + pnpm -F html-js "{{script}}" diff --git a/bindings/js/CMakeLists.txt b/bindings/js/CMakeLists.txt index 5bc0083cc..c40921310 100644 --- a/bindings/js/CMakeLists.txt +++ b/bindings/js/CMakeLists.txt @@ -61,9 +61,9 @@ function(img2num_add_wasm_variant TARGET_NAME) if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(${TARGET_NAME} PRIVATE - -O0 -gsource-map -fsanitize=alignment) + -O0 -g -fsanitize=alignment) target_link_options(${TARGET_NAME} PRIVATE - -gsource-map -fsanitize=alignment + -g -fsanitize=alignment "SHELL:-sASSERTIONS=2" "SHELL:-sSTACK_OVERFLOW_CHECK=1") else() diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 30e052105..341da5093 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -43,10 +43,42 @@ add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" EXCLUDE_FROM_ALL) target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) +# ================================================================= +# Logging level (compile-time stripping via SPDLOG_ACTIVE_LEVEL) +# +# AUTO (default): pick a level from the build type: +# Debug -> TRACE (everything) +# Release/MinSizeRel -> OFF (fully stripped) +# RelWithDebInfo/etc -> INFO +# Or force a level: -DIMG2NUM_LOG_LEVEL=TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF +# ================================================================= +set(IMG2NUM_LOG_LEVEL "AUTO" CACHE STRING "Compile-time spdlog level for Img2Num") +set_property(CACHE IMG2NUM_LOG_LEVEL PROPERTY STRINGS + AUTO TRACE DEBUG INFO WARN ERROR CRITICAL OFF) + +string(TOUPPER "${IMG2NUM_LOG_LEVEL}" _IMG2NUM_LOG_LEVEL) + +if(_IMG2NUM_LOG_LEVEL STREQUAL "AUTO") + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(_IMG2NUM_LOG_LEVEL "TRACE") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + set(_IMG2NUM_LOG_LEVEL "OFF") + else() + set(_IMG2NUM_LOG_LEVEL "INFO") + endif() +endif() + +# Map to spdlog's numeric level macros — these names are spdlog's, keep them. +if(_IMG2NUM_LOG_LEVEL MATCHES "^(TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF)$") + set(_IMG2NUM_SPDLOG_LEVEL "SPDLOG_LEVEL_${_IMG2NUM_LOG_LEVEL}") +else() + message(FATAL_ERROR "IMG2NUM_LOG_LEVEL='${IMG2NUM_LOG_LEVEL}' is not one of AUTO/TRACE/DEBUG/INFO/WARN/ERROR/CRITICAL/OFF") +endif() + +message(STATUS "Img2Num log level: ${_IMG2NUM_LOG_LEVEL} (${_IMG2NUM_SPDLOG_LEVEL})") -add_compile_definitions( - $<$:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_OFF> - $<$>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO> +target_compile_definitions(Img2Num PRIVATE + SPDLOG_ACTIVE_LEVEL=${_IMG2NUM_SPDLOG_LEVEL} ) install(TARGETS spdlog_header_only EXPORT Img2NumTargets) @@ -78,7 +110,7 @@ if (EMSCRIPTEN) target_compile_options(Img2Num PRIVATE "--use-port=emdawnwebgpu") target_link_options(Img2Num PRIVATE "--use-port=emdawnwebgpu" "SHELL:-s ASYNCIFY=1") else() - + find_package(Dawn QUIET) if (Dawn_FOUND) message("Found system Dawn") diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index 7c80a8eaf..427ee4fcc 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -17,6 +16,8 @@ // auto generated by tools/embed_shaders.py #include +#include "internal/log.h" + class GPU { private: wgpu::Instance instance; @@ -117,7 +118,7 @@ class GPU { for (uint32_t i = 0; i < info->messageCount; ++i) { const auto& msg = info->messages[i]; - SPDLOG_INFO( + IMG2NUM_LOG_INFO( "Shader Error [{}] Line {}:{} - {}", msg.type == wgpu::CompilationMessageType::Error ? "ERR" : "WARN", msg.lineNum, msg.linePos, @@ -138,14 +139,14 @@ class GPU { instance = wgpu::CreateInstance(&instanceDesc); if (!instance) { - SPDLOG_INFO("Fatal: WebGPU instance creation failed."); + IMG2NUM_LOG_INFO("Fatal: WebGPU instance creation failed."); return; } // --------------------------------------------------------- // 1. Get Adapter // --------------------------------------------------------- - SPDLOG_INFO("Requesting Adapter..."); + IMG2NUM_LOG_INFO("Requesting Adapter..."); adapter_ready = false; instance.RequestAdapter( @@ -154,9 +155,9 @@ class GPU { [this](wgpu::RequestAdapterStatus status, wgpu::Adapter a, wgpu::StringView msg) { if (status == wgpu::RequestAdapterStatus::Success) { adapter = std::move(a); - SPDLOG_INFO("Adapter Acquired"); + IMG2NUM_LOG_INFO("Adapter Acquired"); } else { - SPDLOG_INFO( + IMG2NUM_LOG_INFO( "Adapter Failed: {}", std::string_view(msg.data ? msg.data : "", msg.length) ); } @@ -173,14 +174,14 @@ class GPU { } if (!adapter) { - SPDLOG_INFO("Fatal: Could not get WebGPU Adapter."); + IMG2NUM_LOG_INFO("Fatal: Could not get WebGPU Adapter."); return; } // --------------------------------------------------------- // 2. Get Device // --------------------------------------------------------- - SPDLOG_INFO("Requesting Device..."); + IMG2NUM_LOG_INFO("Requesting Device..."); device_ready = false; wgpu::DeviceDescriptor deviceDesc = {}; @@ -191,7 +192,7 @@ class GPU { : "Unknown Error (Null message)"; // 2. Print it safely - SPDLOG_INFO( + IMG2NUM_LOG_INFO( "\n[WEBGPU FATAL ERROR] Type: {} | Msg: {}\n", static_cast(type), err_str ); }); @@ -205,7 +206,7 @@ class GPU { if (reason == wgpu::DeviceLostReason::CallbackCancelled) { return; } - SPDLOG_INFO("[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg); + IMG2NUM_LOG_INFO("[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg); } ); @@ -218,8 +219,8 @@ class GPU { // Copy the adapter's physical limits over to your requested limits requiredLimits = supportedLimits; - SPDLOG_INFO("maxBufferSize: {}", requiredLimits.maxBufferSize); - SPDLOG_INFO( + IMG2NUM_LOG_INFO("maxBufferSize: {}", requiredLimits.maxBufferSize); + IMG2NUM_LOG_INFO( "maxStorageBufferBindingSize: {}", requiredLimits.maxStorageBufferBindingSize ); @@ -232,9 +233,9 @@ class GPU { [this](wgpu::RequestDeviceStatus status, wgpu::Device d, wgpu::StringView msg) { if (status == wgpu::RequestDeviceStatus::Success) { device = std::move(d); - SPDLOG_INFO("Device Acquired"); + IMG2NUM_LOG_INFO("Device Acquired"); } else { - SPDLOG_INFO( + IMG2NUM_LOG_INFO( "Device Failed: {}", msg.data && msg.length > 0 ? std::string(msg.data, msg.length) : "Unknown error" @@ -253,18 +254,18 @@ class GPU { } if (!device) { - SPDLOG_INFO("Fatal: Could not get WebGPU Device."); + IMG2NUM_LOG_INFO("Fatal: Could not get WebGPU Device."); return; } if (!validate_device()) { - SPDLOG_INFO("Fatal: Could not get WebGPU Device."); + IMG2NUM_LOG_INFO("Fatal: Could not get WebGPU Device."); return; } queue = device.GetQueue(); gpu_initialized = true; - SPDLOG_INFO("GPU Fully Initialized."); + IMG2NUM_LOG_INFO("GPU Fully Initialized."); }; ~GPU() { diff --git a/core/include/internal/log.h b/core/include/internal/log.h new file mode 100644 index 000000000..9ced4602d --- /dev/null +++ b/core/include/internal/log.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace img2num::third_party_wrappers { + +// Lazily-created library logger. +inline spdlog::logger& logger() { + static const auto instance = [] { + auto l = spdlog::stdout_color_mt("img2num"); + // Sync runtime filtering with compile-time stripping + l->set_level(static_cast(SPDLOG_ACTIVE_LEVEL)); + return l; + }(); + return *instance; +} + +} // namespace img2num::third_party_wrappers + +// Library-scoped logging macros. Same compile-time stripping semantics as +// SPDLOG_INFO etc. (SPDLOG_LOGGER_* checks SPDLOG_ACTIVE_LEVEL identically), +// but routed through the img2num logger instead of spdlog's default logger. +#define IMG2NUM_LOG_TRACE(...) SPDLOG_LOGGER_TRACE(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_DEBUG(...) SPDLOG_LOGGER_DEBUG(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_INFO(...) SPDLOG_LOGGER_INFO(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_WARN(...) SPDLOG_LOGGER_WARN(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_ERROR(...) SPDLOG_LOGGER_ERROR(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_CRITICAL(...) SPDLOG_LOGGER_CRITICAL(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) diff --git a/core/src/internal/bilateral_filter_gpu.cpp b/core/src/internal/bilateral_filter_gpu.cpp index b81dc7b66..3ae6e7512 100644 --- a/core/src/internal/bilateral_filter_gpu.cpp +++ b/core/src/internal/bilateral_filter_gpu.cpp @@ -12,7 +12,7 @@ #include #include // for debug printing -#include +#include "internal/log.h" static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB {0}; static constexpr uint8_t COLOR_SPACE_OPTION_RGB {1}; @@ -53,7 +53,7 @@ void bilateral_filter_gpu( std::memcpy(result.data(), image, width * height * 4); // CIELAB conversion will run as shader - SPDLOG_INFO("begin wgpu portion"); + IMG2NUM_LOG_INFO("begin wgpu portion"); // 1. Create Input Texture wgpu::TextureDescriptor texDesc = {}; texDesc.size = {static_cast(width), static_cast(height), 1}; @@ -62,7 +62,7 @@ void bilateral_filter_gpu( texDesc.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst; wgpu::Texture inputTexture = GPU::getClassInstance().get_device().CreateTexture(&texDesc); - SPDLOG_INFO("upload texture"); + IMG2NUM_LOG_INFO("upload texture"); // Upload data to Input Texture wgpu::TexelCopyTextureInfo dst = {}; dst.texture = inputTexture; @@ -75,7 +75,7 @@ void bilateral_filter_gpu( &dst, image, bytesPerPixel * width * height, &layout, &texDesc.size ); - SPDLOG_INFO("create output texture"); + IMG2NUM_LOG_INFO("create output texture"); // 2. Create Output Texture (Storage) wgpu::TextureDescriptor outDesc = texDesc; outDesc.usage = wgpu::TextureUsage::StorageBinding | wgpu::TextureUsage::CopySrc; @@ -90,7 +90,7 @@ void bilateral_filter_gpu( // filtered lab wgpu::Texture texLabFiltered = GPU::getClassInstance().get_device().CreateTexture(&descLab); - SPDLOG_INFO("create buffer"); + IMG2NUM_LOG_INFO("create buffer"); // 3. Create Uniform Buffer float sr = static_cast(sigma_range); FilterParams params = {static_cast(sigma_spatial), sr, 0.0f, 0.0f}; @@ -231,7 +231,7 @@ void bilateral_filter_gpu( wgpu::CommandBuffer commands = encoder.Finish(); GPU::getClassInstance().get_queue().Submit(1, &commands); - SPDLOG_INFO("queue submit"); + IMG2NUM_LOG_INFO("queue submit"); // static volatile bool waiting = true; @@ -251,7 +251,7 @@ void bilateral_filter_gpu( readBuffer.MapAsync( wgpu::MapMode::Read, 0, bufferSize, wgpu::CallbackMode::AllowProcessEvents, [](wgpu::MapAsyncStatus status, wgpu::StringView message, void* userdata) { - SPDLOG_INFO("In callback"); + IMG2NUM_LOG_INFO("In callback"); bool* flag = static_cast(userdata); bool success = false; if (status == wgpu::MapAsyncStatus::Success) { @@ -265,7 +265,7 @@ void bilateral_filter_gpu( (void*)waiting ); - SPDLOG_INFO("waiting {}", *waiting); + IMG2NUM_LOG_INFO("waiting {}", *waiting); while (*waiting) { GPU::getClassInstance().get_instance().ProcessEvents(); @@ -273,7 +273,7 @@ void bilateral_filter_gpu( emscripten_sleep(10); #endif } - SPDLOG_INFO("done wgpu"); + IMG2NUM_LOG_INFO("done wgpu"); const uint8_t* mappedData = (const uint8_t*)readBuffer.GetConstMappedRange(0, bufferSize); // copy to cpu buffer for (size_t y = 0; y < height; ++y) { @@ -290,7 +290,7 @@ void bilateral_filter_gpu( } readBuffer.Unmap(); std::memcpy(image, result.data(), result.size()); - SPDLOG_INFO("done memcpy"); + IMG2NUM_LOG_INFO("done memcpy"); // explicit clean up diff --git a/core/src/internal/kmeans_gpu.cpp b/core/src/internal/kmeans_gpu.cpp index e55a669fb..b4223edee 100644 --- a/core/src/internal/kmeans_gpu.cpp +++ b/core/src/internal/kmeans_gpu.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include "internal/log.h" #include // Required for std::is_same_v #include @@ -487,7 +487,7 @@ void kmeans_gpu( } } - SPDLOG_INFO("starting"); + IMG2NUM_LOG_INFO("starting"); // Step 2: Initialize centroids switch (color_space) { @@ -501,7 +501,7 @@ void kmeans_gpu( } } - SPDLOG_INFO("kmeans++ init done"); + IMG2NUM_LOG_INFO("kmeans++ init done"); // Step 3: Run k-means iterations int bytesPerPixel {16}; // float pixels @@ -548,7 +548,7 @@ void kmeans_gpu( GPU::getClassInstance().get_device().CreateBuffer(&readCentroidsDesc); // This is the actual KMeans loop - SPDLOG_INFO("start iterations"); + IMG2NUM_LOG_INFO("start iterations"); wgpu::CommandEncoder encoder = GPU::getClassInstance().get_device().CreateCommandEncoder(); for (int32_t iter {0}; iter < max_iter; ++iter) { wgpu::ComputePassEncoder pass1 = encoder.BeginComputePass(); @@ -586,7 +586,7 @@ void kmeans_gpu( wgpu::CommandBuffer commands = encoder.Finish(); GPU::getClassInstance().get_queue().Submit(1, &commands); - SPDLOG_INFO("done iterations"); + IMG2NUM_LOG_INFO("done iterations"); // 4. Map Async & Wait bool* done1 = new bool(false); @@ -606,7 +606,7 @@ void kmeans_gpu( (void*)done1 ); - SPDLOG_INFO("read out"); + IMG2NUM_LOG_INFO("read out"); while (!*done1) { GPU::getClassInstance().get_instance().ProcessEvents(); @@ -615,7 +615,7 @@ void kmeans_gpu( #endif } - SPDLOG_INFO("mapping labels"); + IMG2NUM_LOG_INFO("mapping labels"); const uint8_t* mappedData = (const uint8_t*)readLabelsBuffer.GetConstMappedRange(); // ... Copy data to your C++ vector ... // Copy row by row to remove padding and put data into 'result' @@ -654,7 +654,7 @@ void kmeans_gpu( #endif } - SPDLOG_INFO("mapping centroids"); + IMG2NUM_LOG_INFO("mapping centroids"); const float* mappedDataFloat = (const float*)readCentroidsBuffer.GetConstMappedRange(); // ... Copy data to your C++ vector ... @@ -697,7 +697,7 @@ void kmeans_gpu( } // Write labels to out_labels - SPDLOG_INFO("copying labels out"); + IMG2NUM_LOG_INFO("copying labels out"); std::memcpy(out_labels, labels.data(), labels.size() * sizeof(int32_t)); if (inputTexture) diff --git a/example-apps/html-js/package.json b/example-apps/html-js/package.json index bd28ea8b5..4edff0e42 100644 --- a/example-apps/html-js/package.json +++ b/example-apps/html-js/package.json @@ -7,7 +7,8 @@ "build:deploy": "node scripts/build.mjs --deploy", "start:esm": "node scripts/build.mjs esm && npx --yes serve -l 5173 dist/esm", "start:iife": "node scripts/build.mjs iife && npx --yes serve -l 5175 dist/iife", - "start:umd": "node scripts/build.mjs umd && npx --yes serve -l 5176 dist/umd" + "start:umd": "node scripts/build.mjs umd && npx --yes serve -l 5176 dist/umd", + "start": "pnpm run \"/^start:.*/\"" }, "dependencies": { "img2num": "workspace:*" diff --git a/packages/js/package.json b/packages/js/package.json index 33c4962d0..baac16837 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -61,7 +61,6 @@ "**/build-wasm/**" ], "scripts": { - "prebuild": "just build js", "build:browser": "cross-env TARGET=browser vite build", "build:standalone": "cross-env TARGET=standalone vite build", "build:node-esm": "cross-env TARGET=node-esm vite build", From 037cc70f15e8e3601faccbfe55ee3e8ac6ca9674 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Thu, 20 Aug 2026 22:28:52 +0200 Subject: [PATCH 10/29] ci: fix inverted exit-code handling in raw debug output check ripgrep exits 1 when no matches are found and 0 on matches, so the check failed on a clean tree and passed when violations existed. Handle the three exit codes explicitly (0 = violations -> fail with an error annotation, 1 = clean -> pass, 2+ = rg error -> propagate) instead of relying on the raw exit status under bash -e. --- .github/workflows/no-raw-debug-output.yml | 29 +++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/no-raw-debug-output.yml b/.github/workflows/no-raw-debug-output.yml index 4dc147cc0..ebd3365ba 100644 --- a/.github/workflows/no-raw-debug-output.yml +++ b/.github/workflows/no-raw-debug-output.yml @@ -19,26 +19,25 @@ jobs: - name: Check for iostream usage run: | - streams=( - cin - cout - cerr - clog - wcin - wcout - wcerr - wclog - ) - + streams=(cin cout cerr clog wcin wcout wcerr wclog) + patterns=('#include\s*') - for stream in "${streams[@]}"; do patterns+=("std::$stream") done - + regex=$(IFS='|'; echo "${patterns[*]}") - + + set +e rg -e "$regex" \ --type cpp \ -g '!third_party/**' \ - -g '!example-apps/**' \ No newline at end of file + -g '!example-apps/**' + status=$? + set -e + + case $status in + 0) echo "::error::Raw debug output found (see matches above). Use IMG2NUM_LOG_* macros instead."; exit 1 ;; + 1) echo "No raw debug output found."; exit 0 ;; + *) echo "::error::ripgrep failed with exit code $status"; exit "$status" ;; + esac From 20207c309987ef0359b7a10514c2c6984b0206ee Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Thu, 20 Aug 2026 22:52:07 +0200 Subject: [PATCH 11/29] fix(py): include spdlog in sdist and collect licenses into the wheel The explicit sdist manifest introduced when trimming the Dawn tarball never covered third_party/spdlog, so wheels built from the sdist (the path CI and plain `uv build` take, unlike `uv sync` which builds from the working tree) failed at configure: core's add_subdirectory pointed at a directory absent from the archive. Generalize the third_party excludes to recursive gitwildmatch patterns (**/.git, **/build/, **/test(s)/, ...), replacing the per-path Dawn entries. This also sweeps the nested submodule gitlinks under dawn/third_party that the old excludes never caught. The vk-gl-cts rationale from PR #562 still applies; it is now covered by the **/test/ pattern. Add wheel.license-files so the wheel's dist-info carries license texts for the statically linked third-party code (spdlog, Dawn and its vendored dependencies) alongside our own. Verified by building the wheel from the sdist (uv build) and inspecting the archive for spdlog, absence of .git entries, and collected licenses. --- pyproject.toml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fad6007a8..31b15655e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,25 +55,34 @@ sdist.include = [ "bindings/py/**", "packages/py/**", "third_party/dawn/**", + "third_party/spdlog/**", ] sdist.exclude = [ # Local build output; explicit mode does not read .gitignore, so these # must be excluded here. "packages/py/build/", "packages/py/build-py/", - "third_party/dawn/build/", - # Dawn test corpora: never consumed by any build; the vk-gl-cts tree makes - # tar extraction fragile (deterministic CI unpack failure, PR #562). - "third_party/dawn/test/", - "third_party/dawn/testing/", "third_party/dawn/webgpu-cts/", # WebGPU CTS GN metadata: 43 MB test_list.txt + 8 MB cache tarball. "third_party/dawn/third_party/gn/", # Opt-in macOS toolchain dir (per its README); contains the tree's only # dangling symlink (ranlib -> libtool). "third_party/dawn/src/cmake/HermeticXcode/", - # Dangling submodule gitlink; VCS metadata does not belong in an sdist. - "third_party/dawn/.git", + "third_party/**/.git", + "third_party/**/.github/", + "third_party/**/build/", + "third_party/**/test/", + "third_party/**/tests/", + "third_party/**/testing/", + "third_party/**/bench/", + "third_party/**/example/", + "third_party/**/scripts/", + "third_party/**/logos/", +] +wheel.license-files = [ + "LICENSE", + "third_party/spdlog/LICENSE", + "third_party/dawn/**/*LICENSE*", ] # Derive version from version definition in packages/py/pyproject.toml From df2bcd6abef64978ea11c6cf103fded96b29cf9e Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Mon, 24 Aug 2026 23:02:53 +0200 Subject: [PATCH 12/29] temp --- .clang-format | 3 +- .clang-tidy | 22 ++++-- CMakeLists.txt | 4 +- Dockerfile.dev | 4 +- bindings/js/CMakeLists.txt | 1 - core/CMakeLists.txt | 90 ++++++++++++----------- docker-compose.yml | 3 +- example-apps/.clang-tidy | 4 ++ packages/js/.gitignore | 3 +- packages/js/licenses.plugin.mjs | 58 +++++++++++++++ packages/js/package.json | 3 +- packages/js/vite.config.js | 10 ++- pyproject.toml | 3 + scripts/format_cpp.py | 124 ++++++++++++++++++++++++++++++++ 14 files changed, 276 insertions(+), 56 deletions(-) create mode 100644 example-apps/.clang-tidy create mode 100644 packages/js/licenses.plugin.mjs create mode 100644 scripts/format_cpp.py diff --git a/.clang-format b/.clang-format index b207a986e..bf3cc3ae8 100644 --- a/.clang-format +++ b/.clang-format @@ -7,12 +7,11 @@ ColumnLimit: 100 TabWidth: 4 # --- Braces --- -Cpp11BracedListStyle: true -SpaceBeforeCpp11BracedList: true BreakBeforeBraces: Attach # --- Braced initializers --- Cpp11BracedListStyle: true +SpaceBeforeCpp11BracedList: true # --- Constructor initializer lists --- PackConstructorInitializers: Never diff --git a/.clang-tidy b/.clang-tidy index 853e4d9aa..7865d9a4b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,11 +1,21 @@ Checks: > -*, readability-*, - cppcoreguidelines-avoid-non-const-global-variables -WarningsAsErrors: "" + cppcoreguidelines-avoid-non-const-global-variables, + portability-restrict-system-includes, + bugprone-unsafe-functions +WarningsAsErrors: > + portability-restrict-system-includes, + bugprone-unsafe-functions HeaderFilterRegex: '(core|bindings)/.*\.(h|hpp)$' CheckOptions: - - key: readability-identifier-naming.FunctionCase - value: camelCase - -Checks: "cppcoreguidelines-avoid-non-const-global-variables" + readability-identifier-naming.FunctionCase: camelCase + portability-restrict-system-includes.Includes: >- + *,-iostream,-ostream,-istream,-cstdio,-stdio.h,-print,-syncstream + bugprone-unsafe-functions.CustomFunctions: >- + ::printf, the logging macros in core/include/internal/log.h, direct stdout output is banned; + ::fprintf, the logging macros in core/include/internal/log.h, direct stream output is banned; + ::puts, the logging macros in core/include/internal/log.h,; + ::putchar, the logging macros in core/include/internal/log.h,; + ::vprintf, the logging macros in core/include/internal/log.h,; + ::perror, the logging macros in core/include/internal/log.h, diff --git a/CMakeLists.txt b/CMakeLists.txt index e09b27acf..001c946c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ # ================================================================= # Img2Num Top-Level (Manages all libs and apps) # ================================================================= -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.25) project(Img2NumRootManager LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) @@ -10,6 +10,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # This is important for inter-OS reproducibility set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + # Reusable flags to ensure good quality code set(IMG2NUM_STRICT_CXX_FLAGS $<$,$,$,$>>:-Wpedantic -Werror=pedantic -Werror=c++20-extensions> diff --git a/Dockerfile.dev b/Dockerfile.dev index 29e8fe4b2..c333725aa 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -122,8 +122,10 @@ RUN uv venv /usr/src/app/.venv ENV VIRTUAL_ENV=/usr/src/app/.venv ENV PATH="/usr/src/app/.venv/bin:$PATH" +COPY pyproject.toml uv.lock ./ +COPY example-apps/console-py/pyproject.toml example-apps/console-py/pyproject.toml RUN --mount=type=cache,target=/root/.cache/uv \ - uv pip install scikit_build_core numpy + uv sync --locked --no-install-workspace --group dev --group lint --group build # -------------------------------------------------------------------------------------------------------------- # Node / pnpm setup diff --git a/bindings/js/CMakeLists.txt b/bindings/js/CMakeLists.txt index c40921310..0cdcb0da0 100644 --- a/bindings/js/CMakeLists.txt +++ b/bindings/js/CMakeLists.txt @@ -1,4 +1,3 @@ -cmake_minimum_required(VERSION 3.16) project(JSBindings LANGUAGES C) set(CMAKE_C_STANDARD 17) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 341da5093..dc71c9165 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -38,12 +38,8 @@ add_custom_target(Img2Num_shaders ALL add_library(Img2Num ${CORE_SRC}) -# spdlog (debug logging) -add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" - "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" - EXCLUDE_FROM_ALL) -target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) # ================================================================= +# spdlog (debug logging) # Logging level (compile-time stripping via SPDLOG_ACTIVE_LEVEL) # # AUTO (default): pick a level from the build type: @@ -52,36 +48,43 @@ target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) # RelWithDebInfo/etc -> INFO # Or force a level: -DIMG2NUM_LOG_LEVEL=TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF # ================================================================= -set(IMG2NUM_LOG_LEVEL "AUTO" CACHE STRING "Compile-time spdlog level for Img2Num") -set_property(CACHE IMG2NUM_LOG_LEVEL PROPERTY STRINGS - AUTO TRACE DEBUG INFO WARN ERROR CRITICAL OFF) - -string(TOUPPER "${IMG2NUM_LOG_LEVEL}" _IMG2NUM_LOG_LEVEL) +block() + set(CMAKE_EXPORT_COMPILE_COMMANDS OFF) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog" + "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spdlog/build/" + EXCLUDE_FROM_ALL) + target_link_libraries(Img2Num PRIVATE spdlog::spdlog_header_only) + set(IMG2NUM_LOG_LEVEL "AUTO" CACHE STRING "Compile-time spdlog level for Img2Num") + set_property(CACHE IMG2NUM_LOG_LEVEL PROPERTY STRINGS + AUTO TRACE DEBUG INFO WARN ERROR CRITICAL OFF) + + string(TOUPPER "${IMG2NUM_LOG_LEVEL}" _IMG2NUM_LOG_LEVEL) + + if(_IMG2NUM_LOG_LEVEL STREQUAL "AUTO") + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(_IMG2NUM_LOG_LEVEL "TRACE") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + set(_IMG2NUM_LOG_LEVEL "OFF") + else() + set(_IMG2NUM_LOG_LEVEL "INFO") + endif() + endif() -if(_IMG2NUM_LOG_LEVEL STREQUAL "AUTO") - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(_IMG2NUM_LOG_LEVEL "TRACE") - elseif(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") - set(_IMG2NUM_LOG_LEVEL "OFF") + # Map to spdlog's numeric level macros — these names are spdlog's, keep them. + if(_IMG2NUM_LOG_LEVEL MATCHES "^(TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF)$") + set(_IMG2NUM_SPDLOG_LEVEL "SPDLOG_LEVEL_${_IMG2NUM_LOG_LEVEL}") else() - set(_IMG2NUM_LOG_LEVEL "INFO") + message(FATAL_ERROR "IMG2NUM_LOG_LEVEL='${IMG2NUM_LOG_LEVEL}' is not one of AUTO/TRACE/DEBUG/INFO/WARN/ERROR/CRITICAL/OFF") endif() -endif() - -# Map to spdlog's numeric level macros — these names are spdlog's, keep them. -if(_IMG2NUM_LOG_LEVEL MATCHES "^(TRACE|DEBUG|INFO|WARN|ERROR|CRITICAL|OFF)$") - set(_IMG2NUM_SPDLOG_LEVEL "SPDLOG_LEVEL_${_IMG2NUM_LOG_LEVEL}") -else() - message(FATAL_ERROR "IMG2NUM_LOG_LEVEL='${IMG2NUM_LOG_LEVEL}' is not one of AUTO/TRACE/DEBUG/INFO/WARN/ERROR/CRITICAL/OFF") -endif() -message(STATUS "Img2Num log level: ${_IMG2NUM_LOG_LEVEL} (${_IMG2NUM_SPDLOG_LEVEL})") + message(STATUS "Img2Num log level: ${_IMG2NUM_LOG_LEVEL} (${_IMG2NUM_SPDLOG_LEVEL})") -target_compile_definitions(Img2Num PRIVATE - SPDLOG_ACTIVE_LEVEL=${_IMG2NUM_SPDLOG_LEVEL} -) + target_compile_definitions(Img2Num PRIVATE + SPDLOG_ACTIVE_LEVEL=${_IMG2NUM_SPDLOG_LEVEL} + ) -install(TARGETS spdlog_header_only EXPORT Img2NumTargets) + install(TARGETS spdlog_header_only EXPORT Img2NumTargets) +endblock() target_compile_options(Img2Num PRIVATE ${IMG2NUM_STRICT_CXX_FLAGS}) @@ -111,24 +114,29 @@ if (EMSCRIPTEN) target_link_options(Img2Num PRIVATE "--use-port=emdawnwebgpu" "SHELL:-s ASYNCIFY=1") else() - find_package(Dawn QUIET) - if (Dawn_FOUND) - message("Found system Dawn") - else() - message("Building Dawn from third_party") - set(DAWN_SUPPORTS_GLFW_FOR_WINDOWING OFF CACHE BOOL "Disable GLFW support for windowing" FORCE) - set(DAWN_USE_X11 OFF CACHE BOOL "Disable X11 support" FORCE) - set(DAWN_FETCH_DEPENDENCIES ON) - add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/dawn" "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/dawn/build/" EXCLUDE_FROM_ALL) - endif() + # Dawn WebGPU (prefer installed Dawn over building it — baked into Dockerfile.dev) + block(PROPAGATE Dawn_FOUND DAWN_INCLUDE_DIR) + find_package(Dawn QUIET) + if(Dawn_FOUND) + message("Found system Dawn") + else() + message("Building Dawn from third_party") + set(DAWN_SUPPORTS_GLFW_FOR_WINDOWING OFF CACHE BOOL "Disable GLFW support for windowing" FORCE) + set(DAWN_USE_X11 OFF CACHE BOOL "Disable X11 support" FORCE) + set(DAWN_FETCH_DEPENDENCIES ON) + # Keep Dawn's TUs out of compile_commands.json + set(CMAKE_EXPORT_COMPILE_COMMANDS OFF) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party/dawn" "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/dawn/build/" EXCLUDE_FROM_ALL) + endif() + endblock() # PUBLIC: only expose the directory that contains img2num.h - # PRIVATE: internal headers + # PRIVATE: internal headers and Dawn target_include_directories(Img2Num PUBLIC $ - $ PRIVATE + $ ${CMAKE_CURRENT_SOURCE_DIR}/include/internal ) diff --git a/docker-compose.yml b/docker-compose.yml index 653b6c3b1..e61aeabb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: working_dir: /usr/src/app volumes: - .:/usr/src/app:cached - - /usr/src/app/.venv # PROTECTS the container's .venv from being overwritten + - uv-venv:/usr/src/app/.venv - pnpm-store:/pnpm ports: - "3000:3000" # docs/ start @@ -26,3 +26,4 @@ services: volumes: pnpm-store: + uv-venv: diff --git a/example-apps/.clang-tidy b/example-apps/.clang-tidy new file mode 100644 index 000000000..107111472 --- /dev/null +++ b/example-apps/.clang-tidy @@ -0,0 +1,4 @@ +InheritParentConfig: true +Checks: '-portability-restrict-system-includes' +CheckOptions: + bugprone-unsafe-functions.CustomFunctions: '' diff --git a/packages/js/.gitignore b/packages/js/.gitignore index dd386dd07..26360f165 100644 --- a/packages/js/.gitignore +++ b/packages/js/.gitignore @@ -1 +1,2 @@ -build-wasm +/build-wasm +/THIRD_PARTY_LICENSES.md diff --git a/packages/js/licenses.plugin.mjs b/packages/js/licenses.plugin.mjs new file mode 100644 index 000000000..f1269699e --- /dev/null +++ b/packages/js/licenses.plugin.mjs @@ -0,0 +1,58 @@ +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = fileURLToPath(new URL(".", import.meta.url)); +const REPO_ROOT = path.join(here, "..", ".."); + +// Same roots as [tool.scikit-build].wheel.license-files in pyproject.toml: +// only deps statically compiled into shipped artifacts. stb is example-apps +// only and deliberately not listed. +const LICENSE_ROOTS = ["third_party/spdlog", "third_party/dawn"]; +const LICENSE_FILE = /^(LICENSE|NOTICE|COPYING)(\.|$)/i; +const SKIP_DIRS = new Set([".git", ".github", "build", "test", "tests", "testing", "bench", "docs", "node_modules"]); + +function findLicenseFiles(absDir, relDir, out) { + for (const entry of readdirSync(absDir, { withFileTypes: true })) { + const rel = path.posix.join(relDir, entry.name); + if (entry.isDirectory()) { + if (!SKIP_DIRS.has(entry.name)) findLicenseFiles(path.join(absDir, entry.name), rel, out); + } else if (LICENSE_FILE.test(entry.name)) { + out.push(rel); + } + } +} + +export function thirdPartyLicensesPlugin({ enabled }) { + if (!enabled) return { name: "img2num:third-party-licenses" }; + + return { + name: "img2num:third-party-licenses", + closeBundle() { + const files = []; + for (const root of LICENSE_ROOTS) { + findLicenseFiles(path.join(REPO_ROOT, root), root, files); + } + files.sort(); + + // Guard: an empty result means the submodules aren't checked out or the + // tree moved. Ship nothing rather than a hollow notices file. + if (files.length < 2) { + throw new Error(`[img2num] found only ${files.length} license file(s) under ${LICENSE_ROOTS.join(", ")} -- submodules missing?`); + } + + const sections = files.map((rel) => { + const body = readFileSync(path.join(REPO_ROOT, rel), "utf8").trim(); + return `## ${rel}\n\n\`\`\`\n${body}\n\`\`\``; + }); + + const header = + "# Third-Party Licenses\n\n" + + "License and notice files for code statically compiled into this " + + "package's artifacts, collected at build time from the paths shown. " + + "Do not edit by hand.\n"; + + writeFileSync(path.join(here, "THIRD_PARTY_LICENSES.md"), [header, ...sections].join("\n\n") + "\n"); + }, + }; +} diff --git a/packages/js/package.json b/packages/js/package.json index baac16837..2d3b8afb3 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -54,7 +54,8 @@ }, "files": [ "dist", - "CHANGELOG.md" + "CHANGELOG.md", + "THIRD_PARTY_LICENSES.md" ], "sideEffects": [ "**/*.wasm", diff --git a/packages/js/vite.config.js b/packages/js/vite.config.js index 45ef15dad..392a43626 100644 --- a/packages/js/vite.config.js +++ b/packages/js/vite.config.js @@ -3,6 +3,8 @@ import { fileURLToPath } from "node:url"; import { copyFileSync, existsSync, mkdirSync } from "node:fs"; import path from "node:path"; +import { thirdPartyLicensesPlugin } from "./licenses.plugin.mjs"; + const here = fileURLToPath(new URL(".", import.meta.url)); const TARGET = process.env.TARGET ?? "browser"; @@ -185,7 +187,13 @@ const FILE_NAMES = { }; export default defineConfig({ - plugins: [wasmUrlPlugin(), copyWasmPlugin(), cjsWebgpuGuard()], + plugins: [ + wasmUrlPlugin(), + copyWasmPlugin(), + cjsWebgpuGuard(), + // Generate once per full build; browser is the first target `pnpm build` runs. + thirdPartyLicensesPlugin({ enabled: TARGET === "browser" }), + ], build: { outDir: T.outDir ?? `dist/${TARGET}`, diff --git a/pyproject.toml b/pyproject.toml index 31b15655e..d52028c6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,5 +130,8 @@ dev = [ "docspec-python>=2.2.1,<3", "pybind11-stubgen>=2.5.5,<3", "pydoc-markdown>=4.8.2,<5", +] +lint = [ "ruff>=0.15,<0.16", + "clang-tidy==22.1.8", ] diff --git a/scripts/format_cpp.py b/scripts/format_cpp.py new file mode 100644 index 000000000..dfa736010 --- /dev/null +++ b/scripts/format_cpp.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Format (or check) all C/C++ sources with clang-format. + +Usage: + uv run scripts/format_cpp.py # format in place + uv run scripts/format_cpp.py --check # read-only check, non-zero exit on drift +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SEARCH_DIRS = ("core", "bindings", "example-apps") +SUFFIXES = {".hpp", ".cpp", ".h", ".c"} +MAX_PARALLEL = os.cpu_count() or 4 + + +class Colors: + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + CYAN = "\033[36m" + RESET = "\033[0m" + + +def log_color(message: str, color: str, *, err: bool = False) -> None: + stream = sys.stderr if err else sys.stdout + print(f"{color}{message}{Colors.RESET}", file=stream) + + +def find_files() -> list[Path]: + files: list[Path] = [] + for dir_name in SEARCH_DIRS: + base = ROOT / dir_name + if not base.is_dir(): + continue + for path in base.rglob("*"): + if ( + path.is_file() + and path.suffix in SUFFIXES + and not any( + part.startswith(".") for part in path.relative_to(ROOT).parts + ) + ): + files.append(path) + return sorted(files) + + +def run_clang_format(file: Path, *, check_only: bool) -> bool: + """Run clang-format on a single file. Returns True on success.""" + cmd = ["clang-format", "-style=file"] + cmd += ["--dry-run", "--Werror"] if check_only else ["-i"] + cmd.append(str(file)) + + result = subprocess.run(cmd, capture_output=True, text=True) + rel = file.relative_to(ROOT) + + if result.returncode != 0: + label = "Check failed" if check_only else "Formatting error" + log_color(f"{label}: {rel}", Colors.RED, err=True) + if result.stderr: + print(result.stderr, file=sys.stderr, end="") + return False + + if not check_only: + log_color(f"Formatted: {rel}", Colors.GREEN) + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="Only check formatting without modifying files", + ) + check_only = parser.parse_args().check + + if not (ROOT / ".clang-format").exists(): + log_color( + "No .clang-format file found in repo root. Default style will be used.", + Colors.YELLOW, + ) + + files = find_files() + if not files: + log_color("No C++ files found.", Colors.YELLOW) + return 0 + + log_color( + f"{'Checking' if check_only else 'Formatting'} {len(files)} C++ file(s)...", + Colors.CYAN, + ) + + with ThreadPoolExecutor(max_workers=MAX_PARALLEL) as pool: + results = list( + pool.map(lambda f: run_clang_format(f, check_only=check_only), files) + ) + + if not all(results): + log_color( + f"\nC++ {'format check' if check_only else 'formatting'} " + "complete with errors.", + Colors.RED, + err=True, + ) + return 1 + + log_color( + f"\nC++ {'format check' if check_only else 'formatting'} " + "complete successfully.", + Colors.GREEN, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 1844ba6e5209197c4be7c93d3553d28f76b6be41 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 18:36:01 +0200 Subject: [PATCH 13/29] chore: move C++ lint/format tooling to uv-managed scripts/py workspace Replace the Node-based clang-format wrapper (scripts/format-cpp.js) with Python entry points in a new img2num_dev_scripts workspace member: - scripts/py/{lint_cpp,format_cpp}.py exposed as `uv run lint-cpp` / `uv run format_cpp`, with clang-tidy/clang-format 22.1.8 pinned as package dependencies instead of a root lint group - lint_cpp resolves TUs against one or more compile databases (-p, repeatable), verifies the clang-tidy config up front, skips files absent from every database with a warning, and serializes --fix runs to avoid concurrent header rewrites - img2num_root.py locates the repo root by marker files so scripts work from any CWD Config fixes shaken out by the new entry points: - .clang-tidy: restore identifier-length ignore patterns; split FunctionCase (snake_case free functions) from MethodCase (camelCase methods) - .clang-format: Standard c++20 -> c++17 to match the core's actual language level Remaining C++ diffs are mechanical reformatting from re-running clang-format 22 (braced-init closing brace placement, include ordering, macro line-length wrapping); no functional changes. --- .clang-format | 2 +- .clang-tidy | 6 +- bindings/py/src/img2num_pybind.cpp | 6 +- core/include/internal/LABPixel.h | 6 +- core/include/internal/PixelConverters.h | 12 +- core/include/internal/RGBPixel.h | 6 +- core/include/internal/gpu.h | 8 +- core/include/internal/log.h | 20 ++- core/src/internal/graph.cpp | 6 +- core/src/internal/kmeans_gpu.cpp | 8 +- core/src/internal/labels_to_svg.cpp | 9 +- example-apps/.clang-tidy | 4 +- example-apps/console-cpp/main.cpp | 9 +- package.json | 4 +- pyproject.toml | 6 +- scripts/format-cpp.js | 85 ---------- scripts/{ => py}/format_cpp.py | 3 +- scripts/py/img2num_root.py | 24 +++ scripts/py/lint_cpp.py | 198 ++++++++++++++++++++++++ scripts/py/pyproject.toml | 23 +++ uv.lock | 66 +++++++- 21 files changed, 385 insertions(+), 126 deletions(-) delete mode 100644 scripts/format-cpp.js rename scripts/{ => py}/format_cpp.py (98%) create mode 100644 scripts/py/img2num_root.py create mode 100644 scripts/py/lint_cpp.py create mode 100644 scripts/py/pyproject.toml diff --git a/.clang-format b/.clang-format index bf3cc3ae8..904ca2915 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,5 @@ BasedOnStyle: LLVM -Standard: c++20 +Standard: c++17 # --- Basic formatting --- IndentWidth: 4 diff --git a/.clang-tidy b/.clang-tidy index 7865d9a4b..e66c907bb 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -9,7 +9,11 @@ WarningsAsErrors: > bugprone-unsafe-functions HeaderFilterRegex: '(core|bindings)/.*\.(h|hpp)$' CheckOptions: - readability-identifier-naming.FunctionCase: camelCase + readability-identifier-length.IgnoredVariableNames: "^([a-zA-Z][a-zA-Z0-9]?)$" + readability-identifier-length.IgnoredParameterNames: "^([a-zA-Z][a-zA-Z0-9]?_?)$" + readability-identifier-length.IgnoredLoopCounterNames: "^[a-z]$" + readability-identifier-naming.FunctionCase: lower_case # free functions stay snake_case + readability-identifier-naming.MethodCase: camelBack # class methods stay camelCase portability-restrict-system-includes.Includes: >- *,-iostream,-ostream,-istream,-cstdio,-stdio.h,-print,-syncstream bugprone-unsafe-functions.CustomFunctions: >- diff --git a/bindings/py/src/img2num_pybind.cpp b/bindings/py/src/img2num_pybind.cpp index af615564d..88cf6ce2f 100644 --- a/bindings/py/src/img2num_pybind.cpp +++ b/bindings/py/src/img2num_pybind.cpp @@ -242,9 +242,9 @@ PYBIND11_MODULE(_img2num, m) { const uint8_t* data_ptr {static_cast(data.request().ptr)}; const int32_t* labels_ptr {static_cast(labels.request().ptr)}; - std::string svg {img2num::labels_to_svg( - data_ptr, labels_ptr, width, height, min_area, min_thickness - )}; + std::string svg { + img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area, min_thickness) + }; pybind11::str svg_py_str(std::move(svg)); return pybind11::str(std::move(svg)); diff --git a/core/include/internal/LABPixel.h b/core/include/internal/LABPixel.h index 4ae54f711..6d6cb6352 100644 --- a/core/include/internal/LABPixel.h +++ b/core/include/internal/LABPixel.h @@ -42,9 +42,11 @@ template struct LABPixel : public Pixel { static inline float colorDistance(const LABPixel& a, const LABPixel& b) { LABPixel af { - static_cast(a.l), static_cast(a.a), static_cast(a.b)}; + static_cast(a.l), static_cast(a.a), static_cast(a.b) + }; LABPixel bf { - static_cast(b.l), static_cast(b.a), static_cast(b.b)}; + static_cast(b.l), static_cast(b.a), static_cast(b.b) + }; return std::sqrt( (a.l - b.l) * (a.l - b.l) + (a.a - b.a) * (a.a - b.a) + (a.b - b.b) * (a.b - b.b) ); diff --git a/core/include/internal/PixelConverters.h b/core/include/internal/PixelConverters.h index ff80b9db6..218f5cf6d 100644 --- a/core/include/internal/PixelConverters.h +++ b/core/include/internal/PixelConverters.h @@ -9,21 +9,25 @@ namespace ImageLib { template inline RGBPixel convertRGB(const uint8_t* p) { return RGBPixel { - static_cast(p[0]), static_cast(p[1]), static_cast(p[2])}; + static_cast(p[0]), static_cast(p[1]), static_cast(p[2]) + }; } template inline RGBAPixel convertRGBA(const uint8_t* p) { return RGBAPixel { static_cast(p[0]), static_cast(p[1]), static_cast(p[2]), - static_cast(p[3])}; + static_cast(p[3]) + }; } template inline const PixelConverter (*)(const uint8_t*)> RGB_CONVERTER { - convertRGB, 3}; // 3 bytes per pixel + convertRGB, 3 +}; // 3 bytes per pixel template inline const PixelConverter (*)(const uint8_t*)> RGBA_CONVERTER { - convertRGBA, 4}; // 4 bytes per pixel + convertRGBA, 4 +}; // 4 bytes per pixel } // namespace ImageLib diff --git a/core/include/internal/RGBPixel.h b/core/include/internal/RGBPixel.h index 4be5e6f22..98ec2b535 100644 --- a/core/include/internal/RGBPixel.h +++ b/core/include/internal/RGBPixel.h @@ -36,9 +36,11 @@ template struct RGBPixel : public Pixel { static inline float colorDistance(const RGBPixel& a, const RGBPixel& b) { RGBPixel af { - static_cast(a.red), static_cast(a.green), static_cast(a.blue)}; + static_cast(a.red), static_cast(a.green), static_cast(a.blue) + }; RGBPixel bf { - static_cast(b.red), static_cast(b.green), static_cast(b.blue)}; + static_cast(b.red), static_cast(b.green), static_cast(b.blue) + }; return std::sqrt( (af.red - bf.red) * (af.red - bf.red) + (af.green - bf.green) * (af.green - bf.green) + (af.blue - bf.blue) * (af.blue - bf.blue) diff --git a/core/include/internal/gpu.h b/core/include/internal/gpu.h index 427ee4fcc..ebbbf8905 100644 --- a/core/include/internal/gpu.h +++ b/core/include/internal/gpu.h @@ -14,10 +14,10 @@ #include // auto generated by tools/embed_shaders.py -#include - #include "internal/log.h" +#include + class GPU { private: wgpu::Instance instance; @@ -206,7 +206,9 @@ class GPU { if (reason == wgpu::DeviceLostReason::CallbackCancelled) { return; } - IMG2NUM_LOG_INFO("[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg); + IMG2NUM_LOG_INFO( + "[DEVICE LOST] Reason: {} Msg: {}", static_cast(reason), err_msg + ); } ); diff --git a/core/include/internal/log.h b/core/include/internal/log.h index 9ced4602d..03c27266a 100644 --- a/core/include/internal/log.h +++ b/core/include/internal/log.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include namespace img2num::third_party_wrappers { @@ -21,9 +21,15 @@ inline spdlog::logger& logger() { // Library-scoped logging macros. Same compile-time stripping semantics as // SPDLOG_INFO etc. (SPDLOG_LOGGER_* checks SPDLOG_ACTIVE_LEVEL identically), // but routed through the img2num logger instead of spdlog's default logger. -#define IMG2NUM_LOG_TRACE(...) SPDLOG_LOGGER_TRACE(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) -#define IMG2NUM_LOG_DEBUG(...) SPDLOG_LOGGER_DEBUG(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) -#define IMG2NUM_LOG_INFO(...) SPDLOG_LOGGER_INFO(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) -#define IMG2NUM_LOG_WARN(...) SPDLOG_LOGGER_WARN(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) -#define IMG2NUM_LOG_ERROR(...) SPDLOG_LOGGER_ERROR(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) -#define IMG2NUM_LOG_CRITICAL(...) SPDLOG_LOGGER_CRITICAL(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_TRACE(...) \ + SPDLOG_LOGGER_TRACE(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_DEBUG(...) \ + SPDLOG_LOGGER_DEBUG(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_INFO(...) \ + SPDLOG_LOGGER_INFO(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_WARN(...) \ + SPDLOG_LOGGER_WARN(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_ERROR(...) \ + SPDLOG_LOGGER_ERROR(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) +#define IMG2NUM_LOG_CRITICAL(...) \ + SPDLOG_LOGGER_CRITICAL(&::img2num::third_party_wrappers::logger(), __VA_ARGS__) diff --git a/core/src/internal/graph.cpp b/core/src/internal/graph.cpp index b78031686..3dee3c56e 100644 --- a/core/src/internal/graph.cpp +++ b/core/src/internal/graph.cpp @@ -18,9 +18,11 @@ static inline float colorDistance(const ImageLib::RGBPixel& a, const ImageLib::RGBPixel& b) { ImageLib::RGBPixel af { - static_cast(a.red), static_cast(a.green), static_cast(a.blue)}; + static_cast(a.red), static_cast(a.green), static_cast(a.blue) + }; ImageLib::RGBPixel bf { - static_cast(b.red), static_cast(b.green), static_cast(b.blue)}; + static_cast(b.red), static_cast(b.green), static_cast(b.blue) + }; return std::sqrt( (af.red - bf.red) * (af.red - bf.red) + (af.green - bf.green) * (af.green - bf.green) + (af.blue - bf.blue) * (af.blue - bf.blue) diff --git a/core/src/internal/kmeans_gpu.cpp b/core/src/internal/kmeans_gpu.cpp index b4223edee..f388f7dc7 100644 --- a/core/src/internal/kmeans_gpu.cpp +++ b/core/src/internal/kmeans_gpu.cpp @@ -6,6 +6,7 @@ #include "internal/gpu.h" #include "internal/Image.h" #include "internal/LABAPixel.h" +#include "internal/log.h" #include "internal/PixelConverters.h" #include "internal/RGBAPixel.h" @@ -20,7 +21,6 @@ #include #include #include -#include "internal/log.h" #include // Required for std::is_same_v #include @@ -197,11 +197,13 @@ void kMeansPlusPlusInitGpu( CentroidParams params; if constexpr (std::is_same_v>) { params = CentroidParams { - c.l / 255.0f, c.a / 255.0f, c.b / 255.0f, 1.0f, static_cast(width)}; + c.l / 255.0f, c.a / 255.0f, c.b / 255.0f, 1.0f, static_cast(width) + }; } else { params = CentroidParams { c.red / 255.0f, c.green / 255.0f, c.blue / 255.0f, 1.0f, - static_cast(width)}; + static_cast(width) + }; } GPU::getClassInstance().get_queue().WriteBuffer( diff --git a/core/src/internal/labels_to_svg.cpp b/core/src/internal/labels_to_svg.cpp index a618da3f1..59280830d 100644 --- a/core/src/internal/labels_to_svg.cpp +++ b/core/src/internal/labels_to_svg.cpp @@ -35,7 +35,8 @@ int flood_fill( RGBXY pix = RGBXY { color_array[4 * size_t(index(x, y))], color_array[4 * size_t(index(x, y)) + 1], - color_array[4 * size_t(index(x, y)) + 2], x, y}; + color_array[4 * size_t(index(x, y)) + 2], x, y + }; queue.push({x, y}); @@ -58,7 +59,8 @@ int flood_fill( RGBXY pix1 = RGBXY { color_array[4 * size_t(index(x1, y1))], color_array[4 * size_t(index(x1, y1)) + 1], - color_array[4 * size_t(index(x1, y1)) + 2], x1, y1}; + color_array[4 * size_t(index(x1, y1)) + 2], x1, y1 + }; region_array[size_t(index(x1, y1))] = label_value; out_pixels->push_back(pix1); count++; @@ -115,7 +117,8 @@ void visualize_contours( for (const auto& c : contours) { ImageLib::RGBAPixel rand_color { static_cast(dist(rng)), static_cast(dist(rng)), - static_cast(dist(rng)), 255}; + static_cast(dist(rng)), 255 + }; for (const auto& p : c) { int32_t _x {static_cast(p.x) + xmin}; diff --git a/example-apps/.clang-tidy b/example-apps/.clang-tidy index 107111472..63c56e16d 100644 --- a/example-apps/.clang-tidy +++ b/example-apps/.clang-tidy @@ -1,4 +1,4 @@ InheritParentConfig: true -Checks: '-portability-restrict-system-includes' +Checks: "-portability-restrict-system-includes" CheckOptions: - bugprone-unsafe-functions.CustomFunctions: '' + bugprone-unsafe-functions.CustomFunctions: "" diff --git a/example-apps/console-cpp/main.cpp b/example-apps/console-cpp/main.cpp index 0029f24fb..a0c9b6e9a 100644 --- a/example-apps/console-cpp/main.cpp +++ b/example-apps/console-cpp/main.cpp @@ -31,7 +31,8 @@ int main(int argc, char** argv) { int width {0}, height {0}, channels {0}; // Force load as grayscale uint8_t* image_data_original { - stbi_load(image_path.c_str(), &width, &height, &channels, NUM_CHANNELS)}; + stbi_load(image_path.c_str(), &width, &height, &channels, NUM_CHANNELS) + }; if (!image_data_original) { std::cerr << "Failed to load image: " << stbi_failure_reason() << std::endl; return 1; @@ -83,13 +84,15 @@ int main(int argc, char** argv) { out_path.c_str(), width, height, NUM_CHANNELS, img_data, width * NUM_CHANNELS ) == 1 ? true - : false}; + : false + }; const bool kmeans_save_success { stbi_write_png( kmeans_path.c_str(), width, height, NUM_CHANNELS, out_data, width * NUM_CHANNELS ) == 1 ? true - : false}; + : false + }; std::ofstream svgFile(svg_path); if (!svgFile.is_open()) { diff --git a/package.json b/package.json index 15243392e..1e6c92b44 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "scripts": { "help": "node scripts/help.js", "format": "node scripts/format.js", - "format:check": "node scripts/format.js --check", - "format:cpp": "node scripts/format-cpp.js", + "format:check": "echo 'No longer supported in Img2Num. Use `pnpm run lint` instead.'", + "format:cpp": "uv run format_cpp", "format:cpp:check": "node scripts/format-cpp.js", "format:js": "pnpm prettier --write . --ignore-path .prettierignore --config .prettierrc", "format:js:check": "pnpm prettier --check . --ignore-path .prettierignore --config .prettierrc", diff --git a/pyproject.toml b/pyproject.toml index d52028c6c..dbcfce108 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,10 +97,14 @@ package = true [tool.uv.workspace] members = [ + "scripts/py", # All example apps with "-py" suffix "example-apps/*-py" ] +[tool.uv.sources] +img2num-dev-scripts = { workspace = true } + [tool.ruff] target-version = "py310" line-length = 88 @@ -126,6 +130,7 @@ build-verbosity = 1 [dependency-groups] dev = [ + "img2num_dev_scripts", "docspec>=2.2.1,<3", "docspec-python>=2.2.1,<3", "pybind11-stubgen>=2.5.5,<3", @@ -133,5 +138,4 @@ dev = [ ] lint = [ "ruff>=0.15,<0.16", - "clang-tidy==22.1.8", ] diff --git a/scripts/format-cpp.js b/scripts/format-cpp.js deleted file mode 100644 index b1005fb6a..000000000 --- a/scripts/format-cpp.js +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env node -import { execSync } from "child_process"; -import fg from "fast-glob"; -import { cpus } from "os"; -import { logColor, Colors } from "img2num-dev-scripts"; -import fs from "fs"; - -if (!fs.existsSync(".clang-format")) { - logColor( - "No .clang-format file found in repo root. Default style will be used.", - Colors.YELLOW - ); -} - -// --- CONFIG --- -const RECURSE_C_OR_CPP_FILES = "**/*.{hpp,cpp,h,c}"; -const GLOBS = [ - `core/${RECURSE_C_OR_CPP_FILES}`, - `bindings/${RECURSE_C_OR_CPP_FILES}`, - `example-apps/${RECURSE_C_OR_CPP_FILES}` -]; -const MAX_PARALLEL = cpus().length; - -// --- FIND FILES --- -const files = GLOBS.flatMap((pattern) => fg.sync(pattern, { dot: false })); - -if (!files.length) { - logColor("No C++ files found.", Colors.YELLOW); - process.exit(0); -} - -// --- PARSE ARGS --- -const checkOnly = process.argv.includes("--check"); -const cmdBase = (checkOnly - ? "clang-format --dry-run --Werror" - : "clang-format -i") + " -style=file"; - -logColor( - `${checkOnly ? "Checking" : "Formatting"} ${files.length} C++ file(s)...`, - Colors.CYAN -); - -// --- FORMAT FILES --- -let failed = false; - -const runCommand = (file) => { - try { - execSync(`pnpm exec ${cmdBase} "${file}"`, { stdio: "inherit" }); - if (!checkOnly) logColor(`Formatted: ${file}`, Colors.GREEN); - } catch { - logColor( - `${checkOnly ? "Check failed" : "Formatting error"}: ${file}`, - Colors.RED - ); - failed = true; - } -}; - -// --- PARALLEL EXECUTION --- -const chunkArray = (arr, chunkSize) => { - const chunks = []; - for (let i = 0; i < arr.length; i += chunkSize) { - chunks.push(arr.slice(i, i + chunkSize)); - } - return chunks; -}; - -const chunks = chunkArray(files, MAX_PARALLEL); -chunks.forEach((chunk) => { - chunk.forEach(runCommand); -}); - -// --- SUMMARY --- -if (failed) { - logColor( - `\nC++ ${checkOnly ? "format check" : "formatting"} complete with errors.`, - Colors.RED - ); - process.exit(1); -} else { - logColor( - `\nC++ ${checkOnly ? "format check" : "formatting"} complete successfully.`, - Colors.GREEN - ); -} diff --git a/scripts/format_cpp.py b/scripts/py/format_cpp.py similarity index 98% rename from scripts/format_cpp.py rename to scripts/py/format_cpp.py index dfa736010..9e09f3ff9 100644 --- a/scripts/format_cpp.py +++ b/scripts/py/format_cpp.py @@ -15,7 +15,8 @@ from concurrent.futures import ThreadPoolExecutor from pathlib import Path -ROOT = Path(__file__).resolve().parent.parent +from img2num_root import IMG2NUM_ROOT as ROOT + SEARCH_DIRS = ("core", "bindings", "example-apps") SUFFIXES = {".hpp", ".cpp", ".h", ".c"} MAX_PARALLEL = os.cpu_count() or 4 diff --git a/scripts/py/img2num_root.py b/scripts/py/img2num_root.py new file mode 100644 index 000000000..4f54ea031 --- /dev/null +++ b/scripts/py/img2num_root.py @@ -0,0 +1,24 @@ +from pathlib import Path + +_EXPECTED_AT_ROOT = ( + ".git", + "core", + "bindings", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "README.md", + "LICENSE", +) + + +def _find_root() -> Path: + for parent in Path(__file__).resolve().parents: + if all((parent / name).exists() for name in _EXPECTED_AT_ROOT): + return parent + raise RuntimeError( + "Could not locate the Img2Num repository root " + f"(searched upward from {Path(__file__).resolve()})" + ) + + +IMG2NUM_ROOT = _find_root() diff --git a/scripts/py/lint_cpp.py b/scripts/py/lint_cpp.py new file mode 100644 index 000000000..d84e8067d --- /dev/null +++ b/scripts/py/lint_cpp.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Lint all C/C++ translation units with clang-tidy. + +Requires one or more compile databases (compile_commands.json), generated by +configuring CMake. Every TU under SEARCH_DIRS must appear in at least one +database for full coverage, e.g.: + + cmake -S . -B build # core + C bindings + cmake -S . -B build/py -DIMG2NUM_BUILD_PYTHON=ON # Python bindings + emcmake cmake -S . -B build-wasm # WASM wrapper + +Usage: + uv run lint-cpp # lint using ./build only + uv run lint-cpp -p build -p build/py # union of several databases + uv run lint-cpp --fix # apply suggested fixes +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +from img2num_root import IMG2NUM_ROOT as ROOT + +SEARCH_DIRS = ("core", "bindings") +SUFFIXES = {".cpp", ".c"} # translation units only; headers surface via HeaderFilterRegex +MAX_PARALLEL = os.cpu_count() or 4 + + +class Colors: + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + CYAN = "\033[36m" + RESET = "\033[0m" + + +def log_color(message: str, color: str, *, err: bool = False) -> None: + stream = sys.stderr if err else sys.stdout + print(f"{color}{message}{Colors.RESET}", file=stream) + + +def load_databases(build_dirs: list[Path]) -> dict[Path, Path]: + """Map each TU in any compile database to the first build dir containing it.""" + file_to_build_dir: dict[Path, Path] = {} + for build_dir in build_dirs: + db_path = build_dir / "compile_commands.json" + with db_path.open() as f: + for entry in json.load(f): + file_to_build_dir.setdefault(Path(entry["file"]).resolve(), build_dir) + return file_to_build_dir + + +def find_files(file_to_build_dir: dict[Path, Path]) -> list[tuple[Path, Path]]: + """All TUs under SEARCH_DIRS, paired with the build dir that can lint them. + + Files present in no database (e.g. excluded by every provided CMake + configuration) would make clang-tidy error out, so they are skipped + with a warning naming each file. + """ + candidates: list[Path] = [] + for dir_name in SEARCH_DIRS: + base = ROOT / dir_name + if not base.is_dir(): + continue + for path in base.rglob("*"): + if ( + path.is_file() + and path.suffix in SUFFIXES + and not any( + part.startswith(".") for part in path.relative_to(ROOT).parts + ) + ): + candidates.append(path) + + matched = [ + (p, file_to_build_dir[p.resolve()]) + for p in candidates + if p.resolve() in file_to_build_dir + ] + skipped = [p for p in candidates if p.resolve() not in file_to_build_dir] + if skipped: + log_color( + f"Skipping {len(skipped)} file(s) not present in any compile database " + "(not built under the provided CMake configurations):", + Colors.YELLOW, + err=True, + ) + for p in sorted(skipped): + log_color(f" {p.relative_to(ROOT)}", Colors.YELLOW, err=True) + return sorted(matched) + + +def run_clang_tidy(file: Path, *, build_dir: Path, fix: bool) -> bool: + cmd = ["clang-tidy", "-p", str(build_dir), "--quiet"] + if fix: + cmd.append("--fix") + cmd.append(str(file)) + + result = subprocess.run(cmd, capture_output=True, text=True) + rel = file.relative_to(ROOT) + + if result.returncode != 0: + log_color(f"Lint failed: {rel}", Colors.RED, err=True) + if result.stdout and not result.stdout.lstrip().startswith("USAGE:"): + print(result.stdout, end="") + if result.stderr: + print(result.stderr, file=sys.stderr, end="") + return False + + log_color(f"OK: {rel}", Colors.GREEN) + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "-p", + "--build-dir", + dest="build_dirs", + type=Path, + action="append", + help=( + "Build directory containing compile_commands.json. " + "Repeatable; files are linted against the first database " + "that contains them (default: build)" + ), + ) + parser.add_argument( + "--fix", + action="store_true", + help="Apply clang-tidy's suggested fixes in place", + ) + args = parser.parse_args() + build_dirs: list[Path] = args.build_dirs or [ROOT / "build"] + + missing = [d for d in build_dirs if not (d / "compile_commands.json").exists()] + if missing: + for d in missing: + log_color( + f"No compile database at {d / 'compile_commands.json'}.\n" + "Configure CMake first (the project exports " + "compile_commands.json automatically):\n" + f" cmake -S . -B {d}", + Colors.RED, + err=True, + ) + return 2 + + verify = subprocess.run( + ["clang-tidy", "--verify-config"], + capture_output=True, + text=True, + cwd=ROOT, + ) + if verify.returncode != 0: + log_color("Invalid clang-tidy configuration:", Colors.RED, err=True) + print(verify.stderr or verify.stdout, file=sys.stderr, end="") + return 2 + + files = find_files(load_databases(build_dirs)) + if not files: + log_color( + "No C++ translation units found in the compile database(s).", + Colors.YELLOW, + ) + return 0 + + log_color(f"Linting {len(files)} C++ file(s)...", Colors.CYAN) + + # --fix must not run concurrently: two TUs including the same header can + # both rewrite it and corrupt the file. + workers = 1 if args.fix else MAX_PARALLEL + with ThreadPoolExecutor(max_workers=workers) as pool: + results = list( + pool.map( + lambda pair: run_clang_tidy( + pair[0], build_dir=pair[1], fix=args.fix + ), + files, + ) + ) + + if not all(results): + log_color("\nC++ lint complete with errors.", Colors.RED, err=True) + return 1 + + log_color("\nC++ lint complete successfully.", Colors.GREEN) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/py/pyproject.toml b/scripts/py/pyproject.toml new file mode 100644 index 000000000..e2987b6b3 --- /dev/null +++ b/scripts/py/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "img2num_dev_scripts" +version = "0.0.0" +requires-python = ">=3.10" +dependencies = [ + "clang-tidy==22.1.8", + "clang-format==22.1.8", +] + +[project.scripts] +lint_cpp = "lint_cpp:main" +format_cpp = "format_cpp:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +only-include = [ + "lint_cpp.py", + "format_cpp.py", + "img2num_root.py", +] diff --git a/uv.lock b/uv.lock index 65c634eae..5e8416469 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,7 @@ requires-python = ">=3.10" members = [ "console-py", "img2num", + "img2num-dev-scripts", ] [[package]] @@ -133,6 +134,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538 }, ] +[[package]] +name = "clang-format" +version = "22.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b48aba45ba2638a706df1680e3dcdf96f94aed8e884886192d411d7a0071/clang_format-22.1.8.tar.gz", hash = "sha256:61a23f4fc0ad1932e1b0300ca451108bf1e751eb89f478062d87f888db9190e6", size = 11508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/d8/29b9db6098da1a011ca3f7560c3942fa81404dbbb4367c3bd1d5c435da3b/clang_format-22.1.8-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fc2ac5bd0ea41af49968fb69426207806d5f7016cb8f4bfbd44f4f1ffe8d53f2", size = 1492639 }, + { url = "https://files.pythonhosted.org/packages/2e/55/539cc1036dae16659f50500ca34838cc5b16cd3e98e3faaf164186b98093/clang_format-22.1.8-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d1147107222c0dda3e4869e9e8c4a79f9ed1de83819e5274de42b82adf3d2129", size = 1482323 }, + { url = "https://files.pythonhosted.org/packages/50/25/a9734da014eecc1f54c051ad643a28f2f6643dcc812ac59320e80e2b1a3b/clang_format-22.1.8-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48c3b8dcfe9d4e964ced0e744e0f1f8ddc711bce92e50f6cab21e10f54857d08", size = 1757825 }, + { url = "https://files.pythonhosted.org/packages/5d/19/76bf4dfba7d418f3da4fe89ace66856abd79c8c314e750d5f0fb754de6f5/clang_format-22.1.8-py2.py3-none-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:07312f8a74bda89b6ce32fae46c589cd7bb210a5d3fb2829a70e28ff449f5b80", size = 1888650 }, + { url = "https://files.pythonhosted.org/packages/5c/b0/25fb71006b581c4e1dc4680b61c61842836dd1adb860e02f898deed8a919/clang_format-22.1.8-py2.py3-none-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6d7382dd728431c0cecf3c7db6ac0904e162c7c18889696ab8dc0630ff9349b", size = 2070288 }, + { url = "https://files.pythonhosted.org/packages/36/94/f7c185f2f7c9cbd60878a260a2a03cc4eef0b9b9fbe8eb5045fa9b5ccbe9/clang_format-22.1.8-py2.py3-none-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7763ce0f45b5ff0b5ca7a830d6061ce431dcb644211f9b2bb483bfa66d6c78ca", size = 2101885 }, + { url = "https://files.pythonhosted.org/packages/e5/88/b82c066fa807da4ca2518fecf79071361f6324b77375e5e92c059c0697fd/clang_format-22.1.8-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b00cff6bfd1f1686f073a4fdf1cb937dbd58bf7510c659477805c03afdea0816", size = 1841177 }, + { url = "https://files.pythonhosted.org/packages/03/7c/996fc84930d96db84418fbd16d3935ba77f42f9078d7610e4eae3e0e9294/clang_format-22.1.8-py2.py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:396f66b2237131ae7c5c9a9c34d50b1b9bee60ec6754e8b0dcb943850c747aa6", size = 1684807 }, + { url = "https://files.pythonhosted.org/packages/1b/42/423de2ddedd3e068f7b773d7a4a593266692bc6108a2dd8ad4403d2ae718/clang_format-22.1.8-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02ff8ad2e6a60554cc6b9b34310f71adb04da39e94302e08e3a655b77e4dd31c", size = 2736272 }, + { url = "https://files.pythonhosted.org/packages/c1/6b/fbb98122d35333012d47186d669c1bb9a1cb527faa9a4a12c43124662f8f/clang_format-22.1.8-py2.py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8fc6139349a64f82d24396c815f5584f56da8912482dceadd9e5d7a3f8c17d6c", size = 2515477 }, + { url = "https://files.pythonhosted.org/packages/40/99/a5e00402fe8281754df679a1028132a28ea0c0024d363509d9c29944ab85/clang_format-22.1.8-py2.py3-none-musllinux_1_2_i686.whl", hash = "sha256:65df71f1eab12de161b9059483be55cb72490ec7fc7ca4915b5de49722a76a46", size = 2986602 }, + { url = "https://files.pythonhosted.org/packages/ac/f7/4a02e8f7d54f71c7ee4ce48c35cbe8f2be1afb1fbbca543f8db43f56a959/clang_format-22.1.8-py2.py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:0223471f781647d476ce0e09a0512521043343313e42932d900f97a15eacb510", size = 3120341 }, + { url = "https://files.pythonhosted.org/packages/84/05/6bd5e7bea1679dbed37a94a3b47d2ef0110792f64fd5761b2301d5b34fd2/clang_format-22.1.8-py2.py3-none-musllinux_1_2_s390x.whl", hash = "sha256:41e00922840376f8d1239db8fb5bc6b1d313bd09752344a85b0f8071112e5c7e", size = 3217448 }, + { url = "https://files.pythonhosted.org/packages/56/e3/3fcba146f12b9fbad24034136718d113dfacac03b1cf8273b2aa7bd641b0/clang_format-22.1.8-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:734d22be5c9d3a72a841444817aae8168c8f4bdccb08de491f05673e42ec7304", size = 2848689 }, + { url = "https://files.pythonhosted.org/packages/2e/2b/5a7f2fba71179331b51bb547a02808a56aa515f8637adf035fd34c1d3b0b/clang_format-22.1.8-py2.py3-none-win32.whl", hash = "sha256:a796192453ae56c61e975fc56ee7defb4187013fc3de798cbe608cfe326762aa", size = 1298699 }, + { url = "https://files.pythonhosted.org/packages/08/60/c6783b3190a8f741107a44912a11c39c1a51e254e86a4c43cb0151cea0dd/clang_format-22.1.8-py2.py3-none-win_amd64.whl", hash = "sha256:5fe6ad3e9399d589aff5ead432568a84cdcbbd621f1708340819efd74cbf8176", size = 1465069 }, + { url = "https://files.pythonhosted.org/packages/43/ca/7e1fa4a6044c37c37356bb18fc938d2811754231e448aaffc192dc3774ce/clang_format-22.1.8-py2.py3-none-win_arm64.whl", hash = "sha256:1fac18f32426c6fd7acde7087511bd80e2c549b2cd7477099582c216ae82fa63", size = 1344986 }, +] + +[[package]] +name = "clang-tidy" +version = "22.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d0/f9dc63658854dc4508270daab0e10a34a61330d35c53cfe679b692e57560/clang_tidy-22.1.8.tar.gz", hash = "sha256:d81205d15cc82eea10e9e3b65e99732bb0bc77144535648a4a1f77d9cb4eda50", size = 11746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/14/27f6ed1ffdf3fcc53d4482a5f1292b39751c7426ca70deca8bb7d04f5dae/clang_tidy-22.1.8-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:04b6c97bcdc032763d0d924531def281acd0b5a939bcc79994cbf3c6fe83e1ae", size = 31672907 }, + { url = "https://files.pythonhosted.org/packages/01/23/949977d4304e30eeef80206b37a9305084926ea06b99708961fcbf27913f/clang_tidy-22.1.8-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:916d7cbc9590e719459738fedc5ba76ac91f128d671390e574a0779014c84d73", size = 30823043 }, + { url = "https://files.pythonhosted.org/packages/ac/b7/61ed8c319f2d9ddb9762a550a6fee434bdef7bdc46d5a4b50929f1c90ec0/clang_tidy-22.1.8-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1eaddaa7415e8c5e39aeefbfd15174f1ab2a671c86f7ebb200eb523cf9465559", size = 42155015 }, + { url = "https://files.pythonhosted.org/packages/82/19/0f2668f8f5e2452b096a2b898f2b6bcecbceb6dd0c7f75d1755ce1f18d8b/clang_tidy-22.1.8-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a3de07ba82d4403d8b692ae63a5520d4db5c606014c92c24bbcef9259057bf1", size = 44070377 }, + { url = "https://files.pythonhosted.org/packages/6c/61/7c0c87952090de362d18703a8a2206ae30129c811c66f9d01f4094d59e87/clang_tidy-22.1.8-py2.py3-none-manylinux_2_28_i686.whl", hash = "sha256:879a53cc5ba9824197f97e83bd86686f9c159da18afd004e8f6d161c985f9b31", size = 49709888 }, + { url = "https://files.pythonhosted.org/packages/87/78/cbe472f8a2f29df7c982f1a3782f515059f3e1488c668c946ac04df79dab/clang_tidy-22.1.8-py2.py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:79d109db0bc74c4f20253ea997f73eac4fa6ec26171fa08c05fb82dd3ee94bc1", size = 40445704 }, + { url = "https://files.pythonhosted.org/packages/e2/e0/be72d44c059a936af7d4b355d0c844e934d7f4c3c4eba578783d7fc486d2/clang_tidy-22.1.8-py2.py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:788200119549b80e3b3cc4c1ecc422b6bec6e2559ea2116494fe6c72092ec4c2", size = 41319020 }, + { url = "https://files.pythonhosted.org/packages/97/34/223288ebf27f33b3842ab750ab5915ef3244f7802d4f7633dacf7a195ab2/clang_tidy-22.1.8-py2.py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ad32751aa4697575fb34faabe70ee497aa32d31d4dfffe25f2655797dff1c2c", size = 51914038 }, + { url = "https://files.pythonhosted.org/packages/8d/d1/a8019bfe6dcb103a081ffc0a869cb2b463a826e5005030d0abf211278b53/clang_tidy-22.1.8-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19d11334f5f9af90a655e5ca46b1e472d44ec025a8d10fef95accb3ea19b8c60", size = 46732713 }, + { url = "https://files.pythonhosted.org/packages/c1/ad/6bb2c2a0a7255ca624dca7ee172678b215c62ce79891a487f8ccc90bc48b/clang_tidy-22.1.8-py2.py3-none-win32.whl", hash = "sha256:182dc0b73688add32cd02595d510c0b010f77a645774dbb04e296d347c1a014f", size = 23553468 }, + { url = "https://files.pythonhosted.org/packages/54/af/0580f6145d8a0c218844208a6c90ce539a4bdf65a66b672e6b4604a81c0a/clang_tidy-22.1.8-py2.py3-none-win_amd64.whl", hash = "sha256:df9bf841ecbf501d08b6fa34523be840f59b1a76d16b6ee5edb0e5c7c22b59c2", size = 26666952 }, +] + [[package]] name = "click" version = "8.4.2" @@ -276,8 +321,11 @@ dependencies = [ dev = [ { name = "docspec" }, { name = "docspec-python" }, + { name = "img2num-dev-scripts" }, { name = "pybind11-stubgen" }, { name = "pydoc-markdown" }, +] +lint = [ { name = "ruff" }, ] @@ -288,9 +336,25 @@ requires-dist = [{ name = "numpy", specifier = ">=1.23.5" }] dev = [ { name = "docspec", specifier = ">=2.2.1,<3" }, { name = "docspec-python", specifier = ">=2.2.1,<3" }, + { name = "img2num-dev-scripts", editable = "scripts/py" }, { name = "pybind11-stubgen", specifier = ">=2.5.5,<3" }, { name = "pydoc-markdown", specifier = ">=4.8.2,<5" }, - { name = "ruff", specifier = ">=0.15,<0.16" }, +] +lint = [{ name = "ruff", specifier = ">=0.15,<0.16" }] + +[[package]] +name = "img2num-dev-scripts" +version = "0.0.0" +source = { editable = "scripts/py" } +dependencies = [ + { name = "clang-format" }, + { name = "clang-tidy" }, +] + +[package.metadata] +requires-dist = [ + { name = "clang-format", specifier = "==22.1.8" }, + { name = "clang-tidy", specifier = "==22.1.8" }, ] [[package]] From 906a509f90289555cbf0cafa61b334f553d2a7a6 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 19:36:06 +0200 Subject: [PATCH 14/29] chore: unify lint/format scripts under lint:* and format:* namespaces - Rename eslint/eslint:fix to lint:js/lint:js:fix - Add lint, lint:fix, lint:cpp, lint:cpp:fix meta-scripts using pnpm parallel regex dispatch (scoped to root via --filter=.) - Restore format:check as parallel dispatch of cpp/js check scripts; format_cpp now takes --fix for writes, default is check-only - Update CI to use lint:js (lint:cpp pending initial C++ format commit) - Remove no-raw-debug-output workflow (superseded by clang-tidy via lint:cpp) --- .github/workflows/ci.yml | 6 +++- .github/workflows/no-raw-debug-output.yml | 43 ----------------------- package.json | 30 ++++++++++++---- 3 files changed, 28 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/no-raw-debug-output.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd383cb25..ce51c4130 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,11 @@ jobs: run: pnpm install - name: Run ESLint - run: pnpm run eslint + run: pnpm run lint:js + + # Commented out until the C and C++ files are formatted and committed to the repo + #- name: Run clang-tidy + #run: pnpm run lint:cpp - name: Validate format run: pnpm run format:check diff --git a/.github/workflows/no-raw-debug-output.yml b/.github/workflows/no-raw-debug-output.yml deleted file mode 100644 index ebd3365ba..000000000 --- a/.github/workflows/no-raw-debug-output.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: CI / No Raw Debug Output - -on: - pull_request: - push: - branches: [dev, main] - -permissions: - contents: read - -jobs: - check-debug-output: - name: Check for raw debug output - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Install ripgrep - run: sudo apt-get install -y ripgrep - - - name: Check for iostream usage - run: | - streams=(cin cout cerr clog wcin wcout wcerr wclog) - - patterns=('#include\s*') - for stream in "${streams[@]}"; do - patterns+=("std::$stream") - done - - regex=$(IFS='|'; echo "${patterns[*]}") - - set +e - rg -e "$regex" \ - --type cpp \ - -g '!third_party/**' \ - -g '!example-apps/**' - status=$? - set -e - - case $status in - 0) echo "::error::Raw debug output found (see matches above). Use IMG2NUM_LOG_* macros instead."; exit 1 ;; - 1) echo "No raw debug output found."; exit 0 ;; - *) echo "::error::ripgrep failed with exit code $status"; exit "$status" ;; - esac diff --git a/package.json b/package.json index 1e6c92b44..a90dc9346 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,25 @@ } }, "Linting": { - "eslint": { + "lint": { + "desc": "Run all linters (C++ and JavaScript/TypeScript)" + }, + "lint:fix": { + "desc": "Run all linters and automatically fix issues" + }, + "lint:cpp": { + "desc": "Run clang-tidy on C++ code" + }, + "lint:cpp:fix": { + "desc": "Run clang-tidy and apply fixes" + }, + "lint:js": { "desc": "Run ESLint on example apps, scripts, and docs", "args": [ "--fix Automatically fix fixable issues" ] }, - "eslint:fix": { + "lint:js:fix": { "desc": "Run ESLint and automatically fix issues" } }, @@ -67,15 +79,19 @@ }, "scripts": { "help": "node scripts/help.js", - "format": "node scripts/format.js", - "format:check": "echo 'No longer supported in Img2Num. Use `pnpm run lint` instead.'", + "format": "pnpm run --filter=. --parallel \"/^format:(cpp|js)$/\"", + "format:check": "pnpm run --filter=. --parallel \"/^format:(cpp|js):check$/\"", "format:cpp": "uv run format_cpp", - "format:cpp:check": "node scripts/format-cpp.js", + "format:cpp:check": "uv run format_cpp", "format:js": "pnpm prettier --write . --ignore-path .prettierignore --config .prettierrc", "format:js:check": "pnpm prettier --check . --ignore-path .prettierignore --config .prettierrc", "validate-scripts": "node scripts/validate-scripts.js", - "eslint": "eslint packages/js example-apps/react-js scripts/img2num-dev-scripts docs", - "eslint:fix": "eslint packages/js example-apps/react-js scripts/img2num-dev-scripts docs --fix", + "lint": "pnpm run --filter=. --parallel \"/^lint:(cpp|js)$/\"", + "lint:fix": "pnpm run --filter=. --parallel \"/^lint:(cpp|js):fix$/\"", + "lint:cpp": "uv run lint_cpp", + "lint:cpp:fix": "uv run lint_cpp --fix", + "lint:js": "eslint packages/js example-apps/react-js scripts/img2num-dev-scripts docs", + "lint:js:fix": "eslint packages/js example-apps/react-js scripts/img2num-dev-scripts docs --fix", "editorconfig:check": "editorconfig-checker" }, "devDependencies": { From 9a2b37ac49f2dbbb6b021280a6b3fd952de2219a Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 20:27:55 +0200 Subject: [PATCH 15/29] build: persist scikit-build CMake tree for clang-tidy coverage The Python bindings were configured by scikit-build-core in a throwaway temp directory, so bindings/py/src/img2num_pybind.cpp never had a usable compile_commands.json and was silently skipped by lint-cpp. - Set tool.scikit-build.build-dir = "build-py/{wheel_tag}" so the CMake tree (and its exported compile database) survives the build. The {wheel_tag} placeholder keeps cibuildwheel's per-Python builds from clobbering each other's caches. - Teach lint_cpp to auto-discover all databases (build-c-cpp/, legacy build/, build-py/*/) and lint each TU against the first database that contains it. Explicit -p flags still override discovery. - Warn (instead of failing) on TUs absent from every database, e.g. bindings/js/src/wasm_wrapper.c, whose Emscripten flags clang-tidy can't consume. - Add build-py/ to .gitignore. img2num_pybind.cpp is now linted for the first time (passes clean). Note: the database may retain a stale include path from uv's isolated build env; harmless, since compilers skip nonexistent -I directories and pybind11 headers resolve from .venv instead. Rebuilding with `just build py` refreshes the database if resolution ever breaks. --- pyproject.toml | 1 + scripts/py/lint_cpp.py | 73 +++++++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dbcfce108..29f038992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ build-backend = "scikit_build_core.build" [tool.scikit-build] cmake.source-dir = "." cmake.build-type = "Release" +build-dir = "build-py/{wheel_tag}" cmake.args = [ "-DIMG2NUM_BUILD_PYTHON=ON", "-DIMG2NUM_BUILD_EXAMPLES=OFF", diff --git a/scripts/py/lint_cpp.py b/scripts/py/lint_cpp.py index d84e8067d..ebc4e41dd 100644 --- a/scripts/py/lint_cpp.py +++ b/scripts/py/lint_cpp.py @@ -1,18 +1,23 @@ #!/usr/bin/env python3 """Lint all C/C++ translation units with clang-tidy. -Requires one or more compile databases (compile_commands.json), generated by -configuring CMake. Every TU under SEARCH_DIRS must appear in at least one -database for full coverage, e.g.: +Requires one or more compile databases (compile_commands.json). The project's +normal build flows produce them automatically (CMAKE_EXPORT_COMPILE_COMMANDS +is always exported): - cmake -S . -B build # core + C bindings - cmake -S . -B build/py -DIMG2NUM_BUILD_PYTHON=ON # Python bindings - emcmake cmake -S . -B build-wasm # WASM wrapper + just build cpp -> build-c-cpp/ + just build py -> build-py// (scikit-build-core build-dir) + just build js -> build-wasm/ (not linted by default: + Emscripten flags in its + database break clang-tidy) + +By default, every database found under build-c-cpp/ (or legacy build/) and +build-py/*/ is used. Files present in no database are skipped with a warning. Usage: - uv run lint-cpp # lint using ./build only - uv run lint-cpp -p build -p build/py # union of several databases - uv run lint-cpp --fix # apply suggested fixes + uv run lint_cpp # auto-discover databases + uv run lint_cpp -p build-c-cpp # explicit database(s) + uv run lint_cpp --fix # apply suggested fixes """ from __future__ import annotations @@ -45,6 +50,19 @@ def log_color(message: str, color: str, *, err: bool = False) -> None: print(f"{color}{message}{Colors.RESET}", file=stream) +def has_database(build_dir: Path) -> bool: + return (build_dir / "compile_commands.json").is_file() + + +def default_build_dirs() -> list[Path]: + """Discover databases produced by the project's build flows.""" + candidates = [ROOT / "build-c-cpp", ROOT / "build"] + py_root = ROOT / "build-py" + if py_root.is_dir(): + candidates += sorted(p for p in py_root.iterdir() if p.is_dir()) + return [d for d in candidates if has_database(d)] + + def load_databases(build_dirs: list[Path]) -> dict[Path, Path]: """Map each TU in any compile database to the first build dir containing it.""" file_to_build_dir: dict[Path, Path] = {} @@ -128,7 +146,8 @@ def main() -> int: help=( "Build directory containing compile_commands.json. " "Repeatable; files are linted against the first database " - "that contains them (default: build)" + "that contains them (default: auto-discover build-c-cpp " + "and build-py/*)" ), ) parser.add_argument( @@ -137,20 +156,36 @@ def main() -> int: help="Apply clang-tidy's suggested fixes in place", ) args = parser.parse_args() - build_dirs: list[Path] = args.build_dirs or [ROOT / "build"] - missing = [d for d in build_dirs if not (d / "compile_commands.json").exists()] - if missing: - for d in missing: + if args.build_dirs: + build_dirs = args.build_dirs + missing = [d for d in build_dirs if not has_database(d)] + if missing: + for d in missing: + log_color( + f"No compile database at {d / 'compile_commands.json'}.", + Colors.RED, + err=True, + ) + return 2 + else: + build_dirs = default_build_dirs() + if not build_dirs: log_color( - f"No compile database at {d / 'compile_commands.json'}.\n" - "Configure CMake first (the project exports " - "compile_commands.json automatically):\n" - f" cmake -S . -B {d}", + "No compile databases found.\n" + "Build the project first (databases are exported " + "automatically):\n" + " just build cpp # -> build-c-cpp/\n" + " just build py # -> build-py//", Colors.RED, err=True, ) - return 2 + return 2 + log_color( + "Using compile database(s): " + + ", ".join(str(d.relative_to(ROOT)) for d in build_dirs), + Colors.CYAN, + ) verify = subprocess.run( ["clang-tidy", "--verify-config"], From b670ea6a676fea67c9db6e643853dbbf6cc71bfc Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 20:52:59 +0200 Subject: [PATCH 16/29] build: lint wasm TUs against build-wasm compile database bindings/js/src/wasm_wrapper.c was skipped because emcc injects its sysroot include paths inside the compiler driver, so they never appear in compile_commands.json and clang-tidy fails on the first standard-library include when replaying the recorded command. - Locate the Emscripten sysroot via $EMSDK, falling back to `em-config CACHE`, and pass it per-TU as --extra-arg=-isystem for files owned by build-wasm/. Native databases are unaffected. - Add build-wasm/ to auto-discovery, ordered last so TUs built for both native and wasm targets keep their native database mapping. - Degrade gracefully when no sysroot is found: build-wasm/ is excluded with a warning during discovery, but an explicit `-p build-wasm` is a hard error rather than a silent no-op. - Cache the sysroot lookup so em-config runs at most once across worker threads. --- scripts/py/lint_cpp.py | 109 +++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/scripts/py/lint_cpp.py b/scripts/py/lint_cpp.py index ebc4e41dd..4a9939402 100644 --- a/scripts/py/lint_cpp.py +++ b/scripts/py/lint_cpp.py @@ -7,12 +7,17 @@ just build cpp -> build-c-cpp/ just build py -> build-py// (scikit-build-core build-dir) - just build js -> build-wasm/ (not linted by default: - Emscripten flags in its - database break clang-tidy) - -By default, every database found under build-c-cpp/ (or legacy build/) and -build-py/*/ is used. Files present in no database are skipped with a warning. + just build js -> build-wasm/ (linted when the Emscripten + sysroot can be located via + $EMSDK or em-config) + +By default, every usable database found under build-c-cpp/ (or legacy +build/), build-py/*/, and build-wasm/ is used. Emscripten records its +sysroot include paths internally rather than in the compile command, so +wasm TUs are linted with an explicit -isystem pointing at the sysroot; +if no sysroot can be found, build-wasm/ is excluded and its files are +skipped with a warning. Files present in no database are skipped with a +warning. Usage: uv run lint_cpp # auto-discover databases @@ -25,9 +30,11 @@ import argparse import json import os +import shutil import subprocess import sys from concurrent.futures import ThreadPoolExecutor +from functools import lru_cache from pathlib import Path from img2num_root import IMG2NUM_ROOT as ROOT @@ -35,6 +42,7 @@ SEARCH_DIRS = ("core", "bindings") SUFFIXES = {".cpp", ".c"} # translation units only; headers surface via HeaderFilterRegex MAX_PARALLEL = os.cpu_count() or 4 +WASM_BUILD_DIR_NAME = "build-wasm" class Colors: @@ -54,12 +62,77 @@ def has_database(build_dir: Path) -> bool: return (build_dir / "compile_commands.json").is_file() +@lru_cache(maxsize=1) +def emscripten_sysroot_include() -> Path | None: + """Locate the Emscripten sysroot include dir, or None if unavailable. + + emcc injects its sysroot include paths inside the compiler driver, so + they never appear in compile_commands.json; clang-tidy therefore needs + the path supplied explicitly or every wasm TU fails on the first + standard-library include. + """ + emsdk = os.environ.get("EMSDK") + if emsdk: + inc = Path(emsdk) / "upstream" / "emscripten" / "cache" / "sysroot" / "include" + if inc.is_dir(): + return inc + em_config = shutil.which("em-config") + if em_config: + result = subprocess.run( + [em_config, "CACHE"], capture_output=True, text=True + ) + if result.returncode == 0: + inc = Path(result.stdout.strip()) / "sysroot" / "include" + if inc.is_dir(): + return inc + return None + + +def is_wasm_build_dir(build_dir: Path) -> bool: + return build_dir.name == WASM_BUILD_DIR_NAME + + +def extra_args(build_dir: Path) -> list[str]: + """Per-database flags clang-tidy needs beyond the recorded command.""" + if not is_wasm_build_dir(build_dir): + return [] + inc = emscripten_sysroot_include() + assert inc is not None # wasm dirs are filtered out earlier when None + return [f"--extra-arg=-isystem{inc}"] + + +def usable_build_dirs(candidates: list[Path], *, explicit: bool) -> list[Path]: + """Filter to databases the script can actually lint against. + + A wasm database without a locatable Emscripten sysroot is unusable: + dropped with a warning during discovery, treated as an error when the + user requested it explicitly with -p. + """ + usable: list[Path] = [] + for build_dir in candidates: + if is_wasm_build_dir(build_dir) and emscripten_sysroot_include() is None: + level = Colors.RED if explicit else Colors.YELLOW + log_color( + f"Excluding {build_dir.name}: Emscripten sysroot not found " + "(set $EMSDK or put em-config on PATH); its TUs cannot be " + "parsed without the sysroot headers.", + level, + err=True, + ) + if explicit: + return [] + continue + usable.append(build_dir) + return usable + + def default_build_dirs() -> list[Path]: """Discover databases produced by the project's build flows.""" candidates = [ROOT / "build-c-cpp", ROOT / "build"] py_root = ROOT / "build-py" if py_root.is_dir(): candidates += sorted(p for p in py_root.iterdir() if p.is_dir()) + candidates.append(ROOT / WASM_BUILD_DIR_NAME) return [d for d in candidates if has_database(d)] @@ -104,8 +177,9 @@ def find_files(file_to_build_dir: dict[Path, Path]) -> list[tuple[Path, Path]]: skipped = [p for p in candidates if p.resolve() not in file_to_build_dir] if skipped: log_color( - f"Skipping {len(skipped)} file(s) not present in any compile database " - "(not built under the provided CMake configurations):", + f"Skipping {len(skipped)} file(s) not present in any usable compile " + "database (not built under the provided CMake configurations, or " + "only present in an excluded database):", Colors.YELLOW, err=True, ) @@ -115,7 +189,7 @@ def find_files(file_to_build_dir: dict[Path, Path]) -> list[tuple[Path, Path]]: def run_clang_tidy(file: Path, *, build_dir: Path, fix: bool) -> bool: - cmd = ["clang-tidy", "-p", str(build_dir), "--quiet"] + cmd = ["clang-tidy", "-p", str(build_dir), "--quiet", *extra_args(build_dir)] if fix: cmd.append("--fix") cmd.append(str(file)) @@ -146,8 +220,8 @@ def main() -> int: help=( "Build directory containing compile_commands.json. " "Repeatable; files are linted against the first database " - "that contains them (default: auto-discover build-c-cpp " - "and build-py/*)" + "that contains them (default: auto-discover build-c-cpp, " + "build-py/*, and build-wasm)" ), ) parser.add_argument( @@ -158,8 +232,7 @@ def main() -> int: args = parser.parse_args() if args.build_dirs: - build_dirs = args.build_dirs - missing = [d for d in build_dirs if not has_database(d)] + missing = [d for d in args.build_dirs if not has_database(d)] if missing: for d in missing: log_color( @@ -168,15 +241,19 @@ def main() -> int: err=True, ) return 2 + build_dirs = usable_build_dirs(args.build_dirs, explicit=True) + if not build_dirs: + return 2 else: - build_dirs = default_build_dirs() + build_dirs = usable_build_dirs(default_build_dirs(), explicit=False) if not build_dirs: log_color( - "No compile databases found.\n" + "No usable compile databases found.\n" "Build the project first (databases are exported " "automatically):\n" " just build cpp # -> build-c-cpp/\n" - " just build py # -> build-py//", + " just build py # -> build-py//\n" + " just build js # -> build-wasm/ (needs $EMSDK to lint)", Colors.RED, err=True, ) From 1eea3e4d5d436b0077ad5c607a6ec496242a331b Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 21:21:06 +0200 Subject: [PATCH 17/29] revert: out of scope packages/js change for licensing --- packages/js/.gitignore | 3 +-- packages/js/package.json | 4 ++-- packages/js/vite.config.js | 10 +--------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/js/.gitignore b/packages/js/.gitignore index 26360f165..dd386dd07 100644 --- a/packages/js/.gitignore +++ b/packages/js/.gitignore @@ -1,2 +1 @@ -/build-wasm -/THIRD_PARTY_LICENSES.md +build-wasm diff --git a/packages/js/package.json b/packages/js/package.json index 2d3b8afb3..33c4962d0 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -54,14 +54,14 @@ }, "files": [ "dist", - "CHANGELOG.md", - "THIRD_PARTY_LICENSES.md" + "CHANGELOG.md" ], "sideEffects": [ "**/*.wasm", "**/build-wasm/**" ], "scripts": { + "prebuild": "just build js", "build:browser": "cross-env TARGET=browser vite build", "build:standalone": "cross-env TARGET=standalone vite build", "build:node-esm": "cross-env TARGET=node-esm vite build", diff --git a/packages/js/vite.config.js b/packages/js/vite.config.js index 392a43626..45ef15dad 100644 --- a/packages/js/vite.config.js +++ b/packages/js/vite.config.js @@ -3,8 +3,6 @@ import { fileURLToPath } from "node:url"; import { copyFileSync, existsSync, mkdirSync } from "node:fs"; import path from "node:path"; -import { thirdPartyLicensesPlugin } from "./licenses.plugin.mjs"; - const here = fileURLToPath(new URL(".", import.meta.url)); const TARGET = process.env.TARGET ?? "browser"; @@ -187,13 +185,7 @@ const FILE_NAMES = { }; export default defineConfig({ - plugins: [ - wasmUrlPlugin(), - copyWasmPlugin(), - cjsWebgpuGuard(), - // Generate once per full build; browser is the first target `pnpm build` runs. - thirdPartyLicensesPlugin({ enabled: TARGET === "browser" }), - ], + plugins: [wasmUrlPlugin(), copyWasmPlugin(), cjsWebgpuGuard()], build: { outDir: T.outDir ?? `dist/${TARGET}`, From f8a361de99d2ca7c50fce749a7c63100f8665d47 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 21:33:32 +0200 Subject: [PATCH 18/29] fix(ci.yml): uv sync errors during lint --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce51c4130..4725b5294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,8 @@ jobs: runs-on: ubuntu-latest container: image: ${{ needs.set-image.outputs.image }} + env: + UV_NO_SYNC: "1" # every later `uv run` uses the env as-is steps: - name: Checkout code # No need for submodules since we are not running code @@ -85,6 +87,11 @@ jobs: - name: Install PNPM dependencies run: pnpm install + - name: Sync Python tooling (skip building img2num) + run: uv sync --no-install-project + env: + UV_NO_SYNC: "" # let this one step actually sync + - name: Run ESLint run: pnpm run lint:js From ff21cc84b3461ec530d33a36de3668b0f87e2d8e Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 21:53:16 +0200 Subject: [PATCH 19/29] fix(editorconfig): ignore generated files --- .editorconfig-checker.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index 6baea5041..5c88ab75d 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -9,5 +9,5 @@ "IndentSize": false, "MaxLineLength": false }, - "Exclude": ["node_modules", "dist", "build", ".git", "docs/build", "docs/.docusaurus", "docs/static", "docs/docs/js/api", "example-apps/react-js/src/data/contributor-credits.json", "core/src/internal/resources/.*\\.wgsl$", "third_party", "\\.ase$", "\\.min\\.js$", "\\.min\\.css$", "\\.lock$", "CC-BY-SA-4\\.0\\.txt$", "LICENSE.*", "\\.md$", "\\.mdx$", "\\.py$", "\\.pyi$", "\\.bat$", "\\.ps1$", "^img2num$", "\\.cpp$", "\\.hpp$", "\\.c$", "\\.h$", ".pnpm-store"] + "Exclude": ["node_modules", "dist", ".venv", "^build*", ".git", "docs/build", "docs/.docusaurus", "docs/static", "docs/docs/js/api", "example-apps/react-js/src/data/contributor-credits.json", "core/src/internal/resources/.*\\.wgsl$", "third_party", "\\.ase$", "\\.min\\.js$", "\\.min\\.css$", "\\.lock$", "CC-BY-SA-4\\.0\\.txt$", "LICENSE.*", "\\.md$", "\\.mdx$", "\\.py$", "\\.pyi$", "\\.bat$", "\\.ps1$", "^img2num$", "\\.cpp$", "\\.hpp$", "\\.c$", "\\.h$", ".pnpm-store"] } From 9915ab08b26a47982e449534b8c0f09a89c35091 Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 22:17:16 +0200 Subject: [PATCH 20/29] fix: remove accidentally leftover file licenses.plugin.mjs --- packages/js/licenses.plugin.mjs | 58 --------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 packages/js/licenses.plugin.mjs diff --git a/packages/js/licenses.plugin.mjs b/packages/js/licenses.plugin.mjs deleted file mode 100644 index f1269699e..000000000 --- a/packages/js/licenses.plugin.mjs +++ /dev/null @@ -1,58 +0,0 @@ -import { readFileSync, readdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const here = fileURLToPath(new URL(".", import.meta.url)); -const REPO_ROOT = path.join(here, "..", ".."); - -// Same roots as [tool.scikit-build].wheel.license-files in pyproject.toml: -// only deps statically compiled into shipped artifacts. stb is example-apps -// only and deliberately not listed. -const LICENSE_ROOTS = ["third_party/spdlog", "third_party/dawn"]; -const LICENSE_FILE = /^(LICENSE|NOTICE|COPYING)(\.|$)/i; -const SKIP_DIRS = new Set([".git", ".github", "build", "test", "tests", "testing", "bench", "docs", "node_modules"]); - -function findLicenseFiles(absDir, relDir, out) { - for (const entry of readdirSync(absDir, { withFileTypes: true })) { - const rel = path.posix.join(relDir, entry.name); - if (entry.isDirectory()) { - if (!SKIP_DIRS.has(entry.name)) findLicenseFiles(path.join(absDir, entry.name), rel, out); - } else if (LICENSE_FILE.test(entry.name)) { - out.push(rel); - } - } -} - -export function thirdPartyLicensesPlugin({ enabled }) { - if (!enabled) return { name: "img2num:third-party-licenses" }; - - return { - name: "img2num:third-party-licenses", - closeBundle() { - const files = []; - for (const root of LICENSE_ROOTS) { - findLicenseFiles(path.join(REPO_ROOT, root), root, files); - } - files.sort(); - - // Guard: an empty result means the submodules aren't checked out or the - // tree moved. Ship nothing rather than a hollow notices file. - if (files.length < 2) { - throw new Error(`[img2num] found only ${files.length} license file(s) under ${LICENSE_ROOTS.join(", ")} -- submodules missing?`); - } - - const sections = files.map((rel) => { - const body = readFileSync(path.join(REPO_ROOT, rel), "utf8").trim(); - return `## ${rel}\n\n\`\`\`\n${body}\n\`\`\``; - }); - - const header = - "# Third-Party Licenses\n\n" + - "License and notice files for code statically compiled into this " + - "package's artifacts, collected at build time from the paths shown. " + - "Do not edit by hand.\n"; - - writeFileSync(path.join(here, "THIRD_PARTY_LICENSES.md"), [header, ...sections].join("\n\n") + "\n"); - }, - }; -} From 7959e0576f5873fc44e2a2b33d2daea7f601765a Mon Sep 17 00:00:00 2001 From: Ryan-Millard Date: Tue, 25 Aug 2026 22:32:10 +0200 Subject: [PATCH 21/29] style: fix editorconfig errors --- Justfile | 12 ++++++------ example-apps/html-js/scripts/build.mjs | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Justfile b/Justfile index ef2f4cb9d..fa79103a9 100644 --- a/Justfile +++ b/Justfile @@ -75,12 +75,12 @@ build target build_type="Release" log_level="AUTO": js) just build-wasm {{ build_type }} {{ log_level }} ;; \ py) just build-py {{ build_type }} {{ log_level }} ;; \ packages-js) just build-packages-js {{ build_type }} {{ log_level }} ;; \ - all) just build-c-cpp {{ build_type }} {{ log_level }} && \ - just build-wasm {{ build_type }} {{ log_level }} && \ - just build-py {{ build_type }} {{ log_level }} && \ - just build-packages-js {{ build_type }} {{ log_level }} && \ - just react-js build && \ - just docs build ;; \ + all) just build-c-cpp {{ build_type }} {{ log_level }} && \ + just build-wasm {{ build_type }} {{ log_level }} && \ + just build-py {{ build_type }} {{ log_level }} && \ + just build-packages-js {{ build_type }} {{ log_level }} && \ + just react-js build && \ + just docs build ;; \ esac clean target: diff --git a/example-apps/html-js/scripts/build.mjs b/example-apps/html-js/scripts/build.mjs index 48ce68232..9fe54bd71 100644 --- a/example-apps/html-js/scripts/build.mjs +++ b/example-apps/html-js/scripts/build.mjs @@ -88,7 +88,8 @@ function buildVariant(name) { rmSync(outDir, { recursive: true, force: true }); mkdirSync(outDir, { recursive: true }); - const appCode = readFileSync(path.join(rootDir, "shared", "app.js"), "utf8").trim().split("\n").map(line => " ".repeat(variant.appCodeIndentLevel) + line).join("\n"); + const appJs = readFileSync(path.join(rootDir, "shared", "app.js"), "utf8"); + const appCode = appJs.trim().split("\n").map(line => " ".repeat(variant.appCodeIndentLevel) + line).join("\n"); const loaderExecutable = renderLoader(variant, { cdn: false }).replaceAll("{{APP_CODE}}", appCode); const temp = `