diff --git a/.github/workflows/macdebug.yml b/.github/workflows/macdebug.yml new file mode 100644 index 000000000..55aa543f9 --- /dev/null +++ b/.github/workflows/macdebug.yml @@ -0,0 +1,189 @@ +# Debug builds on macOS, under AddressSanitizer and UndefinedBehaviorSanitizer. +# +# The macOS counterpart of unixdebug.yml, and the same reasoning: every other +# workflow builds macOS in Release, so nothing was ever run there under a +# sanitizer. It also runs the examples, which -D TEST_EXAMPLES=ON registers as +# ctest integration tests (see examples/CMakeLists.txt). +# +# As on Linux, GitHub offers only two instruction sets here, so the third +# configuration is a compiler rather than a third architecture: AppleClang next +# to GCC, on arm64. +# +# Two things make macOS harder than Linux, and shape the matrix below. +# +# GCC on macOS links against libstdc++ while AppleClang links against libc++, +# so a library built with one of them cannot be linked by the other -- the two +# standard libraries are not ABI-compatible. GAOL is no obstacle: codac builds +# it with the compiler of the job (see scripts/CMakeModules/codac_gaol.cmake). +# Catch2 has to be treated the same way, which the Toolchain step below does. +# +# The Python bindings are left out of every job here, sanitized macOS being a +# configuration the test suite cannot currently run them in: the block of +# tests/CMakeLists.txt that hands the sanitizer runtime to the interpreter is +# Linux-only by construction, and says so -- LD_PRELOAD has no equivalent there, +# macOS needing DYLD_INSERT_LIBRARIES and a differently named runtime. Without +# that preload the extension module fails to load on the first unresolved +# __asan_* symbol. The Python half of the suite therefore runs under a sanitizer +# in unixdebug.yml, where the mechanism exists, and macOS covers the C++ half: +# the library, its unit tests and the C++ examples. Turning WITH_PYTHON off also +# removes the need for doxygen here, which doc/CMakeLists.txt only requires for +# the bindings. +on: + push: + branches: ['**'] + tags-ignore: ['**'] # Ignore all tag pushes + pull_request: + +concurrency: + # Keep only the newest run of this workflow for a given branch or pull + # request: a push that supersedes another leaves the older run computing a + # result nobody will read, while its jobs hold runners the newest run is + # waiting for. + # + # The three release branches are excluded. They are where the jobs upload + # their packages to a GitHub release (see the "github.ref_name==" conditions + # further down), and cancelling such a run halfway would leave the release + # with only part of its assets. The same expression is used in every + # workflow of this directory, including the ones that publish nothing, so + # that the rule stays a single thing to know. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'codac1' && github.ref_name != 'codac2' && github.ref_name != 'codac2_codac4matlab' }} + +jobs: + macdebug: + runs-on: ${{ matrix.cfg.os }} + # A sanitized Debug build is roughly an order of magnitude slower to run + # than the Release builds of the other workflows, and the examples are run + # on top of the test suite. Three hours leaves room for that while still + # turning a hang into a red job rather than into six hours of runner time. + timeout-minutes: 180 + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + cfg: + - { os: macos-26 , arch: arm64 , runtime: tahoe , compiler: gcc , with_python: 'OFF', desc: 'macOS Tahoe GCC arm64 Debug ASan+UBSan' } + - { os: macos-15-intel, arch: x86_64, runtime: sequoia, compiler: gcc , with_python: 'OFF', desc: 'macOS Sequoia GCC x86_64 Debug ASan+UBSan' } + - { os: macos-26 , arch: arm64 , runtime: tahoe , compiler: appleclang, with_python: 'OFF', desc: 'macOS Tahoe AppleClang arm64 Debug ASan+UBSan' } + - { os: macos-26 , arch: arm64 , runtime: tahoe , compiler: llvm , with_python: 'OFF', desc: 'macOS Tahoe LLVM Clang arm64 Debug ASan+UBSan' } + name: ${{ matrix.cfg.desc }} + steps: + - uses: actions/checkout@v7 + with: + submodules: true + fetch-depth: 0 + clean: false + + - run: echo "VERBOSE=1" >> $GITHUB_ENV + + # Homebrew's "gcc" formula is the newest GCC it packages, and the binaries + # it installs carry the major version in their name (g++-15 and so on), the + # plain g++ of macOS being a symlink to AppleClang. The version is therefore + # resolved here rather than pinned, and written to the environment for the + # later steps. + # Catch2 is deliberately not installed from Homebrew: the bottle is built + # with AppleClang against libc++, so the GCC jobs cannot link it -- the + # standard-library split described at the top of this file, showing up as + # undefined std::__1:: symbols at link time. Leaving it out lets + # tests/CMakeLists.txt fetch Catch2 and build it with the compiler of the + # job, which is correct for every entry of this matrix. + - name: Toolchain + run: | + case "${{ matrix.cfg.compiler }}" in + gcc) + brew install gcc + GCC_MAJOR=$(brew list --versions gcc | awk '{print $2}' | cut -d. -f1) + echo "CC=gcc-${GCC_MAJOR}" >> "$GITHUB_ENV" + echo "CXX=g++-${GCC_MAJOR}" >> "$GITHUB_ENV" + ;; + llvm) + # Upstream Clang, which is a different compiler from the AppleClang + # of the entry above: its own release cycle, its own diagnostics and + # its own sanitizer runtimes. Homebrew keeps it out of the way of the + # system toolchain, so it has to be named by its prefix, and it needs + # to be pointed at its own libc++ -- the formula says as much -- or + # it compiles against headers newer than the library it links. + brew install llvm + LLVM_PREFIX=$(brew --prefix llvm) + echo "CC=$LLVM_PREFIX/bin/clang" >> "$GITHUB_ENV" + echo "CXX=$LLVM_PREFIX/bin/clang++" >> "$GITHUB_ENV" + echo "LDFLAGS=-L$LLVM_PREFIX/lib/c++ -Wl,-rpath,$LLVM_PREFIX/lib/c++" >> "$GITHUB_ENV" + ;; + *) + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + ;; + esac + + - name: Compiler version + run: $CXX --version + + - name: Configure and build + run: | + mkdir build ; cd build + cmake \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_FLAGS="-fPIC" \ + -D CMAKE_C_FLAGS="-fPIC" \ + -D CMAKE_INSTALL_PREFIX="../codac" \ + -D BUILD_TESTS=ON \ + -D TEST_EXAMPLES=ON \ + -D WITH_PYTHON=${{ matrix.cfg.with_python }} \ + -D PYBIND11_FINDPYTHON=OFF \ + -D WITH_CAPD=OFF \ + .. 2>&1 | tee configure.log + cmake --build . -j 4 + + # The point of this workflow is the sanitizers, and the top-level + # CMakeLists.txt deliberately falls back to an unsanitized Debug build when + # it cannot find their runtime rather than failing to link. That fallback is + # the right default for someone building codac by hand, and exactly the + # wrong outcome here: the job would come out green having checked nothing it + # was written for. GCC's Darwin sanitizer support is the reason this is not + # theoretical. + - name: Check that the sanitizers really are enabled + run: | + if grep -q "will not be sanitized" build/configure.log ; then + echo "This job exists to run the suite under ASan and UBSan, and cmake reported:" + grep "will not be sanitized" build/configure.log + exit 1 + fi + echo "Sanitizers enabled." + + # The unit tests and the examples are the same ctest suite: examples are + # registered as tests by examples/CMakeLists.txt, so this one command runs + # both, and a sanitizer report in either fails the job. + - name: Unit tests and examples + run: | + cd build + ctest -V --output-on-failure 2>&1 | tee ctest.log + + # ctest is run verbose so that everything the sanitizers print reaches the + # log. ASan and UBSan do not agree on what a diagnostic costs: a leak or a + # buffer overflow aborts the process and fails the test, but a UBSan runtime + # error only prints and lets the run continue, so a test can pass having + # reported dozens of undefined behaviours. Without -V that output is thrown + # away for every test that passes -- which is precisely the output worth + # reading here. + - name: Sanitizer diagnostics + if: always() + run: | + log=build/ctest.log + [ -f "$log" ] || { echo "No ctest output to scan." ; exit 0 ; } + # A digest, because the verbose log of the whole suite is far too long + # to scan by eye. The step reports rather than judges: it never fails + # the job, the tests themselves decide that. + n=$(grep -cE "runtime error:|ERROR: AddressSanitizer|ERROR: LeakSanitizer|SUMMARY: (Address|Undefined|Leak)Sanitizer" "$log" || true) + echo "Sanitizer diagnostics found: $n" + if [ "$n" -gt 0 ]; then + echo "--- distinct messages, most frequent first ---" + # awk rather than head: this shell runs with pipefail, and head + # closing the pipe early makes sort die on SIGPIPE, which failed the + # step -- the one thing it was written never to do. + grep -hoE "runtime error: .*|ERROR: (Address|Leak)Sanitizer: [a-z-]+" "$log" \ + | sed -E "s/0x[0-9a-f]+/0xADDR/g" | sort | uniq -c | sort -rn | awk 'NR<=40' + echo "--- first occurrences in context ---" + grep -nE "runtime error:|ERROR: (Address|Leak)Sanitizer" "$log" | awk 'NR<=20' + fi diff --git a/.github/workflows/unixdebug.yml b/.github/workflows/unixdebug.yml new file mode 100644 index 000000000..7e0c058d8 --- /dev/null +++ b/.github/workflows/unixdebug.yml @@ -0,0 +1,226 @@ +# Debug builds on Linux, under AddressSanitizer and UndefinedBehaviorSanitizer, +# plus one coverage run. +# +# The rest of this directory answers "does it build and do the tests pass?" on a +# wide spread of systems, all of it in Release. Nothing there ever runs codac +# under a sanitizer on Linux or macOS: windebugmatrix.yml is the only Debug +# workflow and its only sanitized entries are the MSVC ones (the MinGW ones are +# unsanitized for want of a runtime in that toolchain). The whole GCC/Clang +# sanitizer path of the top-level CMakeLists.txt -- the configuration this +# project is developed in -- was therefore never exercised by CI, so a +# use-after-free or a signed overflow could only ever be caught on a developer's +# machine. This workflow closes that gap, and macdebug.yml does the same for +# macOS. +# +# It also runs the examples, which no other workflow does beyond the two that +# tests.yml builds by hand: -D TEST_EXAMPLES=ON now builds every example against +# the in-tree library and runs it as an integration test (see +# examples/CMakeLists.txt). Under a sanitizer they are worth as much as the unit +# tests: they exercise long chains of the library the way a user writes them. +# +# GitHub only offers x86_64 and arm64 Linux runners, so the third distinct +# configuration below is a compiler rather than a third instruction set: Clang +# and GCC disagree often enough about undefined behaviour, and their sanitizer +# runtimes are different implementations, that running both is worth more than a +# third architecture reached through emulation -- where ASan does not work +# anyway, its shadow memory needing an address space QEMU cannot provide. +on: + push: + branches: ['**'] + tags-ignore: ['**'] # Ignore all tag pushes + pull_request: + +concurrency: + # Keep only the newest run of this workflow for a given branch or pull + # request: a push that supersedes another leaves the older run computing a + # result nobody will read, while its jobs hold runners the newest run is + # waiting for. + # + # The three release branches are excluded. They are where the jobs upload + # their packages to a GitHub release (see the "github.ref_name==" conditions + # further down), and cancelling such a run halfway would leave the release + # with only part of its assets. The same expression is used in every + # workflow of this directory, including the ones that publish nothing, so + # that the rule stays a single thing to know. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'codac1' && github.ref_name != 'codac2' && github.ref_name != 'codac2_codac4matlab' }} + +jobs: + unixdebug: + runs-on: ${{ matrix.cfg.os }} + # A sanitized Debug build is roughly an order of magnitude slower to run + # than the Release builds of the other workflows, and the examples are run + # on top of the test suite. Measured on this matrix: 37 min on GCC arm64, + # 58 min on GCC x86_64, and more than three hours under Clang, whose + # instrumentation is markedly more expensive here -- the first run of this + # workflow spent its whole budget inside the test step and was cut there. + # Six hours is GitHub's own ceiling; it is set to it because a shorter one + # kills a job that is progressing, while ctest already bounds each + # individual test to 1500 s, so a genuinely hung test cannot reach this. + timeout-minutes: 360 + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + cfg: + # The distribution's own compilers are used rather than a pinned + # version from a PPA: on the newest Ubuntu they are already the + # newest GCC and Clang, and every job prints what it actually got, + # so an upgrade of the image shows up in the log instead of + # silently doing nothing. Adding ubuntu-toolchain-r would also put + # a Launchpad round-trip on the critical path of every job, which + # has already taken this repository's CI down with a 504. + - { os: ubuntu-26.04 , arch: x86_64, runtime: resolute, cc: gcc , cxx: g++ , packages: 'g++' , build_type: Debug , with_python: 'ON' , coverage: false, desc: 'Ubuntu 26.04 GCC x86_64 Debug ASan+UBSan' } + - { os: ubuntu-26.04-arm, arch: arm64 , runtime: resolute, cc: gcc , cxx: g++ , packages: 'g++' , build_type: Debug , with_python: 'ON' , coverage: false, desc: 'Ubuntu 26.04 GCC arm64 Debug ASan+UBSan' } + - { os: ubuntu-26.04 , arch: x86_64, runtime: resolute, cc: clang , cxx: clang++ , packages: 'clang', build_type: Debug , with_python: 'ON' , coverage: false, test_timeout: 300, desc: 'Ubuntu 26.04 Clang x86_64 Debug ASan+UBSan' } + - { os: ubuntu-26.04-arm, arch: arm64 , runtime: resolute, cc: clang , cxx: clang++ , packages: 'clang', build_type: Debug , with_python: 'ON' , coverage: false, test_timeout: 300, desc: 'Ubuntu 26.04 Clang arm64 Debug ASan+UBSan' } + - { os: ubuntu-26.04 , arch: x86_64, runtime: resolute, cc: gcc , cxx: g++ , packages: 'g++' , build_type: Release, with_python: 'ON' , coverage: true , desc: 'Ubuntu 26.04 GCC x86_64 Release coverage' } + name: ${{ matrix.cfg.desc }} + steps: + - uses: actions/checkout@v7 + with: + submodules: true + fetch-depth: 0 + clean: false + + - run: echo "VERBOSE=1" >> $GITHUB_ENV + + # setup-python rather than the interpreter of the image: the bindings need + # the development headers, and this is how tests.yml already provides them. + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + + # doxygen and graphviz are what the docstrings of the bindings are generated + # from. pybind11 is deliberately not installed from the distribution: codac + # asks for 3.0.1 and fetches that version itself when it is not found, which + # is what every other workflow of this directory ends up using, whereas the + # package of the newest Ubuntu got picked up first and left codac-sympy + # linking a pybind11::pybind11 target that its CMake config does not define. + - name: Toolchain and dependencies + run: | + sudo apt-get -q update + sudo apt-get -y install ${{ matrix.cfg.packages }} cmake catch2 doxygen graphviz dpkg-dev || true + ${{ matrix.cfg.cxx }} --version + + - name: Python tooling + run: | + python -m pip install --upgrade pip wheel setuptools numpy sympy + if [ "${{ matrix.cfg.coverage }}" = "true" ]; then + python -m pip install --upgrade gcovr + fi + + # The instrumentation is requested through -D WITH_COVERAGE=ON rather than + # by passing --coverage by hand, so that this job and a developer running + # `make coverage` measure the same build with the same tool: the top-level + # CMakeLists.txt is the single place where the flags, the gcov executable + # matching the compiler and the gcovr filters are decided. The build type + # stays Release, so the coverage job compiles exactly what the Release jobs + # of the other workflows compile, instrumentation aside. + - name: Configure and build + env: + CC: ${{ matrix.cfg.cc }} + CXX: ${{ matrix.cfg.cxx }} + run: | + mkdir build ; cd build + cmake \ + -D WITH_COVERAGE=${{ matrix.cfg.coverage && 'ON' || 'OFF' }} \ + -D CMAKE_BUILD_TYPE=${{ matrix.cfg.build_type }} \ + -D CMAKE_CXX_FLAGS="-fPIC" \ + -D CMAKE_C_FLAGS="-fPIC" \ + -D CMAKE_INSTALL_PREFIX="../codac" \ + -D BUILD_TESTS=ON \ + -D TEST_EXAMPLES=ON \ + -D WITH_PYTHON=${{ matrix.cfg.with_python }} \ + -D PYBIND11_FINDPYTHON=OFF \ + -D WITH_CAPD=OFF \ + .. 2>&1 | tee configure.log + cmake --build . -j 4 + + # The point of the Debug jobs is the sanitizers, and the top-level + # CMakeLists.txt deliberately falls back to an unsanitized Debug build when + # it cannot find their runtime rather than failing to link. That fallback is + # the right default for someone building codac by hand, and exactly the + # wrong outcome here: the job would come out green having checked nothing it + # was written for. + - name: Check that the sanitizers really are enabled + if: matrix.cfg.build_type == 'Debug' + run: | + if grep -q "will not be sanitized" build/configure.log ; then + echo "This job exists to run the suite under ASan and UBSan, and cmake reported:" + grep "will not be sanitized" build/configure.log + exit 1 + fi + echo "Sanitizers enabled." + + # The unit tests and the examples are the same ctest suite: examples are + # registered as tests by examples/CMakeLists.txt, so this one command runs + # both, and a sanitizer report in either fails the job. + - name: Unit tests and examples + run: | + cd build + # ctest's own default here is 1500 s per test, which is far above what + # any test of this suite needs even instrumented -- the slowest of them + # takes under three minutes on a developer machine under ASan. Where a + # job sets test_timeout, that bound is tightened so that a test which + # stops progressing is reported as the one at fault, with its output, + # instead of the job being killed hours later with nothing to read. + TIMEOUT_FLAG="" + if [ -n "${{ matrix.cfg.test_timeout }}" ]; then + TIMEOUT_FLAG="--timeout ${{ matrix.cfg.test_timeout }}" + fi + # -V on the Debug jobs only: the coverage job runs the same suite + # without instrumentation, and its verbose output would carry nothing + # the others do not already show. + VERBOSE_FLAG="" + if [ "${{ matrix.cfg.build_type }}" = "Debug" ]; then VERBOSE_FLAG="-V" ; fi + ctest $VERBOSE_FLAG --output-on-failure $TIMEOUT_FLAG 2>&1 | tee ctest.log + + # ctest is run verbose so that everything the sanitizers print reaches the + # log. ASan and UBSan do not agree on what a diagnostic costs: a leak or a + # buffer overflow aborts the process and fails the test, but a UBSan runtime + # error only prints and lets the run continue, so a test can pass having + # reported dozens of undefined behaviours. Without -V that output is thrown + # away for every test that passes -- which is precisely the output worth + # reading here. + - name: Sanitizer diagnostics + if: always() + run: | + log=build/ctest.log + [ -f "$log" ] || { echo "No ctest output to scan." ; exit 0 ; } + # A digest, because the verbose log of the whole suite is far too long + # to scan by eye. The step reports rather than judges: it never fails + # the job, the tests themselves decide that. + n=$(grep -cE "runtime error:|ERROR: AddressSanitizer|ERROR: LeakSanitizer|SUMMARY: (Address|Undefined|Leak)Sanitizer" "$log" || true) + echo "Sanitizer diagnostics found: $n" + if [ "$n" -gt 0 ]; then + echo "--- distinct messages, most frequent first ---" + # awk rather than head: this shell runs with pipefail, and head + # closing the pipe early makes sort die on SIGPIPE, which failed the + # step -- the one thing it was written never to do. + grep -hoE "runtime error: .*|ERROR: (Address|Leak)Sanitizer: [a-z-]+" "$log" \ + | sed -E "s/0x[0-9a-f]+/0xADDR/g" | sort | uniq -c | sort -rn | awk 'NR<=40' + echo "--- first occurrences in context ---" + grep -nE "runtime error:|ERROR: (Address|Leak)Sanitizer" "$log" | awk 'NR<=20' + fi + + # coverage-report, not coverage: the suite has already been run by the step + # above, and this target only turns the counters it left behind into a + # report. The gcovr invocation lives in the top-level CMakeLists.txt, where + # `make coverage` reaches the very same one. + - name: Coverage report + if: matrix.cfg.coverage + run: | + cd build + gcovr --version + cmake --build . --target coverage-report + + - uses: actions/upload-artifact@v7 + if: matrix.cfg.coverage + with: + name: coverage-report + path: | + build/coverage.* + retention-days: 14 diff --git a/.github/workflows/windebugmatrix.yml b/.github/workflows/windebugmatrix.yml new file mode 100644 index 000000000..0e0ec4b0a --- /dev/null +++ b/.github/workflows/windebugmatrix.yml @@ -0,0 +1,165 @@ +# Debug builds and unit tests on Windows, with the latest MinGW and MSVC +# toolchains, on x64, x86 and (MSVC only) arm64, on both Windows 2022 (MSVC +# v143 / Visual Studio 2022) and the very latest Windows images (MSVC v145 / +# Visual Studio 2026, on windows-2025-vs2026 and windows-11-vs2026-arm). +# Unlike vcmatrix.yml/unixmatrix.yml (which build in Release mode and +# package the result), this workflow only compiles and runs the test suite +# in Debug configuration, so that the sanitizers enabled for Debug builds +# (see the "if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR +# CMAKE_CONFIGURATION_TYPES)" block in the top-level CMakeLists.txt) +# actually get exercised on Windows. There is no MinGW entry beyond +# windows-2022/x64/x86, and no MinGW arm64 entry at all: no such +# configuration exists anywhere else in this repo's workflows (only +# vc17/vc18 have an arm64 variant, and only vc17 is built on windows-2022), +# so MinGW stays on windows-2022 here too. +# +# gaol, the interval library codac is built upon, is built by CMake along +# with codac (see scripts/CMakeModules/codac_gaol.cmake), and always in +# Release, whatever the configuration of codac. Under MSVC this means +# codac's own Debug build is forced onto the Release CRT (/MD instead of +# the default /MDd) so it stays link-compatible with that gaol.lib; see the +# CMAKE_MSVC_RUNTIME_LIBRARY override in the top-level CMakeLists.txt for +# the details of what that trades away (debug-CRT heap/iterator checking) +# versus what it keeps (unoptimized code, debug symbols, ASan). +# +# The MinGW entries simply pin the newest MinGW version (same as +# unixmatrix.yml's own mingw15 entries): unlike MSVC, the choco `mingw` +# package does not ship libasan.a/libubsan.a at all here, for either x86 or +# x64, in any of the versions it offers (checked via `g++ +# -print-file-name=libasan*`/`libubsan*` across mingw15 down to mingw11) -- +# there is no version/architecture combination worth hunting for. The +# top-level CMakeLists.txt detects this (the same capability probe used for +# the MSVC ASan runtime DLL below) and simply leaves Debug builds +# unsanitized on a toolchain that lacks the runtime, rather than failing at +# link time with "ld.exe: cannot find -lasan/-lubsan"; actual ASan coverage +# on Windows comes from the MSVC entries in this matrix instead. +on: + push: + branches-ignore: + - codac2_codac4matlab + tags-ignore: ['**'] # Ignore all tag pushes + pull_request: + branches-ignore: + - codac2_codac4matlab + +concurrency: + # Keep only the newest run of this workflow for a given branch or pull + # request: a push that supersedes another leaves the older run computing a + # result nobody will read, while its jobs hold runners the newest run is + # waiting for. + # + # The three release branches are excluded. They are where the jobs upload + # their packages to a GitHub release (see the "github.ref_name==" conditions + # further down), and cancelling such a run halfway would leave the release + # with only part of its assets. The same expression is used in every + # workflow of this directory, including the ones that publish nothing, so + # that the rule stays a single thing to know. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'codac1' && github.ref_name != 'codac2' && github.ref_name != 'codac2_codac4matlab' }} + +jobs: + windebugmatrix: + runs-on: ${{ matrix.cfg.os }} + # A test binary that cannot load a DLL it needs does not always fail fast: + # on the Visual Studio 2026 images every test of the suite instead sat + # until ctest's own 1500 s timeout, so the job kept going for the six hours + # GitHub allows before killing it, twice per run. Two hours is well above + # the twenty minutes a healthy job takes here and turns such a hang into a + # quick red job rather than a day of runner time. + timeout-minutes: 120 + defaults: + run: + shell: ${{ matrix.cfg.shell }} + strategy: + fail-fast: false + matrix: + cfg: + - { os: windows-2022, shell: cmd, arch: x64 , bitness: 64, runtime: mingw15, cmake_params: '-G "MinGW Makefiles" -D CMAKE_BUILD_TYPE=Debug', cmake_flags: '-fPIC', desc: 'Windows MinGW x64 Debug (unsanitized)' } + - { os: windows-2022, shell: cmd, arch: x86 , bitness: 32, runtime: mingw15, cmake_params: '-G "MinGW Makefiles" -D CMAKE_BUILD_TYPE=Debug', cmake_flags: '-fPIC', choco_flags: '--x86', desc: 'Windows MinGW x86 Debug (unsanitized)' } + - { os: windows-2022, shell: cmd, arch: x64 , bitness: 64, runtime: vc17 , cmake_params: '-G "Visual Studio 17" -T v143 -A x64' , cmake_flags: ' /MP4 /wd4267 /wd4244 /wd4305 /wd4996', desc: 'Windows Visual Studio 2022 x64 Debug' } + - { os: windows-2022, shell: cmd, arch: x86 , bitness: 32, runtime: vc17 , cmake_params: '-G "Visual Studio 17" -T v143 -A Win32', cmake_flags: ' /MP4 /wd4267 /wd4244 /wd4305 /wd4996', choco_flags: '--x86', desc: 'Windows Visual Studio 2022 x86 Debug' } + - { os: windows-11-arm, shell: cmd, arch: arm64, bitness: 64, runtime: vc17 , cmake_params: '-G "Visual Studio 17" -T v143 -A arm64', cmake_flags: ' /MP4 /wd4267 /wd4244 /wd4305 /wd4996', desc: 'Windows Visual Studio 2022 arm64 Debug' } + - { os: windows-2025-vs2026 , shell: cmd, arch: x64 , bitness: 64, runtime: vc18, cmake_params: '-G "Visual Studio 18" -T v145 -A x64' , cmake_flags: ' /MP4 /wd4267 /wd4244 /wd4305 /wd4996', desc: 'Windows Visual Studio 2026 x64 Debug' } + - { os: windows-2025-vs2026 , shell: cmd, arch: x86 , bitness: 32, runtime: vc18, cmake_params: '-G "Visual Studio 18" -T v145 -A Win32', cmake_flags: ' /MP4 /wd4267 /wd4244 /wd4305 /wd4996', choco_flags: '--x86', desc: 'Windows Visual Studio 2026 x86 Debug' } + - { os: windows-11-vs2026-arm, shell: cmd, arch: arm64, bitness: 64, runtime: vc18, cmake_params: '-G "Visual Studio 18" -T v145 -A arm64', cmake_flags: ' /MP4 /wd4267 /wd4244 /wd4305 /wd4996', desc: 'Windows Visual Studio 2026 arm64 Debug' } + name: ${{ matrix.cfg.desc }} + steps: + - uses: actions/checkout@v7 + with: + submodules: true + fetch-depth: 0 + clean: false + - run: echo "VERBOSE=1" >> $GITHUB_ENV + shell: bash + - run: | + choco install -y -r --no-progress wget + wget https://lebarsfa.github.io/cache/cmake_extra_tools.zip --no-check-certificate -nv + 7z x cmake_extra_tools.zip -o"%SystemDrive%" -y + del /f /q cmake_extra_tools.zip + wget https://gist.github.com/lebarsfa/237841f9e5dad55ef192713b3b1b2f16/raw/04d77ced3457346c55f183ca12a10dbcb850e6d5/refreshenv.bashrc --no-check-certificate -nv + move /y refreshenv.bashrc %USERPROFILE% + # MinGW is pinned to the single newest version choco offers (15.2.0): + # none of its versions ship libasan/libubsan for this package, for + # either x86 or x64 (see the header comment above), so there is no + # ASan/UBSan-capable candidate left to probe for -- the top-level + # CMakeLists.txt detects the missing runtime itself and builds + # unsanitized instead. BASHMINGWPATH is written to $GITHUB_ENV so every + # later step (regardless of shell) picks it up automatically, without + # needing choco's own refreshenv.bashrc dance. + - run: | + choco install -y -r --no-progress mingw --version=15.2.0 --force ${{ matrix.cfg.choco_flags }} + echo "BASHMINGWPATH=/c/ProgramData/mingw64/mingw${{ matrix.cfg.bitness }}/bin" >> "$GITHUB_ENV" + if: startsWith(matrix.cfg.runtime, 'mingw') + shell: bash + - run: | + if [ -n "${BASHMINGWPATH:-}" ]; then export PATH="$BASHMINGWPATH:$PATH" ; fi + mkdir build ; cd build + cmake -E env CXXFLAGS="${{ matrix.cfg.cmake_flags }}" CFLAGS="${{ matrix.cfg.cmake_flags }}" cmake ${{ matrix.cfg.cmake_params }} -D CMAKE_INSTALL_PREFIX="../codac" -D BUILD_TESTS=ON .. + cmake --build . -j 4 --config Debug --target install + cd .. + shell: bash + - run: | + if [ -n "${BASHMINGWPATH:-}" ]; then export PATH="$BASHMINGWPATH:$PATH" ; fi + cd build && ctest -C Debug -V --output-on-failure + cd .. + shell: bash + # A test killed by a signal loses everything it had written to its + # block-buffered standard output, so ctest reports the signal and nothing + # else. That is all this job has ever shown for the tests that crash on it, + # every one of them a user of Eigen's matrix product (arithmetic_mul and + # arithmetic_div crash where arithmetic_add and arithmetic_sub pass), and a + # bare signal says nothing about where. Re-running the first few failures + # under gdb prints the faulting frame, which is the one thing missing to + # diagnose them. The step reports rather than judges: it only runs after a + # failure, and never turns a green job red on its own. + - name: Backtrace of the failed tests + if: failure() + continue-on-error: true + run: | + if [ -n "${BASHMINGWPATH:-}" ]; then export PATH="$BASHMINGWPATH:$PATH" ; fi + + if ! command -v gdb > /dev/null 2>&1 ; then + echo "No gdb in this toolchain, skipping the backtraces." + exit 0 + fi + + failed=build/Testing/Temporary/LastTestsFailed.log + if [ ! -f "$failed" ] ; then + echo "ctest recorded no failed test, nothing to trace." + exit 0 + fi + + cd build + # LastTestsFailed.log holds one ":" per line; the C++ + # tests are the "_cpp" ones and their executable is the target + # itself, next to the tests directory of this single-configuration + # build. Three backtraces are plenty to identify a common frame, and + # keep this step short. + sed 's/^[0-9]*://' "../$failed" | grep '_cpp$' | head -3 | while read -r name ; do + exe="tests/${name%_cpp}.exe" + [ -f "$exe" ] || continue + echo "===================== $exe =====================" + gdb -batch -ex "set confirm off" -ex run -ex "bt 40" --args "./$exe" 2>&1 | tail -60 + done + shell: bash + diff --git a/CMakeLists.txt b/CMakeLists.txt index f65251466..5a7599dca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,26 @@ #set(CMAKE_C_COMPILER "gcc-7") #set(CMAKE_CXX_COMPILER "/usr/bin/g++-7") - project(codac VERSION ${VERSION} LANGUAGES CXX) + # CMP0091 (CMake >=3.15) makes the CMAKE_MSVC_RUNTIME_LIBRARY variable set + # below actually control MSVC's runtime library selection, instead of + # CMake's older behavior of baking a hardcoded /MD.../MDd into + # CMAKE__FLAGS_. It must be set to NEW before project() -- + # afterwards is too late -- for that variable to have any effect at all. + if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) + endif() + + # C as well as C++: the codac sources are all C++, but mathlib, which GAOL + # depends on, is C, and codac_gaol_build() builds it when no GAOL is installed + # (see scripts/CMakeModules/codac_gaol.cmake). Enabled here, the C compiler + # and its flags are settled -- and cached -- by the first configuration, so + # that what is handed to that build is the same at every later one. Enabled + # further down by a dependency instead, as happened when only CXX was listed, + # the compiler went from the name it was given to its full path between the + # first configuration and the second, the build of GAOL took that for a + # change and was configured again, and everything in codac that includes an + # interval was recompiled. + project(codac VERSION ${VERSION} LANGUAGES C CXX) if(NOT VERSION_ID) set(PROJECT_VERSION_FULL ${PROJECT_VERSION}) @@ -84,6 +103,230 @@ add_compile_options(-Wall -Wextra -Wpedantic) endif() + if(MSVC) + # The gaol interval library is only ever linked as a Release binary: + # codac_gaol_build() (scripts/CMakeModules/codac_gaol.cmake) builds it in + # Release whatever the configuration of codac, and a gaol found on the + # system comes prebuilt that way too. gaol.lib is thus compiled with the + # Release CRT (/MD). MSVC's linker refuses + # to mix object files built against different CRT variants in the same + # binary -- linking that Release-CRT gaol.lib together with codac's own + # object files, which without this override would default to the Debug + # CRT (/MDd) in a Debug build, fails with error LNK2038 ("mismatch + # detected for 'RuntimeLibrary'/'_ITERATOR_DEBUG_LEVEL'") followed by + # fatal error LNK1319. + # + # Forcing every configuration onto the Release CRT keeps codac's own + # Debug build link-compatible with that Release gaol.lib without + # having to build gaol in Debug as well. Release + # itself already defaulted to /MD, so in practice this only changes + # what the Debug configuration uses. Debug builds still get no + # optimization (/Od), full debug symbols (/Zi) and AddressSanitizer + # (see CODAC_MSVC_ASAN_RUNTIME_DLL below) -- what is traded away is + # only the *separate* debug-CRT extras that /MDd would otherwise add on + # top of that (its own heap debugging and iterator-debug-level checks). + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") + endif() + + if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_CONFIGURATION_TYPES) + # CMAKE_BUILD_TYPE only has meaning for single-configuration generators + # (Makefiles, Ninja): multi-configuration generators (Visual Studio, + # Xcode, Ninja Multi-Config -- notably what this project's own CI + # drives MSVC through, via `-G "Visual Studio 17/18"` in + # .github/workflows/*matrix.yml) instead select the actual + # configuration later, at build time (cmake --build . --config + # Debug), not at configure time; CMAKE_BUILD_TYPE stays whatever it + # was defaulted to above ("Release") regardless of what configuration + # is later built. So this outer if() only decides whether it is worth + # *probing* for Debug-only capabilities at all -- true either when a + # single-config build was explicitly configured as Debug, or when a + # multi-config generator defers that choice (CMAKE_CONFIGURATION_TYPES + # is then non-empty) and so might still end up building a Debug + # configuration later. The actual compile/link options below are + # separately wrapped in a $ generator expression, which + # -- unlike CMAKE_BUILD_TYPE -- CMake evaluates correctly against + # whichever configuration is ultimately built, for both single- and + # multi-config generators alike. + if(MSVC) + # cl.exe silently ignores GCC/Clang-style "-f..."/"-g"/"-O0" flags + # with a D9002 warning rather than failing the build, so without this + # branch a Debug build under MSVC would quietly end up *unsanitized* + # instead of erroring -- a behavior gap invisible until someone + # actually diffs test coverage between platforms. + # + # MSVC's AddressSanitizer (/fsanitize=address) has shipped since + # Visual Studio 16.9; there is no UndefinedBehaviorSanitizer on this + # toolchain, so Debug builds here get ASan coverage only, not the + # ASan+UBSan combination GCC/Clang get below. Its runtime is a DLL + # shipped next to cl.exe, rather than a LD_PRELOAD-style shared + # object that the dynamic loader pulls in transparently: *every* + # executable and DLL built with /fsanitize=address needs that + # runtime DLL to be discoverable at load time (next to the binary, + # or on PATH) -- per Microsoft's own documentation, this holds even + # for /MT static-CRT builds, unlike every other CRT component. + # See tests/CMakeLists.txt and python/src/core/CMakeLists.txt for + # how CODAC_MSVC_ASAN_RUNTIME_DLL, set below, is used to make that + # DLL discoverable for the test suite and for the Python bindings. + # + # Source: https://learn.microsoft.com/en-us/cpp/sanitizers/asan-runtime + # + # Rather than enable /fsanitize=address unconditionally, this whole + # feature is opt-in on whether the matching runtime DLL can actually + # be located next to this cl.exe: if it can't (toolset too old, + # target architecture unsupported by MSVC ASan, or a packaging + # layout this check doesn't recognize), Debug builds simply stay + # unsanitized rather than either failing configuration or compiling + # binaries that are guaranteed to fail to load at run time with a + # missing-DLL error. + # + # MSVC ASan runtime binaries are named using Clang's architecture + # conventions (i386/x86_64/aarch64), not MSVC's own (X86/x64/ARM64) -- + # this is documented by Microsoft itself, not a codac convention. + # CMAKE_CXX_COMPILER_ARCHITECTURE_ID (a builtin CMake variable, MSVC + # only) reports the *target* architecture regardless of host, which + # is what a flag like /fsanitize=address needs to match; MSVC + # AddressSanitizer only supports x86, x64 and ARM64 (ARM64 as of + # VS 2022 17.9, as a preview) -- any other value (32-bit ARM, IA64, + # ARM64EC...) is simply left unmapped below, so the search for a + # runtime DLL naturally comes up empty and the feature is skipped, + # exactly as intended. + set(_msvc_asan_dll_arch "") + if(CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "x64") + set(_msvc_asan_dll_arch "x86_64") + elseif(CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "X86") + set(_msvc_asan_dll_arch "i386") + elseif(CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "ARM64") + set(_msvc_asan_dll_arch "aarch64") + endif() + + set(CODAC_MSVC_ASAN_RUNTIME_DLL "") + if(_msvc_asan_dll_arch) + get_filename_component(_msvc_bin_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + # Visual Studio 2022 17.7 Preview 3 unified every CRT configuration + # (/MT, /MTd, /MD, /MDd) onto a single clang_rt.asan_dynamic-{arch}.dll. + # Older toolsets instead shipped a separate debug-CRT DLL + # (clang_rt.asan_dbg_dynamic-{arch}.dll) for /MDd specifically. + # CMAKE_MSVC_RUNTIME_LIBRARY is forced to the Release CRT (/MD) for + # every configuration above (Debug included), so in practice only + # the plain, non-debug DLL is ever needed here now; the /MDd-only + # name is kept as a fallback purely in case that override above is + # ever relaxed back to the CMake default. Rather than pin an exact + # MSVC_VERSION cutoff for the 17.7-era switch -- a compiler-rt + # packaging detail, not something reported by any compiler flag -- + # just look for whichever of the two names actually exists next to + # this cl.exe, newest first. + foreach(_dll_name + "clang_rt.asan_dynamic-${_msvc_asan_dll_arch}.dll" + "clang_rt.asan_dbg_dynamic-${_msvc_asan_dll_arch}.dll" + ) + if(EXISTS "${_msvc_bin_dir}/${_dll_name}") + set(CODAC_MSVC_ASAN_RUNTIME_DLL "${_msvc_bin_dir}/${_dll_name}") + break() + endif() + endforeach() + endif() + + if(CODAC_MSVC_ASAN_RUNTIME_DLL) + add_compile_options($<$:/fsanitize=address>) + # Incremental linking cannot be combined with ASan: the linker drops + # /INCREMENTAL as soon as an input module carries ASan metadata, and + # says so once per link (warning LNK4300, 870 times across the five + # MSVC Debug jobs of .github/workflows/windebugmatrix.yml, VS 2022 and + # VS 2026 alike). Asking for it off removes the cause rather than the + # message, and leaves LNK4300 free to report any other option ASan + # would silently disable. One item per generator expression, for the + # DIRECTORY-scope reason explained below. + add_link_options($<$:/INCREMENTAL:NO>) + # Enabling ASan on MSVC also switches on the MSVC STL's own + # "container annotation" instrumentation for std::string, + # std::vector and (recent toolsets) std::optional: it poisons + # their reserved-but-unused capacity so ASan can catch + # out-of-bounds access within it, and every translation unit + # linked together records, via #pragma detect_mismatch, whether it + # was compiled with that instrumentation on or off. gaol.lib + # (a Release build without ASan -- see the + # CMAKE_MSVC_RUNTIME_LIBRARY comment above for why it is not + # built in Debug here) was compiled without ASan, so its + # object files carry the instrumentation-off value; without this, + # codac's own ASan-instrumented Debug object files would carry the + # opposite value, and MSVC's linker would reject the mix with a + # second error LNK2038 ("mismatch detected for 'annotate_string' / + # 'annotate_vector' / 'annotate_optional'"), on top of the CRT one + # already addressed above. _DISABLE_STRING_ANNOTATION and + # _DISABLE_VECTOR_ANNOTATION are Microsoft's own documented way to + # turn this instrumentation back off so codac's object files match + # gaol.lib's; _DISABLE_OPTIONAL_ANNOTATION is added defensively for + # the same std::optional check (undocumented as of this writing, + # but following Microsoft's existing naming convention for the + # other two) -- worst case it is simply an unused macro if that + # name turns out to be wrong. Kept as three separate + # add_compile_definitions() calls, one flag per $ + # generator expression, for the same reason as the single-flag + # add_compile_options() calls further below: this is a + # DIRECTORY-scope property, which does not evaluate correctly when + # one such expression packs more than one ';'-separated item. + add_compile_definitions($<$:_DISABLE_STRING_ANNOTATION>) + add_compile_definitions($<$:_DISABLE_VECTOR_ANNOTATION>) + add_compile_definitions($<$:_DISABLE_OPTIONAL_ANNOTATION>) + message(STATUS "${COLOR_BLUE}MSVC AddressSanitizer will be enabled for Debug-configuration builds (runtime: ${CODAC_MSVC_ASAN_RUNTIME_DLL}).${COLOR_RESET}") + else() + message(STATUS "${COLOR_RED}No MSVC AddressSanitizer runtime found next to ${CMAKE_CXX_COMPILER} for architecture '${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}': Debug-configuration builds will not be sanitized.${COLOR_RESET}") + endif() + + # Cached so python/src/core/CMakeLists.txt and tests/CMakeLists.txt + # can reuse this result (empty string if not found) without + # repeating the detection above. + set(CODAC_MSVC_ASAN_RUNTIME_DLL ${CODAC_MSVC_ASAN_RUNTIME_DLL} CACHE INTERNAL "Path to the MSVC ASan runtime DLL, empty if unavailable") + else() + # add_compile_options() (a DIRECTORY-scope property, unlike + # target_compile_options()) does not evaluate correctly when a single + # $ generator expression's content packs more than one + # flag separated by ';' -- reproduced locally with both Ninja and + # Unix Makefiles (CMake 3.16 and 4.4.3 alike): the embedded ';' gets + # split before the genexpr is fully evaluated, corrupting it into + # literal, unrecognized compiler arguments (e.g. the literal text + # "$<1:-Wreturn-type" as one broken token). target_compile_options() + # is not affected, but this is DIRECTORY-scope by design (it must + # apply project-wide, not to one target), so instead each flag gets + # its own single-flag generator expression below. + add_compile_options($<$:-Wreturn-type>) + add_compile_options($<$:-fno-omit-frame-pointer>) + add_compile_options($<$:-g>) + add_compile_options($<$:-O0>) + + # -fsanitize=address,undefined is only added once we have actually + # confirmed that this compiler can *link* a sanitized binary, rather + # than unconditionally, the way this project used to. On Linux/macOS + # GCC/Clang, libasan/libubsan are essentially always installed + # alongside the compiler and this check is a formality; the case it + # actually guards against is MinGW-w64: the choco `mingw` package used + # by .github/workflows/windebugmatrix.yml does not ship libasan.a/ + # libubsan.a at all for some GCC versions/architectures, and neither + # x86 (i686-w64-mingw32) nor x86_64 (x86_64-w64-mingw32) is reliably + # covered across the versions that package offers -- there is no + # version/architecture combination worth hardcoding an exception list + # for. Probing capability directly, exactly like the MSVC ASan + # runtime-DLL search above, means + # a Debug configuration simply comes out unsanitized on a toolchain + # that lacks the runtime, instead of configuring successfully and then + # failing later at link time with "cannot find -lasan"/"-lubsan" on + # every single target. + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS "-fsanitize=address,undefined") + check_cxx_source_compiles("int main() { return 0; }" CODAC_COMPILER_SUPPORTS_ASAN_UBSAN) + unset(CMAKE_REQUIRED_FLAGS) + + if(CODAC_COMPILER_SUPPORTS_ASAN_UBSAN) + add_compile_options($<$:-fsanitize=address>) + add_compile_options($<$:-fsanitize=undefined>) + add_link_options($<$:-fsanitize=address,undefined>) + message(STATUS "${COLOR_BLUE}ASan/UBSan will be enabled for Debug-configuration builds.${COLOR_RESET}") + else() + message(STATUS "${COLOR_RED}No ASan/UBSan runtime found for ${CMAKE_CXX_COMPILER}: Debug-configuration builds will not be sanitized.${COLOR_RESET}") + endif() + endif() + endif() + # Temporary attempts to fix errors similar to: # _ number of sections exceeded object file format limit. # _ out of memory allocating XXX bytes. @@ -114,16 +357,122 @@ #if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) #endif() ################################################################################ -# Looking for IBEX +# Looking for GAOL ################################################################################ - find_package(IBEX REQUIRED) - ibex_init_common() # IBEX should have installed this function - message(STATUS "Found IBEX version ${IBEX_VERSION}") + # GAOL is the interval arithmetic library codac2::Interval is built upon, and + # mathlib (libultim) the library GAOL computes its elementary functions with. + # Both used to come from IBEX, which bundles them, and they were all Codac + # needed IBEX for: they are now found, or built, by Codac itself. What comes + # from IBEX in doing so, and who wrote it, is said at the top of + # scripts/CMakeModules/codac_gaol.cmake and of FindGAOL.cmake next to it. + include(codac_gaol) + + # GAOL, found or built, as the imported target Codac::gaol: its include + # directories, its libraries, and the flags floating-point rounding depends + # on, which ibex_init_common() used to put on the command line -- without + # -frounding-math and its companions, the compiler is free to constant-fold in + # the wrong rounding mode, and an interval is no longer a bound. They are + # GAOL's to say: codac_gaol_find() takes them from the CMake package of GAOL + # or from its gaol.pc, and determines them itself only for a GAOL found by its + # files alone. The Codac libraries link Codac::gaol PUBLIC, so that every + # target linking them is compiled with those flags -- the tests, the examples + # and the Python bindings included. This comes before Codac's own flags are + # added to CMAKE_CXX_FLAGS below: a GAOL built by codac_gaol_build() is handed + # CMAKE_CXX_FLAGS as it is given to this build, and chooses its flags itself. + option(ENABLE_FIND_PACKAGE_GAOL "ENABLE_FIND_PACKAGE_GAOL" ON) + codac_gaol_find() + + # Consumers need those flags as much as this build does. What Codac::gaol + # gives is kept as lists, handed over through CODAC_CXX_FLAGS in + # codac-config.cmake, and through the Cflags and Libs lines of codac.pc, which + # cannot link a target (see src/CMakeLists.txt). + codac_gaol_usage(CODAC_GAOL_INCLUDE_DIRS CODAC_INTERVAL_CXX_FLAGS CODAC_GAOL_LINK_ITEMS) + message(STATUS "Interval arithmetic flags: ${CODAC_INTERVAL_CXX_FLAGS}") + message(STATUS "GAOL include directories: ${CODAC_GAOL_INCLUDE_DIRS}") + message(STATUS "GAOL libraries: ${CODAC_GAOL_LINK_ITEMS}") + + # The flags of ibex_init_common() that are not those of interval arithmetic, + # where ibex_init_common() put them, in CMAKE_CXX_FLAGS, so that every target + # is compiled with them; handed over to consumers with the others. + codac_gaol_portability_flags(CODAC_PORTABILITY_CXX_FLAGS) + string(REPLACE ";" " " _codac_portability_cxx_flags "${CODAC_PORTABILITY_CXX_FLAGS}") + string(APPEND CMAKE_CXX_FLAGS " ${_codac_portability_cxx_flags}") + + +################################################################################ +# Treating dependencies' headers as system headers +################################################################################ + + # This project compiles with /W4 or -Wall -Wextra -Wpedantic, and a dependency + # pulled in with FetchContent inherits that level: its headers are ordinary + # include directories, not system ones. Eigen and Catch2 alone accounted for + # about 8300 MSVC warnings per job (C4459, C5054, C4308, C4324) and the 557 + # -Wc2y-extensions Clang reports for Catch2's __COUNTER__, drowning the + # handful of warnings that actually belong to Codac. + # + # FetchContent_Declare(... SYSTEM) does exactly this, but only since CMake + # 3.25, while this project accepts 3.14. Setting the property by hand is + # equivalent and has worked for far longer: the compiler then receives + # -isystem (or /external:I, which CMake already emits for the Visual Studio + # generator alongside /external:W0). + function(codac_mark_target_system tgt) + if(NOT TARGET ${tgt}) + return() + endif() + # A property cannot be set on an ALIAS, so resolve it to the target it names + # (Eigen exports Eigen3::Eigen as an alias of eigen). + get_target_property(_aliased ${tgt} ALIASED_TARGET) + if(_aliased) + set(tgt ${_aliased}) + endif() + get_target_property(_inc ${tgt} INTERFACE_INCLUDE_DIRECTORIES) + if(_inc) + set_target_properties(${tgt} PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${_inc}") + endif() + endfunction() + + + # Records the directories in which a module keeps its public headers, in + # their canonical location under src/. Every consumer (the tests, the + # examples and the Python bindings) lists CODAC_SOURCE_INCLUDE_DIRS first, + # so that a header is opened at src/... rather than through a copy in the + # build tree. Both compile identically, but the path used is the path gcov + # records: reached both ways, a header ends up as two unrelated entries in + # the coverage report, each showing only what its own callers exercised, + # and is reported well below its real coverage. + # + # The variable is a cache entry because it is filled in by src/*/CMakeLists.txt + # and read from tests/, examples/ and python/, which are separate directory + # scopes; src/CMakeLists.txt empties it before the modules append to it, so + # that a reconfigure does not append to what the previous one left behind. + function(codac_publish_include_dirs) + set(CODAC_SOURCE_INCLUDE_DIRS ${CODAC_SOURCE_INCLUDE_DIRS} ${ARGN} + CACHE INTERNAL "Directories holding the public headers of the codac modules") + endfunction() + + + # The companion of the above, for the umbrella headers CMake generates rather + # than the ones kept in the source tree: codac-core.h, codac-graphics.h and + # the rest. Each module writes its own into its own binary directory, so + # ${CMAKE_BINARY_DIR}/src -- where the tests used to look, and the only place + # they looked -- holds none of them. Only the CAPD snippet of the manual + # includes them (every other test reaches for a codac2_*.h directly), which + # is why the omission surfaced as nothing but the CAPD jobs failing on + # "codac-core.h: No such file or directory". + # + # It is a cache entry, and emptied by src/CMakeLists.txt before the modules + # fill it, for the same reasons as CODAC_SOURCE_INCLUDE_DIRS above. + function(codac_publish_generated_include_dirs) + set(CODAC_GENERATED_INCLUDE_DIRS ${CODAC_GENERATED_INCLUDE_DIRS} ${ARGN} + CACHE INTERNAL "Directories holding the umbrella headers generated for the codac modules") + endfunction() ################################################################################ @@ -141,6 +490,9 @@ FetchContent_Declare(Eigen3 URL http://github.com/codac-team/eigen/archive/refs/heads/eigen-5.0.zip) #FetchContent_Declare(Eigen3 URL ${CMAKE_CURRENT_SOURCE_DIR}/3rd/eigen-a0dc3994bd5d4e804dab58decb78c56a5af516ba.zip) # If needed to be self-contained... FetchContent_MakeAvailable(Eigen3) + # Only needed for the FetchContent path: a target coming from + # find_package() is IMPORTED, and CMake already treats those as system. + codac_mark_target_system(Eigen3::Eigen) endif() # Adds Eigen3::Eigen @@ -148,6 +500,11 @@ # Looking for Threads ################################################################################ + # The same preference the generated codac-config.cmake asks of consumers, so + # that both sides of a link end up with the same answer: without it this + # build could settle on -lpthread while a consumer, which does set it, got + # -pthread. + set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) ################################################################################ @@ -160,6 +517,158 @@ find_package(CAPD REQUIRED) endif() +################################################################################ +# Code coverage +################################################################################ + + # WITH_COVERAGE instruments the build and adds two targets: `coverage`, which + # runs the whole ctest suite and turns the counters gcov leaves behind into a + # report, and `coverage-report`, which only does the second half. That is what + # the "Ubuntu 26.04 GCC x86_64 Release coverage" job of + # .github/workflows/unixdebug.yml now calls, so that a developer measuring + # coverage by hand and the job reporting it use the same flags, the same gcov + # and the same filters, and cannot drift apart. Deciding all of that here + # rather than leaving it to the caller is also what spares everyone the traps + # below. + # + # Coverage answers "what do the tests reach", so it is worth turning + # BUILD_TESTS and TEST_EXAMPLES on with it -- the examples are integration + # tests and reach code the unit tests do not. + # + # This block has to come before the add_subdirectory() calls below: + # add_compile_options() is a directory property, and only reaches the targets + # created after it. Declared any later, it configures cleanly, builds + # cleanly, and leaves no .gcno file behind at all -- an empty report rather + # than an error. + option(WITH_COVERAGE "Instrument the build for code coverage (GCC/Clang)" OFF) + + if(WITH_COVERAGE) + + if(MSVC) + message(FATAL_ERROR "WITH_COVERAGE relies on gcov, which MSVC does not provide.") + endif() + + add_compile_options(--coverage) + add_link_options(--coverage) + + # pybind11 adds -flto to the modules it builds, and GCC cannot combine that + # with gcov instrumentation: the link then fails on an undefined vtable, + # the vtable being emitted nowhere once both are on. pybind11 leaves LTO + # alone as soon as CMAKE_INTERPROCEDURAL_OPTIMIZATION is set, which is what + # this does -- silently, because a coverage build that does not link is of + # no use to anyone. + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF) + + # gcov itself is toolchain-specific: the notes and counters Clang writes are + # read by `llvm-cov gcov`, not by GCC's gcov, and pointing gcovr at the + # wrong one yields an empty report rather than an error. The one shipped + # next to the compiler in use is therefore named explicitly. + set(CODAC_GCOV_ARG "") + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # Debian and its derivatives ship the tools of each LLVM release under a + # versioned name in /usr/bin and unversioned in the release's own prefix, + # so both spellings and both places are tried, the version being taken + # from the compiler that will produce the notes. + get_filename_component(_codac_cxx_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + string(REGEX MATCH "^[0-9]+" _codac_llvm_major "${CMAKE_CXX_COMPILER_VERSION}") + find_program(LLVM_COV_EXECUTABLE + NAMES llvm-cov llvm-cov-${_codac_llvm_major} + HINTS ${_codac_cxx_dir} + /usr/lib/llvm-${_codac_llvm_major}/bin) + if(LLVM_COV_EXECUTABLE) + set(CODAC_GCOV_ARG --gcov-executable "${LLVM_COV_EXECUTABLE} gcov") + else() + message(STATUS "${COLOR_RED}llvm-cov was not found next to ${CMAKE_CXX_COMPILER}: the coverage report would come out empty.${COLOR_RESET}") + endif() + endif() + + find_program(GCOVR_EXECUTABLE gcovr) + + if(NOT GCOVR_EXECUTABLE) + + message(STATUS "${COLOR_RED}gcovr was not found: the build is instrumented, but the `coverage` target is unavailable (pip install gcovr).${COLOR_RESET}") + + else() + + # A note on paths, since it decides what the report is worth: gcov + # records the path each translation unit actually opened a header + # through, so a header reached both directly and through a copy in + # the build tree comes out as two unrelated entries, each showing + # only what its own callers exercised. Every consumer (tests, + # examples, Python bindings) therefore lists CODAC_SOURCE_INCLUDE_DIRS + # first -- see codac_publish_include_dirs() above. Without that, a + # header was reported well below its real coverage, and every header + # of graphics/, unsupported/ and extensions/ was measured on a + # build-tree copy that is not the file anyone edits. + # + # --gcov-exclude-directories keeps gcovr from even descending into the + # dependencies FetchContent puts in the build tree, which --exclude alone + # would only drop from the finished report. It matters beyond noise: + # Catch2 compiles with -ffile-prefix-map, so the paths recorded in its + # notes do not resolve from anywhere and gcov fails outright on them; and + # gcov reports negative hit counts on heavily inlined code, Eigen being + # where that first shows, which gcovr treats as fatal unless told + # otherwise. The pattern has no leading ".*/" on purpose: gcovr matches it + # against a path relative to the working directory, which here starts with + # "_deps/" and so has nothing for that slash to match. + set(CODAC_GCOVR_COMMAND + ${GCOVR_EXECUTABLE} + --root ${CMAKE_SOURCE_DIR} + ${CODAC_GCOV_ARG} + --gcov-exclude-directories ".*_deps.*" + --gcov-ignore-parse-errors=negative_hits.warn_once_per_file + --exclude ".*/tests/.*" + --exclude ".*/examples/.*" + --exclude ".*/python/.*" + --print-summary + --html-details ${CMAKE_BINARY_DIR}/coverage.html + --xml ${CMAKE_BINARY_DIR}/coverage.xml + # Where to look for the counters. Without it gcovr falls back on + # --root, i.e. the source tree, and walks every build directory + # sitting under it: a second configuration kept next to this one + # (a Debug tree, an older coverage tree) then has its own .gcda + # read into this report, mixing counters produced by a different + # build of different sources. "." rather than an absolute path + # because the working directory below is already the build tree, + # and because --gcov-exclude-directories above matches against + # paths relative to it. + .) + + # Two targets rather than one, because the counters gcov leaves behind + # accumulate across runs and are read independently of what produced + # them: `coverage` is the whole thing, `coverage-report` re-reads whatever + # the last ctest invocation happened to touch. The second is what to use + # after running part of the suite by hand -- ctest -R something -- which + # is the usual way of asking what one test reaches. + # + # VERBATIM on both, because the arguments above are regular expressions: + # without it CMake hands them to the shell unquoted, which expands + # ".*/tests/.*" as a file pattern -- gcovr then receives "./tests/.." and + # refuses it. + add_custom_target(coverage + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure + COMMAND ${CODAC_GCOVR_COMMAND} + COMMAND ${CMAKE_COMMAND} -E echo + "Coverage report: ${CMAKE_BINARY_DIR}/coverage.html (and coverage.xml, Cobertura)" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Running the tests and measuring what they reach" + VERBATIM) + + add_custom_target(coverage-report + COMMAND ${CODAC_GCOVR_COMMAND} + COMMAND ${CMAKE_COMMAND} -E echo + "Coverage report: ${CMAKE_BINARY_DIR}/coverage.html (and coverage.xml, Cobertura)" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Turning the counters left by the last test run into a report" + VERBATIM) + + message(STATUS "${COLOR_BLUE}Coverage instrumentation enabled; run `make coverage` to produce the report.${COLOR_RESET}") + + endif() + + endif() + + ################################################################################ # Compile sources ################################################################################ diff --git a/doc/manual/manual/functions/parallelepiped/src.cpp b/doc/manual/manual/functions/parallelepiped/src.cpp index 983252d3e..6d3bbaff2 100644 --- a/doc/manual/manual/functions/parallelepiped/src.cpp +++ b/doc/manual/manual/functions/parallelepiped/src.cpp @@ -1,10 +1,23 @@ -#include +/** + * Codac tests + * ---------------------------------------------------------------------------- + * \date 2025 + * \author Maël Godard + * \copyright Copyright 2024 Codac Team + * \license GNU Lesser General Public License (LGPL) + */ + +#include +#include +#include +#include +#include +#include using namespace std; using namespace codac2; - -int main() +TEST_CASE("Parallelepiped evaluation - manual") { // 2D case @@ -72,8 +85,8 @@ int main() // [5-end] // [6-beg] - AnalyticTraj f_lb (AnalyticFunction({X_if},1.1*sqr(X_if)),Interval(-2.0,2.0)); - AnalyticTraj f_ub (AnalyticFunction({X_if},1.2*sqr(X_if)),Interval(-2.0,2.0)); + AnalyticTraj f_lb (Interval(-2.0,2.0),AnalyticFunction({X_if},1.1*sqr(X_if))); + AnalyticTraj f_ub (Interval(-2.0,2.0),AnalyticFunction({X_if},1.2*sqr(X_if))); DefaultFigure::plot_trajectory(f_lb.sampled(0.01)); DefaultFigure::plot_trajectory(f_ub.sampled(0.01)); @@ -88,8 +101,8 @@ int main() Interval X0_if(x0_if, x0_if+dx_if); auto p = f_if.parallelepiped_eval(X0_if); DefaultFigure::draw_parallelepiped(p, Color::dark_green()); - + x0_if+=dx_if; } // [7-end] -} \ No newline at end of file +} diff --git a/doc/manual/manual/functions/parallelepiped/src.py b/doc/manual/manual/functions/parallelepiped/src.py index 12bc442d4..66b473959 100644 --- a/doc/manual/manual/functions/parallelepiped/src.py +++ b/doc/manual/manual/functions/parallelepiped/src.py @@ -59,8 +59,8 @@ # [5-end] # [6-beg] -f_lb = AnalyticTraj(AnalyticFunction([X_if],1.1*sqr(X_if)),Interval(-2.0,2.0)) -f_ub = AnalyticTraj(AnalyticFunction([X_if],1.2*sqr(X_if)),Interval(-2.0,2.0)) +f_lb = AnalyticTraj(Interval(-2.0,2.0),AnalyticFunction([X_if],1.1*sqr(X_if))) +f_ub = AnalyticTraj(Interval(-2.0,2.0),AnalyticFunction([X_if],1.2*sqr(X_if))) DefaultFigure.plot_trajectory(f_lb.sampled(0.01)) DefaultFigure.plot_trajectory(f_ub.sampled(0.01)) diff --git a/doc/manual/manual/functions/peibos/src.cpp b/doc/manual/manual/functions/peibos/src.cpp index 118833a8e..ea9fdcf56 100644 --- a/doc/manual/manual/functions/peibos/src.cpp +++ b/doc/manual/manual/functions/peibos/src.cpp @@ -8,6 +8,8 @@ */ #include +#include +#include #include using namespace std; diff --git a/doc/manual/tuto/cp_robotics/src/CMakeLists.txt b/doc/manual/tuto/cp_robotics/src/CMakeLists.txt index 51161a85e..ad8a88fc7 100644 --- a/doc/manual/tuto/cp_robotics/src/CMakeLists.txt +++ b/doc/manual/tuto/cp_robotics/src/CMakeLists.txt @@ -8,16 +8,6 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Adding IBEX - - # In case you installed IBEX in a local directory, you need - # to specify its path with the CMAKE_PREFIX_PATH option. - # set(CMAKE_PREFIX_PATH "~/ibex-lib/build_install") - - find_package(IBEX REQUIRED) - ibex_init_common() # IBEX should have installed this function - message(STATUS "Found IBEX version ${IBEX_VERSION}") - # Adding Codac # In case you installed Codac in a local directory, you need @@ -38,4 +28,4 @@ ) target_compile_options(${PROJECT_NAME} PUBLIC ${CODAC_CXX_FLAGS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${CODAC_INCLUDE_DIRS}) - target_link_libraries(${PROJECT_NAME} PUBLIC ${CODAC_LIBRARIES} Ibex::ibex) \ No newline at end of file + target_link_libraries(${PROJECT_NAME} PUBLIC ${CODAC_LIBRARIES}) \ No newline at end of file diff --git a/examples/00_graphics/CMakeLists.txt b/examples/00_graphics/CMakeLists.txt index cf4fae020..95b125893 100644 --- a/examples/00_graphics/CMakeLists.txt +++ b/examples/00_graphics/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/01_batman/CMakeLists.txt b/examples/01_batman/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/01_batman/CMakeLists.txt +++ b/examples/01_batman/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/02_centered_form/CMakeLists.txt b/examples/02_centered_form/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/02_centered_form/CMakeLists.txt +++ b/examples/02_centered_form/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/03_sivia/CMakeLists.txt b/examples/03_sivia/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/03_sivia/CMakeLists.txt +++ b/examples/03_sivia/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/04_explored_area/CMakeLists.txt b/examples/04_explored_area/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/04_explored_area/CMakeLists.txt +++ b/examples/04_explored_area/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/05_capd_solver/CMakeLists.txt b/examples/05_capd_solver/CMakeLists.txt index 4d720d238..2d3daae24 100644 --- a/examples/05_capd_solver/CMakeLists.txt +++ b/examples/05_capd_solver/CMakeLists.txt @@ -8,16 +8,6 @@ project(codac_example LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Adding IBEX - -# In case you installed IBEX in a local directory, you need -# to specify its path with the CMAKE_PREFIX_PATH option. -# set(CMAKE_PREFIX_PATH "~/ibex-lib/build_install") - -find_package(IBEX REQUIRED) -ibex_init_common() # IBEX should have installed this function -message(STATUS "Found IBEX version ${IBEX_VERSION}") - # Adding Codac # In case you installed Codac in a local directory, you need @@ -41,5 +31,4 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${CODAC_LIBRARIES} ${CODAC_CAPD_LIBRARY} capd::capd - Ibex::ibex ) \ No newline at end of file diff --git a/examples/06_graphics_3D/CMakeLists.txt b/examples/06_graphics_3D/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/06_graphics_3D/CMakeLists.txt +++ b/examples/06_graphics_3D/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/07_centered_2D/CMakeLists.txt b/examples/07_centered_2D/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/07_centered_2D/CMakeLists.txt +++ b/examples/07_centered_2D/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/08_centered_3D/CMakeLists.txt b/examples/08_centered_3D/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/08_centered_3D/CMakeLists.txt +++ b/examples/08_centered_3D/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/09_robot_simu/CMakeLists.txt b/examples/09_robot_simu/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/09_robot_simu/CMakeLists.txt +++ b/examples/09_robot_simu/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/09_robot_simu/main.cpp b/examples/09_robot_simu/main.cpp index 5882ff8d8..e23a55d08 100644 --- a/examples/09_robot_simu/main.cpp +++ b/examples/09_robot_simu/main.cpp @@ -14,8 +14,8 @@ int main() Figure2D g("Robot simulation", GraphicOutput::VIBES | GraphicOutput::IPE); g.set_axes( - {0,x.codomain()[0].inflate(1.)}, - {1,x.codomain()[1].inflate(1.)} + axis(0,x.codomain()[0].inflate(1.)), + axis(1,x.codomain()[1].inflate(1.)) ).auto_scale(); g.draw_tank(x(5.), 0.5, {Color::black(),Color::yellow()}); diff --git a/examples/10_lie_groups/CMakeLists.txt b/examples/10_lie_groups/CMakeLists.txt index 41d3ccba3..87b2ee865 100644 --- a/examples/10_lie_groups/CMakeLists.txt +++ b/examples/10_lie_groups/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/11_peibos/CMakeLists.txt b/examples/11_peibos/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/11_peibos/CMakeLists.txt +++ b/examples/11_peibos/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/11_peibos/main.cpp b/examples/11_peibos/main.cpp index e55ad6d72..9d0701fa4 100644 --- a/examples/11_peibos/main.cpp +++ b/examples/11_peibos/main.cpp @@ -22,7 +22,7 @@ int main() Figure2D figure_2d ("Henon Map", GraphicOutput::VIBES); figure_2d.set_window_properties({25,50},{500,500}); - figure_2d.set_axes({0,{-1.4,2.2}}, {1,{-0.4,0.3}}); + figure_2d.set_axes(axis(0,{-1.4,2.2}), axis(1,{-0.4,0.3})); for (const auto& p : v_par_2d) { @@ -49,7 +49,7 @@ int main() Figure2D figure_3d_proj ("Conform projected", GraphicOutput::VIBES); figure_3d_proj.set_window_properties({25,600},{500,500}); - figure_3d_proj.set_axes({0,{-1.5,2.5}}, {1,{-2,2}}); + figure_3d_proj.set_axes(axis(0,{-1.5,2.5}), axis(1,{-2,2})); auto v_par_3d = PEIBOS(f_3d, psi0_3d, {id_3d,s1,s1*s1,s1.invert(),s2,s2.invert()}, 0.05, true); diff --git a/examples/12_peibos_capd/CMakeLists.txt b/examples/12_peibos_capd/CMakeLists.txt index 9091ca50a..efb53fdfa 100644 --- a/examples/12_peibos_capd/CMakeLists.txt +++ b/examples/12_peibos_capd/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) @@ -35,5 +31,4 @@ ${CODAC_LIBRARIES} ${CODAC_CAPD_LIBRARY} capd::capd - Ibex::ibex ) \ No newline at end of file diff --git a/examples/13_qinter/CMakeLists.txt b/examples/13_qinter/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/13_qinter/CMakeLists.txt +++ b/examples/13_qinter/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/14_lohner/CMakeLists.txt b/examples/14_lohner/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/14_lohner/CMakeLists.txt +++ b/examples/14_lohner/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/15_sympy/CMakeLists.txt b/examples/15_sympy/CMakeLists.txt index 52197a061..336db6b62 100644 --- a/examples/15_sympy/CMakeLists.txt +++ b/examples/15_sympy/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) @@ -37,5 +33,4 @@ ${CODAC_LIBRARIES} ${CODAC_SYMPY_LIBRARY} # linking to the codac-sympy extension pybind11::embed # linking to pybind11 - Ibex::ibex ) \ No newline at end of file diff --git a/examples/16_visibility/CMakeLists.txt b/examples/16_visibility/CMakeLists.txt index cf9ef35ce..23d13fc6e 100644 --- a/examples/16_visibility/CMakeLists.txt +++ b/examples/16_visibility/CMakeLists.txt @@ -17,10 +17,6 @@ find_package(CODAC REQUIRED) message(STATUS "Found Codac version ${CODAC_VERSION}") -# Initializating Ibex - - ibex_init_common() - # Compilation if(FAST_RELEASE) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2534bcf5f..69356542c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,17 +6,174 @@ if(BUILD_TESTS AND TEST_EXAMPLES) -# add_test(NAME cpp_01_getting_started -# COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tuto/01_getting_started/build/01_getting_started 0) -# if(WITH_PYTHON) -# add_test(NAME py_01_getting_started -# COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/tuto/01_getting_started/01_getting_started.py 0) -# endif() -# -# if(WITH_CAPD) -# # Lie group -# add_test(NAME lie_05 -# COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/lie_group/05_loc/build/codac_lie_05 0) -# endif() - -endif() \ No newline at end of file + # Every example ships a standalone CMakeLists.txt, so that a user can build it + # on its own against an installed codac. Those projects cannot be pulled in + # with add_subdirectory(): each calls project() and find_package(CODAC), which + # would look for a codac that is precisely the one being built. The examples + # are therefore rebuilt here from the same sources, against the in-tree + # library, the way tests/CMakeLists.txt builds the unit tests. Only the public + # headers are visible to them -- the generated umbrella "codac" and the + # installed headers next to it -- so an example that stops compiling here is + # an example a user could no longer build either. + # + # The lists below mirror the add_executable() calls of those standalone + # CMakeLists.txt: an example directory may hold sources that its own project + # does not build (02_centered_form/main_parabolas.cpp, for instance), and + # those are deliberately left out rather than silently compiled. + + set(CODAC_EXAMPLES_CPP + 00_graphics/graphic_examples + 00_graphics/graphic_animation + 01_batman/main + 02_centered_form/main + 03_sivia/main + 04_explored_area/main + 06_graphics_3D/main + 07_centered_2D/main + 08_centered_3D/main + 09_robot_simu/main + 11_peibos/main + 13_qinter/main + 14_lohner/main + 16_visibility/main + ellipsoid_example/main + ) + + set(CODAC_EXAMPLES_PY + 00_graphics/graphic_examples + 00_graphics/graphic_animation + 00_graphics/graphic_colors + 02_centered_form/main + 02_centered_form/main_parabolas + 03_sivia/main + 04_explored_area/main + 06_graphics_3D/main + 09_robot_simu/main + 11_peibos/main + 13_qinter/main + 14_lohner/main + 16_visibility/main + custom_sep/coloration + custom_sep/custom_ctc + custom_sep/custom_sep + ellipsoid_example/main + ) + + set(CODAC_EXAMPLE_LIBRARIES ${PROJECT_NAME}-core ${PROJECT_NAME}-graphics) + + if(WITH_CAPD) + # 10_lie_groups is in this list rather than in the one above because it + # includes , which its own CMakeLists.txt does not say: the + # standalone project builds it only where a codac with CAPD is installed. + list(APPEND CODAC_EXAMPLES_CPP + 05_capd_solver/main + 10_lie_groups/lie_01 + 12_peibos_capd/main + ) + list(APPEND CODAC_EXAMPLE_LIBRARIES ${PROJECT_NAME}-capd capd::capd) + endif() + + # BUILD_SYMPY_EMBED_TESTS is decided in tests/CMakeLists.txt, which the + # top-level CMakeLists.txt adds before this directory. It is turned off + # wherever PYBIND11_FINDPYTHON=OFF, which is every configuration this + # project's CI builds, so the sympy example follows the sympy unit test + # rather than being built on its own terms. + if(WITH_PYTHON AND BUILD_SYMPY_EMBED_TESTS) + list(APPEND CODAC_EXAMPLES_CPP 15_sympy/main) + list(APPEND CODAC_EXAMPLES_PY 15_sympy/main) + list(APPEND CODAC_EXAMPLE_LIBRARIES ${PROJECT_NAME}-sympy) + endif() + + # The examples write their figures (JSON for VIBes, .ipe, .png, .obj) into the + # current directory, so they are run from the build tree rather than from the + # sources they were copied from. Assets they read are found through __FILE__, + # which stays an absolute path, so nothing has to be copied over. + set(CODAC_EXAMPLES_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/output) + file(MAKE_DIRECTORY ${CODAC_EXAMPLES_OUTPUT_DIR}) + + foreach(SRC_EXAMPLE ${CODAC_EXAMPLES_CPP}) + + string(REPLACE "/" "_" EXAMPLE_NAME ${SRC_EXAMPLE}) + set(EXAMPLE_NAME codac2_examples_${EXAMPLE_NAME}) + + add_executable( + ${EXAMPLE_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/${SRC_EXAMPLE}.cpp + ) + + target_include_directories( + ${EXAMPLE_NAME} + SYSTEM PUBLIC + + # First, so that a header is opened at src/... rather than through + # build/include: the path a translation unit used is the path gcov records, + # and the two spellings would split every header into two entries in the + # coverage report. See codac_publish_include_dirs() in the top-level CMakeLists.txt. + ${CODAC_SOURCE_INCLUDE_DIRS} + + ${CMAKE_BINARY_DIR}/src # the generated "codac" umbrella header + ${CODAC_GENERATED_INCLUDE_DIRS} # and the per-module ones (codac-core.h, codac-graphics.h, etc.), each in its own module's binary directory + ) + + target_link_libraries( + ${EXAMPLE_NAME} + PUBLIC + + ${CODAC_EXAMPLE_LIBRARIES} + ) + + # The sympy example runs an embedded Python interpreter, which codac-sympy + # calls into: it links pybind11::embed, as its own CMakeLists.txt does and as + # the sympy unit test does in tests/CMakeLists.txt. + if(SRC_EXAMPLE MATCHES "^15_sympy/") + target_link_libraries(${EXAMPLE_NAME} PRIVATE pybind11::embed) + endif() + + add_test( + NAME ${EXAMPLE_NAME}_cpp + COMMAND ${EXAMPLE_NAME} + WORKING_DIRECTORY ${CODAC_EXAMPLES_OUTPUT_DIR} + ) + + if(CODAC_CPP_TEST_ENV) + set_tests_properties(${EXAMPLE_NAME}_cpp PROPERTIES + ENVIRONMENT "${CODAC_CPP_TEST_ENV}" + ) + endif() + + # The examples are part of the ctest suite here, so `make check` -- which + # rebuilds the suite before running it -- has to build them too. Without + # this it would run ctest against executables that may not exist yet, the + # way it does for the unit tests through the same call in + # tests/CMakeLists.txt. + if(TARGET check) + add_dependencies(check ${EXAMPLE_NAME}) + endif() + + endforeach() + + if(WITH_PYTHON) + + foreach(SRC_EXAMPLE ${CODAC_EXAMPLES_PY}) + + string(REPLACE "/" "_" EXAMPLE_NAME ${SRC_EXAMPLE}) + set(EXAMPLE_NAME codac2_examples_${EXAMPLE_NAME}) + + # PYTHON_TEST_ENV_ARGS carries whatever the Python interpreter needs to + # load a sanitized or a Windows build (LD_PRELOAD, PATH...); it is built + # in tests/CMakeLists.txt and exported from there. + add_test( + NAME ${EXAMPLE_NAME}_py + COMMAND + ${CMAKE_COMMAND} -E env + ${PYTHON_TEST_ENV_ARGS} + ${PYTHON_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/${SRC_EXAMPLE}.py + WORKING_DIRECTORY ${CODAC_EXAMPLES_OUTPUT_DIR} + ) + + endforeach() + + endif() + +endif() diff --git a/examples/custom_sep/custom_sep.py b/examples/custom_sep/custom_sep.py index 4bb71a10c..36cffa7bd 100644 --- a/examples/custom_sep/custom_sep.py +++ b/examples/custom_sep/custom_sep.py @@ -36,5 +36,6 @@ def separate(self, x): # # c = p.connected_subsets(outer_complem) -for bi in c[1].boxes(): - DefaultFigure.draw_box(bi,[Color.red(),Color.red()]) \ No newline at end of file +for ci in c: + for bi in ci.boxes(): + DefaultFigure.draw_box(bi,[Color.red(),Color.red()]) \ No newline at end of file diff --git a/examples/ellipsoid_example/CMakeLists.txt b/examples/ellipsoid_example/CMakeLists.txt index ce82f2152..ad0a314c7 100644 --- a/examples/ellipsoid_example/CMakeLists.txt +++ b/examples/ellipsoid_example/CMakeLists.txt @@ -8,16 +8,6 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Adding IBEX - - # In case you installed IBEX in a local directory, you need - # to specify its path with the CMAKE_PREFIX_PATH option. - # set(CMAKE_PREFIX_PATH "~/ibex-lib/build_install") - - find_package(IBEX REQUIRED) - ibex_init_common() # IBEX should have installed this function - message(STATUS "Found IBEX version ${IBEX_VERSION}") - # Adding Eigen3 # In case you installed Eigen3 in a local directory, you need @@ -41,4 +31,4 @@ add_executable(${PROJECT_NAME} main.cpp) target_compile_options(${PROJECT_NAME} PUBLIC ${CODAC_CXX_FLAGS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${CODAC_INCLUDE_DIRS}) - target_link_libraries(${PROJECT_NAME} PUBLIC ${CODAC_LIBRARIES} Ibex::ibex Eigen3::Eigen) + target_link_libraries(${PROJECT_NAME} PUBLIC ${CODAC_LIBRARIES} Eigen3::Eigen) diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index bab5e84ba..845097694 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -17,7 +17,9 @@ # Adds pybind11::headers, pybind11::module, pybind11::embed set(PYTHON_PACKAGE_NAME ${PROJECT_NAME}) - set(PYTHON_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/python_package") + set(PYTHON_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/python_package" + CACHE INTERNAL "Codac Python package build directory" + ) file(MAKE_DIRECTORY ${PYTHON_PACKAGE_DIR}) execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/${PYTHON_PACKAGE_NAME}/" "${PYTHON_PACKAGE_DIR}/${PYTHON_PACKAGE_NAME}") diff --git a/python/src/core/CMakeLists.txt b/python/src/core/CMakeLists.txt index 9d83a210d..dca29011e 100644 --- a/python/src/core/CMakeLists.txt +++ b/python/src/core/CMakeLists.txt @@ -133,8 +133,13 @@ ) target_include_directories(_core + # First, so that a header is opened at src/... rather than through + # build/include: the path a translation unit used is the path gcov records, + # and the two spellings would split every header into two entries in the + # coverage report. See codac_publish_include_dirs() in the top-level CMakeLists.txt. + PRIVATE ${CODAC_SOURCE_INCLUDE_DIRS} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../../docstring - PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../../../include + PRIVATE ${CMAKE_BINARY_DIR}/src PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/actions/ @@ -157,9 +162,65 @@ ) target_link_libraries(_core - PRIVATE ${PROJECT_NAME}-core ${PROJECT_NAME}-sympy ${LIBS} Ibex::ibex + PRIVATE ${PROJECT_NAME}-core ${PROJECT_NAME}-sympy Threads::Threads Codac::gaol ) + # -------------------------------------------------------------- + # Sanitizer runtime as a shared library (Debug builds only) + # -------------------------------------------------------------- + # + # _core.so is loaded via dlopen() by an unsanitized python + # interpreter. By default Clang/GCC embed the ASan/UBSan runtime + # statically into shared libraries, which leaves symbols like + # __ubsan_vptr_type_cache or __asan_* unresolved at dlopen() time + # (python has no sanitizer runtime of its own to provide them). + # -shared-libsan makes _core.so depend on the sanitizer runtime as + # a proper shared library instead; that runtime is then LD_PRELOAD-ed + # for the python tests (see tests/CMakeLists.txt). + # Not gated behind CMAKE_BUILD_TYPE (see the matching note in the root + # CMakeLists.txt): that variable is meaningless for multi-configuration + # generators, so -shared-libsan is applied via a $ + # generator expression instead, which CMake evaluates correctly for + # both single- and multi-config generators. + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(_core PRIVATE $<$:-shared-libsan>) + target_link_options(_core PRIVATE $<$:-shared-libsan>) + endif() + + # -------------------------------------------------------------- + # MSVC AddressSanitizer runtime DLL (Debug builds only) + # -------------------------------------------------------------- + # + # /fsanitize=address (enabled in Debug mode only if a matching runtime + # was found -- see the MSVC branch in the root CMakeLists.txt, which + # sets CODAC_MSVC_ASAN_RUNTIME_DLL) makes _core.pyd import that runtime + # DLL. Unlike Clang/GCC on Linux, there is no LD_PRELOAD-style env var + # and no "must be the first library loaded" requirement to work around + # on Windows -- the loader simply needs to be able to find that DLL when + # Python calls LoadLibrary() on _core.pyd, which it does by searching + # the directory of the loading module before PATH. So it is enough to + # copy the runtime DLL next to the built _core.pyd, the same way + # _core.pyd itself is already copied into the package folder below. + # CODAC_MSVC_ASAN_RUNTIME_DLL is already empty (and this is then a + # no-op) whenever the root CMakeLists.txt couldn't locate that runtime, + # so /fsanitize=address was in that case never even added. + if(MSVC AND CODAC_MSVC_ASAN_RUNTIME_DLL) + # CODAC_MSVC_ASAN_RUNTIME_DLL is now found regardless of which + # configuration will be built (see the root CMakeLists.txt), since a + # multi-config generator does not know that yet at configure time. So + # the copy itself has to stay conditional on the configuration that is + # actually being built, which a plain if() cannot express: + # $,copy_if_different,echo> selects between the real + # "-E copy_if_different" and "-E echo", which merely prints the two + # paths, so the DLL is only copied for a Debug-configuration build, in + # any generator. "-E true" would read better, but it only exists since + # CMake 3.16 while this project accepts 3.14. + add_custom_command(TARGET _core POST_BUILD + COMMAND ${CMAKE_COMMAND} -E $,copy_if_different,echo> "${CODAC_MSVC_ASAN_RUNTIME_DLL}" "${PYTHON_PACKAGE_DIR}/${PYTHON_PACKAGE_NAME}" + ) + endif() + + # Copy the generated library in the package folder add_custom_command(TARGET _core POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$" "${PYTHON_PACKAGE_DIR}/${PYTHON_PACKAGE_NAME}" diff --git a/python/src/graphics/CMakeLists.txt b/python/src/graphics/CMakeLists.txt index 7b180a814..b35a6683c 100644 --- a/python/src/graphics/CMakeLists.txt +++ b/python/src/graphics/CMakeLists.txt @@ -18,8 +18,13 @@ ) target_include_directories(_graphics + # First, so that a header is opened at src/... rather than through + # build/include: the path a translation unit used is the path gcov records, + # and the two spellings would split every header into two entries in the + # coverage report. See codac_publish_include_dirs() in the top-level CMakeLists.txt. + PRIVATE ${CODAC_SOURCE_INCLUDE_DIRS} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../../docstring - PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../../../include + PRIVATE ${CMAKE_BINARY_DIR}/src PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../core/ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ @@ -28,7 +33,7 @@ ) target_link_libraries(_graphics - PRIVATE ${PROJECT_NAME}-graphics ${LIBS} Ibex::ibex + PRIVATE ${PROJECT_NAME}-graphics Threads::Threads Codac::gaol ) # Copy the generated library in the package folder diff --git a/python/src/unsupported/CMakeLists.txt b/python/src/unsupported/CMakeLists.txt index a413804dd..6b3e93ec6 100644 --- a/python/src/unsupported/CMakeLists.txt +++ b/python/src/unsupported/CMakeLists.txt @@ -9,14 +9,19 @@ ) target_include_directories(_unsupported + # First, so that a header is opened at src/... rather than through + # build/include: the path a translation unit used is the path gcov records, + # and the two spellings would split every header into two entries in the + # coverage report. See codac_publish_include_dirs() in the top-level CMakeLists.txt. + PRIVATE ${CODAC_SOURCE_INCLUDE_DIRS} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../../docstring - PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../../../include + PRIVATE ${CMAKE_BINARY_DIR}/src PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ ) target_link_libraries(_unsupported - PRIVATE ${PROJECT_NAME}-unsupported ${LIBS} Ibex::ibex + PRIVATE ${PROJECT_NAME}-unsupported Threads::Threads Codac::gaol ) # Copy the generated library in the package folder diff --git a/scripts/CMakeModules/FindGAOL.cmake b/scripts/CMakeModules/FindGAOL.cmake new file mode 100644 index 000000000..a8f44ae50 --- /dev/null +++ b/scripts/CMakeModules/FindGAOL.cmake @@ -0,0 +1,208 @@ +# ================================================================== +# Codac - cmake module looking for an installed GAOL +# ================================================================== +# +# Looks for GAOL, the interval arithmetic library Codac is built upon +# (Frederic Goualard, https://frederic.goualard.net, +# https://github.com/goualard-f/GAOL), and for mathlib, the IBM Accurate +# Portable Mathematical Library GAOL computes its elementary functions with, +# which installs itself as libultim. +# +# Where to look, when GAOL is not installed in a directory CMake searches by +# default, is given by two cache entries -- or by CMAKE_PREFIX_PATH: +# +# GAOL_DIR prefix holding include/gaol/gaol.h and lib/ +# MATHLIB_DIR prefix holding include/MathLib.h and lib/; +# GAOL_DIR is searched for mathlib as well, the two being +# usually installed together +# +# What is found is written to the cache entries GAOL_INCDIR, GAOL_LIB, +# MATHLIB_INCDIR and MATHLIB_LIB, which can also be set by hand to bypass the +# search, and summarised in: +# +# GAOL_FOUND +# GAOL_VERSION when gaol/gaol_configuration.h states it +# GAOL_INCLUDE_DIRS +# GAOL_LIBRARIES gaol, then ultim, in link order +# +# No target is defined here. codac_gaol_find() (scripts/CMakeModules/codac_gaol.cmake) +# makes Codac::gaol out of these variables, with the flags of interval +# arithmetic it determines itself, for a GAOL that has neither a CMake package +# nor a gaol.pc, which it looks for first. +# +# Origin +# ------ +# This is the search IBEX runs before building a GAOL of its own, taken out of +# IBEX (https://github.com/ibex-team/ibex-lib, GNU LGPL v3) so that Codac can +# find GAOL without IBEX: +# +# - codac_find_header_custom() and codac_find_library_custom() are the +# functions find_header_custom() and find_library_custom() of IBEX's +# cmake.utils/IbexUtils.cmake, written by Cyril Bouvier for the CMake build +# of IBEX. They are renamed with a codac_ prefix so as not to clash with +# IBEX's own definitions in a project that loads both, and differ in +# two respects: they stay silent when find_package(GAOL QUIET) asks for it, +# and a header found on the default search path is returned as it is, +# rather than wrapped in $ -- that wrapping is how IBEX +# keeps such a directory out of the export files it generates, and Codac +# writes its configuration file by other means (see src/CMakeLists.txt). +# +# - The four calls below, the GAOL_DIR and MATHLIB_DIR hints and the names of +# the result variables are those of IBEX's +# interval_lib_wrapper/gaol/CMakeLists.txt, by Cyril Bouvier as well. +# +# - Reading the version out of gaol/gaol_configuration.h comes from +# interval_lib_wrapper/gaol/FindGaol.cmake of IBEX (Cyril Bouvier). It +# reads the GAOL_MAJOR_VERSION, GAOL_MINOR_VERSION and GAOL_MICRO_VERSION +# macros here, which both the autotools and the CMake builds of GAOL write. + + +################################################################################ +# Functions from IBEX (see above) +################################################################################ + + function(codac_find_header_custom prefix hdrname) + set(opt "") + set(oneArgs "") + set(multiArgs PATHS) + + cmake_parse_arguments(FHC "${opt}" "${oneArgs}" "${multiArgs}" ${ARGN}) + + if(FHC_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown keywords given to codac_find_header_custom(): \"${FHC_UNPARSED_ARGUMENTS}\"") + endif() + + set(MSG "Looking for ${hdrname}") + if(NOT GAOL_FIND_QUIETLY) + message(STATUS "${MSG}") + endif() + + # First look only in PATHS if given + if(FHC_PATHS) + find_path(${prefix}_INCDIR ${hdrname} PATHS ${FHC_PATHS} + DOC "Set to exact include directory to bypass internal test" + PATH_SUFFIXES include NO_DEFAULT_PATH) + endif() + + if(NOT ${prefix}_INCDIR) + # Now look with system and cmake paths + find_path(${prefix}_INCDIR ${hdrname} + DOC "Set to exact include directory to bypass internal test" + PATH_SUFFIXES include) + endif() + + if(NOT GAOL_FIND_QUIETLY) + if(${prefix}_INCDIR) + message(STATUS "${MSG} -- found at ${${prefix}_INCDIR}") + else() + message(STATUS "${MSG} -- not found") + endif() + endif() + + set(${prefix}_INCDIR ${${prefix}_INCDIR} PARENT_SCOPE) + mark_as_advanced(${prefix}_INCDIR) + endfunction() + + + function(codac_find_library_custom prefix libname) + set(opt "") + set(oneArgs "") + set(multiArgs PATHS) + + cmake_parse_arguments(FLC "${opt}" "${oneArgs}" "${multiArgs}" ${ARGN}) + + if(FLC_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown keywords given to codac_find_library_custom(): \"${FLC_UNPARSED_ARGUMENTS}\"") + endif() + + set(MSG "Looking for ${libname}") + if(NOT GAOL_FIND_QUIETLY) + message(STATUS "${MSG}") + endif() + + # First look only in PATHS if given + if(FLC_PATHS) + find_library(${prefix}_LIB ${libname} PATHS ${FLC_PATHS} + DOC "Set to exact lib directory to bypass internal test" + PATH_SUFFIXES lib NO_DEFAULT_PATH) + endif() + + if(NOT ${prefix}_LIB) + # Now look with system and cmake paths + find_library(${prefix}_LIB ${libname} + DOC "Set to exact lib directory to bypass internal test" + PATH_SUFFIXES lib) + endif() + + if(NOT GAOL_FIND_QUIETLY) + if(${prefix}_LIB) + message(STATUS "${MSG} -- found at ${${prefix}_LIB}") + else() + message(STATUS "${MSG} -- not found") + endif() + endif() + + set(${prefix}_LIB ${${prefix}_LIB} PARENT_SCOPE) + mark_as_advanced(${prefix}_LIB) + endfunction() + + +################################################################################ +# Options +################################################################################ + + set(MATHLIB_DIR "" CACHE PATH "Path to the Mathlib/ultim lib and include directories") + set(GAOL_DIR "" CACHE PATH "Path to the Gaol lib and include directories") + + +################################################################################ +# Looking for Mathlib/libultim +################################################################################ + + # Looking for MathLib.h, result is written in MATHLIB_INCDIR + codac_find_header_custom(MATHLIB "MathLib.h" PATHS "${MATHLIB_DIR}" "${GAOL_DIR}") + # Looking for ultim library, result is written in MATHLIB_LIB + codac_find_library_custom(MATHLIB "ultim" PATHS "${MATHLIB_DIR}" "${GAOL_DIR}") + + +################################################################################ +# Looking for Gaol +################################################################################ + + # Looking for gaol/gaol.h, result is written in GAOL_INCDIR + codac_find_header_custom(GAOL "gaol/gaol.h" PATHS "${GAOL_DIR}") + # Looking for gaol library, result is written in GAOL_LIB + codac_find_library_custom(GAOL "gaol" PATHS "${GAOL_DIR}") + + set(GAOL_VERSION "") + if(GAOL_INCDIR AND EXISTS "${GAOL_INCDIR}/gaol/gaol_configuration.h") + file(STRINGS "${GAOL_INCDIR}/gaol/gaol_configuration.h" _gaol_version_lines + REGEX "^#define GAOL_(MAJOR|MINOR|MICRO)_VERSION[ \t]+[0-9]+") + set(_gaol_version_parts "") + foreach(_gaol_part MAJOR MINOR MICRO) + string(REGEX MATCH "GAOL_${_gaol_part}_VERSION[ \t]+([0-9]+)" _gaol_match "${_gaol_version_lines}") + if(_gaol_match) + list(APPEND _gaol_version_parts ${CMAKE_MATCH_1}) + endif() + endforeach() + list(LENGTH _gaol_version_parts _gaol_version_length) + if(_gaol_version_length EQUAL 3) + string(REPLACE ";" "." GAOL_VERSION "${_gaol_version_parts}") + endif() + endif() + + +################################################################################ +# Result +################################################################################ + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(GAOL + REQUIRED_VARS GAOL_LIB GAOL_INCDIR MATHLIB_LIB MATHLIB_INCDIR + VERSION_VAR GAOL_VERSION) + + if(GAOL_FOUND) + set(GAOL_INCLUDE_DIRS ${GAOL_INCDIR} ${MATHLIB_INCDIR}) + list(REMOVE_DUPLICATES GAOL_INCLUDE_DIRS) + set(GAOL_LIBRARIES ${GAOL_LIB} ${MATHLIB_LIB}) + endif() diff --git a/scripts/CMakeModules/codac_gaol.cmake b/scripts/CMakeModules/codac_gaol.cmake new file mode 100644 index 000000000..cca81b821 --- /dev/null +++ b/scripts/CMakeModules/codac_gaol.cmake @@ -0,0 +1,800 @@ +# ================================================================== +# Codac - cmake module for GAOL, the interval arithmetic library +# ================================================================== +# +# codac2::Interval derives from gaol::interval (see +# src/core/domains/interval/codac2_Interval.h). GAOL is written by Frederic +# Goualard (https://frederic.goualard.net, https://github.com/goualard-f/GAOL) +# and distributed under the GNU LGPL v2. It computes its elementary functions +# with mathlib, the IBM Accurate Portable Mathematical Library (libultim), which +# Frederic Goualard distributes along with it, under the GNU GPL v2 or later. +# +# Codac builds the GAOL of Jordan Ninin's fork, https://github.com/Jordan08/GAOL, +# which adds to GAOL a CMake build, the fixes Codac depends on (those of the +# patch IBEX applies to GAOL, and those Visual C++, MinGW and ARM processors +# need) and tests of its bounds. Fixes of GAOL go into the fork rather than into +# Codac. The fork's CMake build downloads mathlib from Frederic Goualard's site, +# and refuses the compilers that do not compute GAOL's intervals right (see its +# README.md). +# +# GAOL's interval operations are inline: the code including its headers, Codac's +# and that of Codac's users, has to be compiled with the flags of interval +# arithmetic, and linked with GAOL and mathlib. Which flags and which libraries +# is GAOL's to say, and codac_gaol_find() takes them from the GAOL it finds or +# builds, in this order: +# +# 1. the CMake package of GAOL, which the CMake build of the fork installs: +# find_package(gaol CONFIG) gives gaol::gaol, which carries the include +# directory, the flags (PUBLIC), __GAOL_PUBLIC__= for Visual C++, and +# mathlib (gaol::ultim); +# 2. pkg-config: the gaol.pc the autotools and meson builds of the fork install +# carries the flags in its Cflags, and GAOL and mathlib in its Libs; +# 3. the files themselves (FindGAOL.cmake), for a GAOL installed with neither, +# whose flags Codac then determines itself (codac_gaol_interval_flags()); +# 4. when none is found, or with ENABLE_FIND_PACKAGE_GAOL OFF, codac_gaol_build() +# builds the fork with its CMake build and installs it with its installer, +# and find_package(gaol CONFIG) takes the package it installed, as in 1. +# +# In 1 to 3, a GAOL older than CODAC_GAOL_MIN_VERSION (see below), or whose +# version cannot be told, is passed over, with a message saying so, and the next +# way is tried, down to building the fork. In 4, a GAOL built too old stops the +# configuration. +# +# All four end in Codac::gaol, the imported target the Codac libraries link +# PUBLIC, and which codac-config.cmake defines again for their users +# (codac_gaol_config_snippet()). +# +# Origin +# ------ +# What follows comes from IBEX (https://github.com/ibex-team/ibex-lib, GNU +# LGPL v3): +# +# - codac_gaol_portability_flags() and codac_gaol_interval_flags() are, divided +# in two, the part of ibex_init_common() (cmake.utils/ibex-config-utils.cmake) +# that sets the floating-point flags, in the version of the IBEX fork +# maintained by Fabrice Le Bars, https://github.com/lebarsfa/ibex-lib, tag +# ibex-2.8.9.20260819, which Codac was built against until now. +# ibex_init_common() was written by Cyril Bouvier for the CMake build of IBEX; +# the flags it sets -- -frounding-math and the others that IEEE 754 double +# support depends on, and their Visual Studio counterparts -- were added to it +# by Fabrice Le Bars. The condition under which -ffloat-store is added is the +# one of the CMake build of the fork of GAOL (its CMakeLists.txt). +# +# - codac_gaol_build() is the "not found, install it" branch of +# interval_lib_wrapper/gaol/CMakeLists.txt, written by Cyril Bouvier: build +# mathlib and GAOL, and install the result next to the library it serves, as +# IBEX does in include/ibex/3rd and lib/ibex/3rd. The CMake build of GAOL and +# mathlib it runs is the fork's, which says where it comes from: from IBEX +# (Cyril Bouvier, Gilles Chabert), with the portability fixes for Visual +# C++, MinGW and ARM of the forks of GAOL and mathlib by Fabrice Le Bars. +# +# - codac_gaol_config_snippet() does, for Codac, what +# create_target_import_and_export() of cmake.utils/IbexUtils.cmake (Cyril +# Bouvier) does for IBEX: the lines that define the imported target of GAOL +# again for a consumer of the installed library. +# +# What differs from IBEX +# ---------------------- +# IBEX extracts GAOL and mathlib from archives kept in its own repository, and +# builds them as a part of itself (add_subdirectory()). Here GAOL is downloaded +# from the fork, at the head of its master branch, and built as a project of its +# own when Codac is configured, for the reasons given at codac_gaol_build(); the +# fork's build downloads mathlib, checked against the SHA256 of its archive. The +# other differences are explained where they occur. + + +# Where a GAOL built by codac_gaol_build() is installed, under the installation +# prefix. IBEX keeps the libraries it builds for itself in include/ibex/3rd and +# lib/ibex/3rd, out of the way of a GAOL installed separately; include/codac +# cannot play that part here, being the name of Codac's umbrella header. GAOL's +# CMake package goes in lib/codac-3rd/cmake/gaol, where the search of +# find_package() does not look: only codac-config.cmake finds it there. +set(CODAC_INSTALL_INCLUDEDIR_3RD "${CMAKE_INSTALL_INCLUDEDIR}/codac-3rd") +set(CODAC_INSTALL_LIBDIR_3RD "${CMAKE_INSTALL_LIBDIR}/codac-3rd") + +# Where codac_gaol_build() downloads, builds and installs GAOL in the build tree +set(CODAC_GAOL_WORK_DIR "${CMAKE_BINARY_DIR}/_deps/gaol") + +# The oldest GAOL Codac accepts: version 4.3.2 of the fork, the first one with all +# the fixes Codac relies on. Among them, the last one: the intersection of disjoint +# intervals is the canonical empty set [NaN, NaN], where GAOL gave reversed bounds, +# [3, 2] for [1, 2] & [3, 4], which is_empty() took for empty but the operations +# computing on the bounds did not ([3, 2] + [0, 1] gave [3, 3]). With an older +# GAOL, the results of Codac could be wrong without any error. The requirement +# holds for the GAOL Codac finds or builds (codac_gaol_find()), and for the users +# of the installed Codac too (codac_gaol_config_snippet()): Codac's interval +# operations are inline in its headers, so they are compiled against the GAOL of +# the program that includes them. The CMake package of the fork declares its +# version compatible with a requested one of the same major version +# (SameMajorVersion): find_package(gaol 4.3.2) accepts 4.3.2 up to, excluding, 5. +set(CODAC_GAOL_MIN_VERSION 4.3.2) + + +################################################################################ +# codac_gaol_portability_flags() +################################################################################ +# +# Returns in the flags of ibex_init_common() (see the top of this file) +# that are not those of interval arithmetic, and that Codac keeps whatever GAOL +# says: the top-level CMakeLists.txt puts them in CMAKE_CXX_FLAGS, where +# ibex_init_common() put them. They are returned as a list, one flag per +# element, because Codac also hands them over to its consumers, through +# CODAC_CXX_FLAGS and codac.pc; for the same reason, the Visual Studio "/D NAME" +# pairs of IBEX are spelt "/DNAME", which target_compile_options() cannot +# mistake for two duplicated "/D" and merge. The rest of ibex_init_common() is +# left out: the installation directories, the build type, the C++ standard and +# the uninstall target are Codac's own decisions, and the Debug-only -Wall +# -DDEBUG (/D DEBUG) have no use here, Codac choosing its own warnings and none +# of its code reading DEBUG. +# +# The two flags IBEX added for filib, which is not GAOL, are kept all the same, +# so that Codac goes on being compiled with the flags it had. +function(codac_gaol_portability_flags outvar) + + include(CheckCXXCompilerFlag) + set(flags "") + + if(MSVC) + list(APPEND flags /D_CRT_SECURE_NO_WARNINGS /D_CRT_NONSTDC_NO_WARNINGS /Zc:__cplusplus /Zc:strictStrings-) + elseif(APPLE) + # Due to warnings on macOS with filib + check_cxx_compiler_flag("-Wno-undefined-var-template" COMPILER_SUPPORTS_WNO_UNDEFINED_VAR_TEMPLATE) + if(COMPILER_SUPPORTS_WNO_UNDEFINED_VAR_TEMPLATE) + list(APPEND flags -Wno-undefined-var-template) + endif() + endif() + # Claim IEEE 754 double compatibility, for filib + list(APPEND flags -D__STDC_IEC_559__=1) + + set(${outvar} ${flags} PARENT_SCOPE) +endfunction() + + +################################################################################ +# codac_gaol_interval_flags() +################################################################################ +# +# Returns in the compilation flags interval arithmetic depends on, for +# a GAOL found by its files alone, without a CMake package or a gaol.pc to say +# them (see codac_gaol_find()). Without -frounding-math and its companions, the +# compiler is free to evaluate a floating-point expression at compile time in +# the default rounding mode, or to contract it, and a bound computed that way no +# longer encloses anything. +# +# From ibex_init_common() (see the top of this file), which appends the same +# flags to CMAKE_C_FLAGS and CMAKE_CXX_FLAGS, but for -ffloat-store, added only +# where the builds of GAOL add it. +function(codac_gaol_interval_flags outvar) + + include(CheckCXXCompilerFlag) + include(CheckCXXSourceCompiles) + set(flags "") + + if(MSVC) + list(APPEND flags /fp:strict) + else() + # Each flag is kept only where the compiler takes it. The check results are + # cached under the names ibex_init_common() gives them, e.g. + # COMPILER_SUPPORTS_FROUNDING_MATH for -frounding-math. + foreach(flag -frounding-math -fno-fast-math -ffp-contract=off + -ffp-mode=full -fp-model=strict -fp:strict -mpc64) + string(MAKE_C_IDENTIFIER "${flag}" _flag_id) + string(TOUPPER "COMPILER_SUPPORTS${_flag_id}" _flag_var) + check_cxx_compiler_flag("${flag}" ${_flag_var}) + if(${_flag_var}) + list(APPEND flags ${flag}) + endif() + endforeach() + + # Not from ibex_init_common(), but Codac's own: on a 32-bit x86 processor, + # doubles are computed in SSE2 rather than on the x87 FPU. Visual Studio, + # in the branch above, computes them in SSE2 already. + # + # Computed on the x87, GAOL's bounds and mathlib's results are only right + # while the precision of the x87 is set to 53 bits, and nothing keeps it + # so: mathlib's Init_Lib() sets it where mathlib has a version for 32-bit x86 + # (see cmake/mathlib/mathlib_configuration.h.in in the fork of GAOL), but + # GAOL, initialised right after, restores the default floating-point + # environment (gaol::init()), whose precision is 64 bits on Linux and + # with MinGW. Built for an i686 computing on the x87 (Clang 21 with + # -mcpu=i686), GAOL returned [1.99975, 1.99975] for exp([1,1]), and the + # bounds of exp, sin and cos missed the exact value for 4000, 3302 and 3913 + # of 4000 random arguments; built with the two flags below, the same + # program gave all of its 48000 bounds bit for bit as on x86_64. In SSE2, + # the precision is not a setting. SSE2 asks nothing more of the processor + # than GAOL's own builds do: they compile GAOL for SSE2 on these systems. + check_cxx_source_compiles(" + #if !defined(__i386__) + #error not a 32-bit x86 target + #endif + int main() { return 0; }" + CODAC_TARGET_IS_X86_32) + if(CODAC_TARGET_IS_X86_32) + check_cxx_compiler_flag("-msse2 -mfpmath=sse" COMPILER_SUPPORTS_MSSE2_MFPMATH_SSE) + if(COMPILER_SUPPORTS_MSSE2_MFPMATH_SSE) + list(APPEND flags -msse2 -mfpmath=sse) + endif() + endif() + + # -ffloat-store only where doubles are still computed in extended precision + # with the flags above (FLT_EVAL_METHOD not 0: a 32-bit x86 target computing + # on the x87), whose 80-bit registers keep more digits than a double: the + # condition under which the CMake, autotools and meson builds of the fork of + # GAOL add it. ibex_init_common() added it wherever the compiler took it. + # Where doubles are computed in double precision (SSE2, ARM and the other + # processors), it brings nothing to the bounds, and makes GCC store every + # floating-point variable in memory rather than in a register: with it, on + # x86_64 with GCC 9.4, x + y took 9.1 ns rather than 3.0, and sqrt(x) 41 ns + # rather than 8.2. + string(REPLACE ";" " " CMAKE_REQUIRED_FLAGS "${flags}") + check_cxx_source_compiles(" + #include + #if defined(FLT_EVAL_METHOD) && FLT_EVAL_METHOD == 0 + #error doubles are computed in double precision + #endif + int main() { return 0; }" + CODAC_DOUBLES_IN_EXTENDED_PRECISION) + unset(CMAKE_REQUIRED_FLAGS) + if(CODAC_DOUBLES_IN_EXTENDED_PRECISION) + check_cxx_compiler_flag(-ffloat-store COMPILER_SUPPORTS_FFLOAT_STORE) + if(COMPILER_SUPPORTS_FFLOAT_STORE) + list(APPEND flags -ffloat-store) + endif() + endif() + + # Not from ibex_init_common() either: a warning when the compiler takes + # -frounding-math but says it does not honour the rounding direction on the + # target, as Clang does for 32-bit ARM processors ("overriding currently + # unsupported rounding mode on this target"). It then optimises the + # negations by which GAOL rounds downward with the rounding direction set + # upward, and no flag or change to GAOL can prevent it. Built by Clang 21 for + # 32-bit ARM, 4556 of 16000 random products, squares and cubes computed by + # GAOL did not enclose their exact value; built by GCC 15, none. The builds + # of the fork of GAOL refuse such a compiler, and so do its headers; the + # warning remains for a GAOL found without its CMake package or its gaol.pc. + if(COMPILER_SUPPORTS_FROUNDING_MATH) + set(CMAKE_REQUIRED_FLAGS "-frounding-math") + check_cxx_source_compiles("int main() { return 0; }" CODAC_COMPILER_HONOURS_ROUNDING_MATH + FAIL_REGEX "unsupported rounding mode") + unset(CMAKE_REQUIRED_FLAGS) + if(NOT CODAC_COMPILER_HONOURS_ROUNDING_MATH) + message(WARNING "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} does not honour the rounding " + "direction on this target (-frounding-math): the intervals computed by Codac may not " + "enclose the values they should. Use a compiler that does, such as GCC.") + endif() + endif() + endif() + + set(${outvar} ${flags} PARENT_SCOPE) +endfunction() + + +################################################################################ +# codac_gaol_step( ...) +################################################################################ +# +# Runs one step of codac_gaol_build(), its output written to , and +# stops the configuration with that output when the step fails, as +# LOG_OUTPUT_ON_FAILURE does for an ExternalProject. +function(codac_gaol_step description log) + execute_process(COMMAND ${ARGN} + OUTPUT_FILE "${log}" ERROR_FILE "${log}" + RESULT_VARIABLE result) + if(NOT result EQUAL 0) + file(READ "${log}" output) + message(FATAL_ERROR "GAOL: ${description} failed (${result}). Its output, in ${log}:\n${output}") + endif() +endfunction() + + +################################################################################ +# codac_gaol_build() +################################################################################ +# +# Downloads GAOL, builds it and mathlib in Release with the CMake build of the +# fork, and installs them in the build tree with its installer, all while Codac +# is configured; sets CODAC_GAOL_INSTALL_TREE, where they are installed, in the +# caller's scope, for codac_gaol_find() to find the CMake package of GAOL there. +# GAOL and mathlib are also installed with Codac, by the same installer, in +# CODAC_INSTALL_INCLUDEDIR_3RD and CODAC_INSTALL_LIBDIR_3RD, since the Codac +# libraries are of no use without them. +# +# While Codac is configured, and not while it is built, as with an +# ExternalProject: the flags and the libraries of GAOL are read from the CMake +# package it installs, which has to exist for find_package() to read it. +# +# A project of its own, as in IBEX's build of GAOL with its autotools, rather +# than the FetchContent that Eigen and Catch2 are brought in with, which would +# build GAOL as a part of this project. Kept apart, GAOL is compiled with the +# flags it chooses, and not with Codac's warnings, sanitizers and coverage +# instrumentation, and it is always built in Release, whatever the +# configuration of Codac, which is what the MSVC runtime choice of the +# top-level CMakeLists.txt counts on. CMAKE_CXX_FLAGS and CMAKE_C_FLAGS are +# handed over as they are when this is called, before Codac adds its own flags +# to them. +# +# GAOL comes from the head of the master branch of the fork (version 4.3.1 of +# GAOL), so that the fixes pushed to the fork reach Codac without a change here. +# Cloned with Git, the sources are brought up to date with the branch at each +# configuration of Codac, which needs network access (without it, the sources +# already downloaded are built, with a warning), and only what a new commit +# changes is compiled again. Without Git, the archive GitHub makes of the +# branch is downloaded instead, only once per build directory: a new commit +# reaches such a build once _deps/gaol is deleted from it. The fork's CMake +# build downloads mathlib 2.1.1 from Frederic Goualard's site, checks its +# checksum, and builds and installs it along with GAOL. +function(codac_gaol_build) + + set(_work "${CODAC_GAOL_WORK_DIR}") + set(_source "${_work}/src") + set(_binary "${_work}/build") + set(_install "${_work}/install") + file(MAKE_DIRECTORY "${_work}") + + message(STATUS "GAOL: downloading the fork of GAOL, building it and installing it in ${_work}") + + # The sources + find_package(Git QUIET) + if(GIT_FOUND) + if(NOT EXISTS "${_source}/.git") + file(REMOVE_RECURSE "${_source}") + # The files as they are in the repository, whatever core.autocrlf says + # (true on the Windows runners of GitHub Actions). + codac_gaol_step("cloning https://github.com/Jordan08/GAOL.git" "${_work}/download.log" + "${GIT_EXECUTABLE}" clone --depth 1 --branch master --config core.autocrlf=false + https://github.com/Jordan08/GAOL.git "${_source}") + else() + execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${_source}" fetch --depth 1 origin master + OUTPUT_FILE "${_work}/update.log" ERROR_FILE "${_work}/update.log" + RESULT_VARIABLE _fetch_result) + if(_fetch_result EQUAL 0) + codac_gaol_step("updating the sources with the master branch" "${_work}/update.log" + "${GIT_EXECUTABLE}" -C "${_source}" reset --hard FETCH_HEAD) + else() + message(WARNING "GAOL could not be brought up to date with the master branch of " + "https://github.com/Jordan08/GAOL.git (see ${_work}/update.log): " + "the sources downloaded before are built.") + endif() + endif() + elseif(NOT EXISTS "${_source}/CMakeLists.txt") + file(DOWNLOAD https://github.com/Jordan08/GAOL/archive/refs/heads/master.zip "${_work}/GAOL-master.zip" + STATUS _download_status LOG _download_log) + list(GET _download_status 0 _download_code) + if(NOT _download_code EQUAL 0) + message(FATAL_ERROR "GAOL: downloading https://github.com/Jordan08/GAOL/archive/refs/heads/master.zip " + "failed (${_download_status}):\n${_download_log}") + endif() + file(REMOVE_RECURSE "${_work}/GAOL-master" "${_source}") + codac_gaol_step("extracting GAOL-master.zip" "${_work}/download.log" + "${CMAKE_COMMAND}" -E chdir "${_work}" "${CMAKE_COMMAND}" -E tar xf GAOL-master.zip) + file(RENAME "${_work}/GAOL-master" "${_source}") + endif() + + # The configuration of GAOL, written as an initial cache (-C) rather than on + # the command line, where the ";" of a list such as CMAKE_OSX_ARCHITECTURES + # would split an argument in two. FORCE, so that a value changed since the + # last configuration of Codac replaces the one in GAOL's cache. + # + # Every value below has to come out the same at each configuration of Codac + # that changes nothing: GAOL is configured again as soon as one of them + # differs, and everything in Codac that includes an interval may then be + # recompiled. This is why the top-level CMakeLists.txt enables C in project() + # rather than leaving it to a dependency, which changed CMAKE_C_COMPILER + # between the first configuration and the second. + set(_cache "") + macro(codac_gaol_cache_entry name type value) + string(APPEND _cache "set(${name} [==[${value}]==] CACHE ${type} \"\" FORCE)\n") + endmacro() + + codac_gaol_cache_entry(CMAKE_BUILD_TYPE STRING Release) + # Codac's Python modules link these archives into shared libraries. + codac_gaol_cache_entry(CMAKE_POSITION_INDEPENDENT_CODE BOOL ON) + # Where Codac is installed, which the gaol.pc GAOL installs with Codac names; + # the installation in the build tree gives its own prefix. + codac_gaol_cache_entry(CMAKE_INSTALL_PREFIX PATH "${CMAKE_INSTALL_PREFIX}") + codac_gaol_cache_entry(CMAKE_INSTALL_INCLUDEDIR PATH "${CODAC_INSTALL_INCLUDEDIR_3RD}") + codac_gaol_cache_entry(CMAKE_INSTALL_LIBDIR PATH "${CODAC_INSTALL_LIBDIR_3RD}") + # The fork's continuous integration runs its tests + codac_gaol_cache_entry(GAOL_BUILD_TESTS BOOL OFF) + # The mathlib the fork downloads and builds, never one installed on this + # machine: the installation of Codac counts on it + codac_gaol_cache_entry(GAOL_FIND_MATHLIB BOOL OFF) + # Without the fused multiply-add instructions of the processor, whose flags + # would reach every target linking GAOL: a library for any processor of the + # architecture + codac_gaol_cache_entry(GAOL_FMA BOOL OFF) + codac_gaol_cache_entry(CMAKE_CXX_FLAGS STRING "${CMAKE_CXX_FLAGS}") + codac_gaol_cache_entry(CMAKE_C_FLAGS STRING "${CMAKE_C_FLAGS}") + + # The generator is handed over below, with its platform (-A) and toolset (-T), + # but nothing else is: the compilers, the flags and the target have to be + # given explicitly, or GAOL would be built for another machine than the one + # Codac is built for. The Visual Studio and Xcode generators take their + # compilers from the toolset and ignore these. + if(NOT CMAKE_GENERATOR MATCHES "Visual Studio|Xcode") + codac_gaol_cache_entry(CMAKE_CXX_COMPILER FILEPATH "${CMAKE_CXX_COMPILER}") + codac_gaol_cache_entry(CMAKE_C_COMPILER FILEPATH "${CMAKE_C_COMPILER}") + codac_gaol_cache_entry(CMAKE_MAKE_PROGRAM FILEPATH "${CMAKE_MAKE_PROGRAM}") + endif() + foreach(_var CMAKE_TOOLCHAIN_FILE CMAKE_MSVC_RUNTIME_LIBRARY CMAKE_GENERATOR_INSTANCE + CMAKE_OSX_DEPLOYMENT_TARGET CMAKE_OSX_SYSROOT CMAKE_OSX_ARCHITECTURES) + # Quoted: a list, as CMAKE_OSX_ARCHITECTURES can be, would give if() as many + # arguments as elements + if(NOT "${${_var}}" STREQUAL "") + codac_gaol_cache_entry(${_var} STRING "${${_var}}") + endif() + endforeach() + # Only when this build is itself a cross-compilation (as the macOS jobs of + # .github/workflows are, by setting CMAKE_SYSTEM_NAME explicitly): passing + # the host's own name would make GAOL's build believe it cross-compiles. + if(CMAKE_CROSSCOMPILING) + codac_gaol_cache_entry(CMAKE_SYSTEM_NAME STRING "${CMAKE_SYSTEM_NAME}") + if(CMAKE_SYSTEM_PROCESSOR) + codac_gaol_cache_entry(CMAKE_SYSTEM_PROCESSOR STRING "${CMAKE_SYSTEM_PROCESSOR}") + endif() + endif() + file(WRITE "${_work}/initial-cache.cmake" "${_cache}") + + set(_generator -G "${CMAKE_GENERATOR}") + if(CMAKE_GENERATOR_PLATFORM) + list(APPEND _generator -A "${CMAKE_GENERATOR_PLATFORM}") + endif() + if(CMAKE_GENERATOR_TOOLSET) + list(APPEND _generator -T "${CMAKE_GENERATOR_TOOLSET}") + endif() + + codac_gaol_step("configuring" "${_work}/configure.log" + "${CMAKE_COMMAND}" ${_generator} -C "${_work}/initial-cache.cmake" -S "${_source}" -B "${_binary}") + # Release whatever the configuration of Codac: --config is what a + # multi-configuration generator reads, and what the others ignore. In + # parallel as CMAKE_BUILD_PARALLEL_LEVEL says, when it is set. + codac_gaol_step("building" "${_work}/build.log" + "${CMAKE_COMMAND}" --build "${_binary}" --config Release) + # The installer of GAOL's build (cmake_install.cmake, which cmake --install + # runs from CMake 3.15 on), with the prefix of the build tree + codac_gaol_step("installing" "${_work}/install.log" + "${CMAKE_COMMAND}" "-DCMAKE_INSTALL_PREFIX=${_install}" -DCMAKE_INSTALL_CONFIG_NAME=Release + -P "${_binary}/cmake_install.cmake") + + # And with Codac, by the same installer, with the prefix Codac is installed + # under, as the installation of Codac gives it (cmake --install --prefix, + # CPack), and into DESTDIR when it is set, which it inherits. + install(CODE " + # GAOL and mathlib, installed by the installer of their CMake build (see + # codac_gaol_build() in scripts/CMakeModules/codac_gaol.cmake) + execute_process(COMMAND \"${CMAKE_COMMAND}\" \"-DCMAKE_INSTALL_PREFIX=\${CMAKE_INSTALL_PREFIX}\" + -DCMAKE_INSTALL_CONFIG_NAME=Release -P \"${_binary}/cmake_install.cmake\" + RESULT_VARIABLE _codac_gaol_install_result) + if(NOT _codac_gaol_install_result EQUAL 0) + message(FATAL_ERROR \"The installation of GAOL failed (\${_codac_gaol_install_result})\") + endif() + ") + + set(CODAC_GAOL_INSTALL_TREE "${_install}" PARENT_SCOPE) +endfunction() + + +################################################################################ +# codac_gaol_find() +################################################################################ +# +# Finds GAOL in the order given at the top of this file, or builds it, and +# defines Codac::gaol, the imported target through which the Codac libraries, +# and the targets linking them, get GAOL's include directories, flags and +# libraries. Sets in the caller's scope: +# +# CODAC_GAOL_FROM "package", "pkg-config" or "files" +# CODAC_GAOL_BUILT_HERE TRUE when codac_gaol_build() built it +# CODAC_GAOL_INSTALL_TREE where codac_gaol_build() installed it +# GAOL_VERSION +# +# An INTERFACE library rather than GAOL's own target, whatever provided GAOL: +# CODAC_LIBRARIES names it, and codac-config.cmake defines it again +# (codac_gaol_config_snippet()), which it could not do under the name gaol::gaol +# without clashing with a find_package(gaol) of the consumer. +function(codac_gaol_find) + + include(CheckCXXCompilerFlag) + set(_from "") + set(_built_here FALSE) + set(_version "") + + if(ENABLE_FIND_PACKAGE_GAOL) + + # 1. The CMake package of GAOL (gaol_DIR, or CMAKE_PREFIX_PATH). A gaol_DIR + # left by an earlier configuration of this build, naming the GAOL + # codac_gaol_build() installed in it, is not a GAOL of this machine: it is + # forgotten, and that GAOL is built again, up to date, if none is found. + string(FIND "${gaol_DIR}" "${CODAC_GAOL_WORK_DIR}/" _in_work_dir) + if(_in_work_dir EQUAL 0) + unset(gaol_DIR CACHE) + endif() + # The version is checked by the package's gaolConfigVersion.cmake, before the + # package is loaded: an older one defines no target, which would clash with + # the gaol::gaol of the GAOL found or built next. + find_package(gaol ${CODAC_GAOL_MIN_VERSION} CONFIG QUIET) + if(gaol_FOUND) + set(_from package) + set(_version "${gaol_VERSION}") + message(STATUS "Found GAOL ${gaol_VERSION}, CMake package in ${gaol_DIR}") + elseif(gaol_CONSIDERED_VERSIONS) + message(STATUS "Found GAOL ${gaol_CONSIDERED_VERSIONS} (CMake package ${gaol_CONSIDERED_CONFIGS}), " + "older than ${CODAC_GAOL_MIN_VERSION}: not used") + endif() + + # 2. pkg-config (PKG_CONFIG_PATH, or CMAKE_PREFIX_PATH). Not with Visual C++, + # whose libraries pkg-config does not name. A gaol.pc whose Cflags lack + # -frounding-math, as the one the meson build of Frederic Goualard's GAOL + # installs, does not describe the flags GAOL needs, and is not used. + if(NOT _from AND NOT MSVC) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(CODAC_GAOL_PC QUIET IMPORTED_TARGET gaol) + if(CODAC_GAOL_PC_FOUND) + check_cxx_compiler_flag(-frounding-math COMPILER_SUPPORTS_FROUNDING_MATH) + if(NOT CODAC_GAOL_PC_VERSION OR CODAC_GAOL_PC_VERSION VERSION_LESS CODAC_GAOL_MIN_VERSION) + message(STATUS "Found gaol.pc ${CODAC_GAOL_PC_VERSION} in ${CODAC_GAOL_PC_PREFIX}, " + "older than ${CODAC_GAOL_MIN_VERSION}: not used") + elseif(COMPILER_SUPPORTS_FROUNDING_MATH AND NOT "-frounding-math" IN_LIST CODAC_GAOL_PC_CFLAGS_OTHER) + message(STATUS "Found gaol.pc in ${CODAC_GAOL_PC_PREFIX}, whose Cflags lack -frounding-math: not used") + else() + set(_from pkg-config) + set(_version "${CODAC_GAOL_PC_VERSION}") + message(STATUS "Found GAOL ${CODAC_GAOL_PC_VERSION}, gaol.pc in ${CODAC_GAOL_PC_PREFIX}") + endif() + endif() + endif() + endif() + + # 3. The files (GAOL_DIR and MATHLIB_DIR, or CMAKE_PREFIX_PATH) + if(NOT _from) + find_package(GAOL MODULE QUIET) + # FindGAOL.cmake reads the version in gaol/gaol_configuration.h; a GAOL that + # does not state it there is not known to be recent enough, and not used. + if(GAOL_FOUND AND (NOT GAOL_VERSION OR GAOL_VERSION VERSION_LESS CODAC_GAOL_MIN_VERSION)) + message(STATUS "Found GAOL ${GAOL_VERSION} in ${GAOL_INCDIR}, older than ${CODAC_GAOL_MIN_VERSION} " + "or of unknown version: not used") + elseif(GAOL_FOUND) + set(_from files) + set(_version "${GAOL_VERSION}") + message(STATUS "Found GAOL ${GAOL_VERSION} in ${GAOL_INCDIR}, without a CMake package or a gaol.pc: " + "compiled with the flags of interval arithmetic Codac determines") + endif() + endif() + endif() + + # 4. Built, and found as in 1 + if(NOT _from) + codac_gaol_build() + unset(gaol_DIR CACHE) + # The version is checked here too: without network access, codac_gaol_build() + # builds the sources it downloaded at an earlier configuration, which may be + # those of a version older than CODAC_GAOL_MIN_VERSION. Codac is then not + # configured, rather than configured on a GAOL it cannot trust. + find_package(gaol ${CODAC_GAOL_MIN_VERSION} CONFIG QUIET NO_DEFAULT_PATH + PATHS "${CODAC_GAOL_INSTALL_TREE}/${CODAC_INSTALL_LIBDIR_3RD}/cmake/gaol") + if(NOT gaol_FOUND AND gaol_CONSIDERED_VERSIONS) + message(FATAL_ERROR "The GAOL built in ${CODAC_GAOL_WORK_DIR} is version ${gaol_CONSIDERED_VERSIONS}, older " + "than ${CODAC_GAOL_MIN_VERSION}, the oldest Codac accepts: its sources could not be " + "brought up to date with the master branch of the fork (see the warning above). " + "Configure Codac again with network access (without Git, after deleting " + "${CODAC_GAOL_WORK_DIR}: the archive of the fork is only downloaded once per build " + "directory).") + elseif(NOT gaol_FOUND) + message(FATAL_ERROR "The CMake package of the GAOL built in ${CODAC_GAOL_WORK_DIR} was not found under " + "${CODAC_GAOL_INSTALL_TREE}/${CODAC_INSTALL_LIBDIR_3RD}/cmake/gaol.") + endif() + set(_from package) + set(_built_here TRUE) + set(_version "${gaol_VERSION}") + message(STATUS "GAOL ${gaol_VERSION} built and installed, CMake package in ${gaol_DIR}") + endif() + + add_library(Codac::gaol INTERFACE IMPORTED) + if(_from STREQUAL "package") + set_target_properties(Codac::gaol PROPERTIES INTERFACE_LINK_LIBRARIES gaol::gaol) + elseif(_from STREQUAL "pkg-config") + set_target_properties(Codac::gaol PROPERTIES INTERFACE_LINK_LIBRARIES PkgConfig::CODAC_GAOL_PC) + else() + codac_gaol_interval_flags(_interval_flags) + set_target_properties(Codac::gaol PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${GAOL_INCLUDE_DIRS}" + INTERFACE_COMPILE_OPTIONS "${_interval_flags}" + INTERFACE_LINK_LIBRARIES "${GAOL_LIBRARIES}") + # For Visual C++, GAOL declares its classes and functions + # __declspec(dllimport), as for a DLL, unless __GAOL_PUBLIC__ is defined + # (gaol/gaol_config.h). None of GAOL's builds -- its autotools and meson + # builds, and the CMake build of the fork -- defines + # _COMPILING__GAOL_PUBLIC__, with which GAOL would export a DLL: the GAOL + # Visual C++ links is a static library, which every file including its + # headers has to be told. gaol::gaol, of the CMake package, tells it the + # same way. + if(MSVC) + set_target_properties(Codac::gaol PROPERTIES INTERFACE_COMPILE_DEFINITIONS "__GAOL_PUBLIC__=") + endif() + endif() + + set(CODAC_GAOL_FROM "${_from}" PARENT_SCOPE) + set(CODAC_GAOL_BUILT_HERE ${_built_here} PARENT_SCOPE) + set(CODAC_GAOL_INSTALL_TREE "${CODAC_GAOL_INSTALL_TREE}" PARENT_SCOPE) + set(GAOL_VERSION "${_version}" PARENT_SCOPE) +endfunction() + + +################################################################################ +# codac_gaol_usage( ) +################################################################################ +# +# Returns what Codac::gaol gives the targets linking it, walked out of it and of +# the targets it links: its include directories, its compilation flags (with +# its definitions, as -D or /D) and what goes on the link line (library files, +# and flags). This is for what cannot link Codac::gaol: CODAC_CXX_FLAGS, codac.pc, +# and codac-config.cmake when GAOL has no CMake package. Generator expressions +# are left out, but for $, which is unwrapped: GAOL's targets +# have no other. +function(codac_gaol_usage include_dirs_var flags_var link_var) + + set(include_dirs "") + set(flags "") + set(link "") + set(queue Codac::gaol) + set(seen "") + + while(queue) + list(GET queue 0 item) + list(REMOVE_AT queue 0) + if(item MATCHES "^\\$$") + set(item "${CMAKE_MATCH_1}") + endif() + if(item IN_LIST seen OR item MATCHES "\\$<") + continue() + endif() + list(APPEND seen "${item}") + + if(NOT TARGET "${item}") + # A library file, a flag, or the name of a library of the system + if(item MATCHES "^-" OR IS_ABSOLUTE "${item}") + list(APPEND link "${item}") + else() + list(APPEND link "-l${item}") + endif() + continue() + endif() + + get_target_property(_value "${item}" INTERFACE_INCLUDE_DIRECTORIES) + if(_value) + list(APPEND include_dirs ${_value}) + endif() + get_target_property(_value "${item}" INTERFACE_COMPILE_OPTIONS) + if(_value) + list(APPEND flags ${_value}) + endif() + get_target_property(_value "${item}" INTERFACE_COMPILE_DEFINITIONS) + if(_value) + foreach(_definition ${_value}) + if(MSVC) + list(APPEND flags "/D${_definition}") + else() + list(APPEND flags "-D${_definition}") + endif() + endforeach() + endif() + + # An imported archive names its file either outright or per configuration. + # An INTERFACE library has no file at all, and before CMake 3.19 merely + # asking one for IMPORTED_LOCATION is a fatal error rather than an empty + # answer ("INTERFACE_LIBRARY targets may only have whitelisted + # properties") -- which is what Debian Bullseye, on CMake 3.18.4, reported. + # TYPE is whitelisted, so it can be asked first, as can the INTERFACE_ + # properties read above and below. + get_target_property(_type "${item}" TYPE) + if(NOT _type STREQUAL "INTERFACE_LIBRARY") + get_target_property(_location "${item}" IMPORTED_LOCATION) + if(NOT _location) + get_target_property(_configurations "${item}" IMPORTED_CONFIGURATIONS) + if(_configurations) + list(GET _configurations 0 _configuration) + get_target_property(_location "${item}" IMPORTED_LOCATION_${_configuration}) + endif() + endif() + if(_location) + list(APPEND link "${_location}") + endif() + endif() + + get_target_property(_value "${item}" INTERFACE_LINK_LIBRARIES) + if(_value) + list(APPEND queue ${_value}) + endif() + get_target_property(_value "${item}" INTERFACE_LINK_OPTIONS) + if(_value) + list(APPEND link ${_value}) + endif() + endwhile() + + foreach(_list include_dirs flags link) + list(FILTER ${_list} EXCLUDE REGEX "\\$<") + endforeach() + if(include_dirs) + list(REMOVE_DUPLICATES include_dirs) + endif() + if(flags) + list(REMOVE_DUPLICATES flags) + endif() + + set(${include_dirs_var} ${include_dirs} PARENT_SCOPE) + set(${flags_var} ${flags} PARENT_SCOPE) + set(${link_var} ${link} PARENT_SCOPE) +endfunction() + + +################################################################################ +# codac_gaol_pkg_config_path() +################################################################################ +# +# When is a path of the GAOL codac_gaol_build() installed in the build +# tree, turns it into the path GAOL is installed at with Codac, under the +# ${prefix} of codac.pc. +function(codac_gaol_pkg_config_path var) + if(CODAC_GAOL_BUILT_HERE) + string(FIND "${${var}}" "${CODAC_GAOL_INSTALL_TREE}/" _position) + if(_position EQUAL 0) + string(LENGTH "${CODAC_GAOL_INSTALL_TREE}/" _length) + string(SUBSTRING "${${var}}" ${_length} -1 _rest) + set(${var} "\${prefix}/${_rest}" PARENT_SCOPE) + endif() + endif() +endfunction() + + +################################################################################ +# codac_gaol_config_snippet() +################################################################################ +# +# Returns the lines of codac-config.cmake that define Codac::gaol for a consumer +# of the installed Codac, which CODAC_LIBRARIES names -- the +# counterpart of the ibex-config-gaol.cmake and ibex-config-ultim.cmake files +# that create_target_import_and_export() writes for IBEX. +# +# - A GAOL with a CMake package is found again by find_package(gaol CONFIG): +# the GAOL Codac built under the prefix Codac is installed under, by a path +# relative to the configuration file, since the prefix may be moved; a GAOL +# found on this machine where it was found, before the usual search. +# - A GAOL found by pkg-config or by its files is named by the paths it was +# found at, with the flags it was compiled with. +# - find_package(gaol) asks for CODAC_GAOL_MIN_VERSION or later, as when Codac +# was built: the consumer compiles Codac's inline interval operations against +# the GAOL it finds, and a package search path of the consumer could lead to +# an older one. +function(codac_gaol_config_snippet outvar) + + if(CODAC_GAOL_FROM STREQUAL "package") + if(CODAC_GAOL_BUILT_HERE) + file(RELATIVE_PATH _to_prefix "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_CMAKE}" "${CMAKE_INSTALL_PREFIX}") + set(_find "get_filename_component(_codac_prefix \"\${CMAKE_CURRENT_LIST_DIR}/${_to_prefix}\" ABSOLUTE) + find_package(gaol ${CODAC_GAOL_MIN_VERSION} CONFIG REQUIRED NO_DEFAULT_PATH + PATHS \"\${_codac_prefix}/${CODAC_INSTALL_LIBDIR_3RD}/cmake/gaol\")") + else() + set(_find "find_package(gaol ${CODAC_GAOL_MIN_VERSION} CONFIG REQUIRED HINTS \"${gaol_DIR}\")") + endif() + set(_properties "INTERFACE_LINK_LIBRARIES gaol::gaol") + else() + codac_gaol_usage(_include_dirs _flags _link) + set(_find "") + set(_properties "INTERFACE_INCLUDE_DIRECTORIES \"${_include_dirs}\" + INTERFACE_COMPILE_OPTIONS \"${_flags}\" + INTERFACE_LINK_LIBRARIES \"${_link}\"") + endif() + + set(${outvar} " + # GAOL, the interval arithmetic library Codac is built upon, with mathlib + # (libultim) and the flags of interval arithmetic, as Codac::gaol, which + # CODAC_LIBRARIES names. + ${_find} + if(NOT TARGET Codac::gaol) + add_library(Codac::gaol INTERFACE IMPORTED) + set_target_properties(Codac::gaol PROPERTIES + ${_properties}) + endif() +" PARENT_SCOPE) +endfunction() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 28fd1c4fc..3f7123efb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,18 @@ # Compiling sources # ================= + # Emptied here, then appended to by each module below through + # codac_publish_include_dirs(). It is a cache entry, so it survives a + # reconfigure: without this reset it would grow a duplicate of every + # directory on each run, and would keep the directories of a module that has + # since been switched off (capd, sympy). + set(CODAC_SOURCE_INCLUDE_DIRS "" CACHE INTERNAL + "Directories holding the public headers of the codac modules") + + # Same treatment for the directories of the generated umbrella headers. + set(CODAC_GENERATED_INCLUDE_DIRS "" CACHE INTERNAL + "Directories holding the umbrella headers generated for the codac modules") + add_subdirectory(core) add_subdirectory(graphics) add_subdirectory(unsupported) @@ -24,9 +36,45 @@ set(CODAC_PKG_CONFIG_FILE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc) - set(CODAC_PKG_CONFIG_CFLAGS "-I\${includedir}/ibex") + set(CODAC_PKG_CONFIG_CFLAGS "") set(CODAC_PKG_CONFIG_LIBS "-L\${libdir}") + # Codac's own flags, which this build puts in CMAKE_CXX_FLAGS + # (codac_gaol_portability_flags() in scripts/CMakeModules/codac_gaol.cmake), + # and the interval arithmetic flags GAOL gives through Codac::gaol, + # CODAC_INTERVAL_CXX_FLAGS, set in the top-level CMakeLists.txt. A CMake + # consumer gets them through CODAC_CXX_FLAGS; a pkg-config consumer has no + # equivalent, and compiling Codac's headers without -frounding-math silently + # gives up the guarantee the whole library rests on. + foreach(_codac_flags CODAC_PORTABILITY_CXX_FLAGS CODAC_INTERVAL_CXX_FLAGS) + if(${_codac_flags}) + string(REPLACE ";" " " _codac_pkg_config_flags "${${_codac_flags}}") + string(APPEND CODAC_PKG_CONFIG_CFLAGS " ${_codac_pkg_config_flags}") + endif() + endforeach() + + # GAOL's include directories and libraries, as Codac::gaol gives them + # (codac_gaol_usage(), in the top-level CMakeLists.txt): where the GAOL found + # on this machine is, or, for the GAOL codac_gaol_build() built, where it is + # installed with Codac, under ${prefix}. They take the place of the + # "Requires: ibex" and -I\${includedir}/ibex of the time Codac reached GAOL + # through IBEX. GAOL itself is not named in "Requires:": a GAOL built with + # Codac installs its gaol.pc under lib/codac-3rd, where pkg-config does not + # look, and a GAOL found by its files has none. The directories the compiler + # searches of its own accord are left out, as CMake leaves them out of its + # command lines; the libraries are named by their full path, gaol before + # ultim, which it depends on. + foreach(dir ${CODAC_GAOL_INCLUDE_DIRS}) + if(NOT dir IN_LIST CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) + codac_gaol_pkg_config_path(dir) + string(APPEND CODAC_PKG_CONFIG_CFLAGS " -I${dir}") + endif() + endforeach() + foreach(item ${CODAC_GAOL_LINK_ITEMS}) + codac_gaol_pkg_config_path(item) + string(APPEND CODAC_PKG_CONFIG_LIBS " ${item}") + endforeach() + file(GENERATE OUTPUT ${CODAC_PKG_CONFIG_FILE} CONTENT " prefix=${CMAKE_INSTALL_PREFIX} includedir=\${prefix}/${CMAKE_INSTALL_INCLUDEDIR} @@ -36,7 +84,7 @@ Description: ${PROJECT_DESCRIPTION} Url: ${PROJECT_HOMEPAGE_URL} Version: ${PROJECT_VERSION} - Requires: ibex + Requires: Cflags: ${CODAC_PKG_CONFIG_CFLAGS} Libs: ${CODAC_PKG_CONFIG_LIBS} ") @@ -49,6 +97,18 @@ set(CODAC_CMAKE_CONFIG_FILE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake) + # The definition of Codac::gaol, written by + # scripts/CMakeModules/codac_gaol.cmake, which knows where GAOL came from. + codac_gaol_config_snippet(CODAC_GAOL_CONFIG_SNIPPET) + + # Codac's own flags (codac_gaol_portability_flags()) and the interval + # arithmetic flags GAOL gives (-frounding-math, etc.), for external projects. + # In that order, which is the order this build hands them to the compiler in + # -- the first through CMAKE_CXX_FLAGS, the second through Codac::gaol, whose + # options come after a target's own -- and decides which of two conflicting + # flags wins. + set(CODAC_CXX_FLAGS ${CODAC_PORTABILITY_CXX_FLAGS} ${CODAC_INTERVAL_CXX_FLAGS}) + file(WRITE ${CODAC_CMAKE_CONFIG_FILE} "# Try to find Codac # This file has been generated by CMake @@ -67,14 +127,33 @@ find_library(CODAC_UNSUPPORTED_LIBRARY NAMES ${PROJECT_NAME}-unsupported PATH_SUFFIXES lib) - find_package(IBEX REQUIRED) +${CODAC_GAOL_CONFIG_SNIPPET} + # Projects written for a Codac that depended on IBEX call ibex_init_common() + # after find_package(CODAC), as the manual used to tell them to, for the + # interval arithmetic flags. Those flags now come with CODAC_CXX_FLAGS, and + # this ibex_init_common() does nothing: it is only there so that such + # projects still configure. A project that finds IBEX itself, before or + # after Codac, gets IBEX's own function instead. + if(NOT COMMAND ibex_init_common) + function(ibex_init_common) + endfunction() + endif() + + # codac-core links PUBLIC against Threads::Threads (see src/core/CMakeLists.txt), + # e.g. for peibos/threading code. That requirement doesn't come back through + # find_library() above (a plain path has no transitive usage requirements), + # so it has to be re-declared here -- otherwise any downstream target that + # actually pulls in that code (such as the 11_peibos example) fails to link + # with an undefined reference to pthread_create. + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) set(CODAC_VERSION ${PROJECT_VERSION}) - set(CODAC_LIBRARIES \${CODAC_CORE_LIBRARY} \${CODAC_GRAPHICS_LIBRARY} \${CODAC_UNSUPPORTED_LIBRARY} Ibex::ibex) + set(CODAC_LIBRARIES \${CODAC_CORE_LIBRARY} \${CODAC_GRAPHICS_LIBRARY} \${CODAC_UNSUPPORTED_LIBRARY} Codac::gaol Threads::Threads) set(CODAC_INCLUDE_DIRS \${CODAC_CORE_INCLUDE_DIR}/../ \${CODAC_CORE_INCLUDE_DIR}/../eigen3/ \${CODAC_CORE_INCLUDE_DIR} \${CODAC_GRAPHICS_INCLUDE_DIR} \${CODAC_UNSUPPORTED_INCLUDE_DIR}) set(CODAC_C_FLAGS \"\") - set(CODAC_CXX_FLAGS \"\") + set(CODAC_CXX_FLAGS \"${CODAC_CXX_FLAGS}\") ") if(WITH_PYTHON) @@ -111,7 +190,7 @@ endif() file(APPEND ${CODAC_CMAKE_CONFIG_FILE} " - set(CODAC_LIBRARIES \${CODAC_LIBRARIES} \${CODAC_GRAPHICS_LIBRARY} \${CODAC_CORE_LIBRARY}) + set(CODAC_LIBRARIES \${CODAC_LIBRARIES} \${CODAC_GRAPHICS_LIBRARY} \${CODAC_CORE_LIBRARY} Codac::gaol Threads::Threads) ") @@ -122,10 +201,28 @@ # ==================================== set(CODAC_MAIN_HEADER ${CMAKE_CURRENT_BINARY_DIR}/codac) - file(WRITE ${CODAC_MAIN_HEADER} "/* This file is generated by CMake */\n\n") - file(APPEND ${CODAC_MAIN_HEADER} "#pragma once\n\n") + + set(CODAC_MAIN_HEADER_CONTENT "/* This file is generated by CMake */\n\n") + string(APPEND CODAC_MAIN_HEADER_CONTENT "#pragma once\n\n") foreach(header_path ${CODAC_MAIN_SUBHEADERS}) get_filename_component(header_name ${header_path} NAME) - file(APPEND ${CODAC_MAIN_HEADER} "#include <${header_name}>\n") + string(APPEND CODAC_MAIN_HEADER_CONTENT "#include <${header_name}>\n") endforeach() + + # Compare with the existing file, if any + if(EXISTS "${CODAC_MAIN_HEADER}") + file(READ "${CODAC_MAIN_HEADER}" EXISTING_CONTENT) + string(MD5 EXISTING_HASH "${EXISTING_CONTENT}") + string(MD5 NEW_HASH "${CODAC_MAIN_HEADER_CONTENT}") + else() + set(EXISTING_HASH "") + set(NEW_HASH "new") + endif() + + # Only rewrite it when it changed: 24 examples #include , and an + # unconditional rewrite moves its timestamp on every cmake run, rebuilding + # all of them although nothing changed. + if(NOT EXISTING_HASH STREQUAL NEW_HASH) + file(WRITE "${CODAC_MAIN_HEADER}" "${CODAC_MAIN_HEADER_CONTENT}") + endif() install(FILES ${CODAC_MAIN_HEADER} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/) \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index db02e9526..3ec49f5b0 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -293,13 +293,27 @@ # Create the target for libcodac-core ################################################################################ - #if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 20) - set(CMAKE_CXX_STANDARD_REQUIRED ON) - #endif() - add_library(${PROJECT_NAME}-core ${CODAC_CORE_SRC}) - target_include_directories(${PROJECT_NAME}-core PUBLIC + + set_target_properties(${PROJECT_NAME}-core PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) + + # The directories holding the public core headers, in their canonical + # location under src/core. Published as a cache variable so that the tests, + # the examples and the Python bindings all reach the headers by the same + # path as codac-core itself, instead of each keeping its own copy of this + # list (which had already drifted). + # + # The path a translation unit used is the path gcov records, so a header + # reached both through src/ and through a copy in the build tree ends up as + # two unrelated entries in the coverage report, each showing only the part + # its own callers exercised, and is reported well below its real coverage. + # Consumers therefore list these directories first; the build tree stays on + # the path for the generated umbrella headers (codac-core.h and the rest). + set(CODAC_CORE_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/actions ${CMAKE_CURRENT_SOURCE_DIR}/contractors ${CMAKE_CURRENT_SOURCE_DIR}/domains @@ -325,9 +339,13 @@ ${CMAKE_CURRENT_SOURCE_DIR}/proj ${CMAKE_CURRENT_SOURCE_DIR}/separators ${CMAKE_CURRENT_SOURCE_DIR}/tools + ${CMAKE_CURRENT_SOURCE_DIR}/tools/ibex ${CMAKE_CURRENT_SOURCE_DIR}/trajectory - ) - target_link_libraries(${PROJECT_NAME}-core PUBLIC Ibex::ibex Eigen3::Eigen Threads::Threads) + CACHE INTERNAL "Directories holding the public headers of codac-core") + + target_include_directories(${PROJECT_NAME}-core PUBLIC ${CODAC_CORE_INCLUDE_DIRS}) + codac_publish_include_dirs(${CODAC_CORE_INCLUDE_DIRS}) + target_link_libraries(${PROJECT_NAME}-core PUBLIC Codac::gaol Eigen3::Eigen Threads::Threads) ################################################################################ @@ -337,36 +355,52 @@ set(CODAC_PKG_CONFIG_CFLAGS "${CODAC_PKG_CONFIG_CFLAGS} -I\${includedir}/${PROJECT_NAME}-core" PARENT_SCOPE) set(CODAC_PKG_CONFIG_LIBS "${CODAC_PKG_CONFIG_LIBS} -l${PROJECT_NAME}-core" PARENT_SCOPE) - + ################################################################################ -# Installation of libcodac-core files +# Installation / build include tree ################################################################################ - -# Getting header files from sources foreach(srcfile ${CODAC_CORE_SRC}) if(srcfile MATCHES "\\.h$" OR srcfile MATCHES "\\.hpp$") list(APPEND CODAC_CORE_HDR ${srcfile}) - file(COPY ${srcfile} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../include) endif() endforeach() + # Generating the file codac-core.h +# ================================ set(CODAC_CORE_MAIN_HEADER ${CMAKE_CURRENT_BINARY_DIR}/codac-core.h) + codac_publish_generated_include_dirs(${CMAKE_CURRENT_BINARY_DIR}) set(CODAC_MAIN_SUBHEADERS ${CODAC_MAIN_SUBHEADERS} "codac-core.h" PARENT_SCOPE) - file(WRITE ${CODAC_CORE_MAIN_HEADER} "/* This file is generated by CMake */\n\n") - file(APPEND ${CODAC_CORE_MAIN_HEADER} "#pragma once\n\n") + + # Generate the content of the umbrella header + set(CODAC_CORE_HEADER_CONTENT "/* This file is generated by CMake */\n\n#pragma once\n\n") foreach(header_path ${CODAC_CORE_HDR}) get_filename_component(header_name ${header_path} NAME) if((NOT header_name MATCHES "^.*_addons.*$") AND (NOT header_name MATCHES "^.*_impl.*$")) - file(APPEND ${CODAC_CORE_MAIN_HEADER} "#include <${header_name}>\n") + string(APPEND CODAC_CORE_HEADER_CONTENT "#include <${header_name}>\n") endif() endforeach() - file(COPY ${CODAC_CORE_MAIN_HEADER} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../include) + + # Compare with the existing file, if any + if(EXISTS "${CODAC_CORE_MAIN_HEADER}") + file(READ "${CODAC_CORE_MAIN_HEADER}" EXISTING_CONTENT) + string(MD5 EXISTING_HASH "${EXISTING_CONTENT}") + string(MD5 NEW_HASH "${CODAC_CORE_HEADER_CONTENT}") + else() + set(EXISTING_HASH "") + set(NEW_HASH "new") + endif() + + # Only rewrite it when it changed + if(NOT EXISTING_HASH STREQUAL NEW_HASH) + file(WRITE "${CODAC_CORE_MAIN_HEADER}" "${CODAC_CORE_HEADER_CONTENT}") + endif() # Install files in system directories +# ==================================== install(TARGETS ${PROJECT_NAME}-core DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES ${CODAC_CORE_HDR} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}-core) diff --git a/src/core/tools/codac2_Approx.h b/src/core/tools/codac2_Approx.h index 721da8249..ef0583af5 100644 --- a/src/core/tools/codac2_Approx.h +++ b/src/core/tools/codac2_Approx.h @@ -40,7 +40,25 @@ namespace codac2 friend bool operator==(const T& x1, const Approx& x2) { if constexpr(std::is_same_v) - return std::fabs(x1-x2._x) < x2._eps; + { + if(std::isnan(x1) && std::isnan(x2._x)) + return true; + + else if(x2._x == std::numeric_limits::max()) + return x1 == std::numeric_limits::max(); + + else if(x2._x == -std::numeric_limits::max()) + return x1 == -std::numeric_limits::max(); + + else if(std::isinf(x2._x)) + return x1 == x2._x; + + else if(x2._x < 1.0 && x2._x > -1.0) + return std::fabs(x1-x2._x) < x2._eps; // absolute error + + else + return std::fabs(x1-x2._x) < x2._eps*std::max(std::fabs(x1),std::fabs(x2._x)); // relative error + } else if(x1.size() != x2._x.size()) return false; diff --git a/src/extensions/capd/CMakeLists.txt b/src/extensions/capd/CMakeLists.txt index 0214cb955..c5e3a3758 100644 --- a/src/extensions/capd/CMakeLists.txt +++ b/src/extensions/capd/CMakeLists.txt @@ -22,7 +22,17 @@ list(APPEND CODAC_CAPD_SRC #endif() add_library(${PROJECT_NAME}-capd ${CODAC_CAPD_SRC}) - target_link_libraries(${PROJECT_NAME}-capd PUBLIC ${PROJECT_NAME}-core Ibex::ibex Eigen3::Eigen capd::capd) + target_link_libraries(${PROJECT_NAME}-capd PUBLIC ${PROJECT_NAME}-core Codac::gaol Eigen3::Eigen capd::capd) + + # The directory holding this module's public headers, in its canonical + # location under src/. It had none declared at all: its headers were only + # reachable through their copy in build/include. Publishing it lets every + # consumer open them at their source path instead -- see + # codac_publish_include_dirs() in the top-level CMakeLists.txt for why that + # decides what the coverage report is worth. + set(CODAC_CAPD_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}) + target_include_directories(${PROJECT_NAME}-capd PUBLIC ${CODAC_CAPD_INCLUDE_DIRS}) + codac_publish_include_dirs(${CODAC_CAPD_INCLUDE_DIRS}) ################################################################################ @@ -42,20 +52,35 @@ list(APPEND CODAC_CAPD_SRC foreach(srcfile ${CODAC_CAPD_SRC}) if(srcfile MATCHES "\\.h$" OR srcfile MATCHES "\\.hpp$") list(APPEND CODAC_CAPD_HDR ${srcfile}) - file(COPY ${srcfile} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../../include) endif() endforeach() # Generating the file codac-capd.h set(CODAC_CAPD_MAIN_HEADER ${CMAKE_CURRENT_BINARY_DIR}/codac-capd.h) - file(WRITE ${CODAC_CAPD_MAIN_HEADER} "/* This file is generated by CMake */\n\n") - file(APPEND ${CODAC_CAPD_MAIN_HEADER} "#pragma once\n\n") + codac_publish_generated_include_dirs(${CMAKE_CURRENT_BINARY_DIR}) + + # Generate the content of the umbrella header + set(CODAC_CAPD_HEADER_CONTENT "/* This file is generated by CMake */\n\n#pragma once\n\n") foreach(header_path ${CODAC_CAPD_HDR}) get_filename_component(header_name ${header_path} NAME) - file(APPEND ${CODAC_CAPD_MAIN_HEADER} "#include <${header_name}>\n") + string(APPEND CODAC_CAPD_HEADER_CONTENT "#include <${header_name}>\n") endforeach() - file(COPY ${CODAC_CAPD_MAIN_HEADER} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../../include) + + # Compare with the existing file, if any + if(EXISTS "${CODAC_CAPD_MAIN_HEADER}") + file(READ "${CODAC_CAPD_MAIN_HEADER}" EXISTING_CONTENT) + string(MD5 EXISTING_HASH "${EXISTING_CONTENT}") + string(MD5 NEW_HASH "${CODAC_CAPD_HEADER_CONTENT}") + else() + set(EXISTING_HASH "") + set(NEW_HASH "new") + endif() + + # Only rewrite it when it changed + if(NOT EXISTING_HASH STREQUAL NEW_HASH) + file(WRITE "${CODAC_CAPD_MAIN_HEADER}" "${CODAC_CAPD_HEADER_CONTENT}") + endif() # Install files in system directories diff --git a/src/extensions/sympy/CMakeLists.txt b/src/extensions/sympy/CMakeLists.txt index d92bae572..99698751d 100644 --- a/src/extensions/sympy/CMakeLists.txt +++ b/src/extensions/sympy/CMakeLists.txt @@ -31,9 +31,19 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(${PROJECT_NAME}-sympy ${CODAC_SYMPY_SRC}) - target_link_libraries(${PROJECT_NAME}-sympy PUBLIC ${PROJECT_NAME}-core Ibex::ibex Eigen3::Eigen) + target_link_libraries(${PROJECT_NAME}-sympy PUBLIC ${PROJECT_NAME}-core Codac::gaol Eigen3::Eigen) target_link_libraries(${PROJECT_NAME}-sympy PRIVATE pybind11::pybind11) + # The directory holding this module's public headers, in its canonical + # location under src/. It had none declared at all: its headers were only + # reachable through their copy in build/include. Publishing it lets every + # consumer open them at their source path instead -- see + # codac_publish_include_dirs() in the top-level CMakeLists.txt for why that + # decides what the coverage report is worth. + set(CODAC_SYMPY_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}) + target_include_directories(${PROJECT_NAME}-sympy PUBLIC ${CODAC_SYMPY_INCLUDE_DIRS}) + codac_publish_include_dirs(${CODAC_SYMPY_INCLUDE_DIRS}) + ################################################################################ # For the generation of the PKG file ################################################################################ @@ -45,18 +55,31 @@ # Installation of libcodac-sympy files ################################################################################ - foreach(srcfile ${CODAC_SYMPY_PUBLIC_HDR}) - file(COPY ${srcfile} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../../include) - endforeach() - + # Generating the file codac-sympy.h set(CODAC_SYMPY_MAIN_HEADER ${CMAKE_CURRENT_BINARY_DIR}/codac-sympy.h) - file(WRITE ${CODAC_SYMPY_MAIN_HEADER} "/* This file is generated by CMake */\n\n") - file(APPEND ${CODAC_SYMPY_MAIN_HEADER} "#pragma once\n\n") + codac_publish_generated_include_dirs(${CMAKE_CURRENT_BINARY_DIR}) + + # Generate the content of the umbrella header + set(CODAC_SYMPY_HEADER_CONTENT "/* This file is generated by CMake */\n\n#pragma once\n\n") foreach(header_path ${CODAC_SYMPY_PUBLIC_HDR}) get_filename_component(header_name ${header_path} NAME) - file(APPEND ${CODAC_SYMPY_MAIN_HEADER} "#include <${header_name}>\n") + string(APPEND CODAC_SYMPY_HEADER_CONTENT "#include <${header_name}>\n") endforeach() - file(COPY ${CODAC_SYMPY_MAIN_HEADER} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../../include) + + # Compare with the existing file, if any + if(EXISTS "${CODAC_SYMPY_MAIN_HEADER}") + file(READ "${CODAC_SYMPY_MAIN_HEADER}" EXISTING_CONTENT) + string(MD5 EXISTING_HASH "${EXISTING_CONTENT}") + string(MD5 NEW_HASH "${CODAC_SYMPY_HEADER_CONTENT}") + else() + set(EXISTING_HASH "") + set(NEW_HASH "new") + endif() + + # Only rewrite it when it changed + if(NOT EXISTING_HASH STREQUAL NEW_HASH) + file(WRITE "${CODAC_SYMPY_MAIN_HEADER}" "${CODAC_SYMPY_HEADER_CONTENT}") + endif() install(TARGETS ${PROJECT_NAME}-sympy DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES ${CODAC_SYMPY_PUBLIC_HDR} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}-sympy) diff --git a/src/graphics/CMakeLists.txt b/src/graphics/CMakeLists.txt index 453a547d1..a9e691253 100644 --- a/src/graphics/CMakeLists.txt +++ b/src/graphics/CMakeLists.txt @@ -46,14 +46,23 @@ #endif() add_library(${PROJECT_NAME}-graphics ${CODAC_GRAPHICS_SRC}) - target_include_directories(${PROJECT_NAME}-graphics PUBLIC + + # The directories holding the public graphics headers, in their canonical + # location under src/graphics. Published so that the tests, the examples and + # the Python bindings reach them by this path rather than through their link + # in build/include -- see codac_publish_include_dirs() in the top-level + # CMakeLists.txt for why that decides what the coverage report is worth. + set(CODAC_GRAPHICS_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rd/ipe ${CMAKE_CURRENT_SOURCE_DIR}/3rd/vibes ${CMAKE_CURRENT_SOURCE_DIR}/figures ${CMAKE_CURRENT_SOURCE_DIR}/paver # deprecated, to be removed ${CMAKE_CURRENT_SOURCE_DIR}/styles ) - target_link_libraries(${PROJECT_NAME}-graphics PUBLIC ${PROJECT_NAME}-core Ibex::ibex Eigen3::Eigen ${PROJECT_NAME}-core) + + target_include_directories(${PROJECT_NAME}-graphics PUBLIC ${CODAC_GRAPHICS_INCLUDE_DIRS}) + codac_publish_include_dirs(${CODAC_GRAPHICS_INCLUDE_DIRS}) + target_link_libraries(${PROJECT_NAME}-graphics PUBLIC ${PROJECT_NAME}-core Codac::gaol Eigen3::Eigen ${PROJECT_NAME}-core) ################################################################################ @@ -73,21 +82,36 @@ foreach(srcfile ${CODAC_GRAPHICS_SRC}) if(srcfile MATCHES "\\.h$" OR srcfile MATCHES "\\.hpp$") list(APPEND CODAC_GRAPHICS_HDR ${srcfile}) - file(COPY ${srcfile} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../include) endif() endforeach() # Generating the file codac-graphics.h set(CODAC_GRAPHICS_MAIN_HEADER ${CMAKE_CURRENT_BINARY_DIR}/codac-graphics.h) + codac_publish_generated_include_dirs(${CMAKE_CURRENT_BINARY_DIR}) set(CODAC_MAIN_SUBHEADERS ${CODAC_MAIN_SUBHEADERS} "codac-graphics.h" PARENT_SCOPE) - file(WRITE ${CODAC_GRAPHICS_MAIN_HEADER} "/* This file is generated by CMake */\n\n") - file(APPEND ${CODAC_GRAPHICS_MAIN_HEADER} "#pragma once\n\n") + + # Generate the content of the umbrella header + set(CODAC_GRAPHICS_HEADER_CONTENT "/* This file is generated by CMake */\n\n#pragma once\n\n") foreach(header_path ${CODAC_GRAPHICS_HDR}) get_filename_component(header_name ${header_path} NAME) - file(APPEND ${CODAC_GRAPHICS_MAIN_HEADER} "#include <${header_name}>\n") + string(APPEND CODAC_GRAPHICS_HEADER_CONTENT "#include <${header_name}>\n") endforeach() - file(COPY ${CODAC_GRAPHICS_MAIN_HEADER} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../include) + + # Compare with the existing file, if any + if(EXISTS "${CODAC_GRAPHICS_MAIN_HEADER}") + file(READ "${CODAC_GRAPHICS_MAIN_HEADER}" EXISTING_CONTENT) + string(MD5 EXISTING_HASH "${EXISTING_CONTENT}") + string(MD5 NEW_HASH "${CODAC_GRAPHICS_HEADER_CONTENT}") + else() + set(EXISTING_HASH "") + set(NEW_HASH "new") + endif() + + # Only rewrite it when it changed + if(NOT EXISTING_HASH STREQUAL NEW_HASH) + file(WRITE "${CODAC_GRAPHICS_MAIN_HEADER}" "${CODAC_GRAPHICS_HEADER_CONTENT}") + endif() # Install files in system directories diff --git a/src/unsupported/CMakeLists.txt b/src/unsupported/CMakeLists.txt index a0a0f8d8f..2f5f1b1a4 100644 --- a/src/unsupported/CMakeLists.txt +++ b/src/unsupported/CMakeLists.txt @@ -18,13 +18,17 @@ #endif() add_library(${PROJECT_NAME}-unsupported ${CODAC_UNSUPPORTED_SRC}) - target_include_directories(${PROJECT_NAME}-unsupported PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/3rd/ipe - ${CMAKE_CURRENT_SOURCE_DIR}/3rd/vibes - ${CMAKE_CURRENT_SOURCE_DIR}/figures - ${CMAKE_CURRENT_SOURCE_DIR}/paver - ${CMAKE_CURRENT_SOURCE_DIR}/styles - ) + + # The directory holding the public unsupported headers. The list used to be + # a copy of the graphics one (3rd/ipe, figures, paver, styles), none of which + # exists here: this module keeps its single header at its own root, so that + # is what has to be on the include path -- and what consumers must reach it + # through rather than through its link in build/include, see + # codac_publish_include_dirs() in the top-level CMakeLists.txt. + set(CODAC_UNSUPPORTED_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}) + + target_include_directories(${PROJECT_NAME}-unsupported PUBLIC ${CODAC_UNSUPPORTED_INCLUDE_DIRS}) + codac_publish_include_dirs(${CODAC_UNSUPPORTED_INCLUDE_DIRS}) target_link_libraries(${PROJECT_NAME}-unsupported PUBLIC ${PROJECT_NAME}-core Eigen3::Eigen) @@ -45,20 +49,35 @@ foreach(srcfile ${CODAC_UNSUPPORTED_SRC}) if(srcfile MATCHES "\\.h$" OR srcfile MATCHES "\\.hpp$") list(APPEND CODAC_UNSUPPORTED_HDR ${srcfile}) - file(COPY ${srcfile} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../include) endif() endforeach() # Generating the file codac-unsupported.h set(CODAC_UNSUPPORTED_MAIN_HEADER ${CMAKE_CURRENT_BINARY_DIR}/codac-unsupported.h) - file(WRITE ${CODAC_UNSUPPORTED_MAIN_HEADER} "/* This file is generated by CMake */\n\n") - file(APPEND ${CODAC_UNSUPPORTED_MAIN_HEADER} "#pragma once\n\n") + codac_publish_generated_include_dirs(${CMAKE_CURRENT_BINARY_DIR}) + + # Generate the content of the umbrella header + set(CODAC_UNSUPPORTED_HEADER_CONTENT "/* This file is generated by CMake */\n\n#pragma once\n\n") foreach(header_path ${CODAC_UNSUPPORTED_HDR}) get_filename_component(header_name ${header_path} NAME) - file(APPEND ${CODAC_UNSUPPORTED_MAIN_HEADER} "#include <${header_name}>\n") + string(APPEND CODAC_UNSUPPORTED_HEADER_CONTENT "#include <${header_name}>\n") endforeach() - file(COPY ${CODAC_UNSUPPORTED_MAIN_HEADER} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/../../include) + + # Compare with the existing file, if any + if(EXISTS "${CODAC_UNSUPPORTED_MAIN_HEADER}") + file(READ "${CODAC_UNSUPPORTED_MAIN_HEADER}" EXISTING_CONTENT) + string(MD5 EXISTING_HASH "${EXISTING_CONTENT}") + string(MD5 NEW_HASH "${CODAC_UNSUPPORTED_HEADER_CONTENT}") + else() + set(EXISTING_HASH "") + set(NEW_HASH "new") + endif() + + # Only rewrite it when it changed + if(NOT EXISTING_HASH STREQUAL NEW_HASH) + file(WRITE "${CODAC_UNSUPPORTED_MAIN_HEADER}" "${CODAC_UNSUPPORTED_HEADER_CONTENT}") + endif() # Install files in system directories diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dff12a6f4..be458b8c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,19 +15,39 @@ else() FetchContent_Declare(Catch2 URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.6.0.zip) #FetchContent_Declare(Catch2 URL ${CMAKE_CURRENT_SOURCE_DIR}/../3rd/Catch2-3.6.0.zip) # If needed to be self-contained... FetchContent_MakeAvailable(Catch2) + # See codac_mark_target_system() in the top-level CMakeLists.txt: without + # this, Catch2's headers inherit this project's warning level. Only the + # FetchContent path needs it -- a find_package() target is IMPORTED, and + # CMake already treats those as system. + codac_mark_target_system(Catch2) + codac_mark_target_system(Catch2::Catch2WithMain) endif() # Adds Catch2::Catch2WithMain + +# Test sources +# ================================================================== + list(APPEND SRC_TESTS # listing files without extension + # ---------------------------------------------------------------- # 3rd - - # Core + # ---------------------------------------------------------------- core/3rd/codac2_tests_eigen + + # ---------------------------------------------------------------- + # Actions + # ---------------------------------------------------------------- + core/actions/codac2_tests_OctaSym + + # ---------------------------------------------------------------- + # Contractors + # ---------------------------------------------------------------- + core/contractors/codac2_tests_CtcAction core/contractors/codac2_tests_CtcCartProd core/contractors/codac2_tests_CtcCtcBoundary @@ -45,36 +65,67 @@ list(APPEND SRC_TESTS # listing files without extension core/contractors/codac2_tests_CtcUnion core/contractors/codac2_tests_CtcVisible core/contractors/codac2_tests_linear_ctc + ../doc/manual/manual/contractors/geometric/src ../doc/manual/manual/contractors/analytic/src ../doc/manual/manual/contractors/set/src + ../doc/manual/manual/contractors/shape/src + + + # ---------------------------------------------------------------- + # Domains + # ---------------------------------------------------------------- core/domains/codac2_tests_BoolInterval + core/domains/ellipsoid/codac2_tests_Ellipsoid + core/domains/interval/codac2_tests_Interval core/domains/interval/codac2_tests_Interval_operations core/domains/interval/codac2_tests_IntervalMatrix core/domains/interval/codac2_tests_IntervalVector + ../doc/manual/manual/intervals/src + core/domains/zonotope/codac2_tests_Parallelepiped_eval core/domains/zonotope/codac2_tests_Parallelepiped core/domains/zonotope/codac2_tests_Zonotope + core/domains/tube/codac2_tests_TDomain core/domains/tube/codac2_tests_Slice core/domains/tube/codac2_tests_Slice_polygon core/domains/tube/codac2_tests_SlicedTube core/domains/tube/codac2_tests_SlicedTube_integral + ../doc/manual/manual/tubes/src + + # ---------------------------------------------------------------- + # Functions + # ---------------------------------------------------------------- + core/functions/analytic/codac2_tests_AnalyticFunction + ../doc/manual/manual/functions/analytic/src + ../doc/manual/manual/functions/parallelepiped/src + + + # ---------------------------------------------------------------- + # Geometry + # ---------------------------------------------------------------- core/geometry/codac2_tests_ConvexPolygon core/geometry/codac2_tests_geometry core/geometry/codac2_tests_Polygon core/geometry/codac2_tests_Segment + ../doc/manual/manual/geometry/src + + # ---------------------------------------------------------------- + # Matrices + # ---------------------------------------------------------------- + core/matrices/codac2_tests_arithmetic_add core/matrices/codac2_tests_arithmetic_div core/matrices/codac2_tests_arithmetic_mul @@ -86,12 +137,30 @@ list(APPEND SRC_TESTS # listing files without extension core/matrices/codac2_tests_inversion core/matrices/codac2_tests_IntFullPivLU core/matrices/codac2_tests_GaussJordan + ../doc/manual/manual/linear/src + + # ---------------------------------------------------------------- + # Operators + # ---------------------------------------------------------------- + core/operators/codac2_tests_operators + + # ---------------------------------------------------------------- + # Peibos + # ---------------------------------------------------------------- + core/peibos/codac2_tests_peibos + ../doc/manual/manual/functions/peibos/src + + + # ---------------------------------------------------------------- + # Separators + # ---------------------------------------------------------------- + core/separators/codac2_tests_SepCartProd core/separators/codac2_tests_SepCtcBoundary core/separators/codac2_tests_SepInter @@ -102,20 +171,42 @@ list(APPEND SRC_TESTS # listing files without extension core/separators/codac2_tests_SepTransform core/separators/codac2_tests_SepUnion core/separators/codac2_tests_SepVisible - + + + # ---------------------------------------------------------------- + # Tools + # ---------------------------------------------------------------- + core/tools/codac2_tests_Approx core/tools/codac2_tests_serialization core/tools/codac2_tests_transformations core/tools/codac2_tests_trunc - core/tools/ibex/codac2_tests_ibex + # core/tools/ibex/codac2_tests_ibex is not built: it includes the headers of + # IBEX, which Codac no longer depends on (the conversions it tests are only + # compiled when a program includes IBEX before them) + ../doc/manual/manual/tools/src - + + + # ---------------------------------------------------------------- + # Trajectory + # ---------------------------------------------------------------- + core/trajectory/codac2_tests_AnalyticTraj core/trajectory/codac2_tests_SampledTraj + + # ---------------------------------------------------------------- + # Graphics + # ---------------------------------------------------------------- + graphics/styles/codac2_tests_Color ) + +# Python files required by tests +# ================================================================== + file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/core/domains/tube/codac2_tests_predefined_tubes.py # Add here other Python files that are not tests but need to be exported to Python package @@ -125,14 +216,26 @@ file(COPY ${CMAKE_BINARY_DIR}/python/python_package/codac/tests ) + +# Libraries +# ================================================================== + set(CODAC_LIBRARIES ${PROJECT_NAME}-core ${PROJECT_NAME}-graphics) +# Sympy +# ================================================================== + option(BUILD_SYMPY_EMBED_TESTS "Build C++ tests that require embedded Python for Sympy" ON) if(WITH_PYTHON AND DEFINED PYBIND11_FINDPYTHON AND NOT PYBIND11_FINDPYTHON) message(STATUS "Disabling Sympy C++ embed tests because PYBIND11_FINDPYTHON=OFF") set(BUILD_SYMPY_EMBED_TESTS OFF) endif() +# BUILD_SYMPY_EMBED_TESTS is an option(), hence a cache entry: the set() above +# only overrides it for this directory, and examples/ would otherwise read the +# cached ON again and build the sympy example where its own test is disabled. +set(BUILD_SYMPY_EMBED_TESTS "${BUILD_SYMPY_EMBED_TESTS}" PARENT_SCOPE) + # Sympy test if (WITH_PYTHON AND BUILD_SYMPY_EMBED_TESTS) list(APPEND SRC_TESTS @@ -141,7 +244,9 @@ if (WITH_PYTHON AND BUILD_SYMPY_EMBED_TESTS) ) endif() -# CAPD test + +# CAPD +# ================================================================== if (WITH_CAPD) list(APPEND SRC_TESTS extensions/capd/codac2_tests_capd @@ -158,26 +263,393 @@ endif() #message(STATUS "Found IBEX version ${IBEX_VERSION}") #endif() + + +# Common Codac include directories +# ================================================================== +# +# IMPORTANT: +# Use the source tree as the canonical header tree. +# +# The headers are now used directly from the source tree. + +# CODAC_SOURCE_INCLUDE_DIRS is filled in by the src/*/CMakeLists.txt of every +# module (core, graphics, unsupported, and capd/sympy when enabled), which are +# all configured before this directory. Using it rather than a second copy of +# the same list keeps the tests compiling against exactly the headers the +# libraries were built from; the copy that used to live here covered core only +# and had already drifted from it. +set(CODAC_CORE_SOURCE_INCLUDE_DIRS ${CODAC_SOURCE_INCLUDE_DIRS}) + + +# ${CMAKE_BINARY_DIR}/src holds the generated "codac" umbrella header; +# CODAC_GENERATED_INCLUDE_DIRS, filled in by the src/*/CMakeLists.txt of every +# module, holds the per-module ones (codac-core.h and the rest), each of which +# is written into its own module's binary directory rather than into this one. +set( + CODAC_HEADERS_DIR + ${CMAKE_BINARY_DIR}/src + ${CODAC_GENERATED_INCLUDE_DIRS} +) + + + +# ------------------------------------------------------------------ +# Sanitizer runtime preload for Python tests (Debug builds only) +# ------------------------------------------------------------------ +# +# In Debug mode, _core.so (the pybind11 module) is compiled with +# -fsanitize=address,undefined and depends on a *shared* sanitizer +# runtime rather than one statically embedded in the .so: +# - Clang needs an explicit -shared-libsan flag to get that (see +# python/src/core/CMakeLists.txt) -- its default is to embed the +# runtime statically into shared libraries. +# - GCC needs no flag at all: unless -static-libasan/-static-libubsan +# is passed (which codac never does), GCC already links libasan.so / +# libubsan.so dynamically by default. Passing Clang's -shared-libsan +# to GCC is not an option either way -- GCC's driver rejects it +# outright as an unrecognized flag. +# Either way, since python3 itself has no sanitizer runtime, that shared +# runtime must be preloaded before Python imports codac, otherwise symbols +# like __ubsan_vptr_type_cache/__asan_* stay unresolved -- or, on GCC, the +# module load aborts immediately with "ASan runtime does not come first in +# initial library list" (reproduced locally: a plain dlopen(), from an +# unsanitized host, of a GCC ASan-built .so fails exactly this way without +# LD_PRELOAD, and succeeds once libasan.so is preloaded). + +set(PYTHON_TEST_ENV_ARGS "PYTHONPATH=${PYTHON_PACKAGE_DIR}") + +# Not gated behind a plain CMAKE_BUILD_TYPE STREQUAL "Debug" check (see the +# matching note in the root CMakeLists.txt): that variable is meaningless +# for multi-configuration generators, which instead pick the actual +# configuration later, at build time. CMAKE_CONFIGURATION_TYPES is set +# precisely on those generators, so probing here also runs whenever it is +# non-empty -- the individual LD_PRELOAD/ASAN_OPTIONS/UBSAN_OPTIONS entries +# appended below are themselves wrapped in a $ generator +# expression (see how PYTHON_TEST_ENV_ARGS is consumed via the ENVIRONMENT +# test property further down), so they still only take effect for an +# actual Debug-configuration test run. +if(WITH_PYTHON AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_CONFIGURATION_TYPES) + AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU" AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + + # This whole block is Linux-specific by construction, not just by + # "is the compiler Clang or GCC": LD_PRELOAD is a Linux/glibc loader + # feature with no equivalent environment variable on Windows, and the + # shared runtime file names it looks for below only exist in that form + # on Linux -- macOS uses DYLD_INSERT_LIBRARIES and ships its runtimes + # under different names (e.g. Clang's combined, no-arch-suffix + # libclang_rt.asan_osx_dynamic.dylib), and Windows uses neither an + # env-var preload mechanism nor this naming (clang-cl names them + # clang_rt.asan_dynamic-.dll; MinGW-w64 GCC ships asan/ubsan as + # DLLs with their own layout). Real MSVC (cl.exe) is already excluded + # by the CMAKE_CXX_COMPILER_ID check above, but clang-cl (Windows) and + # AppleClang (macOS) both also match "Clang" (and MinGW GCC still + # matches "GNU"), so without this explicit Linux check they would + # silently fall through to the "no shared runtime found" warning below + # instead of making clear that the feature just does not apply there. + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + + # Note: `clang --print-runtime-dir` assumes a per-target-runtime-dir + # layout (lib//) which some distro packages (e.g. Ubuntu/ + # Debian's llvm.org apt packages) do not use, instead keeping the legacy + # lib/linux/ layout: the reported directory would not exist there, even + # though the runtime libraries are actually installed. `-print-file-name` + # asks the driver to resolve each file the way it would at link time, + # which works regardless of which layout the distro packaging uses. + # + # The runtime file names themselves are not portable either: Clang embeds + # its own architecture name in them (e.g. libclang_rt.asan-aarch64.so on + # an AArch64 Raspberry Pi OS, libclang_rt.asan-armhf.so on a 32-bit + # ARMv7/ARMv8 hard-float userland). Rather than maintaining our own + # CMAKE_SYSTEM_PROCESSOR -> Clang-runtime-name table (which would drift + # out of sync as LLVM adds targets), ask the compiler itself: + # `-dumpmachine` prints its default target triple (understood by both + # Clang and GCC, and already normalized -- e.g. a 32-bit x86 compiler + # reports "i386", never "i686"), and its first field is, for essentially + # every architecture, precisely the suffix Clang's runtime uses. + # + # The one documented exception is 32-bit ARM, where the suffix instead + # follows the float ABI recorded in the triple's environment field + # (.../gnueabihf -> "armhf" hard-float, vs. plain "arm" otherwise) rather + # than the architecture field (which is typically "arm"/"armv7"/...) -- + # this is an LLVM naming convention, not something any compiler flag + # reports directly, so it is the one case still special-cased below. + execute_process( + COMMAND ${CMAKE_CXX_COMPILER} -dumpmachine + OUTPUT_VARIABLE _clang_target_triple + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(_sanitizer_rt_arch "") + if(_clang_target_triple) + string(REPLACE "-" ";" _clang_triple_fields "${_clang_target_triple}") + list(GET _clang_triple_fields 0 _sanitizer_rt_arch) + if(_sanitizer_rt_arch MATCHES "^armv?[0-9]*l?$") + if(_clang_target_triple MATCHES "eabihf$") + set(_sanitizer_rt_arch "armhf") + else() + set(_sanitizer_rt_arch "arm") + endif() + endif() + endif() + + # The ASan runtime only, deliberately: built with -fsanitize=address, + # undefined, Clang puts the UndefinedBehaviorSanitizer inside the ASan + # runtime, and ships libclang_rt.ubsan_standalone-* for the builds that + # use UBSan on its own. Preloading both therefore loads two copies of the + # same initialisation and the same interceptors into one process. Clang 18 + # tolerated it; Clang 21 wedges the interpreter so thoroughly that even + # SIGKILL does not remove it, which is what made every Python test of the + # Clang jobs of .github/workflows/unixdebug.yml run into its timeout while + # the GCC jobs of the same matrix passed them in about two seconds -- + # GCC's libasan and libubsan being genuinely separate libraries, designed + # to be loaded together. Preloading ASan alone still gives both sets of + # checks, since one runtime implements them both. + set(_sanitizer_rt_candidates "") + if(_sanitizer_rt_arch) + set(_sanitizer_rt_candidates + libclang_rt.asan-${_sanitizer_rt_arch}.so + ) + endif() + + else() # GNU (GCC) + + # Unlike Clang, GCC's driver is built for one target only, so its + # runtime file names carry no architecture suffix -- `-print-file-name` + # already resolves to the right library for whatever target this GCC + # was built for, on every architecture. GCC also splits ASan and UBSan + # into two separately-named libraries (libasan.so / libubsan.so), + # rather than Clang's libclang_rt.asan-*/libclang_rt.ubsan_standalone-* + # naming. + set(_sanitizer_rt_candidates libasan.so libubsan.so) + + endif() + + set(SANITIZER_RT_LIBS "") + foreach(_rt_lib_name ${_sanitizer_rt_candidates}) + execute_process( + COMMAND ${CMAKE_CXX_COMPILER} -print-file-name=${_rt_lib_name} + OUTPUT_VARIABLE _rt_lib_path + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(EXISTS "${_rt_lib_path}") + list(APPEND SANITIZER_RT_LIBS "${_rt_lib_path}") + endif() + endforeach() + + if(SANITIZER_RT_LIBS) + list(JOIN SANITIZER_RT_LIBS ":" SANITIZER_RT_LIBS_JOINED) + list(APPEND PYTHON_TEST_ENV_ARGS "$<$:LD_PRELOAD=${SANITIZER_RT_LIBS_JOINED}>") + + # LeakSanitizer (part of ASan) flags many long-lived, process-lifetime + # allocations made by the Python interpreter and by pybind11 module + # init (interned strings, type objects, strdup'd docstrings...) as + # "leaks", since it can't trace Python's own object graph. This is a + # well-known false-positive source for embedded Python interpreters, + # not something codac's code controls. UBSan and ASan's other checks + # (use-after-free, buffer overflow, UB...) remain fully active. + list(APPEND PYTHON_TEST_ENV_ARGS "$<$:ASAN_OPTIONS=detect_leaks=0>") + list(APPEND PYTHON_TEST_ENV_ARGS "$<$:UBSAN_OPTIONS=print_stacktrace=1>") + message(STATUS "Python tests: LD_PRELOAD=${SANITIZER_RT_LIBS_JOINED}") + else() + message(WARNING + "Debug build with WITH_PYTHON: no shared ASan/UBSan runtime found " + "for ${CMAKE_CXX_COMPILER}. With Clang, make sure the _core target " + "is linked with -shared-libsan; with GCC, make sure it is not " + "linked with -static-libasan/-static-libubsan. Otherwise Python " + "tests will fail with 'undefined symbol' errors, or abort with " + "'ASan runtime does not come first in initial library list'.") + endif() + +endif() + +# ------------------------------------------------------------------ +# Sanitizer runtime discoverability for MSVC (Debug builds only) +# ------------------------------------------------------------------ +# +# CODAC_MSVC_ASAN_RUNTIME_DLL is set by the root CMakeLists.txt (empty if +# no matching runtime could be found, in which case /fsanitize=address was +# never even added -- nothing to do here either). Windows has no +# LD_PRELOAD-style env var and no "must load first" requirement: the +# loader just needs to be able to find the runtime DLL via PATH or next +# to the binary that imports it, when that binary is loaded. Python's +# _core.pyd gets its own copy placed next to it (see +# python/src/core/CMakeLists.txt); the plain C++ test executables and the +# python.exe launched below have no such fixed "next to" location, so +# their directory search is extended via PATH instead, the direct +# Windows analogue of prepending to LD_PRELOAD above. +set(CODAC_MSVC_ASAN_PATH_ENV "") +if(MSVC AND CODAC_MSVC_ASAN_RUNTIME_DLL) + get_filename_component(_msvc_asan_dll_dir "${CODAC_MSVC_ASAN_RUNTIME_DLL}" DIRECTORY) + # Both ctest's ENVIRONMENT test property and the COMMAND argument list + # passed to `cmake -E env` below are themselves ';'-separated CMake + # lists, so the ';' that Windows uses to separate PATH entries has to be + # escaped as '\;' here -- otherwise CMake would silently split this one + # "PATH=......" value into several unrelated list entries. + # + # NOTE: $ENV{PATH} is captured at CMake *configure* time, i.e. whatever + # PATH was active when `cmake` was run to configure the project. If + # ctest is later invoked from a shell with a materially different PATH, + # this won't reflect that -- a limitation inherent to baking an + # environment value into generated build files, not specific to this + # check. + string(REPLACE ";" "\\;" _msvc_asan_path_escaped "${_msvc_asan_dll_dir};$ENV{PATH}") + set(CODAC_MSVC_ASAN_PATH_ENV "PATH=${_msvc_asan_path_escaped}") + # Appended for every configuration rather than wrapped in a $ + # generator expression. `cmake -E env` reads its arguments as VAR=value + # assignments up to the first one that is not, which it then takes for the + # command to run: an element that a generator expression reduced to nothing + # leaves an empty argument in the middle of the list, and the command becomes + # the empty string. CODAC_MSVC_ASAN_RUNTIME_DLL is computed whatever the + # configuration (see the root CMakeLists.txt), so on MSVC the element was + # always there and always empty outside Debug -- every Python test of + # .github/workflows/vcmatrix.yml died on "no such file or directory" while + # the C++ ones passed. Prepending the sanitizer directory to PATH costs a + # Release run nothing, which is what the C++ tests below already rely on. + list(APPEND PYTHON_TEST_ENV_ARGS "${CODAC_MSVC_ASAN_PATH_ENV}") +endif() + + +# Environment of the C++ tests and examples +# ================================================================== +# +# The same environment is needed by every C++ binary this project runs under +# ctest, the examples of examples/CMakeLists.txt included, so it is built once +# here and exported to the parent scope rather than recomputed per test. +# +# UBSAN_OPTIONS only means something to the GCC/Clang single-configuration +# builds, where CMAKE_BUILD_TYPE holds the answer. The MSVC AddressSanitizer +# runtime is a DLL that has to be discoverable at load time by every binary +# compiled with /fsanitize=address: without it on PATH a Debug test cannot even +# start, Windows failing the process creation with 0xC0000135 +# (STATUS_DLL_NOT_FOUND) before a single assertion runs. It is added whatever +# the configuration, because the multi-configuration generators MSVC is driven +# through only choose theirs at build time, and prepending a toolchain +# directory to PATH costs a Release run nothing. + +set(CODAC_CPP_TEST_ENV "") + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND CODAC_CPP_TEST_ENV "UBSAN_OPTIONS=print_stacktrace=1") +endif() + +if(CODAC_MSVC_ASAN_PATH_ENV) + list(APPEND CODAC_CPP_TEST_ENV "${CODAC_MSVC_ASAN_PATH_ENV}") +endif() + +# examples/ is added by the top-level CMakeLists.txt right after this directory +# and runs the same kind of binaries, so both environments are handed over to it. +set(CODAC_CPP_TEST_ENV "${CODAC_CPP_TEST_ENV}" PARENT_SCOPE) +set(PYTHON_TEST_ENV_ARGS "${PYTHON_TEST_ENV_ARGS}" PARENT_SCOPE) + +# Build every test +# ================================================================== + foreach(SRC_TEST ${SRC_TESTS}) string(REPLACE "/" "_" TEST_NAME ${SRC_TEST}) string(REPLACE "codac2_tests_" "" TEST_NAME ${TEST_NAME}) set(TEST_NAME codac2_tests_${TEST_NAME}) + + # --------------------------------------------------------------- # C++ test - add_executable(${TEST_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/${SRC_TEST}.cpp) - set(CODAC_HEADERS_DIR ${CMAKE_CURRENT_BINARY_DIR}/../include) - target_include_directories(${TEST_NAME} SYSTEM PUBLIC ${CODAC_HEADERS_DIR}) - target_link_libraries(${TEST_NAME} PUBLIC Ibex::ibex ${CODAC_LIBRARIES} PRIVATE Catch2::Catch2WithMain) + # --------------------------------------------------------------- + + add_executable( + ${TEST_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/${SRC_TEST}.cpp + ) + + + # Use the same canonical source headers as the libraries, and reach them by + # the same path: the source directories come first so that a header is + # opened at src/... Both compile identically, but the path used is the path gcov + # records, and different paths would split every header into two unrelated + # entries in the coverage report (see codac_publish_include_dirs() in the + # top-level CMakeLists.txt). + # + # CODAC_HEADERS_DIR is kept, after them, because it holds the generated + # umbrella headers: codac, codac-core.h, codac-graphics.h and the rest. + + target_include_directories( + ${TEST_NAME} + SYSTEM PUBLIC + + ${CODAC_CORE_SOURCE_INCLUDE_DIRS} + + ${CODAC_HEADERS_DIR} + ) + + + target_link_libraries( + ${TEST_NAME} + PUBLIC + + ${CODAC_LIBRARIES} + + PRIVATE + + Catch2::Catch2WithMain + ) + + + # Sympy embedded tests + if( + WITH_PYTHON + AND SRC_TEST MATCHES "extensions/sympy/" + ) + + target_link_libraries( + ${TEST_NAME} + PRIVATE + + ${PROJECT_NAME}-sympy + pybind11::embed + ) - if(WITH_PYTHON AND SRC_TEST MATCHES "extensions/sympy/") - target_link_libraries(${TEST_NAME} PRIVATE ${PROJECT_NAME}-sympy pybind11::embed) endif() - add_dependencies(check ${TEST_NAME}) - add_test(NAME ${TEST_NAME}_cpp COMMAND ${TEST_NAME}) + + # C++20 + set_target_properties( + ${TEST_NAME} + PROPERTIES + + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) + + + add_dependencies( + check + ${TEST_NAME} + ) + + add_test( + NAME ${TEST_NAME}_cpp + COMMAND ${TEST_NAME} + ) + + if(CODAC_CPP_TEST_ENV) + set_tests_properties(${TEST_NAME}_cpp PROPERTIES + ENVIRONMENT "${CODAC_CPP_TEST_ENV}" + ) + endif() + + # --------------------------------------------------------------- # Python test - if(WITH_PYTHON) - add_test(NAME ${TEST_NAME}_py COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/${SRC_TEST}.py) + # --------------------------------------------------------------- + + if(WITH_PYTHON AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${SRC_TEST}.py") + add_test( + NAME ${TEST_NAME}_py + COMMAND + ${CMAKE_COMMAND} -E env + ${PYTHON_TEST_ENV_ARGS} + ${PYTHON_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/${SRC_TEST}.py + ) endif() endforeach() diff --git a/tests/core/domains/tube/codac2_tests_SlicedTube_integral.cpp b/tests/core/domains/tube/codac2_tests_SlicedTube_integral.cpp index 4cba879ab..c5bd5294b 100644 --- a/tests/core/domains/tube/codac2_tests_SlicedTube_integral.cpp +++ b/tests/core/domains/tube/codac2_tests_SlicedTube_integral.cpp @@ -151,8 +151,8 @@ TEST_CASE("Computing integration from 0, interval argument") CHECK(Approx(x.integral(Interval(12.5))) == Interval(6.5,20.5)); CHECK(Approx(x.integral(Interval(14.5))) == Interval(7,23.5)); auto p_intv = x.partial_integral(Interval(12.5,14.5)); - CHECK(p_intv.first == Interval(6.,7.)); - CHECK(p_intv.second == Interval(20.5,23.5)); + CHECK(Approx(p_intv.first) == Interval(6.,7.)); + CHECK(Approx(p_intv.second) == Interval(20.5,23.5)); CHECK(Approx(x.integral(Interval(12.5,14.5))) == Interval(6.0,23.5)); CHECK(Approx(x.integral(Interval(0))) == Interval(0)); CHECK(Approx(x.integral(Interval(10.2))) == Interval(9.3,19.7)); diff --git a/tests/test_codac/CMakeLists.txt b/tests/test_codac/CMakeLists.txt index 0f4479bc9..c94d7dfb7 100644 --- a/tests/test_codac/CMakeLists.txt +++ b/tests/test_codac/CMakeLists.txt @@ -8,17 +8,6 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Adding IBEX - - # In case you installed IBEX in a local directory, you need - # to specify its path with the CMAKE_PREFIX_PATH option. - # set(CMAKE_PREFIX_PATH "~/ibex-lib/build_install") - set(CMAKE_PREFIX_PATH "../ibex") - - find_package(IBEX REQUIRED) - ibex_init_common() # IBEX should have installed this function - message(STATUS "Found IBEX version ${IBEX_VERSION}") - # Adding Codac # In case you installed Codac in a local directory, you need @@ -39,4 +28,4 @@ add_executable(${PROJECT_NAME} main.cpp) target_compile_options(${PROJECT_NAME} PUBLIC ${CODAC_CXX_FLAGS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${CODAC_INCLUDE_DIRS}) - target_link_libraries(${PROJECT_NAME} PUBLIC ${CODAC_LIBRARIES} Ibex::ibex) + target_link_libraries(${PROJECT_NAME} PUBLIC ${CODAC_LIBRARIES})