@@ -1024,10 +1024,12 @@ def _init(
10241024 self .warnings = []
10251025
10261026 self .config_prefix = os .getenv ("CONFIG_" , "CONFIG_" )
1027- # Regular expressions for parsing .config files
1028- self ._set_match = _re_match (self .config_prefix + r"([^=]+)=(.*)" )
1027+ # Regular expressions for parsing .config files.
1028+ # Escape the prefix to prevent regex injection via CONFIG_ env var.
1029+ escaped_prefix = re .escape (self .config_prefix )
1030+ self ._set_match = _re_match (escaped_prefix + r"([^=]+)=(.*)" )
10291031 self ._unset_match = _re_match (
1030- r"# {}([^ ]+) is not set" .format (self . config_prefix )
1032+ r"# {}([^ ]+) is not set" .format (escaped_prefix )
10311033 )
10321034
10331035 self .config_header = os .getenv ("KCONFIG_CONFIG_HEADER" , "" )
@@ -3864,33 +3866,37 @@ def _propagate_deps(self, node, visible_if):
38643866 self ._make_and (cur .prompt [1 ], self ._make_and (visible_if , dep )),
38653867 )
38663868
3867- # Propagate dependencies to defaults
3868- if cur .defaults :
3869- cur .defaults = [
3870- (default , self ._make_and (cond , dep ), loc )
3871- for default , cond , loc in cur .defaults
3872- ]
3873-
3874- # Propagate dependencies to ranges
3875- if cur .ranges :
3876- cur .ranges = [
3877- (low , high , self ._make_and (cond , dep ), loc )
3878- for low , high , cond , loc in cur .ranges
3879- ]
3880-
3881- # Propagate dependencies to selects
3882- if cur .selects :
3883- cur .selects = [
3884- (target , self ._make_and (cond , dep ), loc )
3885- for target , cond , loc in cur .selects
3886- ]
3887-
3888- # Propagate dependencies to implies
3889- if cur .implies :
3890- cur .implies = [
3891- (target , self ._make_and (cond , dep ), loc )
3892- for target , cond , loc in cur .implies
3893- ]
3869+ # When dep is y (the common case for top-level symbols),
3870+ # _make_and(cond, dep) returns cond unchanged, so the list
3871+ # comprehensions would rebuild identical lists. Skip them.
3872+ if dep is not self .y :
3873+ # Propagate dependencies to defaults
3874+ if cur .defaults :
3875+ cur .defaults = [
3876+ (default , self ._make_and (cond , dep ), loc )
3877+ for default , cond , loc in cur .defaults
3878+ ]
3879+
3880+ # Propagate dependencies to ranges
3881+ if cur .ranges :
3882+ cur .ranges = [
3883+ (low , high , self ._make_and (cond , dep ), loc )
3884+ for low , high , cond , loc in cur .ranges
3885+ ]
3886+
3887+ # Propagate dependencies to selects
3888+ if cur .selects :
3889+ cur .selects = [
3890+ (target , self ._make_and (cond , dep ), loc )
3891+ for target , cond , loc in cur .selects
3892+ ]
3893+
3894+ # Propagate dependencies to implies
3895+ if cur .implies :
3896+ cur .implies = [
3897+ (target , self ._make_and (cond , dep ), loc )
3898+ for target , cond , loc in cur .implies
3899+ ]
38943900
38953901 elif cur .prompt : # Not a symbol/choice
38963902 # Propagate dependencies to the prompt. 'visible if' is only
@@ -4196,6 +4202,18 @@ def is_num(s):
41964202
41974203 return True
41984204
4205+ # Build a reverse index {symbol -> [nodes]} in a single pass over
4206+ # all nodes, rather than doing a full tree walk per undefined symbol.
4207+ # This reduces the algorithm from O(U*N*R) to O(N*R + U) where U is
4208+ # the number of undefined symbols, N is the number of nodes, and R is
4209+ # the cost of building node.referenced sets.
4210+ sym_to_nodes = {}
4211+ for node in self .node_iter ():
4212+ for ref_sym in node .referenced :
4213+ if ref_sym not in sym_to_nodes :
4214+ sym_to_nodes [ref_sym ] = []
4215+ sym_to_nodes [ref_sym ].append (node )
4216+
41994217 for sym in (self .syms .viewvalues if _IS_PY2 else self .syms .values )():
42004218 # - sym.nodes empty means the symbol is undefined (has no
42014219 # definition locations)
@@ -4206,13 +4224,14 @@ def is_num(s):
42064224 # - The MODULES symbol always exists
42074225 if not sym .nodes and not is_num (sym .name ) and sym .name != "MODULES" :
42084226
4209- msg = "undefined symbol {}:" .format (sym .name )
4210- for node in self . node_iter ( ):
4211- if sym in node . referenced :
4212- msg += "\n \n - Referenced at {}:{}:\n \n {}" .format (
4227+ parts = [ "undefined symbol {}:" .format (sym .name )]
4228+ for node in sym_to_nodes . get ( sym , () ):
4229+ parts . append (
4230+ "\n \n - Referenced at {}:{}:\n \n {}" .format (
42134231 node .loc [0 ], node .loc [1 ], node
42144232 )
4215- self ._warn (msg )
4233+ )
4234+ self ._warn ("" .join (parts ))
42164235
42174236 def _warn (self , msg , loc = None ):
42184237 # For printing general warnings
@@ -6019,6 +6038,8 @@ class MenuNode(object):
60196038 "selects" ,
60206039 "implies" ,
60216040 "ranges" ,
6041+ # Cached referenced set (populated on first access)
6042+ "_cached_referenced" ,
60226043 )
60236044
60246045 def __init__ (self ):
@@ -6029,6 +6050,7 @@ def __init__(self):
60296050 self .selects = []
60306051 self .implies = []
60316052 self .ranges = []
6053+ self ._cached_referenced = None
60326054
60336055 @property
60346056 def filename (self ):
@@ -6088,6 +6110,9 @@ def referenced(self):
60886110 """
60896111 See the class documentation.
60906112 """
6113+ if self ._cached_referenced is not None :
6114+ return self ._cached_referenced
6115+
60916116 # self.dep is included to catch dependencies from a lone 'depends on'
60926117 # when there are no properties to propagate it to
60936118 res = expr_items (self .dep )
@@ -6115,7 +6140,8 @@ def referenced(self):
61156140 res .add (high )
61166141 res |= expr_items (cond )
61176142
6118- return res
6143+ self ._cached_referenced = frozenset (res )
6144+ return self ._cached_referenced
61196145
61206146 def __repr__ (self ):
61216147 """
@@ -6653,6 +6679,63 @@ def standard_config_filename():
66536679 return os .getenv ("KCONFIG_CONFIG" , ".config" )
66546680
66556681
6682+ def _needs_save (kconf ):
6683+ """
6684+ Returns True if the current configuration state differs from what was
6685+ loaded from the .config file (i.e., saving would modify the .config).
6686+
6687+ Used by menuconfig and guiconfig to determine whether to prompt for save.
6688+ """
6689+ if kconf .missing_syms :
6690+ # Assignments to undefined symbols in the .config
6691+ return True
6692+
6693+ for sym in kconf .unique_defined_syms :
6694+ if sym .user_value is None :
6695+ if sym .config_string :
6696+ # Unwritten symbol
6697+ return True
6698+ elif sym .orig_type in _BOOL_TRISTATE :
6699+ if sym .tri_value != sym .user_value :
6700+ # Written bool/tristate symbol, new value
6701+ return True
6702+ elif sym .str_value != sym .user_value :
6703+ # Written string/int/hex symbol, new value
6704+ return True
6705+
6706+ return False
6707+
6708+
6709+ def _extract_controlling_symbols (expr_list ):
6710+ """
6711+ Extracts the primary controlling symbol from each expression string.
6712+ Returns a list of unique symbol names.
6713+
6714+ For "A && B", extracts "A" (the symbol doing the select/imply).
6715+ For "A || B", extracts both "A" and "B".
6716+ For simple "A", extracts "A".
6717+
6718+ Used by menuconfig and guiconfig for select/imply display.
6719+ """
6720+ sym_names = []
6721+ for expr in expr_list :
6722+ # Split on && first - we only want symbols before &&
6723+ # For "FOO && BAR", we want FOO (the selector), not BAR (the condition)
6724+ and_idx = expr .find (" && " )
6725+ primary = expr [:and_idx ].strip () if and_idx != - 1 else expr .strip ()
6726+
6727+ # Now handle || - all parts are equal
6728+ if " || " in primary :
6729+ for part in primary .split (" || " ):
6730+ part = part .strip ()
6731+ if part and part not in sym_names :
6732+ sym_names .append (part )
6733+ elif primary and primary not in sym_names :
6734+ sym_names .append (primary )
6735+
6736+ return sym_names
6737+
6738+
66566739def load_allconfig (kconf , filename ):
66576740 """
66586741 Use Kconfig.load_allconfig() instead, which was added in Kconfiglib 13.4.0.
@@ -7133,7 +7216,7 @@ def _found_dep_loop(loop, cur):
71337216
71347217 if item .weak_rev_dep is not item .kconfig .n :
71357218 msg += "(imply-related dependencies: {})\n \n " .format (
7136- expr_str (item .rev_dep )
7219+ expr_str (item .weak_rev_dep )
71377220 )
71387221
71397222 msg += "...depends again on " + loop [0 ].name_and_loc
0 commit comments