Skip to content

Commit 6f22e4f

Browse files
authored
Merge pull request #1 from shoom1/develop
Release v0.2.1
2 parents 6c7f303 + 824edb6 commit 6f22e4f

16 files changed

Lines changed: 301 additions & 41 deletions

‎.github/workflows/build-image.yml‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626

2727
steps:
2828
- name: Checkout
29-
uses: actions/checkout@v4
29+
uses: actions/checkout@v5
3030

3131
# GitHub-hosted runners ship with ~14 GB free; LLVM source +
3232
# build + image export easily exceeds that. Free ~25 GB by
@@ -53,6 +53,22 @@ jobs:
5353
username: ${{ github.actor }}
5454
password: ${{ secrets.GITHUB_TOKEN }}
5555

56+
# Resolve the P2996 ref we'll actually build. Default: read the
57+
# pinned ARG P2996_REF from Dockerfile. Override: workflow input.
58+
# The resolved SHA drives the image tag so CI can pin to a
59+
# toolchain-versioned tag instead of :latest.
60+
- name: Resolve P2996 ref
61+
id: p2996
62+
run: |
63+
if [ -n "${{ inputs.p2996_ref }}" ]; then
64+
REF="${{ inputs.p2996_ref }}"
65+
else
66+
REF=$(grep -E '^ARG P2996_REF=' Dockerfile | head -1 | cut -d= -f2)
67+
fi
68+
echo "ref=$REF" >> "$GITHUB_OUTPUT"
69+
echo "short=${REF:0:12}" >> "$GITHUB_OUTPUT"
70+
echo "Building with P2996_REF=$REF (tag suffix: ${REF:0:12})"
71+
5672
- name: Build and push
5773
uses: docker/build-push-action@v5
5874
with:
@@ -62,7 +78,9 @@ jobs:
6278
# CI runners have ~7 GB RAM and 2 CPUs — default COMPILE_JOBS=2,
6379
# LINK_JOBS=1 fits comfortably. Override here if runners change.
6480
build-args: |
65-
${{ inputs.p2996_ref != '' && format('P2996_REF={0}', inputs.p2996_ref) || '' }}
81+
P2996_REF=${{ steps.p2996.outputs.ref }}
82+
# :latest floats and is convenient for ad-hoc pulls.
83+
# :p2996-<short SHA> is the immutable tag CI pins to.
6684
tags: |
6785
ghcr.io/${{ github.repository_owner }}/clang-p2996:latest
68-
ghcr.io/${{ github.repository_owner }}/clang-p2996:${{ github.sha }}
86+
ghcr.io/${{ github.repository_owner }}/clang-p2996:p2996-${{ steps.p2996.outputs.short }}

‎.github/workflows/ci.yml‎

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, develop]
66
pull_request:
77
workflow_dispatch:
88

@@ -15,22 +15,40 @@ permissions:
1515
packages: read
1616

1717
jobs:
18+
# Cheap host-level pre-flight checks. Cumulatively run in seconds and
19+
# gate the heavier container jobs so a misconfiguration fails fast on
20+
# the host instead of after a slow image pull.
21+
preflight:
22+
name: Pre-flight checks
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 2
25+
steps:
26+
- uses: actions/checkout@v5
27+
- name: Verify CI image tag matches Dockerfile pin
28+
run: ./scripts/verify-image-pin.sh
29+
- name: Verify all test files are listed in CMakeLists.txt
30+
run: ./scripts/verify-tests-listed.sh
31+
1832
build-and-test:
1933
name: Build & test (Bloomberg Clang P2996 fork)
2034
runs-on: ubuntu-latest
2135
timeout-minutes: 30
36+
needs: preflight
2237

23-
# Pull the prebuilt P2996 image from GHCR. To rebuild it, run the
24-
# "Build clang-p2996 image" workflow manually (Actions tab).
38+
# Pull the prebuilt P2996 image from GHCR. The :p2996-<short SHA>
39+
# tag is immutable and must match the ARG P2996_REF pin in
40+
# Dockerfile — bumping the P2996 ref is a coordinated change across
41+
# both files plus a workflow-dispatched rebuild of the "Build
42+
# clang-p2996 image" workflow. See Dockerfile for the full procedure.
2543
container:
26-
image: ghcr.io/${{ github.repository_owner }}/clang-p2996:latest
44+
image: ghcr.io/${{ github.repository_owner }}/clang-p2996:p2996-9ffb96e3ce36
2745
credentials:
2846
username: ${{ github.actor }}
2947
password: ${{ secrets.GITHUB_TOKEN }}
3048

