diff options
| author | Ulf Magnusson <ulfalizer@gmail.com> | 2019-05-29 07:29:20 +0200 |
|---|---|---|
| committer | Ulf Magnusson <ulfalizer@gmail.com> | 2019-05-31 00:08:14 +0200 |
| commit | 175c1f5a363e51a76a7dc1fe807f7c3faf40ab23 (patch) | |
| tree | 32adbe8643337017845827f324213e156bcebb09 /kconfiglib.py | |
| parent | 556dcdd252bd312b1da21f84f4775cbbba0dd8c4 (diff) | |
Leave unchanged output files untouched
Before writing a configuration file or header file, compare the old
contents of the file against the new contents. If there's no change,
skip the write, to avoid updating the file modification time.
This might avoid triggering redundant rebuilds depending on how the
build system is set up, and could allow for a simpler setup.
Diffstat (limited to 'kconfiglib.py')
| -rw-r--r-- | kconfiglib.py | 155 |
1 files changed, 111 insertions, 44 deletions
diff --git a/kconfiglib.py b/kconfiglib.py index 3bdcb45..11527a7 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -1300,6 +1300,11 @@ class Kconfig(object): write_config(). The order in the C implementation depends on the hash table implementation as of writing, and so won't match. + If 'filename' exists and its contents is identical to what would get + written out, it is left untouched. This avoids updating file metadata + like the modification time and possibly triggering redundant work in + build tools. + filename: Self-explanatory. @@ -1308,37 +1313,45 @@ class Kconfig(object): would usually want it enclosed in '/* */' to make it a C comment, and include a final terminating newline. """ - with self._open(filename, "w") as f: - f.write(header) + self._write_if_changed(filename, self._autoconf_contents(header)) - for sym in self.unique_defined_syms: - # Note: _write_to_conf is determined when the value is - # calculated. This is a hidden function call due to - # property magic. - val = sym.str_value - if not sym._write_to_conf: - continue + def _autoconf_contents(self, header): + # write_autoconf() helper. Returns the contents to write as a string, + # with 'header' at the beginning. + + # "".join()ed later + chunks = [header] + add = chunks.append + + for sym in self.unique_defined_syms: + # Note: _write_to_conf is determined when the value is + # calculated. This is a hidden function call due to + # property magic. + val = sym.str_value + if not sym._write_to_conf: + continue - if sym.orig_type in _BOOL_TRISTATE: - if val == "y": - f.write("#define {}{} 1\n".format( - self.config_prefix, sym.name)) - elif val == "m": - f.write("#define {}{}_MODULE 1\n".format( - self.config_prefix, sym.name)) + if sym.orig_type in _BOOL_TRISTATE: + if val == "y": + add("#define {}{} 1\n" + .format(self.config_prefix, sym.name)) + elif val == "m": + add("#define {}{}_MODULE 1\n" + .format(self.config_prefix, sym.name)) - elif sym.orig_type is STRING: - f.write('#define {}{} "{}"\n' - .format(self.config_prefix, sym.name, - escape(val))) + elif sym.orig_type is STRING: + add('#define {}{} "{}"\n' + .format(self.config_prefix, sym.name, escape(val))) - else: # sym.orig_type in _INT_HEX: - if sym.orig_type is HEX and \ - not val.startswith(("0x", "0X")): - val = "0x" + val + else: # sym.orig_type in _INT_HEX: + if sym.orig_type is HEX and \ + not val.startswith(("0x", "0X")): + val = "0x" + val - f.write("#define {}{} {}\n" - .format(self.config_prefix, sym.name, val)) + add("#define {}{} {}\n" + .format(self.config_prefix, sym.name, val)) + + return "".join(chunks) def write_config(self, filename=None, header="# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n", @@ -1355,6 +1368,11 @@ class Kconfig(object): See the 'Intro to symbol values' section in the module docstring to understand which symbols get written out. + If 'filename' exists and its contents is identical to what would get + written out, it is left untouched. This avoids updating file metadata + like the modification time and possibly triggering redundant work in + build tools. + filename (default: None): Filename to save configuration to (a string). @@ -1386,18 +1404,27 @@ class Kconfig(object): else: verbose = False + contents = self._config_contents(header) + if self._contents_eq(filename, contents): + if verbose: + print("No change to '{}'".format(filename)) + return + if save_old: _save_old(filename) with self._open(filename, "w") as f: - f.write(header) - self._write_config_syms(f) + f.write(contents) if verbose: print("Configuration written to '{}'".format(filename)) - def _write_config_syms(self, f): - # write_config() helper. Writes the actual configuration output to 'f'. + def _config_contents(self, header): + # write_config() helper. Returns the contents to write as a string, + # with 'header' at the beginning. + # + # More memory friendly would be to 'yield' the strings and + # "".join(_config_contents()), but it was a bit slower on my system. # node_iter() was used here before commit 3aea9f7 ("Add '# end of # <menu>' after menus in .config"). Those comments get tricky to @@ -1409,6 +1436,10 @@ class Kconfig(object): # Did we just print an '# end of ...' comment? after_end_comment = False + # "".join()ed later + chunks = [header] + add = chunks.append + node = self.top_node while 1: # Jump to the next node with an iterative tree walk @@ -1420,11 +1451,11 @@ class Kconfig(object): while node.parent: node = node.parent - # Print a comment when leaving visible menus + # Add a comment when leaving visible menus if node.item is MENU and expr_value(node.dep) and \ expr_value(node.visibility) and \ node is not self.top_node: - f.write("# end of {}\n".format(node.prompt[0])) + add("# end of {}\n".format(node.prompt[0])) after_end_comment = True if node.next: @@ -1432,7 +1463,7 @@ class Kconfig(object): break else: # No more nodes - return + return "".join(chunks) # Generate configuration output for the node @@ -1451,14 +1482,14 @@ class Kconfig(object): # Add a blank line before the first symbol printed after an # '# end of ...' comment after_end_comment = False - f.write("\n") - f.write(conf_string) + add("\n") + add(conf_string) elif expr_value(node.dep) and \ ((item is MENU and expr_value(node.visibility)) or item is COMMENT): - f.write("\n#\n# {}\n#\n".format(node.prompt[0])) + add("\n#\n# {}\n#\n".format(node.prompt[0])) after_end_comment = False def write_min_config(self, filename, @@ -1549,6 +1580,11 @@ class Kconfig(object): 3. A new auto.conf with the current symbol values is written, to keep track of them for the next build. + If auto.conf exists and its contents is identical to what would + get written out, it is left untouched. This avoids updating file + metadata like the modification time and possibly triggering + redundant work in build tools. + The last piece of the puzzle is knowing what symbols each source file depends on. Knowing that, dependencies can be added from source files @@ -1661,10 +1697,18 @@ class Kconfig(object): # A separate helper function is neater than complicating write_config() # by passing a flag to it, plus we only need to look at symbols here. - with self._open(join(path, "auto.conf"), "w") as f: - for sym in self.unique_defined_syms: - if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value): - f.write(sym.config_string) + self._write_if_changed( + os.path.join(path, "auto.conf"), + self._old_vals_contents()) + + def _old_vals_contents(self): + # _write_old_vals() helper. Returns the contents to write as a string. + + # Temporary list instead of generator makes this a bit faster + return "".join([ + sym.config_string for sym in self.unique_defined_syms + if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value) + ]) def node_iter(self, unique_syms=False): """ @@ -2015,6 +2059,33 @@ class Kconfig(object): self._tokens = self._tokenize(line) self._reuse_tokens = True + def _write_if_changed(self, filename, contents): + # Writes 'contents' into 'filename', but only if it differs from the + # current contents of the file. + # + # Another variant would be write a temporary file on the same + # filesystem, compare the files, and rename() the temporary file if it + # differs, but it breaks stuff like write_config("/dev/null"), which is + # used out there to force evaluation-related warnings to be generated. + # This simple version pretty failsafe and portable. + + if not self._contents_eq(filename, contents): + with self._open(filename, "w") as f: + f.write(contents) + + def _contents_eq(self, filename, contents): + # Returns True if the contents of 'filename' is 'contents' (a string), + # and False otherwise (including if 'filename' can't be opened/read) + + try: + with self._open(filename, "r") as f: + # Robust re. things like encoding and line endings (mmap() + # trickery isn't) + return f.read(len(contents) + 1) == contents + except IOError: + # If the error here would prevent writing the file as well, we'll + # notice it later + return False # # Tokenization @@ -2317,7 +2388,6 @@ class Kconfig(object): return True return False - # # Preprocessor logic # @@ -2575,7 +2645,6 @@ class Kconfig(object): return "" - # # Parsing # @@ -3282,7 +3351,6 @@ class Kconfig(object): for choice in self.unique_choices: choice._invalidate() - # # Post-parsing menu tree processing, including dependency propagation and # implicit submenu creation @@ -3451,7 +3519,6 @@ class Kconfig(object): target.weak_rev_dep, self._make_and(sym, cond)) - # # Misc. # |
