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