Skip to content

Commit a5c5c75

Browse files
committed
ENH: Add migration sentinel mechanism for between-tag downstream gating
Downstream projects build against ITK main between tags, which can be two years apart, and cannot tell whether a specific change is present. Collect one Markdown file per downstream-visible change from CMake/MigrationSentinels into ITK_MIGRATION_SENTINELS and export it through ITKConfig.cmake. Consumers use the list variable rather than a function: CMake's if() cannot invoke a function, and calling one that older ITK does not define is a hard error. An unset variable simply yields false, so an ITK predating this mechanism takes the legacy branch with no guard. The list is exported to CMake only. Embedding it in itkConfigure.h would change every translation unit's preprocessed output on most merges and invalidate ccache fleet-wide; a test asserts it stays out.
1 parent 4213421 commit a5c5c75

14 files changed

Lines changed: 273 additions & 0 deletions

CMake/ITKConfig.cmake.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ set(ITK_VERSION_MAJOR "@ITK_VERSION_MAJOR@")
2626
set(ITK_VERSION_MINOR "@ITK_VERSION_MINOR@")
2727
set(ITK_VERSION_PATCH "@ITK_VERSION_PATCH@")
2828

29+
# Migration sentinels present in this ITK. Each entry asserts that one
30+
# downstream-visible change is in the code. Intended only for fine-grained
31+
# configure-time decisions between tagged releases; see
32+
# Documentation/docs/contributing/migration_sentinels.md
33+
set(ITK_MIGRATION_SENTINELS "@ITK_MIGRATION_SENTINELS@")
34+
2935
# Remove all legacy code completely.
3036
set(ITK_LEGACY_REMOVE "@ITK_LEGACY_REMOVE@")
3137

CMake/MigrationSentinels/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Migration sentinels
2+
3+
Each `ITK_MIGRATION_*.md` file in this directory asserts that one
4+
downstream-visible change is present in this ITK source tree. The filenames are
5+
collected into `ITK_MIGRATION_SENTINELS` at configure time and exported through
6+
`ITKConfig.cmake`, so downstream projects can gate on a specific change during
7+
the interval between ITK tags.
8+
9+
Add one file per downstream-visible change. See
10+
`Documentation/docs/contributing/migration_sentinels.md`.
11+
12+
This file is not a sentinel: the glob matches only `ITK_MIGRATION_*.md`.

CMake/itkMigrationSentinels.cmake

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Migration sentinels: one Markdown file per downstream-visible change.
2+
#
3+
# The resulting list is exported through ITKConfig.cmake ONLY. It must never
4+
# reach itkConfigure.h or any other compiled header: the value changes on most
5+
# merges, and that header is included nearly everywhere, so embedding it would
6+
# invalidate ccache for every translation unit on every build machine.
7+
function(itk_collect_migration_sentinels _sentinel_dir _out_var)
8+
# CONFIGURE_DEPENDS is required during a real configure: without it, adding
9+
# a sentinel file does not re-run CMake and the new sentinel is silently
10+
# missed. It is rejected in -P script mode (CMAKE_SCRIPT_MODE_FILE set),
11+
# which the CollectTest.cmake unit test runs under.
12+
if(CMAKE_SCRIPT_MODE_FILE)
13+
file(GLOB _sentinel_files "${_sentinel_dir}/ITK_MIGRATION_*.md")
14+
else()
15+
file(
16+
GLOB _sentinel_files
17+
CONFIGURE_DEPENDS
18+
"${_sentinel_dir}/ITK_MIGRATION_*.md"
19+
)
20+
endif()
21+
set(_names "")
22+
foreach(_file IN LISTS _sentinel_files)
23+
get_filename_component(_name "${_file}" NAME_WE)
24+
list(APPEND _names "${_name}")
25+
endforeach()
26+
list(SORT _names)
27+
set(${_out_var} "${_names}" PARENT_SCOPE)
28+
endfunction()

CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ set(
5656
ITK_VERSION
5757
"${ITK_VERSION_MAJOR}.${ITK_VERSION_MINOR}.${ITK_VERSION_PATCH}"
5858
)
59+
60+
# Migration sentinels: exported through ITKConfig.cmake only, never into a
61+
# compiled header (see CMake/itkMigrationSentinels.cmake).
62+
include(itkMigrationSentinels)
63+
itk_collect_migration_sentinels(
64+
"${CMAKE_CURRENT_SOURCE_DIR}/CMake/MigrationSentinels"
65+
ITK_MIGRATION_SENTINELS
66+
)
67+
5968
project(
6069
ITK
6170
VERSION ${ITK_VERSION}
@@ -903,6 +912,7 @@ if(BUILD_TESTING)
903912
)
904913
mark_as_advanced(CTEST_COST_DATA_FILE)
905914
add_subdirectory(Utilities/InstallTest)
915+
add_subdirectory(Utilities/MigrationSentinelTest)
906916
endif()
907917

