From 543d6a45638e619098e0fdfe6bb4c91fae264352 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 25 Nov 2025 21:23:28 +0100 Subject: [PATCH 1/8] Feat: pre-commit autoupdate --- .clang-format | 8 +++++--- .gitignore | 1 + .pre-commit-config.yaml | 19 ++++++++++++++----- CMakeLists.txt | 2 +- examples/cartesian_plane.cpp | 18 ++++++++++++------ 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.clang-format b/.clang-format index 2d264bf..4006393 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,8 @@ --- -Language: Cpp BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignArrayOfStructures: None @@ -89,11 +91,11 @@ IfMacros: - KJ_IF_MAYBE IncludeBlocks: Preserve IncludeCategories: - - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + - Regex: '^"(beman|llvm|llvm-c|clang|clang-c)/' Priority: 2 SortPriority: 0 CaseSensitive: false - - Regex: '^(<|"(gtest|gmock|isl|json)/)' + - Regex: '^(<|"(catch2|gtest|gmock|isl|json)/)' Priority: 3 SortPriority: 0 CaseSensitive: false diff --git a/.gitignore b/.gitignore index 4957963..2deb68a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Miscellaneous build and testing folders build/ +compile_commands.json # Loose files .DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba8f774..e8da2a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,30 +2,39 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + exclude: ^\.clang-(format|tidy)$ - id: check-added-large-files # Clang-format for C++ # This brings in a portable version of clang-format. # See also: https://github.com/ssciwr/clang-format-wheel - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.8 + rev: v21.1.6 hooks: - id: clang-format - types_or: [c++, c] + types_or: [c++, c, json] + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.23.1 + hooks: + - id: gersemi + name: CMake linting + exclude: ^.*/tests/.*/data/ # Exclude test data directories # Markdown linting # Config file: .markdownlint.yaml - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.42.0 + rev: v0.46.0 hooks: - id: markdownlint - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d0e4e8..9b475de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -cmake_minimum_required(VERSION 3.28) +cmake_minimum_required(VERSION 3.28...4.2) include(cmake/bootstrap_vcpkg.cmake) diff --git a/examples/cartesian_plane.cpp b/examples/cartesian_plane.cpp index d73c231..5e2fb55 100644 --- a/examples/cartesian_plane.cpp +++ b/examples/cartesian_plane.cpp @@ -9,6 +9,8 @@ import beman.bounds_test; namespace bt = beman::bounds_test; +namespace { + template concept SignedNumber = std::is_arithmetic_v && std::is_signed_v; @@ -28,22 +30,26 @@ constexpr auto try_reflect_y_axis(Point& p) noexcept { return bt::can_negate(p.x) ? std::optional>{std::in_place, static_cast(-p.x), p.y} : std::nullopt; } +} // namespace + int main() { auto test_reflect = [&](auto& pt) { std::cout << "Original: (" << pt.x << ", " << pt.y << ")\n"; - if (auto r = try_reflect_x_axis(pt)) + if (auto r = try_reflect_x_axis(pt)) { std::cout << "\tRefleted across X: (" << r->x << ", " << r->y << ")\n"; - else + } else { std::cout << "\tRefelction across X would cause overflow\n"; + } - if (auto r = try_reflect_y_axis(pt)) + if (auto r = try_reflect_y_axis(pt)) { std::cout << "\tRefleted across Y: (" << r->x << ", " << r->y << ")\n"; - else + } else { std::cout << "\tRefelction across Y would cause overflow\n"; + } }; - Point p{7, 15}; + Point p{.x = 7, .y = 15}; test_reflect(p); - Point q{12, std::numeric_limits::min()}; + Point q{.x = 12, .y = std::numeric_limits::min()}; test_reflect(q); } From 7d39ee722bf0b468fe15994c61ca41cc5d1aed57 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 25 Nov 2025 21:33:38 +0100 Subject: [PATCH 2/8] Feat: clang-format json files too --- .devcontainer/devcontainer.json | 28 ++++++++++++++-------------- .pre-commit-config.yaml | 14 +++++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e1662e7..a9deb40 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,16 +1,16 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/cpp + // README at: https://github.com/devcontainers/templates/tree/main/src/cpp -{ - "name": "Beman Project Generic Devcontainer", - "image": "ghcr.io/bemanproject/devcontainers-gcc:14", - "postCreateCommand": "pre-commit", - "customizations": { - "vscode": { - "extensions": [ - "ms-vscode.cpptools", - "ms-vscode.cmake-tools" - ] - } - } -} + { + "name": "Beman Project Generic Devcontainer", + "image": "ghcr.io/bemanproject/devcontainers-gcc:14", + "postCreateCommand": "pre-commit", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools" + ] + } + } + } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8da2a3..8bc6d2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,13 +19,13 @@ repos: - id: clang-format types_or: [c++, c, json] - # CMake linting and formatting - - repo: https://github.com/BlankSpruce/gersemi - rev: 0.23.1 - hooks: - - id: gersemi - name: CMake linting - exclude: ^.*/tests/.*/data/ # Exclude test data directories + # # CMake linting and formatting + # - repo: https://github.com/BlankSpruce/gersemi + # rev: 0.23.1 + # hooks: + # - id: gersemi + # name: CMake linting + # exclude: ^.*/tests/.*/data/ # Exclude test data directories # Markdown linting # Config file: .markdownlint.yaml From a7ef13d2771a64642aba1e6d767c2b03e7855521 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 25 Nov 2025 23:54:13 +0100 Subject: [PATCH 3/8] Feat: add support for apple clang --- CMakeLists.txt | 46 +++++++++++++++---- examples/cartesian_plane.cpp | 6 ++- examples/placeholder.cpp | 2 +- include/beman/bounds_test/plat/CMakeLists.txt | 4 +- tests/beman/bounds_test/bounds_test.tests.cpp | 2 +- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b475de..8625fd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,33 +30,61 @@ option( ${PROJECT_IS_TOP_LEVEL} ) -add_library(beman.bounds_test) +# Modules opt in only on compilers that support g++-15 and clang-20+ +set(CMAKE_CXX_SCAN_FOR_MODULES 0) +if(CMAKE_CXX_STANDARD GREATER_EQUAL 20) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 20) + set(CMAKE_CXX_SCAN_FOR_MODULES 1) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) + set(CMAKE_CXX_SCAN_FOR_MODULES 1) + endif() +endif() + +if(CMAKE_CXX_SCAN_FOR_MODULES) + add_library(beman.bounds_test) + set(BEMAN_BOUNDS_TEST_INTERFACE PUBLIC) +else() + add_library(beman.bounds_test INTERFACE) + set(BEMAN_BOUNDS_TEST_INTERFACE INTERFACE) +endif() + add_library(beman::bounds_test ALIAS beman.bounds_test) set_target_properties( beman.bounds_test PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON - EXPORT_NAME bounds_test ) target_sources( beman.bounds_test - PUBLIC + ${BEMAN_BOUNDS_TEST_INTERFACE} FILE_SET HEADERS BASE_DIRS include FILES include/beman/bounds_test/bounds_test.hpp include/beman/bounds_test/plat/common.hpp - - PUBLIC - FILE_SET CXX_MODULES - BASE_DIRS include - FILES - include/beman/bounds_test/beman.bounds_test.cppm ) +if(CMAKE_CXX_SCAN_FOR_MODULES) + set_target_properties( + beman.bounds_test + PROPERTIES + EXPORT_NAME bounds_test + ) + target_compile_definitions(beman.bounds_test PUBLIC HAS_CXX_MODULES) + target_sources( + beman.bounds_test + + PUBLIC + FILE_SET CXX_MODULES + BASE_DIRS include + FILES + include/beman/bounds_test/beman.bounds_test.cppm + ) +endif() + include(GNUInstallDirs) include(cmake/check_plat.cmake) diff --git a/examples/cartesian_plane.cpp b/examples/cartesian_plane.cpp index 5e2fb55..6d2ce07 100644 --- a/examples/cartesian_plane.cpp +++ b/examples/cartesian_plane.cpp @@ -4,8 +4,12 @@ #include #include -// Alternatively: #include +// Alternatively: +#ifndef HAS_CXX_MODULES +#include +#else import beman.bounds_test; +#endif namespace bt = beman::bounds_test; diff --git a/examples/placeholder.cpp b/examples/placeholder.cpp index 98ee19d..d66a596 100644 --- a/examples/placeholder.cpp +++ b/examples/placeholder.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -#ifdef __INTELLISENSE__ +#ifndef HAS_CXX_MODULES #include #else import beman.bounds_test; diff --git a/include/beman/bounds_test/plat/CMakeLists.txt b/include/beman/bounds_test/plat/CMakeLists.txt index 44311a2..4a89376 100644 --- a/include/beman/bounds_test/plat/CMakeLists.txt +++ b/include/beman/bounds_test/plat/CMakeLists.txt @@ -3,7 +3,7 @@ check_plat(HAS_GNU_OVERFLOW HAS_MSVC_OVERFLOW) if(HAS_GNU_OVERFLOW) target_include_directories( beman.bounds_test - PUBLIC + ${BEMAN_BOUNDS_TEST_INTERFACE} $ ) # elseif(HAS_MSVC_OVERFLOW) @@ -15,7 +15,7 @@ if(HAS_GNU_OVERFLOW) else() target_include_directories( beman.bounds_test - PUBLIC + ${BEMAN_BOUNDS_TEST_INTERFACE} $ ) endif() diff --git a/tests/beman/bounds_test/bounds_test.tests.cpp b/tests/beman/bounds_test/bounds_test.tests.cpp index 9a25a88..f56d9e6 100644 --- a/tests/beman/bounds_test/bounds_test.tests.cpp +++ b/tests/beman/bounds_test/bounds_test.tests.cpp @@ -4,7 +4,7 @@ #include #include -#ifdef __INTELLISENSE__ +#ifndef HAS_CXX_MODULES #include #else import beman.bounds_test; From 64681207c06688c3e655c4fc408d48e3322c5727 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 25 Nov 2025 23:59:05 +0100 Subject: [PATCH 4/8] Merge ci_tests.yml form cstring_view --- .github/workflows/ci_tests.yml | 315 ++++++++++++--------------------- 1 file changed, 116 insertions(+), 199 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 6d5d89a..06dffec 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -4,212 +4,129 @@ name: Continuous Integration Tests on: push: + branches: + - main pull_request: workflow_dispatch: schedule: - - cron: "30 15 * * *" + - cron: '30 15 * * *' jobs: - preset-test: - permissions: - actions: read - strategy: - fail-fast: false - matrix: - presets: - - preset: "gcc-debug" - compiler: "gcc:14" - - preset: "gcc-release" - compiler: "gcc:14" - - preset: "llvm-debug" - compiler: "clang:19" - - preset: "llvm-release" - compiler: "clang:19" - # - preset: "appleclang-debug" - # platform: "macos-latest" - # - preset: "appleclang-release" - # platform: "macos-latest" - - preset: "msvc-debug" - platform: "windows-latest" - - preset: "msvc-release" - platform: "windows-latest" - name: "Preset: ${{ matrix.presets.preset }} on ${{ matrix.presets.platform || matrix.presets.compiler }}" - runs-on: ${{ matrix.presets.platform || 'ubuntu-latest' }} - container: - image: ${{ matrix.presets.compiler && 'ghcr.io/bemanproject/testingcontainers-' }}${{ matrix.presets.compiler }} - steps: - - uses: actions/checkout@v4 - - name: Setup Environment - if: ${{ !matrix.presets.compiler }} - uses: ./.github/actions/setup-environment - - name: Get vcpkg deps - shell: bash - if: ${{ !contains(runner.os, 'Windows') }} - run: | - sudo apt-get install -y pkg-config zip - - name: Restore vcpkg cache - id: vcpkg-cache - uses: TAServers/vcpkg-cache@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: Run preset - env: - VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite" - run: cmake --workflow --preset ${{ matrix.presets.preset }} - - catch-test: - permissions: - actions: read - strategy: - fail-fast: false - matrix: - platform: - - description: "GNU 14" - compiler: "gcc:14" - toolchain: "cmake/gnu-toolchain.cmake" - - description: "LLVM 19" - compiler: "clang:19" - toolchain: "cmake/llvm-toolchain.cmake" - - description: "Windows MSVC" - os: windows-latest - toolchain: "cmake/msvc-toolchain.cmake" - #- description: "Macos Appleclang" - # os: macos-latest - # toolchain: "cmake/appleclang-toolchain.cmake" - cpp_version: [20, 23, 26] - cmake_args: - - description: "Default" - - description: "TSan" - args: "-DBEMAN_BUILDSYS_SANITIZER=TSan" - - description: "MaxSan" - args: "-DBEMAN_BUILDSYS_SANITIZER=MaxSan" - include: - - platform: - description: "GCC 14" - compiler: "gcc:14" - toolchain: "cmake/gnu-toolchain.cmake" - cpp_version: 20 - cmake_args: - description: "Werror" - args: "-DCMAKE_CXX_FLAGS='-Werror=all -Werror=extra'" - - platform: - description: "GCC 14" - compiler: "gcc:14" - toolchain: "cmake/gnu-toolchain.cmake" - cpp_version: 20 - cmake_args: - description: "Dynamic" - args: "-DBUILD_SHARED_LIBS=on" - exclude: - # MSVC does not support thread sanitizer - - platform: - description: "Windows MSVC" - cmake_args: - description: "TSan" + beman-submodule-check: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.1.0 - name: "Unit: - ${{ matrix.platform.description }} - ${{ matrix.cpp_version }} - ${{ matrix.cmake_args.description }}" - runs-on: ${{ matrix.platform.os || 'ubuntu-latest' }} - container: - image: ${{ matrix.platform.compiler && 'ghcr.io/bemanproject/testingcontainers-' }}${{ matrix.platform.compiler }} - steps: - - uses: actions/checkout@v4 - - name: Setup Environment - if: ${{ !matrix.platform.compiler }} - uses: ./.github/actions/setup-environment - - name: Build and Test - uses: ./.github/actions/cmake-build-test - with: - cpp_version: ${{ matrix.cpp_version }} - toolchain_file: ${{ matrix.platform.toolchain }} - cmake_extra_args: ${{ matrix.cmake_args.args }} - - configuration-test: - permissions: - actions: read - runs-on: ubuntu-latest - container: - image: ghcr.io/bemanproject/testingcontainers-gcc:14 - strategy: - fail-fast: false - matrix: - args: - - name: "Disable build testing" - arg: "-DBEMAN_BOUNDS_TEST_BUILD_TESTS=OFF" - - name: "Disable example building" - arg: "-DBEMAN_BOUNDS_TEST_BUILD_EXAMPLES=OFF" - - name: "Disable config-file package creation" - arg: "-DBEMAN_BOUNDS_TEST_INSTALL_CONFIG_FILE_PACKAGE=OFF" - name: "CMake: ${{ matrix.args.name }}" - steps: - - uses: actions/checkout@v4 - - name: Build and Test - uses: ./.github/actions/cmake-build-test - with: - cpp_version: 20 - toolchain_file: "cmake/gnu-toolchain.cmake" - cmake_extra_args: ${{ matrix.args.arg }} - disable_test: true + preset-test: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.1.0 + with: + matrix_config: > + [ + {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, + {"preset": "llvm-debug", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "llvm-release", "image": "ghcr.io/bemanproject/infra-containers-clang:latest"}, + {"preset": "appleclang-debug", "runner": "macos-latest"}, + {"preset": "appleclang-release", "runner": "macos-latest"}, + {"preset": "msvc-debug", "runner": "windows-latest"}, + {"preset": "msvc-release", "runner": "windows-latest"} + ] - compiler-test: - permissions: - actions: read - strategy: - fail-fast: false - matrix: - compilers: - - class: gcc - version: 14 - toolchain: "cmake/gnu-toolchain.cmake" - - class: clang - version: 20 - toolchain: "cmake/llvm-toolchain.cmake" - - class: clang - version: 19 - toolchain: "cmake/llvm-toolchain.cmake" - - class: clang - version: 18 - toolchain: "cmake/llvm-toolchain.cmake" - - class: clang - version: 17 - toolchain: "cmake/llvm-toolchain.cmake" - name: "Compiler: ${{ matrix.compilers.class }} ${{ matrix.compilers.version }}" - runs-on: ubuntu-24.04 - container: - image: ghcr.io/bemanproject/testingcontainers-${{ matrix.compilers.class }}:${{ matrix.compilers.version }} - steps: - - uses: actions/checkout@v4 - - name: Build and Test - uses: ./.github/actions/cmake-build-test - with: - cpp_version: 20 - toolchain_file: ${{ matrix.compilers.toolchain }} + build-and-test: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.1.0 + with: + matrix_config: > + { + "gcc": [ + { "versions": ["15"], + "tests": [ + { "cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.TSan", + "Release.MaxSan", "Debug.Werror", "Debug.Dynamic", + "Debug.Coverage" + ] + } + ] + }, + { "cxxversions": ["c++23", "c++20"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + { "versions": ["14", "13"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "clang": [ + { "versions": ["21"], + "tests": [ + {"cxxversions": ["c++26"], + "tests": [ + { "stdlibs": ["libstdc++", "libc++"], + "tests": [ + "Debug.Default", "Release.Default", "Release.TSan", + "Release.MaxSan", "Debug.Werror", "Debug.Dynamic" + ] + } + ] + }, + { "cxxversions": ["c++23", "c++20"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["20", "19", "18"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["17"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] + }, + { "cxxversions": ["c++20"], + "tests": [{"stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "appleclang": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20"], + "tests": [{ "stdlibs": ["libc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "msvc": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++23"], + "tests": [ + { "stdlibs": ["stl"], + "tests": ["Debug.Default", "Release.Default", "Release.MaxSan"] + } + ] + } + ] + } + ] + } create-issue-when-fault: - runs-on: ubuntu-latest - needs: [preset-test, catch-test, configuration-test, compiler-test] + needs: [preset-test, build-and-test] if: failure() && github.event_name == 'schedule' - steps: - # See https://github.com/cli/cli/issues/5075 - - uses: actions/checkout@v4 - - name: Create issue - run: | - issue_num=$(gh issue list -s open -S "[SCHEDULED-BUILD] Build & Test failure" -L 1 --json number | jq 'if length == 0 then -1 else .[0].number end') - - body="**Build-and-Test Failure Report** - - **Time of Failure**: $(date -u '+%B %d, %Y, %H:%M %Z') - - **Commit**: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) - - **Action Run**: [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - - The scheduled build-and-test triggered by cron has failed. - Please investigate the logs and recent changes associated with this commit or rerun the workflow if you believe this is an error." - - if [[ $issue_num -eq -1 ]]; then - gh issue create --repo ${{ github.repository }} --title "[SCHEDULED-BUILD] Build & Test failure" --body "$body" - else - gh issue comment --repo ${{ github.repository }} $issue_num --body "$body" - fi - env: - GH_TOKEN: ${{ github.token }} + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.1.0 From a835c748dfa2e3cf2d69ad2620fdbee89104f1e0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 26 Nov 2025 07:06:54 +0100 Subject: [PATCH 5/8] Feat: use FetchContent instead of vcpk --- CMakeLists.txt | 3 +- CMakePresets.json | 18 +-- cmake/appleclang-toolchain.cmake | 2 + cmake/beman-install-library-config.cmake | 169 +++++++++++++++++++++++ cmake/llvm-libc++-toolchain.cmake | 20 +++ lockfile.json | 3 + tests/beman/bounds_test/CMakeLists.txt | 11 +- 7 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 cmake/beman-install-library-config.cmake create mode 100644 cmake/llvm-libc++-toolchain.cmake create mode 100644 lockfile.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 8625fd3..733c070 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + cmake_minimum_required(VERSION 3.28...4.2) -include(cmake/bootstrap_vcpkg.cmake) +# XXX include(cmake/bootstrap_vcpkg.cmake) project( beman.bounds_test diff --git a/CMakePresets.json b/CMakePresets.json index c8d62c1..8f47c00 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,7 +9,7 @@ "cacheVariables": { "CMAKE_CXX_STANDARD": "20", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "BEMAN_BOUNDS_TEST_BOOTSTRAP_VCPKG": "ON" + "NO_CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "infra/cmake/use-fetch-content.cmake" } }, { @@ -35,7 +35,7 @@ "_debug-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/gnu-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/gnu-toolchain.cmake" } }, { @@ -46,7 +46,7 @@ "_release-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/gnu-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/gnu-toolchain.cmake" } }, { @@ -57,7 +57,7 @@ "_debug-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/llvm-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/llvm-toolchain.cmake" } }, { @@ -68,7 +68,7 @@ "_release-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/llvm-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/llvm-toolchain.cmake" } }, { @@ -79,7 +79,7 @@ "_debug-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/appleclang-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/appleclang-toolchain.cmake" } }, { @@ -90,7 +90,7 @@ "_release-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/appleclang-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/appleclang-toolchain.cmake" } }, { @@ -101,7 +101,7 @@ "_debug-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/msvc-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/msvc-toolchain.cmake" } }, { @@ -112,7 +112,7 @@ "_release-base" ], "cacheVariables": { - "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/msvc-toolchain.cmake" + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/msvc-toolchain.cmake" } } ], diff --git a/cmake/appleclang-toolchain.cmake b/cmake/appleclang-toolchain.cmake index e7a6cc4..5f44e80 100644 --- a/cmake/appleclang-toolchain.cmake +++ b/cmake/appleclang-toolchain.cmake @@ -16,6 +16,8 @@ include_guard(GLOBAL) +# Prevent PATH collision with an LLVM clang installation by using the system +# compiler shims set(CMAKE_C_COMPILER cc) set(CMAKE_CXX_COMPILER c++) diff --git a/cmake/beman-install-library-config.cmake b/cmake/beman-install-library-config.cmake new file mode 100644 index 0000000..e7fd0ad --- /dev/null +++ b/cmake/beman-install-library-config.cmake @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +include_guard(GLOBAL) + +# This file defines the function `beman_install_library` which is used to +# install a library target and its headers, along with optional CMake +# configuration files. +# +# The function is designed to be reusable across different Beman libraries. + +function(beman_install_library name) + # Usage + # ----- + # + # beman_install_library(NAME) + # + # Brief + # ----- + # + # This function installs the specified library target and its headers. + # It also handles the installation of the CMake configuration files if needed. + # + # CMake variables + # --------------- + # + # Note that configuration of the installation is generally controlled by CMake + # cache variables so that they can be controlled by the user or tool running the + # `cmake` command. Neither `CMakeLists.txt` nor `*.cmake` files should set these + # variables directly. + # + # - BEMAN_INSTALL_CONFIG_FILE_PACKAGES: + # List of packages that require config file installation. + # If the package name is in this list, it will install the config file. + # + # - _INSTALL_CONFIG_FILE_PACKAGE: + # Boolean to control config file installation for the specific library. + # The prefix `` is the uppercased name of the library with dots + # replaced by underscores. + # + if(NOT TARGET "${name}") + message(FATAL_ERROR "Target '${name}' does not exist.") + endif() + + if(NOT ARGN STREQUAL "") + message( + FATAL_ERROR + "beman_install_library does not accept extra arguments: ${ARGN}" + ) + endif() + + # Given foo.bar, the component name is bar + string(REPLACE "." ";" name_parts "${name}") + # fail if the name doesn't look like foo.bar + list(LENGTH name_parts name_parts_length) + if(NOT name_parts_length EQUAL 2) + message( + FATAL_ERROR + "beman_install_library expects a name of the form 'beman.', got '${name}'" + ) + endif() + + set(target_name "${name}") + set(install_component_name "${name}") + set(export_name "${name}") + set(package_name "${name}") + list(GET name_parts -1 component_name) + + install( + TARGETS "${target_name}" + COMPONENT "${install_component_name}" + EXPORT "${export_name}" + FILE_SET HEADERS + ) + + set_target_properties( + "${target_name}" + PROPERTIES EXPORT_NAME "${component_name}" + ) + + include(GNUInstallDirs) + + # Determine the prefix for project-specific variables + string(TOUPPER "${name}" project_prefix) + string(REPLACE "." "_" project_prefix "${project_prefix}") + + option( + ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE + "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} + ) + + # By default, install the config package + set(install_config_package ON) + + # Turn OFF installation of config package by default if, + # in order of precedence: + # 1. The specific package variable is set to OFF + # 2. The package name is not in the list of packages to install config files + if(DEFINED BEMAN_INSTALL_CONFIG_FILE_PACKAGES) + if( + NOT "${install_component_name}" + IN_LIST + BEMAN_INSTALL_CONFIG_FILE_PACKAGES + ) + set(install_config_package OFF) + endif() + endif() + if(DEFINED ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE) + set(install_config_package + ${${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE} + ) + endif() + + if(install_config_package) + message( + DEBUG + "beman-install-library: Installing a config package for '${name}'" + ) + + include(CMakePackageConfigHelpers) + + find_file( + config_file_template + NAMES "${package_name}-config.cmake.in" + PATHS "${CMAKE_CURRENT_SOURCE_DIR}" + NO_DEFAULT_PATH + NO_CACHE + REQUIRED + ) + set(config_package_file + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config.cmake" + ) + set(package_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/${package_name}") + configure_package_config_file( + "${config_file_template}" + "${config_package_file}" + INSTALL_DESTINATION "${package_install_dir}" + PATH_VARS PROJECT_NAME PROJECT_VERSION + ) + + set(config_version_file + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config-version.cmake" + ) + write_basic_package_version_file( + "${config_version_file}" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY ExactVersion + ) + + install( + FILES "${config_package_file}" "${config_version_file}" + DESTINATION "${package_install_dir}" + COMPONENT "${install_component_name}" + ) + + set(config_targets_file "${package_name}-targets.cmake") + install( + EXPORT "${export_name}" + DESTINATION "${package_install_dir}" + NAMESPACE beman:: + FILE "${config_targets_file}" + COMPONENT "${install_component_name}" + ) + else() + message( + DEBUG + "beman-install-library: Not installing a config package for '${name}'" + ) + endif() +endfunction() diff --git a/cmake/llvm-libc++-toolchain.cmake b/cmake/llvm-libc++-toolchain.cmake new file mode 100644 index 0000000..76264c6 --- /dev/null +++ b/cmake/llvm-libc++-toolchain.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSL-1.0 + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include(${CMAKE_CURRENT_LIST_DIR}/llvm-toolchain.cmake) + +if(NOT CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") + string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") +endif() diff --git a/lockfile.json b/lockfile.json new file mode 100644 index 0000000..4208a98 --- /dev/null +++ b/lockfile.json @@ -0,0 +1,3 @@ +{ + "dependencies": [] +} diff --git a/tests/beman/bounds_test/CMakeLists.txt b/tests/beman/bounds_test/CMakeLists.txt index 677f22b..c6959c7 100644 --- a/tests/beman/bounds_test/CMakeLists.txt +++ b/tests/beman/bounds_test/CMakeLists.txt @@ -1,6 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -find_package(Catch2 3 REQUIRED CONFIG) +include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.11.0 + EXCLUDE_FROM_ALL + FIND_PACKAGE_ARGS 3 +) +FetchContent_MakeAvailable(Catch2) add_executable(beman.bounds_test.tests) target_sources(beman.bounds_test.tests PRIVATE bounds_test.tests.cpp) From 4cc1e7a111aaff04e7c2c4041fdc69fb45bb3180 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 26 Nov 2025 07:20:23 +0100 Subject: [PATCH 6/8] Feat: Add beman infra repo content --- CMakePresets.json | 2 +- infra/.beman_submodule | 3 + infra/.github/CODEOWNERS | 1 + infra/.github/workflows/beman-submodule.yml | 32 ++ infra/.github/workflows/pre-commit.yml | 78 +++ ...reusable-beman-create-issue-when-fault.yml | 28 + infra/.gitignore | 59 ++ infra/.pre-commit-config.yaml | 32 ++ infra/.pre-commit-hooks.yaml | 7 + infra/LICENSE | 219 +++++++ infra/README.md | 55 ++ infra/cmake/appleclang-toolchain.cmake | 44 ++ .../cmake}/beman-install-library-config.cmake | 0 infra/cmake/gnu-toolchain.cmake | 41 ++ infra/cmake/llvm-libc++-toolchain.cmake | 20 + infra/cmake/llvm-toolchain.cmake | 41 ++ infra/cmake/msvc-toolchain.cmake | 41 ++ infra/cmake/use-fetch-content.cmake | 177 ++++++ infra/tools/beman-submodule/README.md | 63 ++ infra/tools/beman-submodule/beman-submodule | 260 +++++++++ .../test/test_beman_submodule.py | 539 ++++++++++++++++++ 21 files changed, 1741 insertions(+), 1 deletion(-) create mode 100644 infra/.beman_submodule create mode 100644 infra/.github/CODEOWNERS create mode 100644 infra/.github/workflows/beman-submodule.yml create mode 100644 infra/.github/workflows/pre-commit.yml create mode 100644 infra/.github/workflows/reusable-beman-create-issue-when-fault.yml create mode 100644 infra/.gitignore create mode 100644 infra/.pre-commit-config.yaml create mode 100644 infra/.pre-commit-hooks.yaml create mode 100644 infra/LICENSE create mode 100644 infra/README.md create mode 100644 infra/cmake/appleclang-toolchain.cmake rename {cmake => infra/cmake}/beman-install-library-config.cmake (100%) create mode 100644 infra/cmake/gnu-toolchain.cmake create mode 100644 infra/cmake/llvm-libc++-toolchain.cmake create mode 100644 infra/cmake/llvm-toolchain.cmake create mode 100644 infra/cmake/msvc-toolchain.cmake create mode 100644 infra/cmake/use-fetch-content.cmake create mode 100644 infra/tools/beman-submodule/README.md create mode 100755 infra/tools/beman-submodule/beman-submodule create mode 100644 infra/tools/beman-submodule/test/test_beman_submodule.py diff --git a/CMakePresets.json b/CMakePresets.json index 8f47c00..2dd68b3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,7 +9,7 @@ "cacheVariables": { "CMAKE_CXX_STANDARD": "20", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "NO_CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "infra/cmake/use-fetch-content.cmake" + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "infra/cmake/use-fetch-content.cmake" } }, { diff --git a/infra/.beman_submodule b/infra/.beman_submodule new file mode 100644 index 0000000..bfed167 --- /dev/null +++ b/infra/.beman_submodule @@ -0,0 +1,3 @@ +[beman_submodule] +remote=https://github.com/bemanproject/infra.git +commit_hash=bb58b2a1cc894d58a55bf745be78f5d27029e245 diff --git a/infra/.github/CODEOWNERS b/infra/.github/CODEOWNERS new file mode 100644 index 0000000..4ff90a4 --- /dev/null +++ b/infra/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ednolan @neatudarius @rishyak @wusatosi @JeffGarland diff --git a/infra/.github/workflows/beman-submodule.yml b/infra/.github/workflows/beman-submodule.yml new file mode 100644 index 0000000..8435086 --- /dev/null +++ b/infra/.github/workflows/beman-submodule.yml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: beman-submodule tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + beman-submodule-script-ci: + name: beman_module.py ci + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - name: Install pytest + run: | + python3 -m pip install pytest + + - name: Run pytest + run: | + cd tools/beman-submodule/ + pytest diff --git a/infra/.github/workflows/pre-commit.yml b/infra/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..9646831 --- /dev/null +++ b/infra/.github/workflows/pre-commit.yml @@ -0,0 +1,78 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit-push: + name: Pre-Commit check on Push + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # We wish to run pre-commit on all files instead of the changes + # only made in the push commit. + # + # So linting error persists when there's formatting problem. + - uses: pre-commit/action@v3.0.1 + + pre-commit-pr: + name: Pre-Commit check on PR + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request_target' }} + + permissions: + contents: read + checks: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # pull_request_target checkout the base of the repo + # We need to checkout the actual pr to lint the changes. + - name: Checkout pr + run: gh pr checkout ${{ github.event.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # we only lint on the changed file in PR. + - name: Get Changed Files + id: changed-files + uses: tj-actions/changed-files@v45 + + # See: + # https://github.com/tj-actions/changed-files?tab=readme-ov-file#using-local-git-directory- + - uses: pre-commit/action@v3.0.1 + id: run-pre-commit + with: + extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} + + # Review dog posts the suggested change from pre-commit to the pr. + - name: suggester / pre-commit + uses: reviewdog/action-suggester@v1 + if: ${{ failure() && steps.run-pre-commit.conclusion == 'failure' }} + with: + tool_name: pre-commit + level: warning + reviewdog_flags: "-fail-level=error" diff --git a/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml b/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml new file mode 100644 index 0000000..024a51f --- /dev/null +++ b/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: 'Beman issue creation workflow' +on: + workflow_call: + workflow_dispatch: +jobs: + create-issue: + runs-on: ubuntu-latest + steps: + # See https://github.com/cli/cli/issues/5075 + - uses: actions/checkout@v4 + - name: Create issue + run: | + issue_num=$(gh issue list -s open -S "[SCHEDULED-BUILD] infra repo CI job failure" -L 1 --json number | jq 'if length == 0 then -1 else .[0].number end') + body="**CI job failure Report** + - **Time of Failure**: $(date -u '+%B %d, %Y, %H:%M %Z') + - **Commit**: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) + - **Action Run**: [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + The scheduled job triggered by cron has failed. + Please investigate the logs and recent changes associated with this commit or rerun the workflow if you believe this is an error." + if [[ $issue_num -eq -1 ]]; then + gh issue create --repo ${{ github.repository }} --title "[SCHEDULED-BUILD] infra repo CI job failure" --body "$body" --assignee ${{ github.actor }} + else + gh issue comment --repo ${{ github.repository }} $issue_num --body "$body" + fi + env: + GH_TOKEN: ${{ github.token }} diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 0000000..b7cdbb5 --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,59 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Python +__pycache__/ +.pytest_cache/ +*.pyc +*.pyo +*.pyd +*.pyw +*.pyz +*.pywz +*.pyzw +*.pyzwz +*.delete_me + +# MAC OS +*.DS_Store + +# Editor files +.vscode/ +.idea/ + +# Build directories +infra.egg-info/ +beman_tidy.egg-info/ +*.egg-info/ +build/ +dist/ diff --git a/infra/.pre-commit-config.yaml b/infra/.pre-commit-config.yaml new file mode 100644 index 0000000..e806e59 --- /dev/null +++ b/infra/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.22.3 + hooks: + - id: gersemi + name: CMake linting + exclude: ^.*/tests/.*/data/ # Exclude test data directories + + # Python linting and formatting + # config file: ruff.toml (not currently present but add if needed) + # https://docs.astral.sh/ruff/configuration/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 + hooks: + - id: ruff-check + files: ^tools/beman-tidy/ + - id: ruff-format + files: ^tools/beman-tidy/ diff --git a/infra/.pre-commit-hooks.yaml b/infra/.pre-commit-hooks.yaml new file mode 100644 index 0000000..d327587 --- /dev/null +++ b/infra/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: beman-tidy + name: "beman-tidy: bemanification your repo" + entry: ./tools/beman-tidy/beman-tidy + language: script + pass_filenames: false + always_run: true + args: [".", "--verbose"] diff --git a/infra/LICENSE b/infra/LICENSE new file mode 100644 index 0000000..f6db814 --- /dev/null +++ b/infra/LICENSE @@ -0,0 +1,219 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..16b2672 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,55 @@ +# Beman Project Infrastructure Repository + + + +This repository contains the infrastructure for The Beman Project. This is NOT a library repository, +so it does not respect the usual structure of a Beman library repository nor The Beman Standard! + +## Description + +* `cmake/`: CMake modules and toolchain files used by Beman libraries. +* `containers/`: Containers used for CI builds and tests in the Beman org. +* `tools/`: Tools used to manage the infrastructure and the codebase (e.g., linting, formatting, etc.). + +## Usage + +This repository is intended to be used as a beman-submodule in other Beman repositories. See +[the Beman Submodule documentation](./tools/beman-submodule/README.md) for details. + + +### CMake Modules + + +#### `beman_install_library` + +The CMake modules in this repository are intended to be used by Beman libraries. Use the +`beman_add_install_library_config()` function to install your library, along with header +files, any metadata files, and a CMake config file for `find_package()` support. + +```cmake +add_library(beman.something) +add_library(beman::something ALIAS beman.something) + +# ... configure your target as needed ... + +find_package(beman-install-library REQUIRED) +beman_install_library(beman.something) +``` + +Note that the target must be created before calling `beman_install_library()`. The module +also assumes that the target is named using the `beman.something` convention, and it +uses that assumption to derive the names to match other Beman standards and conventions. +If your target does not follow that convention, raise an issue or pull request to add +more configurability to the module. + +The module will configure the target to install: + +* The library target itself +* Any public headers associated with the target +* CMake files for `find_package(beman.something)` support + +Some options for the project and target will also be supported: + +* `BEMAN_INSTALL_CONFIG_FILE_PACKAGES` - a list of package names (e.g., `beman.something`) for which to install the config file + (default: all packages) +* `_INSTALL_CONFIG_FILE_PACKAGE` - a per-project option to enable/disable config file installation (default: `ON` if the project is top-level, `OFF` otherwise). For instance for `beman.something`, the option would be `BEMAN_SOMETHING_INSTALL_CONFIG_FILE_PACKAGE`. diff --git a/infra/cmake/appleclang-toolchain.cmake b/infra/cmake/appleclang-toolchain.cmake new file mode 100644 index 0000000..70ef548 --- /dev/null +++ b/infra/cmake/appleclang-toolchain.cmake @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for apple clang family of compiler. +# Note this is different from LLVM toolchain. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. Note that apple clang does not support leak sanitizer. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include_guard(GLOBAL) + +# Prevent PATH collision with an LLVM clang installation by using the system +# compiler shims +set(CMAKE_C_COMPILER cc) +set(CMAKE_CXX_COMPILER c++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake similarity index 100% rename from cmake/beman-install-library-config.cmake rename to infra/cmake/beman-install-library-config.cmake diff --git a/infra/cmake/gnu-toolchain.cmake b/infra/cmake/gnu-toolchain.cmake new file mode 100644 index 0000000..d3b9f92 --- /dev/null +++ b/infra/cmake/gnu-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for GNU family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures gcc and g++ to use all available non-conflicting +# sanitizers. +# - TSan: configures gcc and g++ to enable the use of thread sanitizer + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/llvm-libc++-toolchain.cmake b/infra/cmake/llvm-libc++-toolchain.cmake new file mode 100644 index 0000000..76264c6 --- /dev/null +++ b/infra/cmake/llvm-libc++-toolchain.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSL-1.0 + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include(${CMAKE_CURRENT_LIST_DIR}/llvm-toolchain.cmake) + +if(NOT CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") + string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") +endif() diff --git a/infra/cmake/llvm-toolchain.cmake b/infra/cmake/llvm-toolchain.cmake new file mode 100644 index 0000000..f1623b7 --- /dev/null +++ b/infra/cmake/llvm-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang) +set(CMAKE_CXX_COMPILER clang++) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + set(SANITIZER_FLAGS + "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" + ) +elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") + set(SANITIZER_FLAGS "-fsanitize=thread") +endif() + +set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/msvc-toolchain.cmake b/infra/cmake/msvc-toolchain.cmake new file mode 100644 index 0000000..bdc24de --- /dev/null +++ b/infra/cmake/msvc-toolchain.cmake @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for MSVC family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures cl to use all available non-conflicting sanitizers. +# +# Note that in other toolchain files, TSan is also a possible value for +# BEMAN_BUILDSYS_SANITIZER, however, MSVC does not support thread sanitizer, +# thus this value is omitted. + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER cl) +set(CMAKE_CXX_COMPILER cl) + +if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") + # /Zi flag (add debug symbol) is needed when using address sanitizer + # See C5072: https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-c5072 + set(SANITIZER_FLAGS "/fsanitize=address /Zi") +endif() + +set(CMAKE_CXX_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") +set(CMAKE_C_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") + +set(RELEASE_FLAGS "/EHsc /permissive- /O2 ${SANITIZER_FLAGS}") + +set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") + +set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/use-fetch-content.cmake b/infra/cmake/use-fetch-content.cmake new file mode 100644 index 0000000..9ac69d3 --- /dev/null +++ b/infra/cmake/use-fetch-content.cmake @@ -0,0 +1,177 @@ +cmake_minimum_required(VERSION 3.24) + +include(FetchContent) + +if(NOT BEMAN_EXEMPLAR_LOCKFILE) + set(BEMAN_EXEMPLAR_LOCKFILE + "lockfile.json" + CACHE FILEPATH + "Path to the dependency lockfile for the Beman Exemplar." + ) +endif() + +set(BemanExemplar_projectDir "${CMAKE_CURRENT_LIST_DIR}/../..") +message(TRACE "BemanExemplar_projectDir=\"${BemanExemplar_projectDir}\"") + +message(TRACE "BEMAN_EXEMPLAR_LOCKFILE=\"${BEMAN_EXEMPLAR_LOCKFILE}\"") +file( + REAL_PATH "${BEMAN_EXEMPLAR_LOCKFILE}" + BemanExemplar_lockfile + BASE_DIRECTORY "${BemanExemplar_projectDir}" + EXPAND_TILDE +) +message(DEBUG "Using lockfile: \"${BemanExemplar_lockfile}\"") + +# Force CMake to reconfigure the project if the lockfile changes +set_property( + DIRECTORY "${BemanExemplar_projectDir}" + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${BemanExemplar_lockfile}" +) + +# For more on the protocol for this function, see: +# https://cmake.org/cmake/help/latest/command/cmake_language.html#provider-commands +function(BemanExemplar_provideDependency method package_name) + # Read the lockfile + file(READ "${BemanExemplar_lockfile}" BemanExemplar_rootObj) + + # Get the "dependencies" field and store it in BemanExemplar_dependenciesObj + string( + JSON BemanExemplar_dependenciesObj + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_rootObj}" + "dependencies" + ) + if(BemanExemplar_error) + message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") + endif() + + # Get the length of the libraries array and store it in BemanExemplar_dependenciesObj + string( + JSON BemanExemplar_numDependencies + ERROR_VARIABLE BemanExemplar_error + LENGTH "${BemanExemplar_dependenciesObj}" + ) + if(BemanExemplar_error) + message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") + endif() + + if(BemanExemplar_numDependencies EQUAL 0) + return() + endif() + + # Loop over each dependency object + math(EXPR BemanExemplar_maxIndex "${BemanExemplar_numDependencies} - 1") + foreach(BemanExemplar_index RANGE "${BemanExemplar_maxIndex}") + set(BemanExemplar_errorPrefix + "${BemanExemplar_lockfile}, dependency ${BemanExemplar_index}" + ) + + # Get the dependency object at BemanExemplar_index + # and store it in BemanExemplar_depObj + string( + JSON BemanExemplar_depObj + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_dependenciesObj}" + "${BemanExemplar_index}" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "name" field and store it in BemanExemplar_name + string( + JSON BemanExemplar_name + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "name" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "package_name" field and store it in BemanExemplar_pkgName + string( + JSON BemanExemplar_pkgName + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "package_name" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "git_repository" field and store it in BemanExemplar_repo + string( + JSON BemanExemplar_repo + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "git_repository" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + # Get the "git_tag" field and store it in BemanExemplar_tag + string( + JSON BemanExemplar_tag + ERROR_VARIABLE BemanExemplar_error + GET "${BemanExemplar_depObj}" + "git_tag" + ) + if(BemanExemplar_error) + message( + FATAL_ERROR + "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" + ) + endif() + + if(method STREQUAL "FIND_PACKAGE") + if(package_name STREQUAL BemanExemplar_pkgName) + string( + APPEND BemanExemplar_debug + "Redirecting find_package calls for ${BemanExemplar_pkgName} " + "to FetchContent logic.\n" + ) + string( + APPEND BemanExemplar_debug + "Fetching ${BemanExemplar_repo} at " + "${BemanExemplar_tag} according to ${BemanExemplar_lockfile}." + ) + message(DEBUG "${BemanExemplar_debug}") + FetchContent_Declare( + "${BemanExemplar_name}" + GIT_REPOSITORY "${BemanExemplar_repo}" + GIT_TAG "${BemanExemplar_tag}" + EXCLUDE_FROM_ALL + ) + set(INSTALL_GTEST OFF) # Disable GoogleTest installation + FetchContent_MakeAvailable("${BemanExemplar_name}") + + # Important! _FOUND tells CMake that `find_package` is + # not needed for this package anymore + set("${BemanExemplar_pkgName}_FOUND" TRUE PARENT_SCOPE) + endif() + endif() + endforeach() +endfunction() + +cmake_language( + SET_DEPENDENCY_PROVIDER BemanExemplar_provideDependency + SUPPORTED_METHODS FIND_PACKAGE +) + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/tools/beman-submodule/README.md b/infra/tools/beman-submodule/README.md new file mode 100644 index 0000000..36883ad --- /dev/null +++ b/infra/tools/beman-submodule/README.md @@ -0,0 +1,63 @@ +# beman-submodule + + + +## What is this script? + +`beman-submodule` provides some of the features of `git submodule`, adding child git +repositories to a parent git repository, but unlike with `git submodule`, the entire child +repo is directly checked in, so only maintainers, not users, need to run this script. The +command line interface mimics `git submodule`'s. + +## How do I add a beman submodule to my repository? + +The first beman submodule you should add is this repository, `infra/`, which you can +bootstrap by running: + + +```sh +curl -s https://raw.githubusercontent.com/bemanproject/infra/refs/heads/main/tools/beman-submodule/beman-submodule | python3 - add https://github.com/bemanproject/infra.git +``` + +Once that's added, you can run the script from `infra/tools/beman-submodule/beman-submodule`. + +## How do I update a beman submodule to the latest trunk? + +You can run `beman-submodule update --remote` to update all beman submodule to latest +trunk, or e.g. `beman-submodule update --remote infra` to update only a specific one. + +## How does it work under the hood? + +Along with the files from the child repository, it creates a dotfile called +`.beman_submodule`, which looks like this: + +```ini +[beman_submodule] +remote=https://github.com/bemanproject/infra.git +commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77 +``` + +## How do I update a beman submodule to a specific commit or change the remote URL? + +You can edit the corresponding lines in the `.beman_submodule` file and run +`beman-submodule update` to update the state of the beman submodule to the new +`.beman_submodule` settings. + +## How can I make CI ensure that my beman submodules are in a valid state? + +Add this job to your CI workflow: + +```yaml + beman-submodule-test: + runs-on: ubuntu-latest + name: "Check beman submodules for consistency" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: beman submodule consistency check + run: | + (set -o pipefail; ./infra/tools/beman-submodule/beman-submodule status | grep -qvF '+') +``` + +This will fail if the contents of any beman submodule don't match what's specified in the +`.beman_submodule` file. diff --git a/infra/tools/beman-submodule/beman-submodule b/infra/tools/beman-submodule/beman-submodule new file mode 100755 index 0000000..66cb96e --- /dev/null +++ b/infra/tools/beman-submodule/beman-submodule @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import argparse +import configparser +import filecmp +import glob +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def directory_compare( + reference: str | Path, actual: str | Path, ignore, allow_untracked_files: bool): + reference, actual = Path(reference), Path(actual) + + compared = filecmp.dircmp(reference, actual, ignore=ignore) + if (compared.left_only + or (compared.right_only and not allow_untracked_files) + or compared.diff_files): + return False + for common_dir in compared.common_dirs: + path1 = reference / common_dir + path2 = actual / common_dir + if not directory_compare(path1, path2, ignore, allow_untracked_files): + return False + return True + +class BemanSubmodule: + def __init__( + self, dirpath: str | Path, remote: str, commit_hash: str, + allow_untracked_files: bool): + self.dirpath = Path(dirpath) + self.remote = remote + self.commit_hash = commit_hash + self.allow_untracked_files = allow_untracked_files + +def parse_beman_submodule_file(path): + config = configparser.ConfigParser() + read_result = config.read(path) + def fail(): + raise Exception(f'Failed to parse {path} as a .beman_submodule file') + if not read_result: + fail() + if not 'beman_submodule' in config: + fail() + if not 'remote' in config['beman_submodule']: + fail() + if not 'commit_hash' in config['beman_submodule']: + fail() + allow_untracked_files = config.getboolean( + 'beman_submodule', 'allow_untracked_files', fallback=False) + return BemanSubmodule( + Path(path).resolve().parent, + config['beman_submodule']['remote'], + config['beman_submodule']['commit_hash'], + allow_untracked_files) + +def get_beman_submodule(path: str | Path): + beman_submodule_filepath = Path(path) / '.beman_submodule' + + if beman_submodule_filepath.is_file(): + return parse_beman_submodule_file(beman_submodule_filepath) + else: + return None + +def find_beman_submodules_in(path): + path = Path(path) + assert path.is_dir() + + result = [] + for dirpath, _, filenames in path.walk(): + if '.beman_submodule' in filenames: + result.append(parse_beman_submodule_file(dirpath / '.beman_submodule')) + return sorted(result, key=lambda module: module.dirpath) + +def cwd_git_repository_path(): + process = subprocess.run( + ['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, + check=False) + if process.returncode == 0: + return process.stdout.strip() + elif "fatal: not a git repository" in process.stderr: + return None + else: + raise Exception("git rev-parse --show-toplevel failed") + +def clone_beman_submodule_into_tmpdir(beman_submodule, remote): + tmpdir = tempfile.TemporaryDirectory() + subprocess.run( + ['git', 'clone', beman_submodule.remote, tmpdir.name], capture_output=True, + check=True) + if not remote: + subprocess.run( + ['git', '-C', tmpdir.name, 'reset', '--hard', beman_submodule.commit_hash], + capture_output=True, check=True) + return tmpdir + +def get_paths(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + paths = set(glob.glob('*', root_dir=Path(tmpdir.name), include_hidden=True)) + paths.remove('.git') + return paths + +def beman_submodule_status(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + if directory_compare( + tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git'], + beman_submodule.allow_untracked_files): + status_character=' ' + else: + status_character='+' + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + relpath = Path(beman_submodule.dirpath).relative_to(Path(parent_repo_path)) + return status_character + ' ' + beman_submodule.commit_hash + ' ' + str(relpath) + +def beman_submodule_update(beman_submodule, remote): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, remote) + tmp_path = Path(tmpdir.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmp_path) + + if beman_submodule.allow_untracked_files: + for path in get_paths(beman_submodule): + path2 = Path(beman_submodule.dirpath) / path + if Path(path2).is_dir(): + shutil.rmtree(path2) + elif Path(path2).is_file(): + os.remove(path2) + else: + shutil.rmtree(beman_submodule.dirpath) + + submodule_path = tmp_path / '.beman_submodule' + with open(submodule_path, 'w') as f: + f.write('[beman_submodule]\n') + f.write(f'remote={beman_submodule.remote}\n') + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if beman_submodule.allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmp_path / '.git') + shutil.copytree(tmp_path, beman_submodule.dirpath, dirs_exist_ok=True) + +def update_command(remote, path): + if not path: + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + beman_submodules = find_beman_submodules_in(parent_repo_path) + else: + beman_submodule = get_beman_submodule(path) + if not beman_submodule: + raise Exception(f'{path} is not a beman_submodule') + beman_submodules = [beman_submodule] + for beman_submodule in beman_submodules: + beman_submodule_update(beman_submodule, remote) + +def add_command(repository, path, allow_untracked_files): + tmpdir = tempfile.TemporaryDirectory() + subprocess.run( + ['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name) + repository_name = os.listdir(tmpdir.name)[0] + if not path: + path = Path(repository_name) + else: + path = Path(path) + if not allow_untracked_files and path.exists(): + raise Exception(f'{path} exists') + path.mkdir(exist_ok=allow_untracked_files) + tmpdir_repo = Path(tmpdir.name) / repository_name + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir_repo) + with open(tmpdir_repo / '.beman_submodule', 'w') as f: + f.write('[beman_submodule]\n') + f.write(f'remote={repository}\n') + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmpdir_repo /'.git') + shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True) + +def status_command(paths): + if not paths: + parent_repo_path = cwd_git_repository_path() + if not parent_repo_path: + raise Exception('this is not a git repository') + beman_submodules = find_beman_submodules_in(parent_repo_path) + else: + beman_submodules = [] + for path in paths: + beman_submodule = get_beman_submodule(path) + if not beman_submodule: + raise Exception(f'{path} is not a beman_submodule') + beman_submodules.append(beman_submodule) + for beman_submodule in beman_submodules: + print(beman_submodule_status(beman_submodule)) + +def get_parser(): + parser = argparse.ArgumentParser(description='Beman pseudo-submodule tool') + subparsers = parser.add_subparsers(dest='command', help='available commands') + parser_update = subparsers.add_parser('update', help='update beman_submodules') + parser_update.add_argument( + '--remote', action='store_true', + help='update a beman_submodule to its latest from upstream') + parser_update.add_argument( + 'beman_submodule_path', nargs='?', + help='relative path to the beman_submodule to update') + parser_add = subparsers.add_parser('add', help='add a new beman_submodule') + parser_add.add_argument('repository', help='git repository to add') + parser_add.add_argument( + 'path', nargs='?', help='path where the repository will be added') + parser_add.add_argument( + '--allow-untracked-files', action='store_true', + help='the beman_submodule will not occupy the subdirectory exclusively') + parser_status = subparsers.add_parser( + 'status', help='show the status of beman_submodules') + parser_status.add_argument('paths', nargs='*') + return parser + +def parse_args(args): + return get_parser().parse_args(args); + +def usage(): + return get_parser().format_help() + +def run_command(args): + if args.command == 'update': + update_command(args.remote, args.beman_submodule_path) + elif args.command == 'add': + add_command(args.repository, args.path, args.allow_untracked_files) + elif args.command == 'status': + status_command(args.paths) + else: + raise Exception(usage()) + +def check_for_git(path): + env = os.environ.copy() + if path is not None: + env["PATH"] = path + return shutil.which("git", path=env.get("PATH")) is not None + +def main(): + try: + if not check_for_git(None): + raise Exception('git not found in PATH') + args = parse_args(sys.argv[1:]) + run_command(args) + except Exception as e: + print("Error:", e, file=sys.stderr) + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/infra/tools/beman-submodule/test/test_beman_submodule.py b/infra/tools/beman-submodule/test/test_beman_submodule.py new file mode 100644 index 0000000..600fc07 --- /dev/null +++ b/infra/tools/beman-submodule/test/test_beman_submodule.py @@ -0,0 +1,539 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +import glob +import os +import pytest +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path + +# https://stackoverflow.com/a/19011259 +import types +import importlib.machinery +loader = importlib.machinery.SourceFileLoader( + 'beman_submodule', + str(Path(__file__).parent.resolve().parent / 'beman-submodule')) +beman_submodule = types.ModuleType(loader.name) +loader.exec_module(beman_submodule) + +def create_test_git_repository(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + def make_commit(a_txt_contents): + with open(tmp_path / 'a.txt', 'w') as f: + f.write(a_txt_contents) + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + make_commit('A') + make_commit('a') + return tmpdir + +def create_test_git_repository2(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'a.txt', 'w') as f: + f.write('a') + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + os.remove(tmp_path / 'a.txt') + subprocess.run( + ['git', 'rm', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'b.txt', 'w') as f: + f.write('b') + subprocess.run( + ['git', 'add', 'b.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + return tmpdir + +def test_directory_compare(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as dir_a, \ + tempfile.TemporaryDirectory() as dir_b: + path_a = Path(dir_a) + path_b = Path(dir_b) + + create_dir_structure(path_a) + create_dir_structure(path_b) + + assert beman_submodule.directory_compare(dir_a, dir_b, [], False) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], False) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], False) + +def test_directory_compare_untracked_files(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as reference, \ + tempfile.TemporaryDirectory() as actual: + path_a = Path(reference) + path_b = Path(actual) + + create_dir_structure(path_a) + create_dir_structure(path_b) + (path_b / 'c.txt').touch() + + assert beman_submodule.directory_compare(reference, actual, [], True) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], True) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], True) + +def test_parse_beman_submodule_file(): + def valid_file(): + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + module = beman_submodule.parse_beman_submodule_file(tmpfile.name) + assert module.dirpath == Path(tmpfile.name).resolve().parent + assert module.remote == 'git@github.com:bemanproject/infra.git' + assert module.commit_hash == '9b88395a86c4290794e503e94d8213b6c442ae77' + valid_file() + def invalid_file_missing_remote(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_missing_remote() + def invalid_file_missing_commit_hash(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[beman_submodule]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_missing_commit_hash() + def invalid_file_wrong_section(): + threw = False + try: + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write('[invalid]\n'.encode('utf-8')) + tmpfile.write( + 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) + tmpfile.write( + 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) + tmpfile.flush() + beman_submodule.parse_beman_submodule_file(tmpfile.name) + except: + threw = True + assert threw + invalid_file_wrong_section() + +def test_get_beman_submodule(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + assert beman_submodule.get_beman_submodule('foo') + os.remove('foo/.beman_submodule') + assert not beman_submodule.get_beman_submodule('foo') + os.chdir(original_cwd) + +def test_find_beman_submodules_in(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + beman_submodules = beman_submodule.find_beman_submodules_in(tmpdir2.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert beman_submodules[0].dirpath == Path(tmpdir2.name) / 'bar' + assert beman_submodules[0].remote == tmpdir.name + assert beman_submodules[0].commit_hash == sha + assert beman_submodules[1].dirpath == Path(tmpdir2.name) / 'foo' + assert beman_submodules[1].remote == tmpdir.name + assert beman_submodules[1].commit_hash == sha + os.chdir(original_cwd) + +def test_cwd_git_repository_path(): + original_cwd = Path.cwd() + tmpdir = tempfile.TemporaryDirectory() + os.chdir(tmpdir.name) + assert not beman_submodule.cwd_git_repository_path() + subprocess.run(['git', 'init']) + assert beman_submodule.cwd_git_repository_path() == tmpdir.name + os.chdir(original_cwd) + +def test_clone_beman_submodule_into_tmpdir(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + module.commit_hash = sha + tmpdir3 = beman_submodule.clone_beman_submodule_into_tmpdir(module, False) + assert not beman_submodule.directory_compare( + tmpdir.name, tmpdir3.name, ['.git'], False) + tmpdir4 = beman_submodule.clone_beman_submodule_into_tmpdir(module, True) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git'], False) + subprocess.run( + ['git', 'reset', '--hard', sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git'], False) + os.chdir(original_cwd) + +def test_get_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + assert beman_submodule.get_paths(module) == set(['a.txt']) + os.chdir(original_cwd) + +def test_beman_submodule_status(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert ' ' + sha + ' foo' == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + with open(Path(tmpdir2.name) / 'foo' / 'a.txt', 'w') as f: + f.write('b') + assert '+ ' + sha + ' foo' == beman_submodule.beman_submodule_status( + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + os.chdir(original_cwd) + +def test_update_command_no_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() + subprocess.run( + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_with_path(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() + subprocess.run( + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + tmpdir_parent_parent_copy = tempfile.TemporaryDirectory() + shutil.copytree(tmpdir.name, tmpdir_parent_parent_copy.name, dirs_exist_ok=True) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_untracked_files(): + tmpdir = create_test_git_repository2() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd(); + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True') + beman_submodule.update_command(False, 'foo') + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + beman_submodule.update_command(True, 'foo') + assert set(['./foo/b.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + +def test_add_command(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + sha = sha_process.stdout.strip() + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n' + os.chdir(original_cwd) + +def test_add_command_untracked_files(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.add_command(tmpdir.name, 'foo', True) + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + +def test_status_command_no_paths(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: + f.write('b') + beman_submodule.status_command([]) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + ' ' + sha + ' foo\n' + os.chdir(original_cwd) + +def test_status_command_with_path(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: + f.write('b') + beman_submodule.status_command(['bar']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + os.chdir(original_cwd) + +def test_status_command_untracked_files(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', True) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.status_command(['foo']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == ' ' + sha + ' foo\n' + os.chdir(original_cwd) + +def test_check_for_git(): + tmpdir = tempfile.TemporaryDirectory() + assert not beman_submodule.check_for_git(tmpdir.name) + fake_git_path = Path(tmpdir.name) / 'git' + with open(fake_git_path, 'w'): + pass + os.chmod(fake_git_path, stat.S_IRWXU) + assert beman_submodule.check_for_git(tmpdir.name) + +def test_parse_args(): + def plain_update(): + args = beman_submodule.parse_args(['update']) + assert args.command == 'update' + assert not args.remote + assert not args.beman_submodule_path + plain_update() + def update_remote(): + args = beman_submodule.parse_args(['update', '--remote']) + assert args.command == 'update' + assert args.remote + assert not args.beman_submodule_path + update_remote() + def update_path(): + args = beman_submodule.parse_args(['update', 'infra/']) + assert args.command == 'update' + assert not args.remote + assert args.beman_submodule_path == 'infra/' + update_path() + def update_path_remote(): + args = beman_submodule.parse_args(['update', '--remote', 'infra/']) + assert args.command == 'update' + assert args.remote + assert args.beman_submodule_path == 'infra/' + update_path_remote() + def plain_add(): + args = beman_submodule.parse_args(['add', 'git@github.com:bemanproject/infra.git']) + assert args.command == 'add' + assert args.repository == 'git@github.com:bemanproject/infra.git' + assert not args.path + plain_add() + def add_path(): + args = beman_submodule.parse_args( + ['add', 'git@github.com:bemanproject/infra.git', 'infra/']) + assert args.command == 'add' + assert args.repository == 'git@github.com:bemanproject/infra.git' + assert args.path == 'infra/' + add_path() + def plain_status(): + args = beman_submodule.parse_args(['status']) + assert args.command == 'status' + assert args.paths == [] + plain_status() + def status_one_module(): + args = beman_submodule.parse_args(['status', 'infra/']) + assert args.command == 'status' + assert args.paths == ['infra/'] + status_one_module() + def status_multiple_modules(): + args = beman_submodule.parse_args(['status', 'infra/', 'foobar/']) + assert args.command == 'status' + assert args.paths == ['infra/', 'foobar/'] + status_multiple_modules() From 0670c48e43f85a85a0dd81a28f795c6062a8927c Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 26 Nov 2025 07:24:19 +0100 Subject: [PATCH 7/8] Feat: apply beman-submodule update --- infra/cmake/use-fetch-content.cmake | 30 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/infra/cmake/use-fetch-content.cmake b/infra/cmake/use-fetch-content.cmake index 9ac69d3..4ed4839 100644 --- a/infra/cmake/use-fetch-content.cmake +++ b/infra/cmake/use-fetch-content.cmake @@ -15,7 +15,8 @@ message(TRACE "BemanExemplar_projectDir=\"${BemanExemplar_projectDir}\"") message(TRACE "BEMAN_EXEMPLAR_LOCKFILE=\"${BEMAN_EXEMPLAR_LOCKFILE}\"") file( - REAL_PATH "${BEMAN_EXEMPLAR_LOCKFILE}" + REAL_PATH + "${BEMAN_EXEMPLAR_LOCKFILE}" BemanExemplar_lockfile BASE_DIRECTORY "${BemanExemplar_projectDir}" EXPAND_TILDE @@ -37,7 +38,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the "dependencies" field and store it in BemanExemplar_dependenciesObj string( - JSON BemanExemplar_dependenciesObj + JSON + BemanExemplar_dependenciesObj ERROR_VARIABLE BemanExemplar_error GET "${BemanExemplar_rootObj}" "dependencies" @@ -48,7 +50,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the length of the libraries array and store it in BemanExemplar_dependenciesObj string( - JSON BemanExemplar_numDependencies + JSON + BemanExemplar_numDependencies ERROR_VARIABLE BemanExemplar_error LENGTH "${BemanExemplar_dependenciesObj}" ) @@ -70,7 +73,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the dependency object at BemanExemplar_index # and store it in BemanExemplar_depObj string( - JSON BemanExemplar_depObj + JSON + BemanExemplar_depObj ERROR_VARIABLE BemanExemplar_error GET "${BemanExemplar_dependenciesObj}" "${BemanExemplar_index}" @@ -84,7 +88,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the "name" field and store it in BemanExemplar_name string( - JSON BemanExemplar_name + JSON + BemanExemplar_name ERROR_VARIABLE BemanExemplar_error GET "${BemanExemplar_depObj}" "name" @@ -98,7 +103,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the "package_name" field and store it in BemanExemplar_pkgName string( - JSON BemanExemplar_pkgName + JSON + BemanExemplar_pkgName ERROR_VARIABLE BemanExemplar_error GET "${BemanExemplar_depObj}" "package_name" @@ -112,7 +118,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the "git_repository" field and store it in BemanExemplar_repo string( - JSON BemanExemplar_repo + JSON + BemanExemplar_repo ERROR_VARIABLE BemanExemplar_error GET "${BemanExemplar_depObj}" "git_repository" @@ -126,7 +133,8 @@ function(BemanExemplar_provideDependency method package_name) # Get the "git_tag" field and store it in BemanExemplar_tag string( - JSON BemanExemplar_tag + JSON + BemanExemplar_tag ERROR_VARIABLE BemanExemplar_error GET "${BemanExemplar_depObj}" "git_tag" @@ -141,12 +149,14 @@ function(BemanExemplar_provideDependency method package_name) if(method STREQUAL "FIND_PACKAGE") if(package_name STREQUAL BemanExemplar_pkgName) string( - APPEND BemanExemplar_debug + APPEND + BemanExemplar_debug "Redirecting find_package calls for ${BemanExemplar_pkgName} " "to FetchContent logic.\n" ) string( - APPEND BemanExemplar_debug + APPEND + BemanExemplar_debug "Fetching ${BemanExemplar_repo} at " "${BemanExemplar_tag} according to ${BemanExemplar_lockfile}." ) From 505ccc26ede4aad172a9757503c7f2df3ac18548 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Fri, 28 Nov 2025 21:15:25 +0100 Subject: [PATCH 8/8] Fix: exclude infra from pre-commt hooks --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8bc6d2d..93b5f35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,3 +38,5 @@ repos: rev: v2.4.1 hooks: - id: codespell + +exclude: 'infra/'