3149
steps:
3250
- name: Checkout
33-
uses: actions/checkout@v4
51+
uses: actions/checkout@v5
3452

3553
- name: Configure
3654
run: |
@@ -45,3 +63,50 @@ jobs:
4563

4664
- name: Run tests
4765
run: ctest --test-dir build --output-on-failure
66+
67+
# Verify the installed package is consumable from a fresh build
68+
# directory via find_package(reflect). Catches export-contract bugs
69+
# (e.g. compiler-specific flags leaking through the INTERFACE) that
70+
# in-source-tree tests miss because they don't go through install.
71+
install-smoke-test:
72+
name: Install + consumer smoke test
73+
runs-on: ubuntu-latest
74+
timeout-minutes: 10
75+
needs: preflight
76+
77+
container:
78+
image: ghcr.io/${{ github.repository_owner }}/clang-p2996:p2996-9ffb96e3ce36
79+
credentials:
80+
username: ${{ github.actor }}
81+
password: ${{ secrets.GITHUB_TOKEN }}
82+
83+
steps:
84+
- name: Checkout
85+
uses: actions/checkout@v5
86+
87+
- name: Build & install reflect to temp prefix
88+
run: |
89+
cmake -S . -B build -G Ninja \
90+
-DCMAKE_BUILD_TYPE=Release \
91+
-DCMAKE_CXX_COMPILER=clang++ \
92+
-DCMAKE_CXX_FLAGS="-stdlib=libc++" \
93+
-DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" \
94+
-DCMAKE_INSTALL_PREFIX=/tmp/reflect-install \
95+
-DREFLECT_BUILD_TESTS=OFF \
96+
-DREFLECT_BUILD_EXAMPLES=OFF \
97+
-DREFLECT_BUILD_BENCHMARKS=OFF
98+
cmake --build build --parallel
99+
cmake --install build
100+
101+
- name: Configure & build consumer against installed package
102+
run: |
103+
cmake -S tests/install-smoke-test -B build-consumer -G Ninja \
104+
-DCMAKE_BUILD_TYPE=Release \
105+
-DCMAKE_CXX_COMPILER=clang++ \
106+
-DCMAKE_CXX_FLAGS="-stdlib=libc++" \
107+
-DCMAKE_EXE_LINKER_FLAGS="-stdlib=libc++" \
108+
-DCMAKE_PREFIX_PATH=/tmp/reflect-install
109+
cmake --build build-consumer --parallel
110+
111+
- name: Run consumer
112+
run: ./build-consumer/consumer

‎CMakeLists.txt‎

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required(VERSION 3.20)
22
project(reflect
3-
VERSION 0.2.0
3+
VERSION 0.2.1
44
DESCRIPTION "C++26 reflection utilities — header-only, zero dependencies"
55
LANGUAGES CXX
66
)
@@ -17,16 +17,21 @@ target_include_directories(reflect INTERFACE
1717
# configure error ("cxx_std_26 is not known to CXX compiler").
1818
target_compile_options(reflect INTERFACE -std=c++26)
1919

20-
# Detect compiler and add reflection flags
21-
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
22-
# Bloomberg Clang fork
23-
target_compile_options(reflect INTERFACE -freflection-latest)
24-
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
25-
# GCC 16+ — reflection is enabled automatically with -std=c++26
26-
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "16.0")
27-
message(WARNING "GCC < 16 does not support C++26 reflection. "
28-
"Build from GCC trunk or use Bloomberg Clang fork.")
29-
endif()
20+
# Compiler-specific reflection flag. Wrapped in a generator expression
21+
# so the decision is made at the *consumer's* build time, not when this
22+
# package is configured/installed. Without this, installing after
23+
# configuring with Clang would leak -freflection-latest into a
24+
# downstream GCC consumer's build via the exported INTERFACE.
25+
target_compile_options(reflect INTERFACE
26+
$<$<CXX_COMPILER_ID:Clang,AppleClang>:-freflection-latest>
27+
)
28+
29+
# GCC 16+ enables reflection automatically with -std=c++26. This warning
30+
# fires only at our configure time and is not exported to consumers.
31+
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU"
32+
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "16.0")
33+
message(WARNING "GCC < 16 does not support C++26 reflection. "
34+
"Build from GCC trunk or use Bloomberg Clang fork.")
3035
endif()
3136

3237
if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
@@ -39,9 +44,9 @@ endif()
3944
# Listed explicitly rather than file(GLOB ...): a glob runs at configure
4045
# time, so a new test_xxx.cpp added on disk wouldn't be picked up until
4146
# CMake re-configures. With an explicit list, adding a test is a 20-second
42-
# tax for guaranteed correctness — a missing entry here surfaces as a
43-
# linker/test-discovery failure on the next build, not a silently-skipped
44-
# test.
47+
# tax for guaranteed determinism. The downside — a forgotten entry would
48+
# silently skip the test rather than failing the build — is caught by
49+
# scripts/verify-tests-listed.sh, run as a CI pre-flight step.
4550
set(REFLECT_TEST_SOURCES
4651
tests/test_args.cpp
4752
tests/test_critical_issues.cpp
@@ -60,6 +65,27 @@ if (REFLECT_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests")
6065
target_link_libraries(${test_target} PRIVATE reflect)
6166
add_test(NAME ${test_name} COMMAND ${test_target})
6267
endforeach()
68+
69+
# Compile-failure tests: each source listed here MUST fail to
70+
# compile. The target is EXCLUDE_FROM_ALL (so the main build stays
71+
# green) and ctest invokes cmake --build on it expecting non-zero
72+
# exit (WILL_FAIL TRUE). A successful compile means the guard the
73+
# test exercises has regressed.
74+
set(REFLECT_COMPILE_FAIL_SOURCES
75+
tests/compile_fail_map_string_view.cpp
76+
)
77+
foreach(cf_src ${REFLECT_COMPILE_FAIL_SOURCES})
78+
get_filename_component(cf_name ${cf_src} NAME_WE)
79+
set(cf_target reflect_${cf_name})
80+
add_executable(${cf_target} EXCLUDE_FROM_ALL ${cf_src})
81+
target_link_libraries(${cf_target} PRIVATE reflect)
82+
add_test(
83+
NAME ${cf_name}
84+
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR}
85+
--target ${cf_target} --config $<CONFIG>
86+
)
87+
set_tests_properties(${cf_name} PROPERTIES WILL_FAIL TRUE)
88+
endforeach()
6389
endif()
6490

6591
# Examples

‎Dockerfile‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,30 @@ RUN apt-get update \
4141
zlib1g-dev \
4242
&& rm -rf /var/lib/apt/lists/*
4343

44-
# Pin to a specific commit for reproducible builds. Update intentionally
45-
# by changing this value (and recording why in the commit message).
44+
# Pinned to a specific upstream commit for reproducible builds. Bumping
45+
# this is a deliberate three-step change:
46+
# 1. update the SHA below
47+
# 2. rebuild via the "Build clang-p2996 image" workflow
48+
# (it tags the image :p2996-<short SHA>)
49+
# 3. update the matching tag in .github/workflows/ci.yml
50+
#
4651
# To find the latest commit on the p2996 branch:
4752
# git ls-remote https://github.com/bloomberg/clang-p2996.git refs/heads/p2996
48-
ARG P2996_REF=p2996
53+
ARG P2996_REF=9ffb96e3ce362289008e14ad2a79a249f58aa90a
4954
ARG INSTALL_PREFIX=/opt/clang-p2996
5055
ARG COMPILE_JOBS=2
5156
ARG LINK_JOBS=1
5257

5358
WORKDIR /src
5459

55-
# Shallow clone of the p2996 branch — saves ~80% of clone time/disk vs full
56-
# history. Then check out the specific commit if requested.
60+
# Shallow-clone the p2996 branch tip (~80% smaller than full history),
61+
# then fetch and check out the pinned commit. Direct shallow clone of an
62+
# arbitrary SHA isn't supported by GitHub's smart-HTTP protocol, so the
63+
# branch hop is necessary.
5764
RUN git clone --depth 1 --branch p2996 \
5865
https://github.com/bloomberg/clang-p2996.git . \
59-
&& if [ "${P2996_REF}" != "p2996" ]; then \
60-
git fetch --depth 1 origin "${P2996_REF}" \
61-
&& git checkout "${P2996_REF}"; \
62-
fi
66+
&& git fetch --depth 1 origin "${P2996_REF}" \
67+
&& git checkout "${P2996_REF}"
6368

6469
# Configure: clang frontend only, libc++/libc++abi/libunwind runtimes,
6570
# x86_64 target only, Release mode. This trims the build by an order of

‎include/reflect/args.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <stdexcept>
3434
#include <string>
3535
#include <string_view>
36+
#include <utility>
3637
#include <vector>
3738

3839
namespace reflect {

‎include/reflect/enum.hpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ template <enumeration E>
243243
constexpr std::optional<E> flags_from_string(
244244
std::string_view str, std::string_view separator = "|")
245245
{
246+
// An empty separator would make str.find(separator) return 0 on
247+
// every iteration, which combined with remove_prefix(0) would
248+
// never advance — infinite loop. Reject it explicitly.
249+
if (separator.empty())
250+
return std::nullopt;
251+
246252
using U = std::underlying_type_t<E>;
247253
U combined = 0;
248254

‎include/reflect/json.hpp‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,18 @@ namespace json_detail {
7979
}
8080

8181
// ---- type categories for JSON dispatch ----
82+
// json_map requires an *owning* std::string key. Accepting non-owning
83+
// string-like keys (std::string_view, const char*) would silently
84+
// dangle on deserialization — parse_string() returns a std::string
85+
// by value, and emplacing it into a view-typed key stores a view
86+
// into the temporary. Restricting the concept here keeps serialize
87+
// and deserialize symmetric: both paths require owning keys.
8288
template <typename T>
8389
concept json_map = requires(T t) {
8490
typename T::key_type;
8591
typename T::mapped_type;
8692
{ t.begin() } -> std::input_iterator;
87-
} && reflect::is_string_like<typename T::key_type>;
93+
} && std::same_as<std::remove_cvref_t<typename T::key_type>, std::string>;
8894

8995
template <typename T>
9096
concept json_array = reflect::is_range<T> && !json_map<T>;

‎include/reflect/tuple.hpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ constexpr auto to_ref_tuple(T& obj) {
8080

8181
template <reflectable T, typename Tuple>
8282
constexpr T from_tuple(Tuple const& t) {
83+
// Require exact size match. A shorter tuple would aggregate-initialize
84+
// the leading fields and silently default the rest — surprising for
85+
// a "↔" conversion. A longer tuple is already a hard error from the
86+
// brace-init list, but we catch it here too with a clearer message.
87+
static_assert(
88+
std::tuple_size_v<std::remove_cvref_t<Tuple>>
89+
== detail::tuple_members_of(^^T).size(),
90+
"from_tuple: tuple size must equal the struct's field count.");
8391
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
8492
return T{std::get<Is>(t)...};
8593
}(std::make_index_sequence<std::tuple_size_v<std::remove_cvref_t<Tuple>>>{});

‎scripts/docker-build.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ docker run --rm \
2828
-v "$REPO_ROOT":/work \
2929
-w /work \
3030
"$IMAGE" \
31-
bash -c "cmake --preset $PRESET && cmake --build --preset $PRESET"
31+
bash -c 'cmake --preset "$1" && cmake --build --preset "$1"' bash "$PRESET"

‎scripts/verify-image-pin.sh‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
# Verify that the clang-p2996 image tag pinned in .github/workflows/ci.yml
3+
# matches the upstream P2996 ref pinned in Dockerfile. Catches the human
4+
# error of bumping one without the other (drift between the two files).
5+
set -euo pipefail
6+
7+
DOCKERFILE_REF=$(grep -E '^ARG P2996_REF=' Dockerfile | head -1 | cut -d= -f2)
8+
EXPECTED_TAG="p2996-${DOCKERFILE_REF:0:12}"
9+
10+
# Check every clang-p2996 tag reference in ci.yml — there are multiple
11+
# container jobs, and one could drift while another stays correct.
12+
count=0
13+
bad=0
14+
while IFS= read -r tag; do
15+
count=$((count + 1))
16+
if [ "$tag" != "$EXPECTED_TAG" ]; then
17+
echo "ERROR: ci.yml has clang-p2996:$tag" >&2
18+
bad=1
19+
fi
20+
done < <(grep -oE 'clang-p2996:[A-Za-z0-9._-]+' .github/workflows/ci.yml | cut -d: -f2)
21+
22+
if [ "$count" -eq 0 ]; then
23+
echo "ERROR: no clang-p2996:<tag> references found in ci.yml" >&2
24+
exit 1
25+
fi
26+
27+
if [ "$bad" -ne 0 ]; then
28+
echo " Dockerfile pins P2996_REF=$DOCKERFILE_REF" >&2
29+
echo " expected all ci.yml tags to be: $EXPECTED_TAG" >&2
30+
exit 1
31+
fi
32+
33+
echo "OK: $count ci.yml tag(s) all match Dockerfile P2996_REF '$DOCKERFILE_REF'"

0 commit comments

Comments
 (0)