1 """
2 A script that replaces an old file with a new one, only if the contents
3 actually changed.  If not, the new file is simply deleted.
4 
5 This avoids wholesale rebuilds when a code (re)generation phase does not
6 actually change the in-tree generated code.
7 """
8 
9 import contextlib
10 import os
11 import os.path
12 import sys
13 
14 
15 @contextlib.contextmanager
16 def 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 
49 def 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 
72 if __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