diff --git a/Misc/NEWS.d/next/Build/2017-09-20-20-38-28.bpo-31536.4UPttT.rst b/Misc/NEWS.d/next/Build/2017-09-20-20-38-28.bpo-31536.4UPttT.rst new file mode 100644 index 000000000000000..c9f206b8b2a20c3 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2017-09-20-20-38-28.bpo-31536.4UPttT.rst @@ -0,0 +1 @@ +Avoid wholesale rebuild after `make regen-opcode` if nothing changed. diff --git a/Tools/scripts/generate_opcode_h.py b/Tools/scripts/generate_opcode_h.py index 6622a3c8155bcb3..0229f916a65f82a 100644 --- a/Tools/scripts/generate_opcode_h.py +++ b/Tools/scripts/generate_opcode_h.py @@ -1,5 +1,7 @@ # This script generates the opcode.h header file. +import contextlib +import os import sys import tokenize @@ -34,6 +36,34 @@ #endif /* !Py_OPCODE_H */ """ +@contextlib.contextmanager +def open_for_replace(dest_path, mode): + """ + Simulate opening *dest_path* with *mode*, but only overwrite it + if the contents changed at the end. + When the contents did not change, the original metadata is kept intact, + avoiding wholesale rebuilds. + """ + tmp_path = '%s.tmp%s' % (dest_path, os.getpid()) + try: + with open(tmp_path, mode) as f: + yield f + except: + try: + os.unlink(tmp_path) + except OSError: + pass + raise + else: + with open(dest_path, 'rb') as f: + old_contents = f.read() + with open(tmp_path, 'rb') as f: + new_contents = f.read() + if old_contents != new_contents: + os.replace(tmp_path, dest_path) + else: + os.unlink(tmp_path) + def main(opcode_py, outfile='Include/opcode.h'): opcode = {} @@ -45,7 +75,7 @@ def main(opcode_py, outfile='Include/opcode.h'): code = fp.read() exec(code, opcode) opmap = opcode['opmap'] - with open(outfile, 'w') as fobj: + with open_for_replace(outfile, 'w') as fobj: fobj.write(header) for name in opcode['opname']: if name in opmap: