File: update_versions.py

package info (click to toggle)
cmake 4.2.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 152,344 kB
  • sloc: ansic: 403,894; cpp: 303,807; sh: 4,097; python: 3,582; yacc: 3,106; lex: 1,279; f90: 538; asm: 471; lisp: 375; cs: 270; java: 266; fortran: 239; objc: 215; perl: 213; xml: 198; makefile: 108; javascript: 83; pascal: 63; tcl: 55; php: 25; ruby: 22
file content (141 lines) | stat: -rwxr-xr-x 5,694 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
"""
This script inserts "versionadded" directives into .rst documents found in the
Help/ directory and module documentation comments found in the Modules/
directory. It can be run from any directory within the CMake repository.

Each file is assigned a CMake version in which it first appears,
according to the git version tags.

Options:

  --overwrite     Replace existing "versionadded" directives.
                  Default: existing directives are left unchanged.

  --baseline      Files present in this tag don't need a version directive.
                  Default: v3.0.0

  --since         Files present in this tag will be ignored.
                  Only newer files will be operated on.
                  Default: v3.0.0

  --next-version  The next CMake version, which hasn't been tagged yet.
                  Default: extracted from Source/CMakeVersion.cmake
"""
import re
import pathlib
import subprocess
import argparse

tag_re = re.compile(r'^v[34]\.(\d+)\.(\d+)(?:-rc(\d+))?$')
path_re = re.compile(r'Help/(?!dev|guide|manual|cpack_|release).*\.rst|Modules/[^/]*\.cmake$')

def git_root():
    """Return the root of the .git repository from the current directory."""
    result = subprocess.run(
        ['git', 'rev-parse', '--show-toplevel'], check=True, universal_newlines=True, capture_output=True)
    return pathlib.Path(result.stdout.strip())

def git_tags():
    """Return a list of CMake version tags from the repository."""
    result = subprocess.run(['git', 'tag'], check=True, universal_newlines=True, capture_output=True)
    return [tag for tag in result.stdout.splitlines() if tag_re.match(tag)]

def git_list_tree(ref):
    """Return a list of help and module files in a given git reference."""
    result = subprocess.run(
        ['git', 'ls-tree', '-r', '--full-name', '--name-only', ref, ':/'],
        check=True, universal_newlines=True, capture_output=True)
    return [path for path in result.stdout.splitlines() if path_re.match(path)]

def tag_version(tag):
    """Extract a clean CMake version from a git version tag."""
    return re.sub(r'^v|\.0(-rc\d+)?$', '', tag)

def tag_sortkey(tag):
    """Sorting key for a git version tag."""
    return tuple(int(part or '1000') for part in tag_re.match(tag).groups())

def make_version_map(baseline, since, next_version):
    """Map repository file paths to CMake versions in which they first appear."""
    versions = {}
    if next_version:
        for path in git_list_tree('HEAD'):
            versions[path] = next_version
    for tag in sorted(git_tags(), key=tag_sortkey, reverse=True):
        version = tag_version(tag)
        for path in git_list_tree(tag):
            versions[path] = version
    if baseline:
        for path in git_list_tree(baseline):
            versions[path] = None
    if since:
        for path in git_list_tree(since):
            versions.pop(path, None)
    return versions

cmake_version_re = re.compile(
    rb'set\(CMake_VERSION_MAJOR\s+(\d+)\)\s+set\(CMake_VERSION_MINOR\s+(\d+)\)\s+set\(CMake_VERSION_PATCH\s+(\d+)\)', re.S)

def cmake_version(path):
    """Extract the current MAJOR.MINOR CMake version from CMakeVersion.cmake found at `path`."""
    match = cmake_version_re.search(path.read_bytes())
    major, minor, patch = map(int, match.groups())
    minor += patch > 20000000  # nightly version will become the next minor
    return f'{major}.{minor}'

stamp_re = re.compile(
    rb'(?P<PREFIX>(^|\[\.rst:\r?\n)[^\r\n]+\r?\n[*^\-=#]+(?P<NL>\r?\n))(?P<STAMP>\s*\.\. versionadded::[^\r\n]*\r?\n)?')
stamp_pattern_add = rb'\g<PREFIX>\g<NL>.. versionadded:: VERSION\g<NL>'
stamp_pattern_remove = rb'\g<PREFIX>'

def update_file(path, version, overwrite):
    try:
        data = path.read_bytes()
    except FileNotFoundError as e:
        return False

    def _replacement(match):
        if not overwrite and match.start('STAMP') != -1:
            return match.group()
        if version:
            pattern = stamp_pattern_add.replace(b'VERSION', version.encode('utf-8'))
        else:
            pattern = stamp_pattern_remove
        return match.expand(pattern)

    new_data, nrepl = stamp_re.subn(_replacement, data, 1)
    if nrepl and new_data != data:
        path.write_bytes(new_data)
        return True
    return False

def update_repo(repo_root, version_map, overwrite):
    total = 0
    for path, version in version_map.items():
        if update_file(repo_root / path, version, overwrite):
            print(f"Version {version or '<none>':6} for {path}")
            total += 1
    print(f"Updated {total} file(s)")

def main():
    parser = argparse.ArgumentParser(allow_abbrev=False)
    parser.add_argument('--overwrite', action='store_true', help="overwrite existing version tags")
    parser.add_argument('--baseline', metavar='TAG', default='v3.0.0',
        help="files present in this tag don't need a version directive (default: v3.0.0)")
    parser.add_argument('--since', metavar='TAG',
        help="apply changes only to files added after this tag")
    parser.add_argument('--next-version', metavar='VER',
        help="version for files not present in any tag (default: from CMakeVersion.cmake)")
    args = parser.parse_args()

    try:
        repo_root = git_root()
        next_version = args.next_version or cmake_version(repo_root / 'Source/CMakeVersion.cmake')
        version_map = make_version_map(args.baseline, args.since, next_version)
        update_repo(repo_root, version_map, args.overwrite)
    except subprocess.CalledProcessError as e:
        print(f"Command '{' '.join(e.cmd)}' returned code {e.returncode}:\n{e.stderr.strip()}")

if __name__ == '__main__':
    main()