Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Kitware
GitHub Repository: Kitware/CMake
Path: blob/master/Utilities/Sphinx/update_versions.py
3148 views
1
#!/usr/bin/env python3
2
"""
3
This script inserts "versionadded" directives into .rst documents found in the
4
Help/ directory and module documentation comments found in the Modules/
5
directory. It can be run from any directory within the CMake repository.
6
7
Each file is assigned a CMake version in which it first appears,
8
according to the git version tags.
9
10
Options:
11
12
--overwrite Replace existing "versionadded" directives.
13
Default: existing directives are left unchanged.
14
15
--baseline Files present in this tag don't need a version directive.
16
Default: v3.0.0
17
18
--since Files present in this tag will be ignored.
19
Only newer files will be operated on.
20
Default: v3.0.0
21
22
--next-version The next CMake version, which hasn't been tagged yet.
23
Default: extracted from Source/CMakeVersion.cmake
24
"""
25
import re
26
import pathlib
27
import subprocess
28
import argparse
29
30
tag_re = re.compile(r'^v[34]\.(\d+)\.(\d+)(?:-rc(\d+))?$')
31
path_re = re.compile(r'Help/(?!dev|guide|manual|cpack_|release).*\.rst|Modules/[^/]*\.cmake$')
32
33
def git_root():
34
"""Return the root of the .git repository from the current directory."""
35
result = subprocess.run(
36
['git', 'rev-parse', '--show-toplevel'], check=True, universal_newlines=True, capture_output=True)
37
return pathlib.Path(result.stdout.strip())
38
39
def git_tags():
40
"""Return a list of CMake version tags from the repository."""
41
result = subprocess.run(['git', 'tag'], check=True, universal_newlines=True, capture_output=True)
42
return [tag for tag in result.stdout.splitlines() if tag_re.match(tag)]
43
44
def git_list_tree(ref):
45
"""Return a list of help and module files in a given git reference."""
46
result = subprocess.run(
47
['git', 'ls-tree', '-r', '--full-name', '--name-only', ref, ':/'],
48
check=True, universal_newlines=True, capture_output=True)
49
return [path for path in result.stdout.splitlines() if path_re.match(path)]
50
51
def tag_version(tag):
52
"""Extract a clean CMake version from a git version tag."""
53
return re.sub(r'^v|\.0(-rc\d+)?$', '', tag)
54
55
def tag_sortkey(tag):
56
"""Sorting key for a git version tag."""
57
return tuple(int(part or '1000') for part in tag_re.match(tag).groups())
58
59
def make_version_map(baseline, since, next_version):
60
"""Map repository file paths to CMake versions in which they first appear."""
61
versions = {}
62
if next_version:
63
for path in git_list_tree('HEAD'):
64
versions[path] = next_version
65
for tag in sorted(git_tags(), key=tag_sortkey, reverse=True):
66
version = tag_version(tag)
67
for path in git_list_tree(tag):
68
versions[path] = version
69
if baseline:
70
for path in git_list_tree(baseline):
71
versions[path] = None
72
if since:
73
for path in git_list_tree(since):
74
versions.pop(path, None)
75
return versions
76
77
cmake_version_re = re.compile(
78
rb'set\(CMake_VERSION_MAJOR\s+(\d+)\)\s+set\(CMake_VERSION_MINOR\s+(\d+)\)\s+set\(CMake_VERSION_PATCH\s+(\d+)\)', re.S)
79
80
def cmake_version(path):
81
"""Extract the current MAJOR.MINOR CMake version from CMakeVersion.cmake found at `path`."""
82
match = cmake_version_re.search(path.read_bytes())
83
major, minor, patch = map(int, match.groups())
84
minor += patch > 20000000 # nightly version will become the next minor
85
return f'{major}.{minor}'
86
87
stamp_re = re.compile(
88
rb'(?P<PREFIX>(^|\[\.rst:\r?\n)[^\r\n]+\r?\n[*^\-=#]+(?P<NL>\r?\n))(?P<STAMP>\s*\.\. versionadded::[^\r\n]*\r?\n)?')
89
stamp_pattern_add = rb'\g<PREFIX>\g<NL>.. versionadded:: VERSION\g<NL>'
90
stamp_pattern_remove = rb'\g<PREFIX>'
91
92
def update_file(path, version, overwrite):
93
try:
94
data = path.read_bytes()
95
except FileNotFoundError as e:
96
return False
97
98
def _replacement(match):
99
if not overwrite and match.start('STAMP') != -1:
100
return match.group()
101
if version:
102
pattern = stamp_pattern_add.replace(b'VERSION', version.encode('utf-8'))
103
else:
104
pattern = stamp_pattern_remove
105
return match.expand(pattern)
106
107
new_data, nrepl = stamp_re.subn(_replacement, data, 1)
108
if nrepl and new_data != data:
109
path.write_bytes(new_data)
110
return True
111
return False
112
113
def update_repo(repo_root, version_map, overwrite):
114
total = 0
115
for path, version in version_map.items():
116
if update_file(repo_root / path, version, overwrite):
117
print(f"Version {version or '<none>':6} for {path}")
118
total += 1
119
print(f"Updated {total} file(s)")
120
121
def main():
122
parser = argparse.ArgumentParser(allow_abbrev=False)
123
parser.add_argument('--overwrite', action='store_true', help="overwrite existing version tags")
124
parser.add_argument('--baseline', metavar='TAG', default='v3.0.0',
125
help="files present in this tag don't need a version directive (default: v3.0.0)")
126
parser.add_argument('--since', metavar='TAG',
127
help="apply changes only to files added after this tag")
128
parser.add_argument('--next-version', metavar='VER',
129
help="version for files not present in any tag (default: from CMakeVersion.cmake)")
130
args = parser.parse_args()
131
132
try:
133
repo_root = git_root()
134
next_version = args.next_version or cmake_version(repo_root / 'Source/CMakeVersion.cmake')
135
version_map = make_version_map(args.baseline, args.since, next_version)
136
update_repo(repo_root, version_map, args.overwrite)
137
except subprocess.CalledProcessError as e:
138
print(f"Command '{' '.join(e.cmd)}' returned code {e.returncode}:\n{e.stderr.strip()}")
139
140
if __name__ == '__main__':
141
main()
142
143