diff --git a/.github/actions/build-ci-image/action.yml b/.github/actions/build-ci-image/action.yml index a897ef39..05929e61 100644 --- a/.github/actions/build-ci-image/action.yml +++ b/.github/actions/build-ci-image/action.yml @@ -57,7 +57,7 @@ runs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ inputs.image }} tags: ${{ inputs.tags }} @@ -68,13 +68,13 @@ runs: org.opencontainers.image.title=libfn/ci/${{ inputs.title }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ${{ inputs.registry }} username: ${{ inputs.username }} @@ -82,7 +82,7 @@ runs: - name: Build and push by digest id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: platforms: ${{ inputs.platform }} build-args: ${{ inputs.build_args }} @@ -105,7 +105,7 @@ runs: - name: Upload digest if: ${{ inputs.push }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: digests-${{ inputs.title }}-${{ env.PLATFORM_PAIR }} path: /tmp/digests/* diff --git a/.github/actions/merge-ci-images/action.yml b/.github/actions/merge-ci-images/action.yml index 39762d3d..1b6b73dd 100644 --- a/.github/actions/merge-ci-images/action.yml +++ b/.github/actions/merge-ci-images/action.yml @@ -27,24 +27,24 @@ runs: using: composite steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: path: /tmp/digests pattern: digests-${{ inputs.title }}-* merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ inputs.image }} tags: ${{ inputs.tags }} - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ${{ inputs.registry }} username: ${{ inputs.username }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ad5e52ad --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directories: + - "/" + - "/.github/actions/build-ci-image" + - "/.github/actions/merge-ci-images" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6216692..cfb42e47 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,8 @@ jobs: - "clang:20" - "clang:21" - "clang:22" + - "clang-libstdcxx:21" + - "clang-libstdcxx:22" exclude: - mode: cxx23 compiler: "gcc:12" @@ -61,9 +63,16 @@ jobs: compiler: "clang:16" - mode: cxx23 compiler: "clang:17" + include: + - mode: all + configuration: Debug + compiler: "gcc:14" + - mode: all + configuration: Release + compiler: "gcc:14" container: libfn.azurecr.io/ci-build-${{ matrix.compiler }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Prepare build run: | @@ -77,6 +86,7 @@ jobs: printf "C++ compilation options: %s\n" "$FLAGS" - name: Build and test ${{ matrix.mode }} + if: matrix.mode != 'all' run: | cd .build cmake --build . --target ${{ matrix.mode }} @@ -84,10 +94,9 @@ jobs: # Build and test all for one arbitrary configuration - name: Build and test all - if: ${{ matrix.compiler == 'gcc:14' && matrix.mode == 'cxx23' }} + if: matrix.mode == 'all' run: | cd .build - cmake --build . --target clean cmake --build . ctest --output-on-failure @@ -124,20 +133,33 @@ jobs: - mode: cxx23 compiler: appleclang osver: 14 + include: + - mode: all + configuration: Debug + compiler: appleclang + osver: 15 + clangrelease: NA + - mode: all + configuration: Release + compiler: appleclang + osver: 15 + clangrelease: NA steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Prepare build run: | mkdir .build cd .build if [[ "${{ matrix.compiler }}" == "appleclang" ]]; then - export CXX="$(which clang++)" - export CC="$(which clang)" + CXX="$(which clang++)" + CC="$(which clang)" + export CXX CC fi if [[ "${{ matrix.compiler }}" == "clang" ]]; then - export CXX="$(brew --prefix llvm@${{ matrix.clangrelease }})/bin/clang++" - export CC="$(brew --prefix llvm@${{ matrix.clangrelease }})/bin/clang" + CXX="$(brew --prefix llvm@${{ matrix.clangrelease }})/bin/clang++" + CC="$(brew --prefix llvm@${{ matrix.clangrelease }})/bin/clang" + export CXX CC fi cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} ${{ matrix.mode == 'cxx20' && '-DDISABLE_CXX23=ON' || '' }} .. COMPILER=$( grep -iE "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) @@ -147,6 +169,7 @@ jobs: printf "C++ compilation options: %s\n" "$FLAGS" - name: Build and test ${{ matrix.mode }} + if: matrix.mode != 'all' run: | cd .build cmake --build . --target ${{ matrix.mode }} @@ -154,15 +177,14 @@ jobs: # Build and test all for one arbitrary configuration - name: Build and test all - if: ${{ matrix.compiler == 'appleclang' && matrix.osver == '15' && matrix.mode == 'cxx23' }} + if: matrix.mode == 'all' run: | cd .build - cmake --build . --target clean cmake --build . ctest --output-on-failure windows: - runs-on: windows-${{ matrix.osver }} + runs-on: ${{ matrix.vs.runner }} strategy: fail-fast: false matrix: @@ -171,19 +193,21 @@ jobs: configuration: - Debug - Release - compiler: - - "Visual Studio 17 2022" - osver: - - 2025 + vs: + - generator: "Visual Studio 17 2022" + runner: "windows-2022" + - generator: "Visual Studio 18 2026" + runner: "windows-2025-vs2026" + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Prepare build shell: bash run: | mkdir .build cd .build - cmake -G "${{ matrix.compiler }}" -A x64 -DDISABLE_CXX23=ON .. + cmake -G "${{ matrix.vs.generator }}" -A x64 -DDISABLE_CXX23=ON .. LINKER=$( grep -iE "^CMAKE_LINKER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) printf "C++ linker path: %s\n" "$LINKER" @@ -207,12 +231,10 @@ jobs: - gcc - clang steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install Nix - uses: cachix/install-nix-action@v30 + uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 42829010..158de331 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -62,9 +62,11 @@ jobs: run: | image=${{ matrix.image }} name=${image%%:*} - echo "IMAGE=${name}" >> "${GITHUB_ENV}" - echo "RELEASE=${image##*:}" >> "${GITHUB_ENV}" - echo "COMPILER=${name%%-*}" >> "${GITHUB_ENV}" + { + echo "IMAGE=${name}" + echo "RELEASE=${image##*:}" + echo "COMPILER=${name%%-*}" + } >> "${GITHUB_ENV}" - name: Determine Debian version run: | @@ -83,7 +85,7 @@ jobs: fi fi - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: .github @@ -142,7 +144,7 @@ jobs: echo "IMAGE=${image%%:*}" >> "${GITHUB_ENV}" echo "RELEASE=${image##*:}" >> "${GITHUB_ENV}" - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: .github diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 712a5d80..9747fc03 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -19,6 +19,8 @@ on: env: REGISTRY_IMAGE: libfn.azurecr.io/ci-docs + DEBIAN_VERSION: trixie + JAVA_VERSION: openjdk-21-jdk DOXYGEN_RELEASE: 1.12.0 ZNAI_RELEASE: 1.73 CATCH_RELEASE: 3.14.0 @@ -36,7 +38,7 @@ jobs: - linux/arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: .github @@ -52,6 +54,8 @@ jobs: username: ${{ secrets.AZURECR_NAME }} password: ${{ secrets.AZURECR_PASS }} build_args: | + DEBIAN_VERSION=${{ env.DEBIAN_VERSION }} + JAVA_VERSION=${{ env.JAVA_VERSION }} DOXYGEN_RELEASE=${{ env.DOXYGEN_RELEASE }} ZNAI_RELEASE=${{ env.ZNAI_RELEASE }} CATCH_RELEASE=${{ env.CATCH_RELEASE }} @@ -68,7 +72,7 @@ jobs: - build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: .github diff --git a/.github/workflows/ci-pre-commit.yml b/.github/workflows/ci-pre-commit.yml index 34647771..ea7e8c3b 100644 --- a/.github/workflows/ci-pre-commit.yml +++ b/.github/workflows/ci-pre-commit.yml @@ -32,7 +32,7 @@ jobs: - linux/arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: .github @@ -59,7 +59,7 @@ jobs: - build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: .github diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8d8bd7f0..4d34e58b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -29,7 +29,7 @@ jobs: GCOV: /usr/local/bin/gcov steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Verify compiler compatibility env: @@ -47,9 +47,10 @@ jobs: printf "CXX=%s\nCXXFLAGS=%s\n" "$CXX" "$CXXFLAGS" $CXX --version | head -1 FILE=$(mktemp --tmpdir XXXXXX.cpp) - printf "$SOURCE\n" > $FILE + printf '%s\n' "$SOURCE" > "$FILE" OUT=$(mktemp --tmpdir XXXXXX) - $CXX -std=c++2b $CXXFLAGS -Wall $FILE -o $OUT + # shellcheck disable=SC2086 # $CXXFLAGS may carry multiple flags; word-splitting intentional + $CXX -std=c++2b $CXXFLAGS -Wall "$FILE" -o "$OUT" $OUT - name: Build with coverage instrumentation and compilation database @@ -65,17 +66,18 @@ jobs: printf "C++ compiler path: %s\n" "$COMPILER" $COMPILER --version printf "gcov version: %s\n" "$( $GCOV --version | head -1 )" - cmake --build . --target tests -- -j$(nproc) + cmake --build . --target tests -- -j"$(nproc)" - name: Generate coverage data with gcov shell: bash run: | cd .build ctest -L 'tests_p?fn' -j1 # generate .gcda files - $GCOV -pbc -r -s $( realpath .. ) $( find tests -type f -name '*.gcno' ) # generate .gcov files + # shellcheck disable=SC2046 # find emits one path per result; intentional word-splitting into gcov arg list + $GCOV -pbc -r -s "$(realpath ..)" $(find tests -type f -name '*.gcno') # generate .gcov files - name: Upload .gcov files - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 with: fail_ci_if_error: true disable_search: false diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 780a58fc..548a9ee6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: group: runners-arm64 container: libfn.azurecr.io/ci-docs:latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 # Guardrail to ensure that we do not update docs after a bad merge - name: Build and run tests @@ -46,16 +46,16 @@ jobs: cmake --build . --target export_docs - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: ./.build/docs name: docs-develop - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 with: artifact_name: docs-develop diff --git a/.github/workflows/licence.yml b/.github/workflows/licence.yml index dd139818..fe0e4dad 100644 --- a/.github/workflows/licence.yml +++ b/.github/workflows/licence.yml @@ -39,9 +39,9 @@ jobs: runs-on: group: runners-arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: fossas/fossa-action@v1 + - uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0 with: api-key: ${{ secrets.FOSSA_API_KEY }} @@ -52,7 +52,7 @@ jobs: env: LICENCE_FILE: LICENSE.md steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: sparse-checkout: . fetch-depth: 0 @@ -60,7 +60,7 @@ jobs: - name: Compare licence file id: assert run: - git diff --exit-code origin/${{ github.base_ref }} -- ${LICENCE_FILE} + git diff --exit-code origin/${{ github.base_ref }} -- "${LICENCE_FILE}" - name: What happened? if: failure() && steps.assert.outcome == 'failure' diff --git a/.github/workflows/package-test-bazel.yml b/.github/workflows/package-test-bazel.yml index 150dfad8..dcfd4ac2 100644 --- a/.github/workflows/package-test-bazel.yml +++ b/.github/workflows/package-test-bazel.yml @@ -41,7 +41,7 @@ jobs: run: working-directory: test_package steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set Bazel version run: | diff --git a/.github/workflows/package-test-conan.yml b/.github/workflows/package-test-conan.yml index a21ac4f6..1baa9739 100644 --- a/.github/workflows/package-test-conan.yml +++ b/.github/workflows/package-test-conan.yml @@ -26,19 +26,30 @@ jobs: runs-on: group: runners-intel container: libfn.azurecr.io/ci-build-gcc:15 + strategy: + fail-fast: false + matrix: + include: + - name: cxx23 + disable_cxx23: 'False' + cppstd: '23' + - name: cxx20 + disable_cxx23: 'True' + cppstd: '20' + - name: cxx20-std23 + disable_cxx23: 'True' + cppstd: '23' + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Detect Conan profile run: | conan profile detect echo "tools.cmake.cmaketoolchain:generator=Ninja" >> ~/.conan2/global.conf - - name: Test C++23 packaging - run: conan create . --build=missing -o "&:disable_cxx23=False" -s "compiler.cppstd=23" - - - name: Test C++20 packaging - run: conan create . --build=missing -o "&:disable_cxx23=True" -s "compiler.cppstd=20" - - - name: Test C++20 packaging with C++23 compiler - run: conan create . --build=missing -o "&:disable_cxx23=True" -s "compiler.cppstd=23" + - name: Test ${{ matrix.name }} packaging + run: | + conan create . --build=missing \ + -o "&:disable_cxx23=${{ matrix.disable_cxx23 }}" \ + -s "compiler.cppstd=${{ matrix.cppstd }}" diff --git a/.github/workflows/package-test-nix.yml b/.github/workflows/package-test-nix.yml index 473abf57..a7183b09 100644 --- a/.github/workflows/package-test-nix.yml +++ b/.github/workflows/package-test-nix.yml @@ -33,26 +33,27 @@ jobs: test_nix: runs-on: group: runners-intel + strategy: + fail-fast: false + matrix: + include: + - name: cxx23 + target: consumer + - name: cxx20 + target: consumer-no-cxx23 + - name: cxx20-std23 + target: consumer-no-cxx23-std23 steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Install Nix - uses: cachix/install-nix-action@v30 + uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - name: Test C++23 packaging - run: nix -L build './test_package#consumer' - --override-input libfn "git+file://$(pwd)?rev=${{ github.sha }}&shallow=1" - - - name: Test C++20 packaging - run: nix -L build './test_package#consumer-no-cxx23' - --override-input libfn "git+file://$(pwd)?rev=${{ github.sha }}&shallow=1" - - - name: Test C++20 packaging with C++23 compiler - run: nix -L build './test_package#consumer-no-cxx23-std23' - --override-input libfn "git+file://$(pwd)?rev=${{ github.sha }}&shallow=1" + - name: Test ${{ matrix.name }} packaging + run: | + nix -L build './test_package#${{ matrix.target }}' \ + --override-input libfn "git+file://$(pwd)?rev=${{ github.sha }}&shallow=1" diff --git a/.github/workflows/package-test-vcpkg.yml b/.github/workflows/package-test-vcpkg.yml index 55fc044c..35450efc 100644 --- a/.github/workflows/package-test-vcpkg.yml +++ b/.github/workflows/package-test-vcpkg.yml @@ -29,50 +29,41 @@ jobs: test_vcpkg: runs-on: group: runners-intel + strategy: + fail-fast: false + matrix: + include: + - name: cxx23 + install_spec: libfn + cmake_extra: "" + - name: cxx20 + install_spec: "libfn[disable-cxx23]" + cmake_extra: "" + - name: cxx20-std23 + install_spec: "libfn[disable-cxx23]" + cmake_extra: "-DCMAKE_CXX_STANDARD=23" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Install libfn (C++23 enabled) + - name: Install libfn env: VCPKG_LIBFN_SOURCE_PATH: ${{ github.workspace }} - run: $VCPKG_INSTALLATION_ROOT/vcpkg install libfn - --overlay-ports=ports - --triplet x64-linux - - - name: Test C++23 packaging run: | - cmake -B .build-cxx23 test_package \ - -DCMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake \ - -DVCPKG_TARGET_TRIPLET=x64-linux - cmake --build .build-cxx23 - .build-cxx23/pfn_quine - .build-cxx23/fn_quine - - - name: Uninstall libfn before re-installing with C++23 disabled - run: $VCPKG_INSTALLATION_ROOT/vcpkg remove libfn - --triplet x64-linux + "$VCPKG_INSTALLATION_ROOT/vcpkg" install "${{ matrix.install_spec }}" \ + --overlay-ports=ports \ + --triplet x64-linux - - name: Install libfn (C++23 disabled) - env: - VCPKG_LIBFN_SOURCE_PATH: ${{ github.workspace }} - run: $VCPKG_INSTALLATION_ROOT/vcpkg install "libfn[disable-cxx23]" - --overlay-ports=ports - --triplet x64-linux - - - name: Test C++20 packaging + - name: Configure and build ${{ matrix.name }} run: | - cmake -B .build-cxx20 test_package \ - -DCMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake \ - -DVCPKG_TARGET_TRIPLET=x64-linux - cmake --build .build-cxx20 - .build-cxx20/pfn_quine + cmake -B .build test_package \ + "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \ + -DVCPKG_TARGET_TRIPLET=x64-linux ${{ matrix.cmake_extra }} + cmake --build .build - - name: Test C++20 packaging with C++23 compiler - run: | - cmake -B .build-cxx20-std23 test_package \ - -DCMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake \ - -DVCPKG_TARGET_TRIPLET=x64-linux \ - -DCMAKE_CXX_STANDARD=23 - cmake --build .build-cxx20-std23 - .build-cxx20-std23/pfn_quine + - name: Run pfn_quine + run: .build/pfn_quine + + - name: Run fn_quine + if: matrix.name == 'cxx23' + run: .build/fn_quine diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 6ba91ed9..6dc5e4b8 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -16,7 +16,7 @@ jobs: container: libfn.azurecr.io/ci-pre-commit:latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Prepare run: | diff --git a/.github/workflows/sonarcloud-pr-scan.yml b/.github/workflows/sonarcloud-pr-scan.yml index e9500b30..c69bbecd 100644 --- a/.github/workflows/sonarcloud-pr-scan.yml +++ b/.github/workflows/sonarcloud-pr-scan.yml @@ -36,7 +36,7 @@ jobs: steps: # Download the artifact outside the workspace so the subsequent checkout does not wipe it. - name: Download SonarCloud inputs artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: sonarcloud-pr-inputs github-token: ${{ secrets.GITHUB_TOKEN }} @@ -90,7 +90,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Check out pull request head - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ steps.pr.outputs.head_sha }} fetch-depth: 0 # Full history for blame info diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index b0121747..01fab931 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -18,7 +18,7 @@ jobs: GCOV: /usr/local/bin/gcov steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 # Full history for blame info @@ -35,13 +35,14 @@ jobs: printf "C++ compiler path: %s\n" "$COMPILER" $COMPILER --version printf "gcov version: %s\n" "$( $GCOV --version | head -1 )" - cmake --build . --target tests -- -j$(nproc) + cmake --build . --target tests -- -j"$(nproc)" - name: Generate coverage data with gcov run: | cd .build ctest -L 'tests_(p?fn)' -j1 # generate .gcda files - $GCOV -pbc -r -s $(realpath ..) $(find tests -type f -name '*.gcno') # generate .gcov files + # shellcheck disable=SC2046 # find emits one path per result; intentional word-splitting into gcov arg list + $GCOV -pbc -r -s "$(realpath ..)" $(find tests -type f -name '*.gcno') # generate .gcov files # Pull requests opened from forks do not receive repository secrets, so # the SonarScanner cannot run here. For those events, package the build @@ -73,6 +74,7 @@ jobs: printf "%s\n" "$PR_HEAD_SHA" > .sonar-pr/head_sha printf "%s\n" "$PR_HEAD_REF" > .sonar-pr/head_ref printf "%s\n" "$PR_BASE_REF" > .sonar-pr/base_ref + # shellcheck disable=SC2046 # find emits one path per result; intentional word-splitting into tar arg list tar -czf sonar-inputs.tar.gz \ .build/compile_commands.json \ $(find .build -name '*.gcov' -print) \ @@ -80,7 +82,7 @@ jobs: - name: Upload SonarCloud inputs (pull requests only) if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sonarcloud-pr-inputs path: sonar-inputs.tar.gz diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..f3240e73 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,9 @@ +# Audits listed below to be enabled incrementally in the future. +rules: + artipacked: { disable: true } + dangerous-triggers: { disable: true } + dependabot-cooldown: { disable: true } + excessive-permissions: { disable: true } + github-env: { disable: true } + template-injection: { disable: true } + unpinned-images: { disable: true } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f5a18a54..50b19768 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,14 @@ repos: hooks: - id: clang-format types_or: [c++, c] +- repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor +- repo: https://github.com/rhysd/actionlint + rev: v1.7.12 + hooks: + - id: actionlint - repo: local hooks: - id: sync-versions diff --git a/CLAUDE.md b/CLAUDE.md index 0368324f..7f99648c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,10 @@ Conventions for AI agents in this repo (you are the primary reader — keep this - Comment only where the *why* is non-obvious (constraint/invariant/workaround/surprise); don't restate code; no boilerplate docstrings. - Don't create `.md`/summary/planning files unless asked. +## Tooling + +- Recommended: `clangd-lsp@claude-plugins-official` for symbol navigation + post-edit diagnostics on C++. Requires a populated `compile_commands.json` (re-run CMake configure if clangd reports spurious errors in template-heavy headers). + ## Memory - Keep memory current as facts change. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f4ac305..9e14715d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ This is for working *on* libfn; to *use* the library, see the [README](README.md ## Development environment -Building and testing libfn needs a recent C++23 toolchain (the monadic `std::optional`/`std::expected` of ISO/IEC 14882:2023). The recommended compilers are [gcc 13][gcc-standard-support] and [clang 18][clang-standard-support]; if your OS does not ship one recent enough, use the [devcontainer] or [Nix][nix] (see [nix/README.md][nixmd]). +Building and testing libfn needs a recent C++23 toolchain (the monadic `std::optional`/`std::expected` of ISO/IEC 14882:2023). The minimum recommended compilers are [gcc 13][gcc-standard-support] and [clang 18][clang-standard-support]; if your OS does not ship one recent enough, use the [devcontainer] or [Nix][nix] (see [nix/README.md][nixmd]). ## Building locally @@ -50,6 +50,22 @@ pre-commit run --all-files If a hook modifies files (e.g. clang-format, or the version sync above), the commit is aborted — re-stage the changes and commit again. +## GitHub Actions workflow pitfalls + +A few conventions for files under `.github/workflows/`: + +* **Don't pin `ref:` on `actions/checkout` without a reason.** The default already pins to `github.sha` for `push`, `pull_request`, and `workflow_dispatch`, so `ref: ${{ github.sha }}` is redundant in the common case. If you do need it (e.g. so the working tree matches a downstream nix input's `?rev=${{ github.sha }}`), leave a comment saying so. + +* **Pin actions to a commit SHA, not a tag.** `uses: owner/repo@v1` is a supply-chain trust decision — whoever can move the tag can execute code in your workflow. Pin the SHA with a trailing version comment (e.g. `actions/checkout@34e1148… # v4.3.1`); Dependabot keeps it current and `zizmor` enforces it via pre-commit. + +* **One concern per job; matrix-ify variations.** Don't pack several install/build/test sequences into one job, especially if they need an inter-step cleanup (`vcpkg remove`, `cmake --build . --target clean`). Each variation should be its own matrix entry; gate per-variation steps with `if: matrix.name == 'X'` when needed. + +* **The "build everything" run gets its own matrix slot.** If one combination should run a superset of the per-mode targets, add it via `matrix.include` (e.g. `mode: all`) and gate the mode-specific step with `if: matrix.mode != 'all'`. Avoid the build → clean → rebuild dance inside a single job. + +* **Multi-line `run:` uses `|` and `\`.** The plain-scalar form (`run: cmd` followed by indented continuation lines) folds into one shell line, which is awkward to copy from a diff into a terminal. The literal-block form (`run: |` + lines ending in `\`) reads and pastes as a shell snippet. + +* **Insecure shell inside `run:` blocks.** `actionlint` runs `shellcheck` via pre-commit on every `run:` block if `shellcheck` is on `$PATH`; without it the check is silently skipped. Install `shellcheck` to avoid surprises when CI runs against your PR. + [clang-standard-support]: https://clang.llvm.org/cxx_status.html [gcc-standard-support]: https://gcc.gnu.org/projects/cxx-status.html diff --git a/ci/clang/Dockerfile b/ci/clang/Dockerfile index 08b7b78c..9e762f62 100644 --- a/ci/clang/Dockerfile +++ b/ci/clang/Dockerfile @@ -75,7 +75,6 @@ ENV CMAKE_FIND_CATCH2=1 FROM base AS clang-bazel ARG BAZEL_RELEASE -ENV USE_BAZEL_FALLBACK_VERSION="warn:${BAZEL_RELEASE}" ARG DEBIAN_FRONTEND=noninteractive # Install libstdc++ so clang libstdc++ (i.e. default) Bazel builds work; base has only libc++ RUN set -ex ;\ @@ -88,3 +87,4 @@ COPY fetch-bazel.sh /work/fetch-bazel.sh RUN set -ex ;\ sh /work/fetch-bazel.sh ;\ rm /work/fetch-bazel.sh +ENV USE_BAZEL_FALLBACK_VERSION="warn:${BAZEL_RELEASE}" diff --git a/ci/docs/Dockerfile b/ci/docs/Dockerfile index 8b0202d2..575142f1 100644 --- a/ci/docs/Dockerfile +++ b/ci/docs/Dockerfile @@ -1,5 +1,6 @@ ARG GCC_RELEASE=15 -ARG GCC_DIST=${GCC_RELEASE}-trixie +ARG DEBIAN_VERSION=trixie +ARG GCC_DIST=${GCC_RELEASE}-${DEBIAN_VERSION} FROM gcc:${GCC_DIST} AS upstream RUN set -ex ;\ find /usr/local/ -type f ;\ @@ -7,12 +8,11 @@ RUN set -ex ;\ cat /etc/os-release ;\ /usr/local/bin/gcc --version -FROM debian:trixie AS build +ARG DEBIAN_VERSION=trixie +FROM debian:${DEBIAN_VERSION} AS build ARG DOXYGEN_RELEASE ARG ZNAI_RELEASE -ENV CMAKE_GENERATOR=Ninja -ENV CMAKE_BUILD_TYPE=Debug ARG DEBIAN_FRONTEND=noninteractive WORKDIR /work RUN set -ex ;\ @@ -22,6 +22,7 @@ RUN set -ex ;\ python3 bison flex libiconv-hook-dev unzip ;\ apt-get clean +ENV CMAKE_GENERATOR=Ninja RUN set -ex ;\ : "${DOXYGEN_RELEASE:?DOXYGEN_RELEASE is not set}" ;\ DIST="https://www.doxygen.nl/files" ;\ @@ -45,7 +46,8 @@ RUN set -ex ;\ mv znai_tmp/dist /opt/znai ;\ rm -rf ${FILE} znai_tmp -FROM debian:trixie +ARG DEBIAN_VERSION=trixie +FROM debian:${DEBIAN_VERSION} COPY --from=upstream /usr/local/ /usr/local/ COPY --from=upstream /etc/ld.so.conf.d/ /etc/ld.so.conf.d/ COPY --from=build /usr/local/ /usr/local/ @@ -73,20 +75,20 @@ RUN set -ex ;\ update-alternatives --auto cc ;\ update-alternatives --auto gcc -ENV PATH=${PATH}:/opt/znai -ENV CC=/usr/bin/gcc -ENV CXX=/usr/bin/g++ -ENV CMAKE_GENERATOR=Ninja -ENV CMAKE_BUILD_TYPE=Debug +ARG JAVA_VERSION=openjdk-21-jdk ARG DEBIAN_FRONTEND=noninteractive RUN set -ex ;\ apt-get update ;\ apt-get install -y --no-install-recommends \ ca-certificates gnupg unzip wget vim curl jq \ cmake ninja-build binutils libc6-dev git \ - openjdk-21-jdk graphviz ;\ + ${JAVA_VERSION} graphviz ;\ apt-get clean +ENV CC=/usr/bin/gcc +ENV CXX=/usr/bin/g++ +ENV CMAKE_GENERATOR=Ninja +ENV CMAKE_BUILD_TYPE=Debug ARG CATCH_RELEASE WORKDIR /work COPY build-catch.sh /work/build-catch.sh @@ -94,3 +96,4 @@ RUN set -ex ;\ sh /work/build-catch.sh ;\ rm /work/build-catch.sh ENV CMAKE_FIND_CATCH2=1 +ENV PATH=${PATH}:/opt/znai diff --git a/ci/gcc/Dockerfile b/ci/gcc/Dockerfile index b23e0238..8805ccb9 100644 --- a/ci/gcc/Dockerfile +++ b/ci/gcc/Dockerfile @@ -69,9 +69,9 @@ ENV CMAKE_FIND_CATCH2=1 FROM base AS gcc-bazel ARG BAZEL_RELEASE -ENV USE_BAZEL_FALLBACK_VERSION="warn:${BAZEL_RELEASE}" WORKDIR /work COPY fetch-bazel.sh /work/fetch-bazel.sh RUN set -ex ;\ sh /work/fetch-bazel.sh ;\ rm /work/fetch-bazel.sh +ENV USE_BAZEL_FALLBACK_VERSION="warn:${BAZEL_RELEASE}" diff --git a/ci/pre-commit/Dockerfile b/ci/pre-commit/Dockerfile index f5b7c66a..a0de632b 100644 --- a/ci/pre-commit/Dockerfile +++ b/ci/pre-commit/Dockerfile @@ -1,11 +1,13 @@ -FROM debian:bookworm-slim +FROM debian:trixie-slim ARG DEBIAN_FRONTEND=noninteractive RUN set -ex ;\ + echo "deb http://deb.debian.org/debian trixie-backports main" > /etc/apt/sources.list.d/backports.list ;\ apt-get update ;\ apt-get install -y --no-install-recommends \ python3 python3-pip python3-venv \ ca-certificates wget vim git ;\ + apt-get install -y --no-install-recommends -t trixie-backports shellcheck ;\ apt-get clean COPY requirements.txt /requirements.txt diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake index d71d9771..74a61089 100644 --- a/cmake/CompilationOptions.cmake +++ b/cmake/CompilationOptions.cmake @@ -26,9 +26,7 @@ function(append_compilation_options) # MSVC's declares a global `unexpected` that shadows the std::expected/std::unexpected # vocabulary used by libfn; _HAS_CXX23 (MSVC STL's C++23-mode switch) drops the legacy declaration. - # REQUIRED: without it tests/pfn/expected.cpp fails to compile on MSVC /std:c++20 - # (C4430 "int assumed" on `unexpected`). Must be INTERFACE because may be included - # before any libfn header. (Redundant once MSVC builds use /std:c++23, where _HAS_CXX23 is auto-on.) + # Must be INTERFACE because may be included before any libfn header. target_compile_definitions(${Options_NAME} INTERFACE $<$:_HAS_CXX23>) endif() @@ -58,9 +56,8 @@ function(append_compilation_options) target_compile_options(${Options_NAME} PRIVATE $,/Od,/O2>) elseif(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|(Apple)?Clang)") target_compile_options(${Options_NAME} PRIVATE - $<$:-O0> + $,-O0,-O2> $<$:-fno-omit-frame-pointer> - $<$>:-O2> ) if(LIBFN_SANITIZERS) diff --git a/conanfile.py b/conanfile.py index a77463b8..09fb15bc 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,7 +1,6 @@ import os from conan import ConanFile -from conan.tools.build import check_min_cppstd from conan.tools.files import copy, load from conan.tools.layout import basic_layout @@ -18,7 +17,6 @@ class LibfnConan(ConanFile): topics = ("functional", "header-only", "monadic", "expected", "optional", "cpp23") package_type = "header-library" - settings = "os", "arch", "compiler", "build_type" no_copy_source = True options = { @@ -39,14 +37,6 @@ def set_version(self): def layout(self): basic_layout(self) - def validate(self): - if self.options.disable_cxx23: - # Only pfn (C++20-compatible polyfills) is available in this mode. - check_min_cppstd(self, 20) - else: - # fn (the main component) requires C++23. - check_min_cppstd(self, 23) - def package_id(self): # Intentional: header-only library, so all files are identical regardless of options. package_info() runs # at consume time with the consumer's options, so the conditional fn component exposure works correctly. diff --git a/nix/catch2_3.nix b/nix/catch2_3.nix deleted file mode 100644 index 3a937a50..00000000 --- a/nix/catch2_3.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ lib -, stdenv -, fetchFromGitHub -, cmake -, python3 -}: - -stdenv.mkDerivation rec { - pname = "catch2_local"; - version = "3.14.0"; - - src = fetchFromGitHub { - owner = "catchorg"; - repo = "Catch2"; - rev = "v${version}"; - hash = "sha256-tegAa+cNF7pJcW33B+VZ86ZlDG7dwS3o6QnN/XvTI2A="; - }; - - nativeBuildInputs = [ - cmake - ]; - - # On Nix, @lib_dir@ and @include_dir@ already expand to absolute paths under - # /nix/store/..., so the upstream "${prefix}/@…@" join produces a double slash - # that fixupPhase rejects. See https://github.com/NixOS/nixpkgs/issues/144170. - postPatch = '' - substituteInPlace CMake/catch2.pc.in \ - --replace 'libdir=''${prefix}/@lib_dir@' 'libdir=@lib_dir@' \ - --replace 'includedir=''${prefix}/@include_dir@' 'includedir=@include_dir@' - substituteInPlace CMake/catch2-with-main.pc.in \ - --replace 'libdir=''${prefix}/@lib_dir@' 'libdir=@lib_dir@' - ''; - - hardeningDisable = [ "trivialautovarinit" ]; - - cmakeFlags = [ - "-DCATCH_DEVELOPMENT_BUILD=ON" - "-DCATCH_BUILD_TESTING=OFF" - ]; - - meta = { - description = "Modern, C++-native, test framework for unit-tests"; - homepage = "https://github.com/catchorg/Catch2"; - changelog = "https://github.com/catchorg/Catch2/blob/${src.rev}/docs/release-notes.md"; - license = lib.licenses.boost; - maintainers = with lib.maintainers; [ dotlambda ]; - platforms = with lib.platforms; unix ++ windows; - }; -} diff --git a/nix/package.nix b/nix/package.nix index 95c9c5dd..86dfcafa 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -9,9 +9,6 @@ , disableCxx23 ? false }: -let - catch2_local = pkgs.callPackage ./catch2_3.nix { inherit stdenv; }; -in stdenv.mkDerivation { name = "libfn"; @@ -27,7 +24,8 @@ stdenv.mkDerivation { ]; nativeBuildInputs = [ cmake ninja ccache llvmPackages_21.clang-tools ]; - buildInputs = [ catch2_local ]; + # Rebuild catch2_3 with the consumer's stdenv so its stdlib ABI matches libfn's. + buildInputs = [ (pkgs.catch2_3.override { inherit stdenv; }) ]; checkInputs = [ ]; doCheck = enableTests; diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 86bab107..ca8cc204 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -4184,10 +4184,13 @@ TEST_CASE("expected void", "[expected_void][polyfill]") constexpr T a = fn(T(unexpect, Error::file_not_found)); static_assert(a.error() == Error::file_not_found); +// clang+libstdc++: constexpr eval can't track std::expected's _M_unex across error->value transition +#if !(defined(__clang__) && defined(__GLIBCXX__) && defined(PFN_TEST_VALIDATION)) constexpr T b = fn(T(std::in_place)); static_assert(b.has_value()); #ifndef PFN_TEST_VALIDATION static_assert(not b.has_error()); +#endif #endif SUCCEED(); @@ -4476,10 +4479,13 @@ TEST_CASE("expected void", "[expected_void][polyfill]") static_assert(not a.has_value() && a.error() == Error::file_not_found); #endif +// clang+libstdc++: constexpr eval can't track std::expected's _M_unex across error->value transition +#if !(defined(__clang__) && defined(__GLIBCXX__) && defined(PFN_TEST_VALIDATION)) constexpr T b = fn(d); static_assert(b.has_value()); #ifndef PFN_TEST_VALIDATION static_assert(not b.has_error()); +#endif #endif SUCCEED(); @@ -4542,6 +4548,8 @@ TEST_CASE("expected void", "[expected_void][polyfill]") SECTION("from error") { +// clang+libstdc++: constexpr eval can't track std::expected's _M_unex across error->value transition +#if !(defined(__clang__) && defined(__GLIBCXX__) && defined(PFN_TEST_VALIDATION)) constexpr auto fn = []() constexpr -> T { T tmp{unexpect, Error::unknown}; tmp.emplace(); @@ -4552,6 +4560,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") static_assert(a.has_value()); #ifndef PFN_TEST_VALIDATION static_assert(not a.has_error()); +#endif #endif SUCCEED(); @@ -4829,10 +4838,13 @@ TEST_CASE("expected void", "[expected_void][polyfill]") return v; }; +// clang+libstdc++: constexpr eval can't track std::expected's _M_unex across error->value transition +#if !(defined(__clang__) && defined(__GLIBCXX__) && defined(PFN_TEST_VALIDATION)) constexpr T a = fn(T(unexpect, Error::file_not_found)); static_assert(a.has_value()); #ifndef PFN_TEST_VALIDATION static_assert(not a.has_error()); +#endif #endif constexpr T b = fn(T());