| """ |
| 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. |
| """ |
| |
| import contextlib |
| import os |
| import os.path |
| import sys |
| |
| |
| @contextlib.contextmanager |
| def updating_file_with_tmpfile(filename, tmpfile=None): |
| """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, tmpfile, *, create=False): |
| try: |
| targetfile = open(filename, 'rb') |
| except FileNotFoundError: |
| if not create: |
| raise # re-raise |
| 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 |