1"""
2A script that replaces an old file with a new one, only if the contents
3actually changed.  If not, the new file is simply deleted.
4
5This avoids wholesale rebuilds when a code (re)generation phase does not
6actually change the in-tree generated code.
7"""
8
9import contextlib
10import os
11import os.path
12import sys
13
14
15@contextlib.contextmanager
16def updating_file_with_tmpfile(filename, tmpfile=None):
17    """A context manager for updating a file via a temp file.
18
19    The context manager provides two open files: the source file open
20    for reading, and the temp file, open for writing.
21
22    Upon exiting: both files are closed, and the source file is replaced
23    with the temp file.
24    """
25    # XXX Optionally use tempfile.TemporaryFile?
26    if not tmpfile:
27        tmpfile = filename + '.tmp'
28    elif os.path.isdir(tmpfile):
29        tmpfile = os.path.join(tmpfile, filename + '.tmp')
30
31    with open(filename, 'rb') as infile:
32        line = infile.readline()
33
34    if line.endswith(b'\r\n'):
35        newline = "\r\n"
36    elif line.endswith(b'\r'):
37        newline = "\r"
38    elif line.endswith(b'\n'):
39        newline = "\n"
40    else:
41        raise ValueError(f"unknown end of line: {filename}: {line!a}")
42
43    with open(tmpfile, 'w', newline=newline) as outfile:
44        with open(filename) as infile:
45            yield infile, outfile
46    update_file_with_tmpfile(filename, tmpfile)
47
48
49def update_file_with_tmpfile(filename, tmpfile, *, create=False):
50    try:
51        targetfile = open(filename, 'rb')
52    except FileNotFoundError:
53        if not create:
54            raise  # re-raise
55        outcome = 'created'
56        os.replace(tmpfile, filename)
57    else:
58        with targetfile:
59            old_contents = targetfile.read()
60        with open(tmpfile, 'rb') as f:
61            new_contents = f.read()
62        # Now compare!
63        if old_contents != new_contents:
64            outcome = 'updated'
65            os.replace(tmpfile, filename)
66        else:
67            outcome = 'same'
68            os.unlink(tmpfile)
69    return outcome
70
71
72if __name__ == '__main__':
73    import argparse
74    parser = argparse.ArgumentParser()
75    parser.add_argument('--create', action='store_true')
76    parser.add_argument('--exitcode', action='store_true')
77    parser.add_argument('filename', help='path to be updated')
78    parser.add_argument('tmpfile', help='path with new contents')
79    args = parser.parse_args()
80    kwargs = vars(args)
81    setexitcode = kwargs.pop('exitcode')
82
83    outcome = update_file_with_tmpfile(**kwargs)
84    if setexitcode:
85        if outcome == 'same':
86            sys.exit(0)
87        elif outcome == 'updated':
88            sys.exit(1)
89        elif outcome == 'created':
90            sys.exit(2)
91        else:
92            raise NotImplementedError
93