Skip to content

Commit 382f3cc

Browse files
hjmjohnsonblowekamp
andcommitted
ENH: Auto-compute ITK_VERSION_TWEAK from git commit count
Set the fourth (tweak) version component at configure time to the number of commits since the closest v* tag, instead of a hand-edited date. A Python helper computes it from `git describe` in a working tree, and from the git-archive-substituted `.git_archival.txt` in an exported tarball (the setuptools-scm mechanism), falling back to 0 when neither is available. ITK_VERSION_TWEAK and ITK_VERSION_FULL are exported through ITKConfig.cmake for downstream between-tag gating. The value never reaches itkConfigure.h, so the per-commit count cannot invalidate ccache. Supersedes the manual-file approach of #6667. Co-Authored-By: Bradley Lowekamp <321061+blowekamp@users.noreply.github.com>
1 parent 37ecafc commit 382f3cc

8 files changed

Lines changed: 185 additions & 2 deletions

File tree

.git_archival.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node: $Format:%H$
2+
node-date: $Format:%cI$
3+
describe-name: $Format:%(describe:tags=true,match=v[0-9]*)$
4+
ref-names: $Format:%D$

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
.git* export-ignore
22
.hooks* export-ignore
33
.mailmap export-ignore
4+
# Override the .git* export-ignore above: this file MUST ship in archives, and
5+
# git substitutes the describe string into it so tarballs compute the tweak.
6+
.git_archival.txt -export-ignore export-subst
47

58
*.bat -crlf
69
*.bin -crlf

CMake/ITKConfig.cmake.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ set(ITK_REQUIRED_LINK_FLAGS "@ITK_REQUIRED_LINK_FLAGS@")
2525
set(ITK_VERSION_MAJOR "@ITK_VERSION_MAJOR@")
2626
set(ITK_VERSION_MINOR "@ITK_VERSION_MINOR@")
2727
set(ITK_VERSION_PATCH "@ITK_VERSION_PATCH@")
28+
# Tweak is the commit count since the closest v* tag; enables fine-grained
29+
# downstream gating between release tags via ITK_VERSION_FULL.
30+
set(ITK_VERSION_TWEAK "@ITK_VERSION_TWEAK@")
31+
set(
32+
ITK_VERSION_FULL
33+
"@ITK_VERSION_MAJOR@.@ITK_VERSION_MINOR@.@ITK_VERSION_PATCH@.@ITK_VERSION_TWEAK@"
34+
)
2835

2936
# Remove all legacy code completely.
3037
set(ITK_LEGACY_REMOVE "@ITK_LEGACY_REMOVE@")

CMake/ITKConfigVersion.cmake.in

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
set(PACKAGE_VERSION "@ITK_VERSION_MAJOR@.@ITK_VERSION_MINOR@.@ITK_VERSION_PATCH@")
1+
set(
2+
PACKAGE_VERSION
3+
"@ITK_VERSION_MAJOR@.@ITK_VERSION_MINOR@.@ITK_VERSION_PATCH@.@ITK_VERSION_TWEAK@"
4+
)
25
if(NOT "${PACKAGE_FIND_VERSION}" VERSION_GREATER "${PACKAGE_VERSION}")
36
set(PACKAGE_VERSION_COMPATIBLE 1) # compatible with older
4-
if("${PACKAGE_FIND_VERSION}" VERSION_EQUAL "${PACKAGE_VERSION}")
7+
if(
8+
"${PACKAGE_FIND_VERSION}" VERSION_EQUAL "${PACKAGE_VERSION}"
9+
OR "${PACKAGE_FIND_VERSION}" VERSION_EQUAL
10+
"@ITK_VERSION_MAJOR@.@ITK_VERSION_MINOR@.@ITK_VERSION_PATCH@"
11+
)
512
set(PACKAGE_VERSION_EXACT 1) # exact match for this version
613
endif()
714
endif()

CMake/itkVersionTweak.cmake

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Compute ITK_VERSION_TWEAK: commits since the closest v* tag, at configure
2+
# time. The tweak must never reach itkConfigure.h, so a per-commit change of
3+
# this count cannot invalidate ccache.
4+
function(_itk_compute_version_tweak outvar)
5+
set(tweak "")
6+
set(script "${ITK_SOURCE_DIR}/Utilities/Maintenance/ComputeVersionTweak.py")
7+
8+
set(python "${Python3_EXECUTABLE}")
9+
if(NOT python)
10+
find_program(
11+
ITK_TWEAK_Python3_EXECUTABLE
12+
NAMES
13+
python3
14+
python
15+
)
16+
mark_as_advanced(ITK_TWEAK_Python3_EXECUTABLE)
17+
set(python "${ITK_TWEAK_Python3_EXECUTABLE}")
18+
endif()
19+
if(python AND EXISTS "${script}")
20+
execute_process(
21+
COMMAND
22+
"${python}" "${script}" "${ITK_SOURCE_DIR}"
23+
OUTPUT_VARIABLE tweak
24+
OUTPUT_STRIP_TRAILING_WHITESPACE
25+
ERROR_QUIET
26+
)
27+
endif()
28+
29+
if(NOT tweak MATCHES "^[0-9]+$")
30+
find_package(Git QUIET)
31+
if(Git_FOUND)
32+
execute_process(
33+
COMMAND
34+
"${GIT_EXECUTABLE}" -C "${ITK_SOURCE_DIR}" describe --tags --long
35+
--match "v[0-9]*"
36+
OUTPUT_VARIABLE describe
37+
OUTPUT_STRIP_TRAILING_WHITESPACE
38+
ERROR_QUIET
39+
)
40+
if(describe MATCHES "-([0-9]+)-g[0-9a-f]+$")
41+
set(tweak "${CMAKE_MATCH_1}")
42+
endif()
43+
endif()
44+
endif()
45+
46+
if(NOT tweak MATCHES "^[0-9]+$")
47+
set(tweak "0")
48+
endif()
49+
set(${outvar} "${tweak}" PARENT_SCOPE)
50+
endfunction()
51+
52+
_itk_compute_version_tweak(ITK_VERSION_TWEAK)
53+
set(ITK_VERSION_FULL "${ITK_VERSION}.${ITK_VERSION_TWEAK}")

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ project(
6767
C
6868
)
6969

