diff --git a/.github/workflows/conan.yml b/.github/workflows/conan.yml index be15e8e..1c85a4b 100644 --- a/.github/workflows/conan.yml +++ b/.github/workflows/conan.yml @@ -36,10 +36,9 @@ jobs: image: ubuntu-24.04 cc: gcc-15 cxx: g++-15 - - name: macOS (Clang) - image: macos-latest - cc: clang - cxx: clang++ + # macOS disabled: Apple Clang cannot build quiver (missing + # std::ranges::iota, consteval support, std::submdspan, + # std::execution::par). - name: Windows (MSVC) image: windows-latest cc: cl @@ -48,11 +47,17 @@ jobs: MESON_VERSION: "1.7.2" FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true CCACHE_DIR: ${{ github.workspace }}/.ccache + SUBPROJECT_PAT: ${{ secrets.SUBPROJECT_PAT }} steps: - name: Checkout code uses: actions/checkout@v6 + - name: Configure git for private subprojects + if: env.SUBPROJECT_PAT != '' + shell: bash + run: git config --global url."https://x-access-token:${SUBPROJECT_PAT}@github.com/".insteadOf "https://github.com/" + - name: Set up Python uses: actions/setup-python@v6 with: @@ -62,7 +67,13 @@ jobs: - name: Install GCC 15 (Linux) if: runner.os == 'Linux' run: | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + # Retry PPA setup — Launchpad occasionally returns transient + # GPGKeyTemporarilyNotFoundError (HTTP 500) errors. + for i in 1 2 3 4 5; do + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test && break + echo "Attempt $i failed, retrying in 10s..." + sleep 10 + done sudo apt-get update sudo apt-get install -y gcc-15 g++-15 ccache @@ -72,8 +83,9 @@ jobs: sudo apt-get install -y \ libvulkan-dev glslang-dev libshaderc-dev \ libglfw3-dev \ - qt6-base-dev libqt6opengl6-dev libqt6openglwidgets6-dev \ + qt6-base-dev \ libgl-dev libgl1-mesa-dev \ + libreadline-dev \ libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev \ libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev \ libxdamage-dev libxfixes-dev libxi-dev libxinerama-dev \ @@ -87,10 +99,6 @@ jobs: libxcb-present-dev libxcb-composite0-dev libxcb-ewmh-dev \ libxcb-res0-dev libxcb-util-dev - - name: Install ccache (macOS) - if: runner.os == 'macOS' - run: brew install ccache - - name: Setup MSVC (Windows) if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 @@ -100,7 +108,17 @@ jobs: - name: Install pkg-config (Windows) if: runner.os == 'Windows' - run: choco install pkgconfiglite + shell: pwsh + run: | + # Retry choco install — chocolatey community feed occasionally + # returns 504 Gateway Timeout errors. + foreach ($i in 1..5) { + choco install pkgconfiglite -y + if ($LASTEXITCODE -eq 0) { break } + Write-Host "Attempt $i failed, retrying in 10s..." + Start-Sleep -Seconds 10 + } + if ($LASTEXITCODE -ne 0) { exit 1 } # -- Conan -- - name: Create conan profile (Unix) @@ -123,7 +141,13 @@ jobs: conan profile show - name: Install conan dependencies - run: conan install . --output-folder=builddir/conan --build=missing + run: > + conan install . + --output-folder=builddir/conan + --build=missing + -o qt=False + -o visualization=False + -o protobuf=False # -- Caches -- - name: Cache ccache @@ -143,11 +167,26 @@ jobs: ccache -z # -- Build & Test -- + - name: Write compiler native file (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + mkdir -p builddir + { + echo "[binaries]" + echo "c = '${{ matrix.platform.cc }}'" + echo "cpp = '${{ matrix.platform.cxx }}'" + } > builddir/compiler.ini + - name: Configure env: CC: ${{ runner.os != 'Windows' && format('ccache {0}', matrix.platform.cc) || matrix.platform.cc }} CXX: ${{ runner.os != 'Windows' && format('ccache {0}', matrix.platform.cxx) || matrix.platform.cxx }} - run: meson setup builddir/ --native-file builddir/conan/conan_meson_native.ini + run: > + meson setup builddir/ + --wrap-mode=forcefallback + --native-file builddir/conan/conan_meson_native.ini + ${{ runner.os != 'Windows' && '--native-file builddir/compiler.ini' || '' }} - name: Build run: meson compile -C builddir/ @@ -156,6 +195,24 @@ jobs: if: runner.os != 'Windows' run: ccache -s + - name: Set conan library path (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + # Use pkg-config to resolve library directories from conan .pc files + # so shared libs (e.g. libtbb) are found at test runtime. + # The .pc files use ${prefix} variables that raw grep cannot resolve. + export PKG_CONFIG_PATH="builddir/conan:${PKG_CONFIG_PATH:-}" + CONAN_LIB_DIRS="" + for pc in builddir/conan/*.pc; do + pkg=$(basename "$pc" .pc) + dirs=$(pkg-config --libs-only-L "$pkg" 2>/dev/null | tr ' ' '\n' | sed 's/^-L//' | tr '\n' ':') + CONAN_LIB_DIRS="${CONAN_LIB_DIRS}${dirs}" + done + # Deduplicate + CONAN_LIB_DIRS=$(echo "$CONAN_LIB_DIRS" | tr ':' '\n' | sort -u | tr '\n' ':' | sed 's/:$//') + echo "LD_LIBRARY_PATH=${CONAN_LIB_DIRS}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" + - name: Test run: meson test -C builddir/ -v diff --git a/.github/workflows/system-packages.yml b/.github/workflows/system-packages.yml index e9bda38..8b5fc57 100644 --- a/.github/workflows/system-packages.yml +++ b/.github/workflows/system-packages.yml @@ -40,18 +40,23 @@ jobs: image: ubuntu-24.04 cc: gcc-15 cxx: g++-15 - - name: macOS (Clang) - image: macos-latest - cc: clang - cxx: clang++ + extra_args: "" + # macOS disabled: Apple Clang cannot build quiver (missing + # std::ranges::iota, consteval support, std::submdspan, + # std::execution::par). env: MESON_VERSION: "1.7.2" CCACHE_DIR: ${{ github.workspace }}/.ccache + SUBPROJECT_PAT: ${{ secrets.SUBPROJECT_PAT }} steps: - name: Checkout code uses: actions/checkout@v6 + - name: Configure git for private subprojects + if: env.SUBPROJECT_PAT != '' + run: git config --global url."https://x-access-token:${SUBPROJECT_PAT}@github.com/".insteadOf "https://github.com/" + - name: Set up Python uses: actions/setup-python@v6 with: @@ -59,17 +64,22 @@ jobs: # -- Toolchain & packages -- - name: Install packages (Linux) - if: runner.os == 'Linux' run: | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + # Retry PPA setup — Launchpad occasionally returns transient + # GPGKeyTemporarilyNotFoundError (HTTP 500) errors. + for i in 1 2 3 4 5; do + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test && break + echo "Attempt $i failed, retrying in 10s..." + sleep 10 + done sudo apt-get update sudo apt-get install -y gcc-15 g++-15 ccache sudo apt-get install -y \ - libfmt-dev libspdlog-dev libeigen3-dev \ - nlohmann-json3-dev catch2 \ + libeigen3-dev \ + nlohmann-json3-dev catch2 libtbb-dev libreadline-dev \ libvulkan-dev glslang-dev libshaderc-dev \ libglfw3-dev \ - qt6-base-dev libqt6opengl6-dev libqt6openglwidgets6-dev \ + qt6-base-dev \ libgl-dev libgl1-mesa-dev \ libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev \ libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev \ @@ -84,10 +94,6 @@ jobs: libxcb-present-dev libxcb-composite0-dev libxcb-ewmh-dev \ libxcb-res0-dev libxcb-util-dev - - name: Install packages (macOS) - if: runner.os == 'macOS' - run: brew install spdlog fmt catch2 nlohmann-json cli11 eigen ccache - - name: Install Meson and Ninja run: python -m pip install meson==${{ env.MESON_VERSION }} ninja @@ -120,7 +126,9 @@ jobs: run: > meson setup build/ --buildtype=${{ matrix.build_type }} + --wrap-mode=forcefallback -Dtesting=true + ${{ matrix.platform.extra_args }} - name: Build run: meson compile -C build/ diff --git a/.gitignore b/.gitignore index c9c9928..b992d82 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,20 @@ build* # meson wrap likes to add this file .meson-subproject-wrap-hash.txt + +# Ignore auto-promoted wrap-redirect files from subprojects. +# Only directly-needed wraps are whitelisted below. +subprojects/*.wrap +!subprojects/catch2.wrap +!subprojects/cli11.wrap +!subprojects/colormap_shaders.wrap +!subprojects/eigen.wrap +!subprojects/fmt.wrap +!subprojects/imgui.wrap +!subprojects/nlohmann_json.wrap +!subprojects/partio.wrap +!subprojects/quiver.wrap +!subprojects/spdlog.wrap +!subprojects/zipper.wrap + docs/ diff --git a/conanfile.py b/conanfile.py index 952fc1d..e3192b4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -24,15 +24,17 @@ ("qt", [True, False], True, ["qt/6.8.3"]), ("protobuf", [True, False], True, ["protobuf/5.27.0"]), ("openvdb", [True, False], False, ["openvdb/11.0.0"]), - ("alembic", [True, False], True, ["alembic/1.8.6"]), + # Alembic's conan package does not produce a pkg-config file on Windows, + # so meson cannot find it. Disable by default until upstream is fixed. + ("alembic", [True, False], False, ["alembic/1.8.6"]), ("embree", [True, False], False, ["embree3/3.13.5"]), ("perfetto", [True, False], False, ["perfetto/50.1"]), ("pngpp", [True, False], False, ["pngpp/0.2.10"]), # Options with no Conan deps (handled entirely by Meson) ("json", [True, False], True, []), - ("partio", [True, False], True, []), - ("igl", [True, False], True, []), - ("eltopo", [True, False], True, []), + ("partio", [True, False], False, []), # cmake subproject; disabled in CI + ("igl", [True, False], False, []), # cmake subproject; disabled in CI + ("eltopo", [True, False], False, []), # cmake subproject; disabled in CI ] __OPTIONS__ = {name: values for name, values, default, deps in __OPTIONAL_FLAGS_WITH_DEPS__} @@ -57,10 +59,12 @@ def requirements(self): for dep in dependencies(self): self.requires(dep) + # Quiver (pulled as meson subproject) needs TBB for parallel execution. + self.requires("onetbb/2022.0.0") + # Pin transitive dependency versions to avoid conflicts. self.requires("fmt/11.0.2", override=True) self.requires("abseil/20240722.0", override=True) - self.requires("onetbb/2022.0.0", override=True) self.requires("boost/1.88.0", override=True) if self.options.visualization: self.requires("vulkan-headers/1.3.268.0", override=True) @@ -70,6 +74,8 @@ def requirements(self): self.requires("spirv-headers/1.3.268.0", override=True) def configure(self): + # onetbb requires hwloc as shared library + self.options["hwloc"].shared = True if self.options.visualization: self.options["glfw"].vulkan_static = True self.options["qt"].with_vulkan = True diff --git a/core/src/filesystem/prepend_to_filename.cpp b/core/src/filesystem/prepend_to_filename.cpp index 66e0d75..a2a23f5 100644 --- a/core/src/filesystem/prepend_to_filename.cpp +++ b/core/src/filesystem/prepend_to_filename.cpp @@ -5,6 +5,7 @@ namespace balsa::filesystem { std::filesystem::path prepend_to_filename(const std::filesystem::path &orig, const std::string &prefix) { auto parent = orig.parent_path(); auto filename = orig.filename(); - return parent / (prefix + std::string(filename)); + // std::filesystem::path is not implicitly convertible to std::string on MSVC + return parent / (prefix + filename.string()); } }// namespace balsa::filesystem diff --git a/core/src/logging/json_sink.cpp b/core/src/logging/json_sink.cpp index 7b82d27..cec39fe 100644 --- a/core/src/logging/json_sink.cpp +++ b/core/src/logging/json_sink.cpp @@ -14,7 +14,9 @@ void set_json_format(spdlog::logger &logger, bool messages_are_json) { } } std::shared_ptr make_json_file_logger(const std::string &name, const std::filesystem::path &path, bool messages_are_json) { - auto logger = spdlog::basic_logger_mt(name, path); + // spdlog::basic_logger_mt expects filename_t (std::wstring on Windows), + // so convert std::filesystem::path via .string() for portability. + auto logger = spdlog::basic_logger_mt(name, path.string()); set_json_format(*logger, messages_are_json); return logger; } diff --git a/core/tests/test_iterators.cpp b/core/tests/test_iterators.cpp index 3ab2bea..e047441 100644 --- a/core/tests/test_iterators.cpp +++ b/core/tests/test_iterators.cpp @@ -59,6 +59,7 @@ TEST_CASE("ranges heterogeneous line", "[ranges]") { for (auto &&num : nums) { std::cout << num << std::endl; } +#if defined(__cpp_lib_ranges_concat) std::array ret; auto inp = std::views::concat(nums, std::views::repeat(float(0))) | std::views::take(6); std::ranges::copy(inp, ret.begin()); @@ -66,4 +67,5 @@ TEST_CASE("ranges heterogeneous line", "[ranges]") { std::cout << v << " "; } std::cout << std::endl; +#endif } diff --git a/meson.build b/meson.build index f500302..a63bcc6 100644 --- a/meson.build +++ b/meson.build @@ -1,12 +1,15 @@ project('balsa', 'cpp', version : '0.1', - default_options : ['warning_level=3', 'cpp_std=c++26']) + default_options : ['warning_level=3', 'cpp_std=c++26,vc++latest']) cc = meson.get_compiler('cpp') -dl_lib = cc.find_library('dl') +dl_lib = cc.find_library('dl', required: false) # ── Core dependencies (system or WrapDB subproject fallback) ────────────── -spdlog_dep = dependency('spdlog', version: '>=1.9.2', default_options: ['tests=disabled']) +# Force spdlog to static when building as subproject: spdlog's wrapdb meson.build +# leaks -DFMT_EXPORT into interface compile_args when built as shared with std::format, +# which breaks fmt's own headers for any target that also depends on fmt directly. +spdlog_dep = dependency('spdlog', version: '>=1.9.2', default_options: ['tests=disabled', 'default_library=static']) eigen_dep = dependency('eigen3', version: '>=3.4.0') zipper_proj = subproject('zipper', default_options: {'testing': false}) @@ -59,7 +62,7 @@ endif subdir('core') if get_option('quiver') - quiver_proj = subproject('quiver', default_options: ['testing=false', 'tools=false', 'examples=false']) + quiver_proj = subproject('quiver', default_options: ['testing=false', 'tools=false', 'examples=false', 'bundled_spdlog=true']) quiver_dep = quiver_proj.get_variable('quiver_dep') endif diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index 7e9c5c4..9d93ef9 100644 --- a/subprojects/fmt.wrap +++ b/subprojects/fmt.wrap @@ -6,6 +6,7 @@ source_hash = aa3e8fbb6a0066c03454434add1f1fc23299e85758ceec0d7d2d974431481e40 source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_12.0.0-1/fmt-12.0.0.tar.gz patch_filename = fmt_12.0.0-1_patch.zip patch_url = https://wrapdb.mesonbuild.com/v2/fmt_12.0.0-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_12.0.0-1/fmt_12.0.0-1_patch.zip patch_hash = 307f288ebf3850abf2f0c50ac1fb07de97df9538d39146d802f3c0d6cada8998 wrapdb_version = 12.0.0-1 diff --git a/subprojects/quiver.wrap b/subprojects/quiver.wrap index a6c3a2b..5b6159a 100644 --- a/subprojects/quiver.wrap +++ b/subprojects/quiver.wrap @@ -1,3 +1,3 @@ [wrap-git] -url = git@github.com:mtao/quiver.git +url = https://github.com/mtao/quiver.git revision = main diff --git a/visualization/src/vulkan/native_film.cpp b/visualization/src/vulkan/native_film.cpp index af75750..7866a01 100644 --- a/visualization/src/vulkan/native_film.cpp +++ b/visualization/src/vulkan/native_film.cpp @@ -11,13 +11,13 @@ namespace { -static VKAPI_ATTR vk::Bool32 VKAPI_CALL - debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - vk::DebugUtilsMessageTypeFlagsEXT messageType, - const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData, +static VKAPI_ATTR VkBool32 VKAPI_CALL + debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData, void * /*pUserData*/) { spdlog::level::level_enum slevel = spdlog::level::debug; - switch (messageSeverity) { + switch (static_cast(messageSeverity)) { case vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose: slevel = spdlog::level::debug; break; @@ -38,18 +38,19 @@ static VKAPI_ATTR vk::Bool32 VKAPI_CALL logger = spdlog::default_logger(); } + auto typeFlags = static_cast(messageType); std::string_view type; - if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) { + if (typeFlags & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) { type = "Validation"; - } else if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance) { + } else if (typeFlags & vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance) { type = "Performance"; - } else if (messageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eDeviceAddressBinding) { + } else if (typeFlags & vk::DebugUtilsMessageTypeFlagBitsEXT::eDeviceAddressBinding) { type = "DeviceAddressBinding"; } else { type = "General"; } logger->log(slevel, "Vk{}: {}", type, pCallbackData->pMessage); - return vk::False; + return VK_FALSE; } }// namespace