build: require std::format (C++20 <format>) for Python builds; GCC>=13 fast-path#961
Conversation
Cytnx targets C++20 and calls std::format in pybind/symmetry_py.cpp and pybind/unitensor_py.cpp. libstdc++ implements the C++20 <format> library only from GCC 13, so GCC 12 and earlier fail to compile those files with "'format' is not a member of 'std'". Add an early CMake FATAL_ERROR when the GNU host compiler is older than 13, with an actionable message (how to point CMake at g++-13). Only GNU is gated; Clang/AppleClang provide <format> via their own standard library and are validated separately. Split out of #950 (the GCC-floor portion only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request adds a compiler check in CMakeLists.txt to enforce GCC >= 13, ensuring support for C++20's std::format. The reviewer rightly pointed out that gating only GCC is fragile since other compilers like Clang on Linux also rely on libstdc++ and would bypass this check while still failing to compile. They suggested a more robust, compiler-agnostic approach using check_cxx_source_compiles to directly verify support.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13) | ||
| message(FATAL_ERROR | ||
| "GCC >= 13 is required: Cytnx uses std::format (C++20 <format>), which " | ||
| "libstdc++ implements only from GCC 13. Found GCC " | ||
| "${CMAKE_CXX_COMPILER_VERSION} at ${CMAKE_CXX_COMPILER}.\n" | ||
| "Install GCC 13+ and point CMake at it, e.g. " | ||
| "-DCMAKE_CXX_COMPILER=g++-13 (and -DCMAKE_C_COMPILER=gcc-13), " | ||
| "or set the CXX/CC environment variables.") | ||
| endif() |
There was a problem hiding this comment.
Gating only GCC by version is fragile because other compilers (such as Clang on Linux) use libstdc++ by default. If the system's libstdc++ is < 13, Clang will still fail to compile std::format but will bypass this check, leading to the exact "confusing mid-build compile error" this PR aims to prevent.
Using check_cxx_source_compiles to directly check for <format> support is the standard, robust, and compiler-agnostic way to verify C++20 std::format support.
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <format>
int main() {
auto s = std::format(\"{}\", 42);
return 0;
}
" HAS_STD_FORMAT)
if(NOT HAS_STD_FORMAT)
message(FATAL_ERROR
"C++20 <format> support (std::format) is required but not found. "
"If you are using GCC, GCC >= 13 is required. If you are using Clang, "
"ensure your standard library (libstdc++ or libc++) supports <format>.")
endif()
There was a problem hiding this comment.
Addressed in 6fb2a26. Replaced the version-only gate with an authoritative check_cxx_source_compiles probe of std::format (compiler-agnostic), keeping the GCC>=13 branch only as a friendlier fast-path message. The probe actually calls std::format (not just #include <format>) so it also catches libc++'s experimental-gated / AppleClang availability-gated cases, and it compiles at C++20 via CMAKE_CXX_STANDARD (CMP0067 NEW) so it doesn't false-negative at the compiler's default C++17.
Posted by Claude Code on behalf of @pcchen
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f74ecc2e1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Enforce a GCC >= 13 floor here with an early, actionable message instead of a | ||
| # confusing mid-build compile error. Only GNU is gated: Clang/AppleClang provide | ||
| # <format> via their own standard library and are validated separately. | ||
| if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13) |
There was a problem hiding this comment.
Gate the GCC floor only for Python builds
When configuring a C++-only build with -DBUILD_PYTHON=OFF, this unconditional check now rejects GCC 12 even though the cited std::format users are only pybind/symmetry_py.cpp and pybind/unitensor_py.cpp, and those sources are added exclusively inside the later IF(BUILD_PYTHON) block. This turns a previously valid C++ package/library build into a configure-time failure; the GCC >= 13 requirement should be tied to BUILD_PYTHON unless another non-pybind source also requires <format>.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 6d43f97. The GCC>=13 fast-path is now wrapped in if(BUILD_PYTHON) — std::format is used only by the pybind layer (pybind/symmetry_py.cpp, pybind/unitensor_py.cpp), so a C++-only build (-DBUILD_PYTHON=OFF) skips the floor. The error message also points at -DBUILD_PYTHON=OFF as an escape hatch.
Posted by Claude Code on behalf of @pcchen
There was a problem hiding this comment.
We may use std::format project-wide, like error message handling, in the future.
Add an authoritative, compiler-agnostic check_cxx_source_compiles probe for std::format at C++20, so any toolchain lacking <format> fails early regardless of compiler/standard-library pairing. The GCC>=13 check is kept only as a friendlier fast-path message. Rationale: std::format's availability is a standard-library property, not a Clang front-end version (Clang may use libc++ or libstdc++), so a Clang version gate would be unreliable. libstdc++ ships it from GCC 13; libc++ from LLVM 17. The probe honors CMAKE_CXX_STANDARD (cmake_minimum_required >= 3.25 => CMP0067 NEW), so it compiles at C++20 rather than the default C++17 (which would false-negative). Verified locally that the probe detects std::format only when compiled at C++20. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6fb2a26816
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| check_cxx_source_compiles( | ||
| "#include <format> | ||
| int main() { return std::format(\"{}\", 42).size() == 2 ? 0 : 1; }" | ||
| CYTNX_HAS_STD_FORMAT) |
There was a problem hiding this comment.
Gate the std::format probe on Python builds
With -DBUILD_PYTHON=OFF on a toolchain that lacks <format> (for example Clang using libstdc++ 12), this unconditional probe now fails configure even though the std::format call sites are only in pybind/symmetry_py.cpp and pybind/unitensor_py.cpp, which are added only inside the later IF(BUILD_PYTHON) block. Fresh evidence beyond the earlier GCC-only comment is that this new compiler-agnostic probe preserves the same C++-only build regression even if the GCC fast-path is guarded.
Useful? React with 👍 / 👎.
|
Good point — and it checks out.
There is no So I agree: the Two caveats worth a maintainer decision:
If we go with gating: I'll wrap the block in Posted by Claude Code on behalf of @pcchen |
std::format is used only by the Python bindings (pybind/symmetry_py.cpp, pybind/unitensor_py.cpp), which compile only when BUILD_PYTHON=ON. The core C++ library (include/, src/) does not use it, so a C++-only build (-DBUILD_PYTHON=OFF) needs just a C++20 compiler, not GCC 13. Wrap the GCC-13 fast-path and the std::format probe in if(BUILD_PYTHON) so the floor applies only to Python builds, and mention -DBUILD_PYTHON=OFF in the error messages as an escape hatch. Addresses review feedback (gate the GCC floor only for Python builds). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6d43f974db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| check_cxx_source_compiles( | ||
| "#include <format> | ||
| int main() { return std::format(\"{}\", 42).size() == 2 ? 0 : 1; }" | ||
| CYTNX_HAS_STD_FORMAT) |
There was a problem hiding this comment.
Probe std::format with the same availability define
On macOS builds that set a deployment target below libc++'s std::format availability, this probe now fails before targets are created, but the real Python sources inherit _LIBCPP_DISABLE_AVAILABILITY from cytnx via target_link_libraries(pycytnx PUBLIC cytnx). Because the check does not use the same compile definition, cibuildwheel-style macOS builds can be rejected at configure time even though the actual pybind translation units would compile with the project's existing target flags.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — confirmed and fixed in 2341283.
Verified the mechanism: cytnx sets target_compile_definitions(cytnx PUBLIC _LIBCPP_DISABLE_AVAILABILITY) (CMakeLists.txt:245) and pycytnx links it PUBLIC (line 415), so the real pybind TUs get the define but the probe did not.
Reproduced locally with a low deployment target:
CMAKE_OSX_DEPLOYMENT_TARGET=12.0 |
probe result |
|---|---|
without _LIBCPP_DISABLE_AVAILABILITY |
fails (would wrongly FATAL_ERROR) |
with _LIBCPP_DISABLE_AVAILABILITY |
passes (matches real build) |
Fix sets CMAKE_REQUIRED_DEFINITIONS=-D_LIBCPP_DISABLE_AVAILABILITY around the check_cxx_source_compiles call (and unsets it after), so the probe compiles the test TU the same way the pybind sources are compiled. It's a no-op off libc++.
Posted by Claude Code on behalf of @pcchen
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #961 +/- ##
=======================================
Coverage 30.87% 30.87%
=======================================
Files 229 229
Lines 34757 34757
Branches 14409 14409
=======================================
Hits 10730 10730
Misses 16720 16720
Partials 7307 7307
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
The real pybind sources compile with _LIBCPP_DISABLE_AVAILABILITY, inherited from the cytnx target (target_compile_definitions(cytnx PUBLIC _LIBCPP_DISABLE_AVAILABILITY) -> pycytnx via PUBLIC link). On macOS/libc++ that disables std::format's deployment-target availability guards. The configure-time probe did not set it, so a low-deployment-target macOS build (e.g. cibuildwheel) could be rejected at configure time even though the actual translation units compile fine. Set CMAKE_REQUIRED_DEFINITIONS=-D_LIBCPP_DISABLE_AVAILABILITY around the check so the probe matches the real build. No-op off libc++. Reproduced locally: with CMAKE_OSX_DEPLOYMENT_TARGET=12.0 the probe fails without the define and passes with it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| # Friendlier fast-path message for the most common miss (old GCC), before the | ||
| # generic probe. | ||
| if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13) | ||
| message(FATAL_ERROR | ||
| "GCC >= 13 is required for the Python bindings (BUILD_PYTHON=ON): Cytnx " | ||
| "uses std::format (C++20 <format>), which libstdc++ implements only from " | ||
| "GCC 13. Found GCC ${CMAKE_CXX_COMPILER_VERSION} at ${CMAKE_CXX_COMPILER}.\n" | ||
| "Install GCC 13+ and point CMake at it, e.g. -DCMAKE_CXX_COMPILER=g++-13 " | ||
| "(and -DCMAKE_C_COMPILER=gcc-13), set the CXX/CC environment variables, or " | ||
| "configure a C++-only build with -DBUILD_PYTHON=OFF.") | ||
| endif() |
There was a problem hiding this comment.
Like what the bot suggested, we don't need version guard if we have feature guard.
The current change is fine, however, changing the whole project to C++20 means we allow developers use C++20 features in the whole project. We can move this guard out of BUILD_PYTHON, so that the usage of std::format outside pybind can also be guarded at the configure time.
| project(CYTNX VERSION ${CYTNX_VERSION} LANGUAGES CXX C) | ||
|
|
||
| # ##################################################################### | ||
| # ## HOST COMPILER REQUIREMENT: C++20 std::format (Python bindings only) |
There was a problem hiding this comment.
This is not python binding only. PairWiseSum uses std::span which is a C++20 feature.
Summary
Enforce, at configure time, that the toolchain can compile
std::format— theonly thing that pins Cytnx's compiler floor — and fail early with an actionable
message instead of a confusing mid-build error. This is split out of #950 (the
host-compiler slice only); the CUDA/cuTENSOR gates, finder fixes, diagnostics, and
docs from #950 will follow as separate PRs.
Why
std::formatis the constraint (explicitly)Cytnx compiles as C++20 (
CMAKE_CXX_STANDARD 20) and usesstd::format— butonly in the Python binding layer:
pybind/symmetry_py.cpp(#include <format>,std::format(...))pybind/unitensor_py.cpp(#include <format>,std::format(...))There is no
std::formatininclude/orsrc/.std::format(the C++20<format>library, P0645) is missing from early C++20-era standard libraries, and the minimum
version differs per implementation — and is not implied by the compiler front-end
version, since Clang can pair with either standard library:
-fexperimental-library; AppleClang additionally has macOS deployment-target availability markers)What this PR does (
CMakeLists.txtonly)check_cxx_source_compilesprobe that actuallycalls
std::format(not just#include <format>), so it catches every toolchainthat lacks a usable
std::formatregardless of compiler/stdlib pairing — includinglibc++'s experimental-gated and AppleClang availability-gated cases. The probe is
compiled at C++20 via
CMAKE_CXX_STANDARD(cmake_minimum_required >= 3.25⇒policy
CMP0067is NEW), so it doesn't false-negative at the compiler's default C++17.GCC < 13branch emits a targeted message (the mostcommon miss) before the generic probe.
BUILD_PYTHON. Becausestd::formatis used only by the pybind layer,the whole requirement lives inside
if(BUILD_PYTHON). A C++-only build(
-DBUILD_PYTHON=OFF) skips the check and builds on any C++20 compiler (e.g. GCC11/12). The error messages point at
-DBUILD_PYTHON=OFFas an escape hatch.If
include//src/ever adoptstd::format, move the guard out of theBUILD_PYTHONblock (noted in a code comment).
Testing
cmakevalidation:standard must be (and is) honored;
BUILD_PYTHON=ONruns the probe,BUILD_PYTHON=OFFskips it;Review points addressed
check_cxx_source_compiles" → feature-check probe (6fb2a268).std::formatprobe only for Python builds" →if(BUILD_PYTHON)guard (6d43f974).Related
🤖 Generated with Claude Code