| 
									
										
										
										
											2017-09-20 23:57:56 +02:00
										 |  |  | """
 | 
					
						
							|  |  |  | 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. | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 17:25:11 -06:00
										 |  |  | import contextlib | 
					
						
							| 
									
										
										
										
											2017-09-20 23:57:56 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2021-08-30 17:25:11 -06:00
										 |  |  | import os.path | 
					
						
							| 
									
										
										
										
											2017-09-20 23:57:56 +02:00
										 |  |  | import sys | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 17:25:11 -06:00
										 |  |  | @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') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 20:12:25 +02:00
										 |  |  |     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: | 
					
						
							| 
									
										
										
										
											2021-08-30 17:25:11 -06:00
										 |  |  |         with open(filename) as infile: | 
					
						
							|  |  |  |             yield infile, outfile | 
					
						
							|  |  |  |     update_file_with_tmpfile(filename, tmpfile) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-24 14:35:47 -06:00
										 |  |  | def update_file_with_tmpfile(filename, tmpfile, *, create=False): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         targetfile = open(filename, 'rb') | 
					
						
							|  |  |  |     except FileNotFoundError: | 
					
						
							|  |  |  |         if not create: | 
					
						
							|  |  |  |             raise  # re-raise | 
					
						
							|  |  |  |         outcome = 'created' | 
					
						
							| 
									
										
										
										
											2021-08-30 17:25:11 -06:00
										 |  |  |         os.replace(tmpfile, filename) | 
					
						
							| 
									
										
										
										
											2017-09-20 23:57:56 +02:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2021-09-24 14:35:47 -06:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2017-09-20 23:57:56 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2021-09-24 14:35:47 -06:00
										 |  |  |     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 |