aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Tools/build/update_file.py
blob: b4a5fb6e778ae8c14dc9c740ae49aaf1fd62b6af (plain) (blame)
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
"""
A script that replaces an old file with a new one, only if the contents
actually changed.  If not, the new file is simply deleted.

This avoids wholesale rebuilds when a code (re)generation phase does not
actually change the in-tree generated code.
"""

from __future__ import annotations

import contextlib
import os
import os.path
import sys

TYPE_CHECKING = False
if TYPE_CHECKING:
    import typing
    from collections.abc import Iterator
    from io import TextIOWrapper

    _Outcome: typing.TypeAlias = typing.Literal['created', 'updated', 'same']


@contextlib.contextmanager
def updating_file_with_tmpfile(
    filename: str,
    tmpfile: str | None = None,
) -> Iterator[tuple[TextIOWrapper, TextIOWrapper]]:
    """A context manager for updating a file via a temp file.

    The context manager provides two open files: the source file open
    for reading, and the temp file, open for writing.

    Upon exiting: both files are closed, and the source file is replaced
    with the temp file.
    """
    # XXX Optionally use tempfile.TemporaryFile?
    if not tmpfile:
        tmpfile = filename + '.tmp'
    elif os.path.isdir(tmpfile):
        tmpfile = os.path.join(tmpfile, filename + '.tmp')

    with open(filename, 'rb') as infile:
        line = infile.readline()

    if line.endswith(b'\r\n'):
        newline = "\r\n"
    elif line.endswith(b'\r'):
        newline = "\r"
    elif line.endswith(b'\n'):
        newline = "\n"
    else:
        raise ValueError(f"unknown end of line: {filename}: {line!a}")

    with open(tmpfile, 'w', newline=newline) as outfile:
        with open(filename) as infile:
            yield infile, outfile
    update_file_with_tmpfile(filename, tmpfile)


def update_file_with_tmpfile(
    filename: str,
    tmpfile: str,
    *,
    create: bool = False,
) -> _Outcome:
    try:
        targetfile = open(filename, 'rb')
    except FileNotFoundError:
        if not create:
            raise  # re-raise
        outcome: _Outcome = 'created'
        os.replace(tmpfile, filename)
    else:
        with targetfile:
            old_contents = targetfile.read()
        with open(tmpfile, 'rb') as f:
            new_contents = f.read()
        # Now compare!
        if old_contents != new_contents:
            outcome = 'updated'
            os.replace(tmpfile, filename)
        else:
            outcome = 'same'
            os.unlink(tmpfile)
    return outcome


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--create', action='store_true')
    parser.add_argument('--exitcode', action='store_true')
    parser.add_argument('filename', help='path to be updated')
    parser.add_argument('tmpfile', help='path with new contents')
    args = parser.parse_args()
    kwargs = vars(args)
    setexitcode = kwargs.pop('exitcode')

    outcome = update_file_with_tmpfile(**kwargs)
    if setexitcode:
        if outcome == 'same':
            sys.exit(0)
        elif outcome == 'updated':
            sys.exit(1)
        elif outcome == 'created':
            sys.exit(2)
        else:
            raise NotImplementedError