diff options
| -rw-r--r-- | README.rst | 5 | ||||
| -rwxr-xr-x | genconfig.py | 10 | ||||
| -rw-r--r-- | kconfiglib.py | 155 |
3 files changed, 125 insertions, 45 deletions
@@ -149,6 +149,11 @@ Getting started ``config.h``. Normally, ``genconfig`` would be run automatically as part of the build. + + Before writing a header file or other configuration output, Kconfiglib + compares the old contents of the file against the new contents. If there's + no change, the write is skipped. This avoids updating file metadata like the + modification time, and might save work depending on your build setup. Adding new configuration output formats should be relatively straightforward. See the implementation of ``write_config()`` in `kconfiglib.py diff --git a/genconfig.py b/genconfig.py index c575c9a..ba2bb73 100755 --- a/genconfig.py +++ b/genconfig.py @@ -7,10 +7,18 @@ Generates a header file with #defines from the configuration, matching the format of include/generated/autoconf.h in the Linux kernel. -Optionally creates/updates a directory structure with one file per symbol that +Optionally, also writes the configuration output as a .config file. See +--config-out. + +Optionally, creates/updates a directory structure with one file per symbol that can be used to implement incremental builds. See the docstring for Kconfig.sync_deps() in kconfiglib.py. +Before writing a header file or other configuration output, Kconfiglib compares +the old contents of the file against the new contents. If there's no change, +the write is skipped. This avoids updating file metadata like the modification +time, and might save work depending on your build setup. + By default, the configuration is generated from '.config'. A different configuration file can be passed in the KCONFIG_CONFIG environment variable. """ 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. # |
