From c3832a588a1542e0f3b8f47eddde666f7d8a7dee Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 6 Dec 2025 17:23:25 -0500 Subject: [PATCH 01/19] Improve keeping the lsp server happy Make sure the compilation database is correct and includes the std flag. --- Makefile | 14 +++++++------- etc/clang-flags.cmake | 2 +- etc/gcc-flags.cmake | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 87a6657..7337cf2 100755 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ else endif -_build_path?=$(_build_dir)/$(_build_name) +_build_path?=$(shell realpath --relative-to=$(CURDIR) $(_build_dir)/$(_build_name)) define run_cmake = cmake \ @@ -54,18 +54,18 @@ $(_build_path): $(_build_path)/CMakeCache.txt: | $(_build_path) .gitmodules cd $(_build_path) && $(run_cmake) - -rm compile_commands.json - ln -s $(_build_path)/compile_commands.json $(_build_path)/compile_commands.json : $(_build_path)/CMakeCache.txt -compile_commands.json: $(_build_path)/compile_commands.json - -rm compile_commands.json - ln -s $(_build_path)/compile_commands.json +.PHONY: compile_commands.json +compile_commands.json: + if [ "$(shell readlink compile_commands.json)" != "$(_build_path)/compile_commands.json" ] ; then \ + ln -sf $(_build_path)/compile_commands.json ; \ + fi TARGET:=all +compile: $(_build_path)/CMakeCache.txt compile: compile_commands.json -compile: $(_build_path)/CMakeCache.txt ## Compile the project compile: ## Compile the project cmake --build $(_build_path) --config $(CONFIG) --target all -- -k 0 diff --git a/etc/clang-flags.cmake b/etc/clang-flags.cmake index c7dbb09..36ec05a 100644 --- a/etc/clang-flags.cmake +++ b/etc/clang-flags.cmake @@ -3,7 +3,7 @@ include_guard(GLOBAL) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_FLAGS - "-stdlib=libc++ -Wall -Wextra " + "-stdlib=libc++ -Wall -Wextra -std=gnu++20" CACHE STRING "CXX_FLAGS" FORCE diff --git a/etc/gcc-flags.cmake b/etc/gcc-flags.cmake index f75dd22..ebb52b6 100644 --- a/etc/gcc-flags.cmake +++ b/etc/gcc-flags.cmake @@ -2,7 +2,7 @@ include_guard(GLOBAL) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_FLAGS "-Wall -Wextra " CACHE STRING "CXX_FLAGS" FORCE) +set(CMAKE_CXX_FLAGS "-Wall -Wextra -std=gnu++20" CACHE STRING "CXX_FLAGS" FORCE) set(CMAKE_CXX_FLAGS_DEBUG "-O0 -fno-inline -g3" From a7e5eb15841a31bd024fe51e88c27bd74648fd10 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 7 Dec 2025 15:49:35 -0500 Subject: [PATCH 02/19] Fix realpath as it might not exist yet Use string manip to normalize the relative build path. --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7337cf2..5a7c9c6 100755 --- a/Makefile +++ b/Makefile @@ -34,7 +34,9 @@ else endif -_build_path?=$(shell realpath --relative-to=$(CURDIR) $(_build_dir)/$(_build_name)) +_build_path?=$(_build_dir)/$(_build_name) +_build_path:=$(subst //,/,$(_build_path)) +_build_path:=$(patsubst %/,%,$(_build_path)) define run_cmake = cmake \ From ca440a962a56dc4dd14cca57fb065e89ab189bea Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 7 Dec 2025 15:50:59 -0500 Subject: [PATCH 03/19] Squashed 'infra/' content from commit 68ad626 git-subtree-dir: infra git-subtree-split: 68ad626d1974ee05145ad0d475bd4c25946dda61 --- .github/CODEOWNERS | 1 + .github/workflows/beman-submodule.yml | 32 ++ .github/workflows/pre-commit.yml | 78 +++ ...reusable-beman-create-issue-when-fault.yml | 28 + .gitignore | 59 ++ .pre-commit-config.yaml | 21 + LICENSE | 219 +++++++ README.md | 55 ++ cmake/appleclang-toolchain.cmake | 44 ++ cmake/beman-install-library-config.cmake | 169 ++++++ cmake/gnu-toolchain.cmake | 41 ++ cmake/llvm-libc++-toolchain.cmake | 20 + cmake/llvm-toolchain.cmake | 41 ++ cmake/msvc-toolchain.cmake | 41 ++ cmake/use-fetch-content.cmake | 187 ++++++ tools/beman-submodule/README.md | 63 ++ tools/beman-submodule/beman-submodule | 260 +++++++++ .../test/test_beman_submodule.py | 539 ++++++++++++++++++ 18 files changed, 1898 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/beman-submodule.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/reusable-beman-create-issue-when-fault.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/appleclang-toolchain.cmake create mode 100644 cmake/beman-install-library-config.cmake create mode 100644 cmake/gnu-toolchain.cmake create mode 100644 cmake/llvm-libc++-toolchain.cmake create mode 100644 cmake/llvm-toolchain.cmake create mode 100644 cmake/msvc-toolchain.cmake create mode 100644 cmake/use-fetch-content.cmake create mode 100644 tools/beman-submodule/README.md create mode 100755 tools/beman-submodule/beman-submodule create mode 100644 tools/beman-submodule/test/test_beman_submodule.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4ff90a4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ednolan @neatudarius @rishyak @wusatosi @JeffGarland diff --git a/.github/workflows/beman-submodule.yml b/.github/workflows/beman-submodule.yml new file mode 100644 index 0000000..8435086 --- /dev/null +++ b/.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/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..9646831 --- /dev/null +++ b/.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/.github/workflows/reusable-beman-create-issue-when-fault.yml b/.github/workflows/reusable-beman-create-issue-when-fault.yml new file mode 100644 index 0000000..024a51f --- /dev/null +++ b/.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/.gitignore b/.gitignore new file mode 100644 index 0000000..b7cdbb5 --- /dev/null +++ b/.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/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bc4dd84 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6db814 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..16b2672 --- /dev/null +++ b/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/cmake/appleclang-toolchain.cmake b/cmake/appleclang-toolchain.cmake new file mode 100644 index 0000000..70ef548 --- /dev/null +++ b/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/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/gnu-toolchain.cmake b/cmake/gnu-toolchain.cmake new file mode 100644 index 0000000..d3b9f92 --- /dev/null +++ b/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/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/cmake/llvm-toolchain.cmake b/cmake/llvm-toolchain.cmake new file mode 100644 index 0000000..f1623b7 --- /dev/null +++ b/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/cmake/msvc-toolchain.cmake b/cmake/msvc-toolchain.cmake new file mode 100644 index 0000000..bdc24de --- /dev/null +++ b/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/cmake/use-fetch-content.cmake b/cmake/use-fetch-content.cmake new file mode 100644 index 0000000..4ed4839 --- /dev/null +++ b/cmake/use-fetch-content.cmake @@ -0,0 +1,187 @@ +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/tools/beman-submodule/README.md b/tools/beman-submodule/README.md new file mode 100644 index 0000000..36883ad --- /dev/null +++ b/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/tools/beman-submodule/beman-submodule b/tools/beman-submodule/beman-submodule new file mode 100755 index 0000000..66cb96e --- /dev/null +++ b/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/tools/beman-submodule/test/test_beman_submodule.py b/tools/beman-submodule/test/test_beman_submodule.py new file mode 100644 index 0000000..600fc07 --- /dev/null +++ b/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 fbcef488dab8197d9a0cc3b775b538b622d3ae89 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sun, 7 Dec 2025 15:51:41 -0500 Subject: [PATCH 04/19] Scripts to add and update infra Import the beman infra project as a subtree and update. --- scripts/install-infra.sh | 10 ++++++++++ scripts/update-infra.sh | 9 +++++++++ 2 files changed, 19 insertions(+) create mode 100755 scripts/install-infra.sh create mode 100755 scripts/update-infra.sh diff --git a/scripts/install-infra.sh b/scripts/install-infra.sh new file mode 100755 index 0000000..ab66841 --- /dev/null +++ b/scripts/install-infra.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# scripts/install-infra.sh -*-shell-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# Install beman infra subtree + +git remote add -f bemanproject-infra https://github.com/bemanproject/infra.git +git fetch bemanproject-infra main +git subtree add -P infra bemanproject-infra main --squash diff --git a/scripts/update-infra.sh b/scripts/update-infra.sh new file mode 100755 index 0000000..bfd6375 --- /dev/null +++ b/scripts/update-infra.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# scripts/install-infra.sh -*-shell-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +# Install beman infra subtree + +git fetch bemanproject-infra main +git subtree pull --prefix=infra bemanproject-infra main --squash From dcac3be0f380db56cc79e8821bdf52d569a2552d Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Mon, 8 Dec 2025 09:25:36 -0500 Subject: [PATCH 05/19] Use beman infra for installation Use beman-install-library from beman/infra --- CMakeLists.txt | 53 ++++++++++--------- Makefile | 3 +- README.md | 2 +- include/beman/optional/CMakeLists.txt | 48 ++++++++--------- .../cmake/beman-install-library-config.cmake | 5 +- tests/beman/optional/CMakeLists.txt | 12 ++--- 6 files changed, 65 insertions(+), 58 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e355ee..d2438cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.27) -project(beman_optional VERSION 0.0.0 LANGUAGES CXX) +project(beman.optional VERSION 0.0.0 LANGUAGES CXX) # Includes include(CTest) @@ -20,21 +20,21 @@ option( set(CMAKE_VERIFY_INTERFACE_HEADER_SETS ON) -# Create the library target and named header set for beman_optional -add_library(beman_optional INTERFACE) +# Create the library target and named header set for beman.optional +add_library(beman.optional INTERFACE) target_sources( - beman_optional - PUBLIC FILE_SET beman_optional_headers TYPE HEADERS BASE_DIRS include + beman.optional + PUBLIC FILE_SET HEADERS TYPE HEADERS BASE_DIRS include ) if(OPTIONAL_ENABLE_TESTING) find_package(GTest QUIET) if(GTest_FOUND) - # Create the library target and named header set for testing beman_optional + # Create the library target and named header set for testing beman.optional # and mark the set private - add_executable(beman_optional_test) + add_executable(beman.optional.test) target_sources( - beman_optional_test + beman.optional.test PRIVATE FILE_SET beman_optional_test_headers TYPE HEADERS @@ -57,24 +57,27 @@ add_subdirectory(include/beman/optional) add_subdirectory(examples) -include(CMakePackageConfigHelpers) - -# This will be used to replace @PACKAGE_cmakeModulesDir@ -set(cmakeModulesDir cmake/beman) -configure_package_config_file( - cmake/Config.cmake.in - BemanOptionalConfig.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ - PATH_VARS cmakeModulesDir - NO_SET_AND_CHECK_MACRO - NO_CHECK_REQUIRED_COMPONENTS_MACRO -) +find_package(beman-install-library REQUIRED) +beman_install_library(beman.optional) -install( - FILES ${CMAKE_CURRENT_BINARY_DIR}/BemanOptionalConfig.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ - COMPONENT beman_optional_development -) +# include(CMakePackageConfigHelpers) + +# # This will be used to replace @PACKAGE_cmakeModulesDir@ +# set(cmakeModulesDir cmake/beman) +# configure_package_config_file( +# cmake/Config.cmake.in +# BemanOptionalConfig.cmake +# INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ +# PATH_VARS cmakeModulesDir +# NO_SET_AND_CHECK_MACRO +# NO_CHECK_REQUIRED_COMPONENTS_MACRO +# ) + +# install( +# FILES ${CMAKE_CURRENT_BINARY_DIR}/BemanOptionalConfig.cmake +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ +# COMPONENT beman.optional_development +# ) # Coverage configure_file("cmake/gcovr.cfg.in" gcovr.cfg @ONLY) diff --git a/Makefile b/Makefile index 5a7c9c6..bcbd968 100755 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ define run_cmake = -DCMAKE_CONFIGURATION_TYPES=$(_configuration_types) \ -DCMAKE_INSTALL_PREFIX=$(abspath $(INSTALL_PREFIX)) \ -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \ + -DCMAKE_PREFIX_PATH=$(CURDIR)/infra/cmake \ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="./cmake/use-fetch-content.cmake" \ $(_cmake_args) \ $(CURDIR) @@ -75,7 +76,7 @@ compile-headers: $(_build_path)/CMakeCache.txt ## Compile the headers cmake --build $(_build_path) --config $(CONFIG) --target all_verify_interface_header_sets -- -k 0 install: $(_build_path)/CMakeCache.txt compile ## Install the project - cmake --install $(_build_path) --config $(CONFIG) --component beman_optional_development --verbose + cmake --install $(_build_path) --config $(CONFIG) --component beman.optional --verbose ctest: $(_build_path)/CMakeCache.txt ## Run CTest on current build cd $(_build_path) && ctest --output-on-failure -C $(CONFIG) diff --git a/README.md b/README.md index bdcf1f9..9dddc7c 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Full set of supported toolchains can be found in [.github/workflows/ci.yml](.git #### Preset CMake Flows -This project strives to be as normal and simple a CMake project as possible. This build workflow in particular will work, producing a static `beman_optional` library, ready to package: +This project strives to be as normal and simple a CMake project as possible. This build workflow in particular will work, producing a static `beman.optional` library, ready to package: ```shell # List available preset configurations: diff --git a/include/beman/optional/CMakeLists.txt b/include/beman/optional/CMakeLists.txt index 67dd817..89145a9 100644 --- a/include/beman/optional/CMakeLists.txt +++ b/include/beman/optional/CMakeLists.txt @@ -2,9 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception target_sources( - beman_optional + beman.optional PUBLIC - FILE_SET beman_optional_headers + FILE_SET HEADERS TYPE HEADERS FILES optional.hpp @@ -14,27 +14,27 @@ target_sources( detail/stl_interfaces/iterator_interface.hpp ) -install( - TARGETS beman_optional - FILE_SET beman_optional_headers - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT beman_optional_development -) +# install( +# TARGETS beman.optional +# FILE_SET beman.optional_headers +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +# COMPONENT beman.optional_development +# ) -install( - TARGETS beman_optional - EXPORT beman_optional_export - DESTINATION ${CMAKE_INSTALL_LIBDIR} - FILE_SET beman_optional_headers - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT beman_optional_development -) +# install( +# TARGETS beman.optional +# EXPORT beman.optional_export +# DESTINATION ${CMAKE_INSTALL_LIBDIR} +# FILE_SET beman.optional_headers +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +# COMPONENT beman.optional_development +# ) -install( - EXPORT beman_optional_export - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ - NAMESPACE Beman::Optional:: - FILE beman_optional.cmake - EXPORT_LINK_INTERFACE_LIBRARIES - COMPONENT beman_optional_development -) +# install( +# EXPORT beman.optional_export +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ +# NAMESPACE Beman::Optional:: +# FILE beman.optional.cmake +# EXPORT_LINK_INTERFACE_LIBRARIES +# COMPONENT beman.optional_development +# ) diff --git a/infra/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake index e7fd0ad..b9f3a21 100644 --- a/infra/cmake/beman-install-library-config.cmake +++ b/infra/cmake/beman-install-library-config.cmake @@ -121,7 +121,10 @@ function(beman_install_library name) find_file( config_file_template NAMES "${package_name}-config.cmake.in" - PATHS "${CMAKE_CURRENT_SOURCE_DIR}" + PATHS + "${CMAKE_CURRENT_SOURCE_DIR}" + "${PROJECT_SOURCE_DIR}/cmake" + "${CMAKE_SOURCE_DIR}/cmake" NO_DEFAULT_PATH NO_CACHE REQUIRED diff --git a/tests/beman/optional/CMakeLists.txt b/tests/beman/optional/CMakeLists.txt index 71d4b5e..625b00d 100644 --- a/tests/beman/optional/CMakeLists.txt +++ b/tests/beman/optional/CMakeLists.txt @@ -4,7 +4,7 @@ # cmake-format: on target_sources( - beman_optional_test + beman.optional.test PRIVATE optional.test.cpp optional_constexpr.test.cpp @@ -15,7 +15,7 @@ target_sources( ) target_sources( - beman_optional_test + beman.optional.test PRIVATE FILE_SET beman_optional_test_headers TYPE HEADERS @@ -23,8 +23,8 @@ target_sources( ) target_link_libraries( - beman_optional_test - PRIVATE beman_optional GTest::gtest GTest::gtest_main + beman.optional.test + PRIVATE beman.optional GTest::gtest GTest::gtest_main ) # Issue #32: Re-enable ASAN run CI/clang-19. # @@ -32,11 +32,11 @@ target_link_libraries( # platforms. Temporary switch to gtest_add_tests and skip some Asan checks. # Change also applied for CI flows. include(GoogleTest) -gtest_add_tests(TARGET beman_optional_test "" AUTO) +gtest_add_tests(TARGET beman.optional.test "" AUTO) add_library(constructor_fails test_constructor_fail.cpp) -target_link_libraries(constructor_fails PRIVATE beman_optional) +target_link_libraries(constructor_fails PRIVATE beman.optional) set_target_properties( constructor_fails From b5c0a80843132b3395d2b1a8b8e79ca0a23bfb22 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Mon, 8 Dec 2025 09:25:36 -0500 Subject: [PATCH 06/19] Use beman infra for installation Use beman-install-library from beman/infra --- CMakeLists.txt | 53 ++++++++++--------- Makefile | 3 +- README.md | 2 +- cmake/beman.optional-config.cmake.in | 7 +++ include/beman/optional/CMakeLists.txt | 48 ++++++++--------- .../cmake/beman-install-library-config.cmake | 5 +- tests/beman/optional/CMakeLists.txt | 12 ++--- 7 files changed, 72 insertions(+), 58 deletions(-) create mode 100644 cmake/beman.optional-config.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e355ee..d2438cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.27) -project(beman_optional VERSION 0.0.0 LANGUAGES CXX) +project(beman.optional VERSION 0.0.0 LANGUAGES CXX) # Includes include(CTest) @@ -20,21 +20,21 @@ option( set(CMAKE_VERIFY_INTERFACE_HEADER_SETS ON) -# Create the library target and named header set for beman_optional -add_library(beman_optional INTERFACE) +# Create the library target and named header set for beman.optional +add_library(beman.optional INTERFACE) target_sources( - beman_optional - PUBLIC FILE_SET beman_optional_headers TYPE HEADERS BASE_DIRS include + beman.optional + PUBLIC FILE_SET HEADERS TYPE HEADERS BASE_DIRS include ) if(OPTIONAL_ENABLE_TESTING) find_package(GTest QUIET) if(GTest_FOUND) - # Create the library target and named header set for testing beman_optional + # Create the library target and named header set for testing beman.optional # and mark the set private - add_executable(beman_optional_test) + add_executable(beman.optional.test) target_sources( - beman_optional_test + beman.optional.test PRIVATE FILE_SET beman_optional_test_headers TYPE HEADERS @@ -57,24 +57,27 @@ add_subdirectory(include/beman/optional) add_subdirectory(examples) -include(CMakePackageConfigHelpers) - -# This will be used to replace @PACKAGE_cmakeModulesDir@ -set(cmakeModulesDir cmake/beman) -configure_package_config_file( - cmake/Config.cmake.in - BemanOptionalConfig.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ - PATH_VARS cmakeModulesDir - NO_SET_AND_CHECK_MACRO - NO_CHECK_REQUIRED_COMPONENTS_MACRO -) +find_package(beman-install-library REQUIRED) +beman_install_library(beman.optional) -install( - FILES ${CMAKE_CURRENT_BINARY_DIR}/BemanOptionalConfig.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ - COMPONENT beman_optional_development -) +# include(CMakePackageConfigHelpers) + +# # This will be used to replace @PACKAGE_cmakeModulesDir@ +# set(cmakeModulesDir cmake/beman) +# configure_package_config_file( +# cmake/Config.cmake.in +# BemanOptionalConfig.cmake +# INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ +# PATH_VARS cmakeModulesDir +# NO_SET_AND_CHECK_MACRO +# NO_CHECK_REQUIRED_COMPONENTS_MACRO +# ) + +# install( +# FILES ${CMAKE_CURRENT_BINARY_DIR}/BemanOptionalConfig.cmake +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ +# COMPONENT beman.optional_development +# ) # Coverage configure_file("cmake/gcovr.cfg.in" gcovr.cfg @ONLY) diff --git a/Makefile b/Makefile index 5a7c9c6..bcbd968 100755 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ define run_cmake = -DCMAKE_CONFIGURATION_TYPES=$(_configuration_types) \ -DCMAKE_INSTALL_PREFIX=$(abspath $(INSTALL_PREFIX)) \ -DCMAKE_EXPORT_COMPILE_COMMANDS=1 \ + -DCMAKE_PREFIX_PATH=$(CURDIR)/infra/cmake \ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="./cmake/use-fetch-content.cmake" \ $(_cmake_args) \ $(CURDIR) @@ -75,7 +76,7 @@ compile-headers: $(_build_path)/CMakeCache.txt ## Compile the headers cmake --build $(_build_path) --config $(CONFIG) --target all_verify_interface_header_sets -- -k 0 install: $(_build_path)/CMakeCache.txt compile ## Install the project - cmake --install $(_build_path) --config $(CONFIG) --component beman_optional_development --verbose + cmake --install $(_build_path) --config $(CONFIG) --component beman.optional --verbose ctest: $(_build_path)/CMakeCache.txt ## Run CTest on current build cd $(_build_path) && ctest --output-on-failure -C $(CONFIG) diff --git a/README.md b/README.md index bdcf1f9..9dddc7c 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Full set of supported toolchains can be found in [.github/workflows/ci.yml](.git #### Preset CMake Flows -This project strives to be as normal and simple a CMake project as possible. This build workflow in particular will work, producing a static `beman_optional` library, ready to package: +This project strives to be as normal and simple a CMake project as possible. This build workflow in particular will work, producing a static `beman.optional` library, ready to package: ```shell # List available preset configurations: diff --git a/cmake/beman.optional-config.cmake.in b/cmake/beman.optional-config.cmake.in new file mode 100644 index 0000000..a595128 --- /dev/null +++ b/cmake/beman.optional-config.cmake.in @@ -0,0 +1,7 @@ +set(BEMAN_OPTIONAL_VERSION @PROJECT_VERSION@) + +@PACKAGE_INIT@ + +include(${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake) + +check_required_components(@PROJECT_NAME@) diff --git a/include/beman/optional/CMakeLists.txt b/include/beman/optional/CMakeLists.txt index 67dd817..89145a9 100644 --- a/include/beman/optional/CMakeLists.txt +++ b/include/beman/optional/CMakeLists.txt @@ -2,9 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception target_sources( - beman_optional + beman.optional PUBLIC - FILE_SET beman_optional_headers + FILE_SET HEADERS TYPE HEADERS FILES optional.hpp @@ -14,27 +14,27 @@ target_sources( detail/stl_interfaces/iterator_interface.hpp ) -install( - TARGETS beman_optional - FILE_SET beman_optional_headers - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT beman_optional_development -) +# install( +# TARGETS beman.optional +# FILE_SET beman.optional_headers +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +# COMPONENT beman.optional_development +# ) -install( - TARGETS beman_optional - EXPORT beman_optional_export - DESTINATION ${CMAKE_INSTALL_LIBDIR} - FILE_SET beman_optional_headers - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - COMPONENT beman_optional_development -) +# install( +# TARGETS beman.optional +# EXPORT beman.optional_export +# DESTINATION ${CMAKE_INSTALL_LIBDIR} +# FILE_SET beman.optional_headers +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +# COMPONENT beman.optional_development +# ) -install( - EXPORT beman_optional_export - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ - NAMESPACE Beman::Optional:: - FILE beman_optional.cmake - EXPORT_LINK_INTERFACE_LIBRARIES - COMPONENT beman_optional_development -) +# install( +# EXPORT beman.optional_export +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ +# NAMESPACE Beman::Optional:: +# FILE beman.optional.cmake +# EXPORT_LINK_INTERFACE_LIBRARIES +# COMPONENT beman.optional_development +# ) diff --git a/infra/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake index e7fd0ad..b9f3a21 100644 --- a/infra/cmake/beman-install-library-config.cmake +++ b/infra/cmake/beman-install-library-config.cmake @@ -121,7 +121,10 @@ function(beman_install_library name) find_file( config_file_template NAMES "${package_name}-config.cmake.in" - PATHS "${CMAKE_CURRENT_SOURCE_DIR}" + PATHS + "${CMAKE_CURRENT_SOURCE_DIR}" + "${PROJECT_SOURCE_DIR}/cmake" + "${CMAKE_SOURCE_DIR}/cmake" NO_DEFAULT_PATH NO_CACHE REQUIRED diff --git a/tests/beman/optional/CMakeLists.txt b/tests/beman/optional/CMakeLists.txt index 71d4b5e..625b00d 100644 --- a/tests/beman/optional/CMakeLists.txt +++ b/tests/beman/optional/CMakeLists.txt @@ -4,7 +4,7 @@ # cmake-format: on target_sources( - beman_optional_test + beman.optional.test PRIVATE optional.test.cpp optional_constexpr.test.cpp @@ -15,7 +15,7 @@ target_sources( ) target_sources( - beman_optional_test + beman.optional.test PRIVATE FILE_SET beman_optional_test_headers TYPE HEADERS @@ -23,8 +23,8 @@ target_sources( ) target_link_libraries( - beman_optional_test - PRIVATE beman_optional GTest::gtest GTest::gtest_main + beman.optional.test + PRIVATE beman.optional GTest::gtest GTest::gtest_main ) # Issue #32: Re-enable ASAN run CI/clang-19. # @@ -32,11 +32,11 @@ target_link_libraries( # platforms. Temporary switch to gtest_add_tests and skip some Asan checks. # Change also applied for CI flows. include(GoogleTest) -gtest_add_tests(TARGET beman_optional_test "" AUTO) +gtest_add_tests(TARGET beman.optional.test "" AUTO) add_library(constructor_fails test_constructor_fail.cpp) -target_link_libraries(constructor_fails PRIVATE beman_optional) +target_link_libraries(constructor_fails PRIVATE beman.optional) set_target_properties( constructor_fails From 96fff8e08b1dca7cb67bb8e5c213660e0ca826af Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Mon, 8 Dec 2025 22:14:37 -0500 Subject: [PATCH 07/19] Update cmake formatter --- .pre-commit-config.yaml | 6 ++++-- cmake/use-fetch-content.cmake | 27 +++++++++------------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0785b6..9201088 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,14 +13,14 @@ repos: # 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: v21.1.2 + rev: v21.1.7 hooks: - id: clang-format types_or: [c++, c] # CMake linting and formatting - repo: https://github.com/BlankSpruce/gersemi - rev: 0.22.3 + rev: 0.24.0 hooks: - id: gersemi name: CMake linting @@ -44,3 +44,5 @@ repos: (?x)^( papers/.* )$ + +exclude: 'infra/' diff --git a/cmake/use-fetch-content.cmake b/cmake/use-fetch-content.cmake index 4547fb4..bbe7706 100644 --- a/cmake/use-fetch-content.cmake +++ b/cmake/use-fetch-content.cmake @@ -13,8 +13,7 @@ message(TRACE "BemanOptional_projectDir=\"${BemanOptional_projectDir}\"") message(TRACE "BEMAN_OPTIONAL_LOCKFILE=\"${BEMAN_OPTIONAL_LOCKFILE}\"") file( - REAL_PATH - "${BEMAN_OPTIONAL_LOCKFILE}" + REAL_PATH "${BEMAN_OPTIONAL_LOCKFILE}" BemanOptional_lockfile BASE_DIRECTORY "${BemanOptional_projectDir}" EXPAND_TILDE @@ -36,8 +35,7 @@ function(BemanOptional_provideDependency method package_name) # Get the "dependencies" field and store it in BemanOptional_dependenciesObj string( - JSON - BemanOptional_dependenciesObj + JSON BemanOptional_dependenciesObj ERROR_VARIABLE BemanOptional_error GET "${BemanOptional_rootObj}" "dependencies" @@ -48,8 +46,7 @@ function(BemanOptional_provideDependency method package_name) # Get the length of the libraries array and store it in BemanOptional_dependenciesObj string( - JSON - BemanOptional_numDependencies + JSON BemanOptional_numDependencies ERROR_VARIABLE BemanOptional_error LENGTH "${BemanOptional_dependenciesObj}" ) @@ -67,8 +64,7 @@ function(BemanOptional_provideDependency method package_name) # Get the dependency object at BemanOptional_index # and store it in BemanOptional_depObj string( - JSON - BemanOptional_depObj + JSON BemanOptional_depObj ERROR_VARIABLE BemanOptional_error GET "${BemanOptional_dependenciesObj}" "${BemanOptional_index}" @@ -82,8 +78,7 @@ function(BemanOptional_provideDependency method package_name) # Get the "name" field and store it in BemanOptional_name string( - JSON - BemanOptional_name + JSON BemanOptional_name ERROR_VARIABLE BemanOptional_error GET "${BemanOptional_depObj}" "name" @@ -97,8 +92,7 @@ function(BemanOptional_provideDependency method package_name) # Get the "package_name" field and store it in BemanOptional_pkgName string( - JSON - BemanOptional_pkgName + JSON BemanOptional_pkgName ERROR_VARIABLE BemanOptional_error GET "${BemanOptional_depObj}" "package_name" @@ -112,8 +106,7 @@ function(BemanOptional_provideDependency method package_name) # Get the "git_repository" field and store it in BemanOptional_repo string( - JSON - BemanOptional_repo + JSON BemanOptional_repo ERROR_VARIABLE BemanOptional_error GET "${BemanOptional_depObj}" "git_repository" @@ -127,8 +120,7 @@ function(BemanOptional_provideDependency method package_name) # Get the "git_tag" field and store it in BemanOptional_tag string( - JSON - BemanOptional_tag + JSON BemanOptional_tag ERROR_VARIABLE BemanOptional_error GET "${BemanOptional_depObj}" "git_tag" @@ -143,8 +135,7 @@ function(BemanOptional_provideDependency method package_name) if(method STREQUAL "FIND_PACKAGE") if(package_name STREQUAL BemanOptional_pkgName) string( - APPEND - BemanOptional_debug + APPEND BemanOptional_debug "Redirecting find_package calls for ${BemanOptional_pkgName} " "to FetchContent logic fetching ${BemanOptional_repo} at " "${BemanOptional_tag} according to ${BemanOptional_lockfile}." From 4a9220c248fc1d7894b401a38d425fb816658eba Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Mon, 8 Dec 2025 22:29:20 -0500 Subject: [PATCH 08/19] Install with better named header file_set --- CMakeLists.txt | 26 +++--------------- examples/CMakeLists.txt | 8 +++--- include/beman/optional/CMakeLists.txt | 27 +------------------ .../cmake/beman-install-library-config.cmake | 20 +++++++++----- tests/beman/optional/CMakeLists.txt | 11 +------- 5 files changed, 23 insertions(+), 69 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2438cd..d0cc596 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,4 @@ # CMakeLists.txt -*-CMake-*- -# # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception cmake_minimum_required(VERSION 3.27) @@ -24,7 +23,7 @@ set(CMAKE_VERIFY_INTERFACE_HEADER_SETS ON) add_library(beman.optional INTERFACE) target_sources( beman.optional - PUBLIC FILE_SET HEADERS TYPE HEADERS BASE_DIRS include + PUBLIC FILE_SET beman_optional_headers TYPE HEADERS BASE_DIRS include ) if(OPTIONAL_ENABLE_TESTING) @@ -42,6 +41,8 @@ if(OPTIONAL_ENABLE_TESTING) ) # Tests add_subdirectory(tests/beman/optional) + include(GoogleTest) + gtest_discover_tests(beman.optional.test) else() message( WARNING @@ -58,26 +59,7 @@ add_subdirectory(include/beman/optional) add_subdirectory(examples) find_package(beman-install-library REQUIRED) -beman_install_library(beman.optional) - -# include(CMakePackageConfigHelpers) - -# # This will be used to replace @PACKAGE_cmakeModulesDir@ -# set(cmakeModulesDir cmake/beman) -# configure_package_config_file( -# cmake/Config.cmake.in -# BemanOptionalConfig.cmake -# INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ -# PATH_VARS cmakeModulesDir -# NO_SET_AND_CHECK_MACRO -# NO_CHECK_REQUIRED_COMPONENTS_MACRO -# ) - -# install( -# FILES ${CMAKE_CURRENT_BINARY_DIR}/BemanOptionalConfig.cmake -# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ -# COMPONENT beman.optional_development -# ) +beman_install_library(beman.optional FILE_SET beman_optional_headers) # Coverage configure_file("cmake/gcovr.cfg.in" gcovr.cfg @ONLY) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 801b83f..0d25c6e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,9 +1,7 @@ -# cmake-format: off -# examples/CMakeLists.txt -*-makefile-*- +# examples/CMakeLists.txt -*-cmake-*- # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# cmake-format: on -set(BEMAN_OPTIONAL_LIBRARY "beman_optional") +set(BEMAN_OPTIONAL_LIBRARY "beman.optional") include(GNUInstallDirs) @@ -31,7 +29,7 @@ foreach(example ${EXAMPLES}) # Install . install( TARGETS ${example} - COMPONENT beman_optional_examples + COMPONENT beman.optional_examples DESTINATION ${CMAKE_INSTALL_BINDIR} ) endforeach() diff --git a/include/beman/optional/CMakeLists.txt b/include/beman/optional/CMakeLists.txt index 89145a9..6227f00 100644 --- a/include/beman/optional/CMakeLists.txt +++ b/include/beman/optional/CMakeLists.txt @@ -4,7 +4,7 @@ target_sources( beman.optional PUBLIC - FILE_SET HEADERS + FILE_SET beman_optional_headers TYPE HEADERS FILES optional.hpp @@ -13,28 +13,3 @@ target_sources( detail/stl_interfaces/fwd.hpp detail/stl_interfaces/iterator_interface.hpp ) - -# install( -# TARGETS beman.optional -# FILE_SET beman.optional_headers -# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} -# COMPONENT beman.optional_development -# ) - -# install( -# TARGETS beman.optional -# EXPORT beman.optional_export -# DESTINATION ${CMAKE_INSTALL_LIBDIR} -# FILE_SET beman.optional_headers -# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} -# COMPONENT beman.optional_development -# ) - -# install( -# EXPORT beman.optional_export -# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/beman/optional/ -# NAMESPACE Beman::Optional:: -# FILE beman.optional.cmake -# EXPORT_LINK_INTERFACE_LIBRARIES -# COMPONENT beman.optional_development -# ) diff --git a/infra/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake index b9f3a21..fe2e818 100644 --- a/infra/cmake/beman-install-library-config.cmake +++ b/infra/cmake/beman-install-library-config.cmake @@ -36,15 +36,23 @@ function(beman_install_library name) # The prefix `` is the uppercased name of the library with dots # replaced by underscores. # + set(options "") + set(multiValueArgs "") + set(multiValueArgs "FILE_SET") + cmake_parse_arguments( + BEMAN_INSTALL_LIBRARY + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN} + ) + 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}" - ) + if(NOT BEMAN_INSTALL_LIBRARY_FILE_SET) + set(BEMAN_INSTALL_LIBRARY_FILE_SET "HEADERS") endif() # Given foo.bar, the component name is bar @@ -68,7 +76,7 @@ function(beman_install_library name) TARGETS "${target_name}" COMPONENT "${install_component_name}" EXPORT "${export_name}" - FILE_SET HEADERS + FILE_SET "${BEMAN_INSTALL_LIBRARY_FILE_SET}" ) set_target_properties( diff --git a/tests/beman/optional/CMakeLists.txt b/tests/beman/optional/CMakeLists.txt index 625b00d..6723bd3 100644 --- a/tests/beman/optional/CMakeLists.txt +++ b/tests/beman/optional/CMakeLists.txt @@ -1,7 +1,5 @@ -# cmake-format: off -# src/beman/optional/tests/CMakeLists.txt -*-makefile-*- +# tests/beman/optional/CMakeLists.txt -*-cmake-*- # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# cmake-format: on target_sources( beman.optional.test @@ -26,13 +24,6 @@ target_link_libraries( beman.optional.test PRIVATE beman.optional GTest::gtest GTest::gtest_main ) -# Issue #32: Re-enable ASAN run CI/clang-19. -# -# Note: clang-19 + gtest_discover_tests + Asan setup causes errors on some -# platforms. Temporary switch to gtest_add_tests and skip some Asan checks. -# Change also applied for CI flows. -include(GoogleTest) -gtest_add_tests(TARGET beman.optional.test "" AUTO) add_library(constructor_fails test_constructor_fail.cpp) From 07920a88182d34a2087bfe804e12b7b35f877107 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Tue, 9 Dec 2025 07:14:19 -0500 Subject: [PATCH 09/19] Copy github workflows from `exemplar` Copy the workflows from exemplar, before replacing the ci workflow. --- .github/workflows/ci_tests.yml | 140 +++++++++ .github/workflows/pre-commit-check.yml | 13 + .github/workflows/pre-commit-update.yml | 15 + CMakePresets.json | 375 ++++++++++++------------ 4 files changed, 351 insertions(+), 192 deletions(-) create mode 100644 .github/workflows/ci_tests.yml create mode 100644 .github/workflows/pre-commit-check.yml create mode 100644 .github/workflows/pre-commit-update.yml diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml new file mode 100644 index 0000000..4ae60bf --- /dev/null +++ b/.github/workflows/ci_tests.yml @@ -0,0 +1,140 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Continuous Integration Tests + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + schedule: + - cron: '30 15 * * *' + +jobs: + beman-submodule-check: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.1.0 + + 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"} + ] + + 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", "c++17"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + { "versions": ["14", "13"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + }, + { + "versions": ["12", "11"], + "tests": [ + { "cxxversions": ["c++23", "c++20", "c++17"], + "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", "c++17"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["20", "19", "18"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + "tests": [ + {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} + ] + } + ] + }, + { "versions": ["17"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] + }, + { "cxxversions": ["c++20", "c++17"], + "tests": [{"stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] + } + ] + } + ], + "appleclang": [ + { "versions": ["latest"], + "tests": [ + { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + "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: + needs: [preset-test, build-and-test] + if: failure() && github.event_name == 'schedule' + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.1.0 diff --git a/.github/workflows/pre-commit-check.yml b/.github/workflows/pre-commit-check.yml new file mode 100644 index 0000000..70895b4 --- /dev/null +++ b/.github/workflows/pre-commit-check.yml @@ -0,0 +1,13 @@ +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: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-pre-commit.yml@1.1.0 diff --git a/.github/workflows/pre-commit-update.yml b/.github/workflows/pre-commit-update.yml new file mode 100644 index 0000000..ec7ac74 --- /dev/null +++ b/.github/workflows/pre-commit-update.yml @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: Weekly pre-commit autoupdate + +on: + workflow_dispatch: + schedule: + - cron: "0 16 * * 0" + +jobs: + auto-update-pre-commit: + uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.1.0 + secrets: + APP_ID: ${{ secrets.AUTO_PR_BOT_APP_ID }} + PRIVATE_KEY: ${{ secrets.AUTO_PR_BOT_PRIVATE_KEY }} diff --git a/CMakePresets.json b/CMakePresets.json index 11abeb1..483e1a3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,175 +1,188 @@ { "version": 6, - "cmakeMinimumRequired": { - "major": 3, - "minor": 29, - "patch": 2 - }, "configurePresets": [ { - "name": "common", - "description": "General settings that apply to all configurations", + "name": "_root-config", "hidden": true, - "generator": "Ninja Multi-Config", - "binaryDir": "${sourceDir}/.build/${presetName}", - "installDir": "${sourceDir}/.install/${presetName}", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_CONFIGURATION_TYPES": "RelWithDebInfo;Debug;Tsan;Asan" + "CMAKE_CXX_STANDARD": "20", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "./infra/cmake/use-fetch-content.cmake" } }, { - "name": "system", - "inherits": "common", - "displayName": "System compiler", - "description": "Build with default cc and c++ compilers", - "toolchainFile": "${sourceDir}/etc/toolchain.cmake" + "name": "_debug-base", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "BEMAN_BUILDSYS_SANITIZER": "MaxSan" + } }, { - "name": "gcc-14", - "inherits": "common", - "displayName": "GCC 14", - "description": "Build with GCC 14 compilers", - "toolchainFile": "${sourceDir}/etc/gcc-14-toolchain.cmake" + "name": "_release-base", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } }, { - "name": "gcc-13", - "inherits": "common", - "displayName": "GCC 13", - "description": "Build with GCC 13 compilers", - "toolchainFile": "${sourceDir}/etc/gcc-13-toolchain.cmake" + "name": "gcc-debug", + "displayName": "GCC Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" + } + }, + { + "name": "gcc-release", + "displayName": "GCC Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" + } }, { - "name": "gcc-12", - "inherits": "common", - "displayName": "GCC 12", - "description": "Build with GCC 12 compilers", - "toolchainFile": "${sourceDir}/etc/gcc-12-toolchain.cmake" + "name": "llvm-debug", + "displayName": "Clang Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" + } }, { - "name": "clang-20", - "inherits": "common", - "displayName": "Clang 20", - "description": "Build with Clang 20 compilers", - "toolchainFile": "${sourceDir}/etc/clang-20-toolchain.cmake" + "name": "llvm-release", + "displayName": "Clang Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" + } }, { - "name": "clang-19", - "inherits": "common", - "displayName": "Clang 19", - "description": "Build with Clang 19 compilers", - "toolchainFile": "${sourceDir}/etc/clang-19-toolchain.cmake" + "name": "appleclang-debug", + "displayName": "Appleclang Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" + } }, { - "name": "clang-18", - "inherits": "common", - "displayName": "Clang 18", - "description": "Build with Clang 18 compilers", - "toolchainFile": "${sourceDir}/etc/clang-18-toolchain.cmake" + "name": "appleclang-release", + "displayName": "Appleclang Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" + } }, { - "name": "clang-17", - "inherits": "common", - "displayName": "Clang 17", - "description": "Build with Clang 17 compilers", - "toolchainFile": "${sourceDir}/etc/clang-17-toolchain.cmake" + "name": "msvc-debug", + "displayName": "MSVC Debug Build", + "inherits": [ + "_root-config", + "_debug-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" + } }, { - "name": "clang-16", - "inherits": "common", - "displayName": "Clang 16", - "description": "Build with Clang 16 compilers", - "toolchainFile": "${sourceDir}/etc/clang-16-toolchain.cmake" + "name": "msvc-release", + "displayName": "MSVC Release Build", + "inherits": [ + "_root-config", + "_release-base" + ], + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" + } } ], "buildPresets": [ { - "name": "common", + "name": "_root-build", "hidden": true, - "configuration": "Asan", - "targets": [ - "all_verify_interface_header_sets", - "all" - ] + "jobs": 0 }, { - "name": "system", - "inherits": "common", - "configurePreset": "system", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "gcc-debug", + "configurePreset": "gcc-debug", + "inherits": [ + "_root-build" ] }, { - "name": "gcc-14", - "inherits": "common", - "configurePreset": "gcc-14", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "gcc-release", + "configurePreset": "gcc-release", + "inherits": [ + "_root-build" ] }, { - "name": "gcc-13", - "inherits": "common", - "configurePreset": "gcc-13", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "llvm-debug", + "configurePreset": "llvm-debug", + "inherits": [ + "_root-build" ] }, { - "name": "gcc-12", - "inherits": "common", - "configurePreset": "gcc-12" - }, - { - "name": "clang-20", - "inherits": "common", - "configurePreset": "clang-20" + "name": "llvm-release", + "configurePreset": "llvm-release", + "inherits": [ + "_root-build" + ] }, { - "name": "clang-19", - "inherits": "common", - "configurePreset": "clang-19", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "appleclang-debug", + "configurePreset": "appleclang-debug", + "inherits": [ + "_root-build" ] }, { - "name": "clang-18", - "inherits": "common", - "configurePreset": "clang-18", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "appleclang-release", + "configurePreset": "appleclang-release", + "inherits": [ + "_root-build" ] }, { - "name": "clang-17", - "inherits": "common", - "configurePreset": "clang-17", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "msvc-debug", + "configurePreset": "msvc-debug", + "inherits": [ + "_root-build" ] }, { - "name": "clang-16", - "inherits": "common", - "configurePreset": "clang-16", - "targets": [ - "all_verify_interface_header_sets", - "all" + "name": "msvc-release", + "configurePreset": "msvc-release", + "inherits": [ + "_root-build" ] } ], "testPresets": [ { - "name": "common", + "name": "_test_base", "hidden": true, - "configuration": "Asan", "output": { "outputOnFailure": true }, @@ -179,202 +192,180 @@ } }, { - "name": "system", - "inherits": "common", - "configurePreset": "system" + "name": "gcc-debug", + "inherits": "_test_base", + "configurePreset": "gcc-debug" }, { - "name": "gcc-14", - "inherits": "common", - "configurePreset": "gcc-14" + "name": "gcc-release", + "inherits": "_test_base", + "configurePreset": "gcc-release" }, { - "name": "gcc-13", - "inherits": "common", - "configurePreset": "gcc-13" + "name": "llvm-debug", + "inherits": "_test_base", + "configurePreset": "llvm-debug" }, { - "name": "gcc-12", - "inherits": "common", - "configurePreset": "gcc-12" + "name": "llvm-release", + "inherits": "_test_base", + "configurePreset": "llvm-release" }, { - "name": "clang-20", - "inherits": "common", - "configurePreset": "clang-20" + "name": "appleclang-debug", + "inherits": "_test_base", + "configurePreset": "appleclang-debug" }, { - "name": "clang-19", - "inherits": "common", - "configurePreset": "clang-19" + "name": "appleclang-release", + "inherits": "_test_base", + "configurePreset": "appleclang-release" }, { - "name": "clang-18", - "inherits": "common", - "configurePreset": "clang-18" + "name": "msvc-debug", + "inherits": "_test_base", + "configurePreset": "msvc-debug" }, { - "name": "clang-17", - "inherits": "common", - "configurePreset": "clang-17" - }, - { - "name": "clang-16", - "inherits": "common", - "configurePreset": "clang-16" + "name": "msvc-release", + "inherits": "_test_base", + "configurePreset": "msvc-release" } ], "workflowPresets": [ { - "name": "system", - "steps": [ - { - "type": "configure", - "name": "system" - }, - { - "type": "build", - "name": "system" - }, - { - "type": "test", - "name": "system" - } - ] - }, - { - "name": "gcc-14", + "name": "gcc-debug", "steps": [ { "type": "configure", - "name": "gcc-14" + "name": "gcc-debug" }, { "type": "build", - "name": "gcc-14" + "name": "gcc-debug" }, { "type": "test", - "name": "gcc-14" + "name": "gcc-debug" } ] }, { - "name": "gcc-13", + "name": "gcc-release", "steps": [ { "type": "configure", - "name": "gcc-13" + "name": "gcc-release" }, { "type": "build", - "name": "gcc-13" + "name": "gcc-release" }, { "type": "test", - "name": "gcc-13" + "name": "gcc-release" } ] }, { - "name": "gcc-12", + "name": "llvm-debug", "steps": [ { "type": "configure", - "name": "gcc-12" + "name": "llvm-debug" }, { "type": "build", - "name": "gcc-12" + "name": "llvm-debug" }, { "type": "test", - "name": "gcc-12" + "name": "llvm-debug" } ] }, { - "name": "clang-20", + "name": "llvm-release", "steps": [ { "type": "configure", - "name": "clang-20" + "name": "llvm-release" }, { "type": "build", - "name": "clang-20" + "name": "llvm-release" }, { "type": "test", - "name": "clang-20" + "name": "llvm-release" } ] }, { - "name": "clang-19", + "name": "appleclang-debug", "steps": [ { "type": "configure", - "name": "clang-19" + "name": "appleclang-debug" }, { "type": "build", - "name": "clang-19" + "name": "appleclang-debug" }, { "type": "test", - "name": "clang-19" + "name": "appleclang-debug" } ] }, { - "name": "clang-18", + "name": "appleclang-release", "steps": [ { "type": "configure", - "name": "clang-18" + "name": "appleclang-release" }, { "type": "build", - "name": "clang-18" + "name": "appleclang-release" }, { "type": "test", - "name": "clang-18" + "name": "appleclang-release" } ] }, { - "name": "clang-17", + "name": "msvc-debug", "steps": [ { "type": "configure", - "name": "clang-17" + "name": "msvc-debug" }, { "type": "build", - "name": "clang-17" + "name": "msvc-debug" }, { "type": "test", - "name": "clang-17" + "name": "msvc-debug" } ] }, { - "name": "clang-16", + "name": "msvc-release", "steps": [ { "type": "configure", - "name": "clang-16" + "name": "msvc-release" }, { "type": "build", - "name": "clang-16" + "name": "msvc-release" }, { "type": "test", - "name": "clang-16" + "name": "msvc-release" } ] } From 3fa9a289a556d6620a47a4fa8c285c625c8d2302 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Tue, 9 Dec 2025 07:22:10 -0500 Subject: [PATCH 10/19] Add cmake prefix path to old ci Add the cmake prefix path to old CI config. --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfa582b..3fd91d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,15 +19,15 @@ jobs: fail-fast: false matrix: config: - - {name: "Ubuntu Clang 21", tag: "clang:21", toolchain: "clang-21", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" "} - - {name: "Ubuntu Clang 20", tag: "clang:20", toolchain: "clang-20", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" "} + - {name: "Ubuntu Clang 21", tag: "clang:21", toolchain: "clang-21", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" "} + - {name: "Ubuntu Clang 20", tag: "clang:20", toolchain: "clang-20", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" "} # Note: clang-19 + Asan setup causes errors on some platforms. Temporary skip some checks via .asan_options. - - {name: "Ubuntu Clang 19", tag: "clang:19", toolchain: "clang-19", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" ", asan_options: "new_delete_type_mismatch=0"} - - {name: "Ubuntu Clang 18", tag: "clang:18", toolchain: "clang-18", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" "} - - {name: "Ubuntu Clang 17", tag: "clang:17", toolchain: "clang-17", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" "} - - {name: "Ubuntu GCC 14", tag: "gcc:14", toolchain: "gcc-14", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan;Gcov\" ", coverage: true} - - {name: "Ubuntu GCC 13", tag: "gcc:13", toolchain: "gcc-13", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" "} - - {name: "Ubuntu GCC 12", tag: "gcc:12", toolchain: "gcc-12", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" "} + - {name: "Ubuntu Clang 19", tag: "clang:19", toolchain: "clang-19", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" ", asan_options: "new_delete_type_mismatch=0"} + - {name: "Ubuntu Clang 18", tag: "clang:18", toolchain: "clang-18", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" "} + - {name: "Ubuntu Clang 17", tag: "clang:17", toolchain: "clang-17", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" "} + - {name: "Ubuntu GCC 14", tag: "gcc:14", toolchain: "gcc-14", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan;Gcov\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" ", coverage: true} + - {name: "Ubuntu GCC 13", tag: "gcc:13", toolchain: "gcc-13", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" "} + - {name: "Ubuntu GCC 12", tag: "gcc:12", toolchain: "gcc-12", cmake_args: "-G \"Ninja Multi-Config\" -DCMAKE_CONFIGURATION_TYPES=\"RelWithDebInfo;Asan\" -DCMAKE_PREFIX_PATH=\"./infra/cmake\" "} steps: - uses: actions/checkout@v3 with: From fff70367a7d28189d7ba05790bb0cfef693dc0d4 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Fri, 19 Dec 2025 22:13:57 -0500 Subject: [PATCH 11/19] Add clang-22 --- etc/clang-22-toolchain.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 etc/clang-22-toolchain.cmake diff --git a/etc/clang-22-toolchain.cmake b/etc/clang-22-toolchain.cmake new file mode 100644 index 0000000..a0614e1 --- /dev/null +++ b/etc/clang-22-toolchain.cmake @@ -0,0 +1,10 @@ +# etc/clang-22-toolchain.cmake -*-cmake-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang-22) +set(CMAKE_CXX_COMPILER clang++-22) +set(GCOV_EXECUTABLE "llvm-cov-22 gcov" CACHE STRING "GCOV executable" FORCE) + +include("${CMAKE_CURRENT_LIST_DIR}/clang-flags.cmake") From 9dc55db11780db103dfbe76eed03ed085026c253 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 13:11:13 -0500 Subject: [PATCH 12/19] Remove old compilers and standard from CI Clang 17 is too old. C++17 is not supported. --- .github/workflows/ci_tests.yml | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 4ae60bf..e01e64a 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -49,22 +49,22 @@ jobs: } ] }, - { "cxxversions": ["c++23", "c++20", "c++17"], + { "cxxversions": ["c++23", "c++20"], "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] } ] }, { "versions": ["14", "13"], "tests": [ - { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + { "cxxversions": ["c++26", "c++23", "c++20"], "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] } ] }, { - "versions": ["12", "11"], + "versions": ["12"], "tests": [ - { "cxxversions": ["c++23", "c++20", "c++17"], + { "cxxversions": ["c++23", "c++20"], "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] } ] @@ -83,7 +83,7 @@ jobs: } ] }, - { "cxxversions": ["c++23", "c++20", "c++17"], + { "cxxversions": ["c++23", "c++20"], "tests": [ {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} ] @@ -92,28 +92,18 @@ jobs: }, { "versions": ["20", "19", "18"], "tests": [ - { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + { "cxxversions": ["c++26", "c++23", "c++20"], "tests": [ {"stdlibs": ["libstdc++", "libc++"], "tests": ["Release.Default"]} ] } ] - }, - { "versions": ["17"], - "tests": [ - { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], - "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] - }, - { "cxxversions": ["c++20", "c++17"], - "tests": [{"stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] - } - ] } ], "appleclang": [ { "versions": ["latest"], "tests": [ - { "cxxversions": ["c++26", "c++23", "c++20", "c++17"], + { "cxxversions": ["c++26", "c++23", "c++20"], "tests": [{ "stdlibs": ["libc++"], "tests": ["Release.Default"]}] } ] From 17646046b58a21903830f9bb5353525ab770491c Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 14:41:38 -0500 Subject: [PATCH 13/19] Clang now has optional as a range Exclude the example for optional not being a range. --- examples/concept_checks.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/concept_checks.cpp b/examples/concept_checks.cpp index 2a12e03..41016b3 100644 --- a/examples/concept_checks.cpp +++ b/examples/concept_checks.cpp @@ -3,7 +3,7 @@ #include #include - +#include #include namespace test { @@ -38,10 +38,10 @@ struct derived : public base { } // namespace test -const auto test_concepts_disabled = [](auto&& opt) { +const auto test_concepts_disabled = []([[maybe_unused]] auto&& opt) { +#ifndef __cpp_lib_optional_range_support // The optional type is the opt type without the reference. using optional = std::remove_reference_t; - // Check std::ranges concepts not enabled. static_assert(!std::ranges::range); static_assert(!std::ranges::view); @@ -53,6 +53,7 @@ const auto test_concepts_disabled = [](auto&& opt) { static_assert(!std::ranges::viewable_range); static_assert(!std::ranges::random_access_range); static_assert(!std::ranges::sized_range); +#endif }; const auto test_concepts_enabled = [](auto&& opt) { From c70ab50aed5a189d053873c561b4d7ab44fae4c4 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 14:42:35 -0500 Subject: [PATCH 14/19] Include vector when used Clang 21 and libc++ with C++26 don't leak . Include it, as it should be anyway. --- examples/pythagorean_triples.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pythagorean_triples.cpp b/examples/pythagorean_triples.cpp index c5f4c6a..9e92486 100644 --- a/examples/pythagorean_triples.cpp +++ b/examples/pythagorean_triples.cpp @@ -8,6 +8,7 @@ #include #include #include +#include int main() { // Example from P3168R2: generate an infinite sequence of Pythagorean triples. From 4506d1edb10290906999b7777bf3ef56ae7ca0b2 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 14:57:52 -0500 Subject: [PATCH 15/19] Attempt to make MSVC happy MSVC is unhappy with the definition of free swap. --- include/beman/optional/optional.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/beman/optional/optional.hpp b/include/beman/optional/optional.hpp index f08b089..adced83 100644 --- a/include/beman/optional/optional.hpp +++ b/include/beman/optional/optional.hpp @@ -1431,10 +1431,10 @@ constexpr std::compare_three_way_result_t operator<=>(const optional& x // 22.5.9 Specialized algorithms[optional.specalg] template -constexpr void swap(optional& lhs, optional& rhs) noexcept(noexcept(lhs.swap(rhs))) +constexpr void swap(optional& x, optional& y) noexcept(noexcept(x.swap(y))) requires std::is_move_constructible_v && std::is_swappable_v { - return lhs.swap(rhs); + return x.swap(y); } template From f296cfd6f7e31af3eac1e0468cf238e0b9e65dcb Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 15:17:23 -0500 Subject: [PATCH 16/19] MSVC dislikes type and variable name collision Rename "empty" variables. --- tests/beman/optional/optional_ref.test.cpp | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/beman/optional/optional_ref.test.cpp b/tests/beman/optional/optional_ref.test.cpp index 8c02519..42d1ca6 100644 --- a/tests/beman/optional/optional_ref.test.cpp +++ b/tests/beman/optional/optional_ref.test.cpp @@ -55,9 +55,9 @@ TEST(OptionalRefTest, Constructors) { beman::optional::optional b3 = d2; beman::optional::optional b4{d2}; - beman::optional::optional empty; - beman::optional::optional fromEmpty(empty); - beman::optional::optional fromEmpty2 = empty; + beman::optional::optional emptyDerived; + beman::optional::optional fromEmpty(emptyDerived); + beman::optional::optional fromEmpty2 = emptyDerived; /* * template @@ -132,14 +132,14 @@ TEST(OptionalRefTest, Assignment) { EXPECT_TRUE(i2); EXPECT_TRUE(*i2 = 7); - beman::optional::optional empty; - EXPECT_FALSE(empty); - i2 = empty; + beman::optional::optional emptyInt; + EXPECT_FALSE(emptyInt); + i2 = emptyInt; EXPECT_FALSE(i2); int eight = 8; - int& result = empty.emplace(eight); - EXPECT_TRUE(empty); - EXPECT_EQ(empty, 8); + int& result = emptyInt.emplace(eight); + EXPECT_TRUE(emptyInt); + EXPECT_EQ(emptyInt, 8); EXPECT_EQ(&result, &eight); beman::optional::optional o; @@ -182,8 +182,8 @@ TEST(OptionalRefTest, ConstRefAssignment) { i = 5; EXPECT_EQ(*c1, 5); - const beman::optional::optional empty(beman::optional::nullopt); - c1 = empty; + const beman::optional::optional emptyInt(beman::optional::nullopt); + c1 = emptyInt; EXPECT_FALSE(c1); } @@ -199,8 +199,8 @@ TEST(OptionalRefTest, ConvertingConstRvalRef) { i = 5; EXPECT_EQ(*c1, 5); - const beman::optional::optional empty(beman::optional::nullopt); - c1 = std::move(empty); + const beman::optional::optional emptyInt(beman::optional::nullopt); + c1 = std::move(emptyInt); EXPECT_FALSE(c1); } From 8a42e763663d99165f0b12eb350c5264d4f5e3b3 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 15:47:26 -0500 Subject: [PATCH 17/19] Remove tests for MSVC that fail only for MSVC MSVC fails to convert some of the highly constrained test types. --- tests/beman/optional/optional.test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/beman/optional/optional.test.cpp b/tests/beman/optional/optional.test.cpp index c76bb6f..6294a7e 100644 --- a/tests/beman/optional/optional.test.cpp +++ b/tests/beman/optional/optional.test.cpp @@ -1066,6 +1066,7 @@ TEST(OptionalTest, OptionalFromOptionalRef) { ASSERT_TRUE(o2); } +#if !defined(_MSC_VER) // MSVC errors, GCC & Clang do not TEST(OptionalTest, OptionalFromOptionalRefExplicit) { using beman::optional::tests::copyable_from_non_const_lvalue_only; using beman::optional::tests::explicitly_convertible_from_non_const_lvalue_only; @@ -1079,6 +1080,7 @@ TEST(OptionalTest, OptionalFromOptionalRefExplicit) { beman::optional::optional o5(std::move(o3)); ASSERT_TRUE(o5); } +#endif TEST(OptionalTest, OptionalFromOptionalConstRef) { using beman::optional::tests::copyable_from_const_lvalue_only; @@ -1111,6 +1113,7 @@ TEST(OptionalTest, OptionalFromOptionalConstRef) { ASSERT_TRUE(o2); } +#if !defined(_MSC_VER) // MSVC errors, GCC & Clang do not TEST(OptionalTest, OptionalFromOptionalConstRefExplicit) { using beman::optional::tests::copyable_from_const_lvalue_only; using beman::optional::tests::explicitly_convertible_from_const_lvalue_only; @@ -1124,6 +1127,7 @@ TEST(OptionalTest, OptionalFromOptionalConstRefExplicit) { beman::optional::optional o5(std::move(o3)); ASSERT_TRUE(o5); } +#endif TEST(OptionalTest, OptionalOfAnyWorks) { beman::optional::optional o1 = 42; From 73a6c0f8ef344e5c7115cfed65c89b4968d2175c Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 15:55:05 -0500 Subject: [PATCH 18/19] MSVC constexpr failure call to immediate function is not a constant expression --- tests/beman/optional/optional_range_support.test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/beman/optional/optional_range_support.test.cpp b/tests/beman/optional/optional_range_support.test.cpp index e292b08..5429dff 100644 --- a/tests/beman/optional/optional_range_support.test.cpp +++ b/tests/beman/optional/optional_range_support.test.cpp @@ -334,7 +334,9 @@ TEST(RangeSupportTest, LoopOptionalAssignment) { constexpr_expect_eq(opt_int.value(), expected_value); return true; }; +#if !defined(_MSC_VER) // MSVC:call to immediate function is not a constant expression EXPECT_TRUE(constify(lambda())); +#endif EXPECT_TRUE(lambda()); } @@ -381,6 +383,7 @@ TEST(RangeSupportTest, PythagoreanTriples) { // Generate first 10 Pythagorean triples. // https://mathworld.wolfram.com/PythagoreanTriple.html auto&& r = triples | std::views::take(10); +#if !defined(_MSC_VER) // MSVC:call to immediate function is not a constant expression CONSTEXPR_EXPECT_TRUE(std::ranges::equal(r, std::vector{ std::tuple{3, 4, 5}, @@ -394,6 +397,7 @@ TEST(RangeSupportTest, PythagoreanTriples) { std::tuple{10, 24, 26}, std::tuple{20, 21, 29}, })); +#endif } if (!std::is_constant_evaluated()) { // too many steps for the default constant evaluation limit From c26daff9778072db6f16f46d5974076fff691c51 Mon Sep 17 00:00:00 2001 From: Steve Downey Date: Sat, 20 Dec 2025 18:25:10 -0500 Subject: [PATCH 19/19] Workaround MSVC ternary bug MSVC treats the ternary as creating a temporary and value returns garbage for rvalue ref optionals. --- include/beman/optional/optional.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/beman/optional/optional.hpp b/include/beman/optional/optional.hpp index adced83..266a07d 100644 --- a/include/beman/optional/optional.hpp +++ b/include/beman/optional/optional.hpp @@ -1101,7 +1101,14 @@ inline constexpr const T& optional::value() const& { } template inline constexpr T&& optional::value() && { +#if defined(_MSC_VER) // MSVC BUG -- ternary creates temporary + if (has_value()) { + return std::move(value_); + } + throw bad_optional_access(); +#else return has_value() ? std::move(value_) : throw bad_optional_access(); +#endif } /// Returns the contained value if there is one, otherwise returns `u`