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