908918
#-----------------------------------------------------------------------------
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
itk_add_test(
2+
NAME MigrationSentinelCollect
3+
COMMAND
4+
${CMAKE_COMMAND}
5+
-DITK_SOURCE_DIR=${ITK_SOURCE_DIR}
6+
-P
7+
${CMAKE_CURRENT_SOURCE_DIR}/CollectTest.cmake
8+
)
9+
10+
itk_add_test(
11+
NAME MigrationSentinelDownstreamIdiom
12+
COMMAND
13+
${CMAKE_COMMAND}
14+
-P
15+
${CMAKE_CURRENT_SOURCE_DIR}/DownstreamIdiomTest.cmake
16+
)
17+
18+
itk_add_test(
19+
NAME MigrationSentinelConfigureHeaderGuard
20+
COMMAND
21+
${CMAKE_COMMAND}
22+
-DITK_CONFIGURE_HEADER=${ITK_BINARY_DIR}/Modules/Core/Common/itkConfigure.h
23+
-P
24+
${CMAKE_CURRENT_SOURCE_DIR}/ConfigureHeaderGuardTest.cmake
25+
)
26+
27+
itk_add_test(
28+
NAME MigrationSentinelConsumer
29+
COMMAND
30+
${CMAKE_COMMAND}
31+
-DCONSUMER_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/Consumer
32+
-DCONSUMER_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}/ConsumerBuild
33+
-DITK_BINARY_DIR=${ITK_BINARY_DIR}
34+
-P
35+
${CMAKE_CURRENT_SOURCE_DIR}/ConsumerTest.cmake
36+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Verifies itk_collect_migration_sentinels() against fixed fixtures.
2+
# Fixtures are used rather than the real sentinel directory so these tests do
3+
# not break when real sentinels are expired at tag time.
4+
cmake_minimum_required(VERSION 3.16)
5+
include("${ITK_SOURCE_DIR}/CMake/itkMigrationSentinels.cmake")
6+
7+
set(_failures 0)
8+
macro(expect_eq _actual _expected _what)
9+
if(NOT "${_actual}" STREQUAL "${_expected}")
10+
message(SEND_ERROR "${_what}: expected [${_expected}] got [${_actual}]")
11+
math(EXPR _failures "${_failures} + 1")
12+
endif()
13+
endmacro()
14+
15+
# Populated fixture: two sentinels, plus a README.md and a stray file that the
16+
# glob must ignore.
17+
itk_collect_migration_sentinels(
18+
"${CMAKE_CURRENT_LIST_DIR}/Fixtures/Populated" _got
19+
)
20+
expect_eq("${_got}" "ITK_MIGRATION_MATH_SVD;ITK_MIGRATION_PR6532"
21+
"populated fixture (sorted, README.md excluded)"
22+
)
23+
24+
# Empty fixture: models ITK immediately after a tag, when all sentinels have
25+
# been expired. Must yield an empty list, not an error.
26+
itk_collect_migration_sentinels(
27+
"${CMAKE_CURRENT_LIST_DIR}/Fixtures/Empty" _got_empty
28+
)
29+
expect_eq("${_got_empty}" "" "empty fixture")
30+
31+
if(_failures GREATER 0)
32+
message(FATAL_ERROR "${_failures} collection check(s) failed")
33+
endif()
34+
message(STATUS "CollectTest: all checks passed")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# ccache guard: the sentinel list must never reach a compiled header.
2+
# If it did, every translation unit's preprocessed output would change on most
3+
# merges and ccache would miss fleet-wide.
4+
cmake_minimum_required(VERSION 3.16)
5+
6+
if(NOT EXISTS "${ITK_CONFIGURE_HEADER}")
7+
message(FATAL_ERROR "itkConfigure.h not found at ${ITK_CONFIGURE_HEADER}")
8+
endif()
9+
10+
file(READ "${ITK_CONFIGURE_HEADER}" _contents)
11+
string(FIND "${_contents}" "ITK_MIGRATION" _pos)
12+
if(NOT _pos EQUAL -1)
13+
message(
14+
FATAL_ERROR
15+
"itkConfigure.h contains 'ITK_MIGRATION'. The migration sentinel list must "
16+
"live only in ITKConfig.cmake; embedding it in a compiled header "
17+
"invalidates ccache for every translation unit on every merge."
18+
)
19+
endif()
20+
message(STATUS "ConfigureHeaderGuardTest: itkConfigure.h is free of sentinels")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Minimal consumer: proves ITKConfig.cmake exports ITK_MIGRATION_SENTINELS.
2+
cmake_minimum_required(VERSION 3.16)
3+
project(MigrationSentinelConsumer NONE)
4+
5+
find_package(ITK REQUIRED)
6+
7+
# DEFINED rather than truthiness: the list is legitimately empty when no
8+
# sentinels are pending, but it must still be exported.
9+
if(NOT DEFINED ITK_MIGRATION_SENTINELS)
10+
message(
11+
FATAL_ERROR
12+
"ITKConfig.cmake did not export ITK_MIGRATION_SENTINELS. Downstream "
13+
"projects cannot gate on migrations without it."
14+
)
15+
endif()
16+
17+
message(
18+
STATUS
19+
"Consumer saw ITK_MIGRATION_SENTINELS=[${ITK_MIGRATION_SENTINELS}]"
20+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Configures the minimal consumer against the ITK build tree.
2+
cmake_minimum_required(VERSION 3.16)
3+
4+
execute_process(
5+
COMMAND
6+
"${CMAKE_COMMAND}" -S "${CONSUMER_SOURCE_DIR}" -B "${CONSUMER_BINARY_DIR}"
7+
-DITK_DIR:PATH=${ITK_BINARY_DIR}
8+
RESULT_VARIABLE _result
9+
OUTPUT_VARIABLE _output
10+
ERROR_VARIABLE _output
11+
)
12+
13+
if(NOT _result EQUAL 0)
14+
message(FATAL_ERROR "consumer configure failed:\n${_output}")
15+
endif()
16+
message(STATUS "ConsumerTest passed:\n${_output}")
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Verifies the two-arm downstream idiom in all four states.
2+
cmake_minimum_required(VERSION 3.16)
3+
4+
set(_failures 0)
5+
macro(expect_branch _cond_result _expected _what)
6+
if(NOT "${_cond_result}" STREQUAL "${_expected}")
7+
message(SEND_ERROR "${_what}: expected ${_expected} got ${_cond_result}")
8+
math(EXPR _failures "${_failures} + 1")
9+
endif()
10+
endmacro()
11+
12+
# State 1: ITK predating the mechanism. ITK_MIGRATION_SENTINELS is never set.
13+
set(ITK_VERSION 6.0.0)
14+
unset(ITK_MIGRATION_SENTINELS)
15+
if(
16+
ITK_VERSION
17+
VERSION_GREATER_EQUAL
18+
6.1.0
19+
OR
20+
ITK_MIGRATION_PR6532
21+
IN_LIST
22+
ITK_MIGRATION_SENTINELS
23+
)
24+
set(_r TRUE)
25+
else()
26+
set(_r FALSE)
27+
endif()
28+
expect_branch("${_r}" "FALSE" "old ITK, list unset -> legacy branch")
29+
30+
# State 2: sentinel present, version still below the future tag.
31+
set(
32+
ITK_MIGRATION_SENTINELS
33+
ITK_MIGRATION_MATH_SVD
34+
ITK_MIGRATION_PR6532
35+
)
36+
if(
37+
ITK_VERSION
38+
VERSION_GREATER_EQUAL
39+
6.1.0
40+
OR
41+
ITK_MIGRATION_PR6532
42+
IN_LIST
43+
ITK_MIGRATION_SENTINELS
44+
)
45+
set(_r TRUE)
46+
else()
47+
set(_r FALSE)
48+
endif()
49+
expect_branch("${_r}" "TRUE" "sentinel present -> new branch")
50+
51+
# State 3: mechanism present but this sentinel absent.
52+
if(
53+
ITK_VERSION
54+
VERSION_GREATER_EQUAL
55+
6.1.0
56+
OR
57+
ITK_MIGRATION_PR9999
58+
IN_LIST
59+
ITK_MIGRATION_SENTINELS
60+
)
61+
set(_r TRUE)
62+
else()
63+
set(_r FALSE)
64+
endif()
65+
expect_branch("${_r}" "FALSE" "sentinel absent -> legacy branch")
66+
67+
# State 4: after the tag. Sentinels expired and removed, version arm carries it.
68+
set(ITK_VERSION 6.1.0)
69+
unset(ITK_MIGRATION_SENTINELS)
70+
if(
71+
ITK_VERSION
72+
VERSION_GREATER_EQUAL
73+
6.1.0
74+
OR
75+
ITK_MIGRATION_PR6532
76+
IN_LIST
77+
ITK_MIGRATION_SENTINELS
78+
)
79+
set(_r TRUE)
80+
else()
81+
set(_r FALSE)
82+
endif()
83+
expect_branch("${_r}" "TRUE" "post-tag, sentinels expired -> new branch")
84+
85+
if(_failures GREATER 0)
86+
message(FATAL_ERROR "${_failures} downstream-idiom check(s) failed")
87+
endif()
88+
message(STATUS "DownstreamIdiomTest: all checks passed")

0 commit comments

Comments
 (0)