70+
# Fourth (tweak) version component, auto-computed as the commit count since
71+
# the closest v* tag. Enables downstream between-tag gating via
72+
# ITK_VERSION_FULL; see Documentation/docs/migration_guides/index.md.
73+
include(itkVersionTweak)
74+
7075
# Required as a cache variable for FetchContent parent projects can
7176
# resolve ITK's CMake macros and config paths.
7277
set(

Documentation/docs/migration_guides/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
These migration guides explain how to update major versions of ITK, whic may contain breaking changes to the API.
44

5+
## Fine-grained versioning between release tags
6+
7+
ITK's fourth (tweak) version component is the number of commits since the
8+
closest `v*` tag, computed automatically at configure time. It exists so
9+
downstream projects building against untagged ITK commits — for example during
10+
a `git bisect` — can make configure-time decisions at commit granularity
11+
between release tags:
12+
13+
```cmake
14+
find_package(ITK REQUIRED)
15+
if(ITK_VERSION_FULL VERSION_GREATER_EQUAL 6.0.0.6461)
16+
set(BASELINE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Baseline/post-change)
17+
else()
18+
set(BASELINE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Baseline/legacy)
19+
endif()
20+
```
21+
22+
`ITK_VERSION_FULL` and `ITK_VERSION_TWEAK` are exported through
23+
`ITKConfig.cmake`. Against an older ITK that predates the tweak component,
24+
`ITK_VERSION_FULL` is unset and the comparison is false, so the legacy branch
25+
is taken.
26+
27+
The count comes from `git describe` in a working tree, and from the
28+
`git archive`-substituted `.git_archival.txt` in an exported source tarball; it
29+
falls back to `0` only when neither git history nor that substitution is
30+
available. No manual edits are needed — the tweak advances with every commit.
31+
532
```{toctree}
633
:hidden:
734
:maxdepth: 3
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
"""Print ITK_VERSION_TWEAK: commits since the closest ``v*`` tag.
3+
4+
CMake runs this at configure time to set the fourth (tweak) version
5+
component. The count comes from ``git describe`` in a working tree, or from
6+
the ``git archive``-substituted ``.git_archival.txt`` in an exported tarball;
7+
it falls back to ``0`` when neither is available.
8+
"""
9+
10+
import re
11+
import subprocess
12+
import sys
13+
from pathlib import Path
14+
15+
SOURCE_DIR = Path(__file__).resolve().parents[2]
16+
DESCRIBE_COUNT = re.compile(r"-(\d+)-g[0-9a-f]+$")
17+
VERSION_TAG = re.compile(r"^v[0-9]\S*$")
18+
19+
20+
def _count_from_describe(describe: str) -> int | None:
21+
match = DESCRIBE_COUNT.search(describe)
22+
if match:
23+
return int(match.group(1))
24+
if VERSION_TAG.match(describe):
25+
return 0
26+
return None
27+
28+
29+
def from_git(source_dir: Path) -> int | None:
30+
try:
31+
result = subprocess.run(
32+
[
33+
"git",
34+
"-C",
35+
str(source_dir),
36+
"describe",
37+
"--tags",
38+
"--long",
39+
"--match",
40+
"v[0-9]*",
41+
],
42+
capture_output=True,
43+
text=True,
44+
check=True,
45+
)
46+
except (OSError, subprocess.CalledProcessError):
47+
return None
48+
return _count_from_describe(result.stdout.strip())
49+
50+
51+
def from_archival(source_dir: Path) -> int | None:
52+
try:
53+
text = (source_dir / ".git_archival.txt").read_text()
54+
except OSError:
55+
return None
56+
for line in text.splitlines():
57+
if line.startswith("describe-name:"):
58+
value = line.split(":", 1)[1].strip()
59+
if not value or value.startswith("$Format"):
60+
return None
61+
return _count_from_describe(value)
62+
return None
63+
64+
65+
def main() -> int:
66+
source_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else SOURCE_DIR
67+
for compute in (from_git, from_archival):
68+
count = compute(source_dir)
69+
if count is not None:
70+
print(count)
71+
return 0
72+
print(0)
73+
return 0
74+
75+
76+
if __name__ == "__main__":
77+
sys.exit(main())

0 commit comments

Comments
 (0)