1
0

update_versions.py 5.6 KB

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