1# SPDX-License-Identifier: GPL-2.0+
2# Copyright 2022 Google LLC
3# Written by Simon Glass <sjg@chromium.org>
4#
5
6"""Utility functions for dealing with Kconfig .confing files"""
7
8import re
9
10from u_boot_pylib import tools
11
12RE_LINE = re.compile(r'(# )?CONFIG_([A-Z0-9_]+)(=(.*)| is not set)')
13RE_CFG = re.compile(r'(~?)(CONFIG_)?([A-Z0-9_]+)(=.*)?')
14
15def make_cfg_line(opt, adj):
16    """Make a new config line for an option
17
18    Args:
19        opt (str): Option to process, without CONFIG_ prefix
20        adj (str): Adjustment to make (C is config option without prefix):
21             C to enable C
22             ~C to disable C
23             C=val to set the value of C (val must have quotes if C is
24                 a string Kconfig)
25
26    Returns:
27        str: New line to use, one of:
28            CONFIG_opt=y               - option is enabled
29            # CONFIG_opt is not set    - option is disabled
30            CONFIG_opt=val             - option is getting a new value (val is
31                in quotes if this is a string)
32    """
33    if adj[0] == '~':
34        return f'# CONFIG_{opt} is not set'
35    if '=' in adj:
36        return f'CONFIG_{adj}'
37    return f'CONFIG_{opt}=y'
38
39def adjust_cfg_line(line, adjust_cfg, done=None):
40    """Make an adjustment to a single of line from a .config file
41
42    This processes a .config line, producing a new line if a change for this
43    CONFIG is requested in adjust_cfg
44
45    Args:
46        line (str): line to process, e.g. '# CONFIG_FRED is not set' or
47            'CONFIG_FRED=y' or 'CONFIG_FRED=0x123' or 'CONFIG_FRED="fred"'
48        adjust_cfg (dict of str): Changes to make to .config file before
49                building:
50             key: str config to change, without the CONFIG_ prefix, e.g.
51                 FRED
52             value: str change to make (C is config option without prefix):
53                 C to enable C
54                 ~C to disable C
55                 C=val to set the value of C (val must have quotes if C is
56                     a string Kconfig)
57        done (set of set): Adds the config option to this set if it is changed
58            in some way. This is used to track which ones have been processed.
59            None to skip.
60
61    Returns:
62        tuple:
63            str: New string for this line (maybe unchanged)
64            str: Adjustment string that was used
65    """
66    out_line = line
67    m_line = RE_LINE.match(line)
68    adj = None
69    if m_line:
70        _, opt, _, _ = m_line.groups()
71        adj = adjust_cfg.get(opt)
72        if adj:
73            out_line = make_cfg_line(opt, adj)
74            if done is not None:
75                done.add(opt)
76
77    return out_line, adj
78
79def adjust_cfg_lines(lines, adjust_cfg):
80    """Make adjustments to a list of lines from a .config file
81
82    Args:
83        lines (list of str): List of lines to process
84        adjust_cfg (dict of str): Changes to make to .config file before
85                building:
86             key: str config to change, without the CONFIG_ prefix, e.g.
87                 FRED
88             value: str change to make (C is config option without prefix):
89                 C to enable C
90                 ~C to disable C
91                 C=val to set the value of C (val must have quotes if C is
92                     a string Kconfig)
93
94    Returns:
95        list of str: New list of lines resulting from the processing
96    """
97    out_lines = []
98    done = set()
99    for line in lines:
100        out_line, _ = adjust_cfg_line(line, adjust_cfg, done)
101        out_lines.append(out_line)
102
103    for opt in adjust_cfg:
104        if opt not in done:
105            adj = adjust_cfg.get(opt)
106            out_line = make_cfg_line(opt, adj)
107            out_lines.append(out_line)
108
109    return out_lines
110
111def adjust_cfg_file(fname, adjust_cfg):
112    """Make adjustments to a .config file
113
114    Args:
115        fname (str): Filename of .config file to change
116        adjust_cfg (dict of str): Changes to make to .config file before
117                building:
118             key: str config to change, without the CONFIG_ prefix, e.g.
119                 FRED
120             value: str change to make (C is config option without prefix):
121                 C to enable C
122                 ~C to disable C
123                 C=val to set the value of C (val must have quotes if C is
124                     a string Kconfig)
125    """
126    lines = tools.read_file(fname, binary=False).splitlines()
127    out_lines = adjust_cfg_lines(lines, adjust_cfg)
128    out = '\n'.join(out_lines) + '\n'
129    tools.write_file(fname, out, binary=False)
130
131def convert_list_to_dict(adjust_cfg_list):
132    """Convert a list of config changes into the dict used by adjust_cfg_file()
133
134    Args:
135        adjust_cfg_list (list of str): List of changes to make to .config file
136            before building. Each is one of (where C is the config option with
137            or without the CONFIG_ prefix)
138
139                C to enable C
140                ~C to disable C
141                C=val to set the value of C (val must have quotes if C is
142                    a string Kconfig
143
144    Returns:
145        dict of str: Changes to make to .config file before building:
146             key: str config to change, without the CONFIG_ prefix, e.g. FRED
147             value: str change to make (C is config option without prefix):
148                 C to enable C
149                 ~C to disable C
150                 C=val to set the value of C (val must have quotes if C is
151                     a string Kconfig)
152
153    Raises:
154        ValueError: if an item in adjust_cfg_list has invalid syntax
155    """
156    result = {}
157    for cfg in adjust_cfg_list or []:
158        m_cfg = RE_CFG.match(cfg)
159        if not m_cfg:
160            raise ValueError(f"Invalid CONFIG adjustment '{cfg}'")
161        negate, _, opt, val = m_cfg.groups()
162        result[opt] = f'%s{opt}%s' % (negate or '', val or '')
163
164    return result
165
166def check_cfg_lines(lines, adjust_cfg):
167    """Check that lines do not conflict with the requested changes
168
169    If a line enables a CONFIG which was requested to be disabled, etc., then
170    this is an error. This function finds such errors.
171
172    Args:
173        lines (list of str): List of lines to process
174        adjust_cfg (dict of str): Changes to make to .config file before
175                building:
176             key: str config to change, without the CONFIG_ prefix, e.g.
177                 FRED
178             value: str change to make (C is config option without prefix):
179                 C to enable C
180                 ~C to disable C
181                 C=val to set the value of C (val must have quotes if C is
182                     a string Kconfig)
183
184    Returns:
185        list of tuple: list of errors, each a tuple:
186            str: cfg adjustment requested
187            str: line of the config that conflicts
188    """
189    bad = []
190    done = set()
191    for line in lines:
192        out_line, adj = adjust_cfg_line(line, adjust_cfg, done)
193        if out_line != line:
194            bad.append([adj, line])
195
196    for opt in adjust_cfg:
197        if opt not in done:
198            adj = adjust_cfg.get(opt)
199            out_line = make_cfg_line(opt, adj)
200            bad.append([adj, f'Missing expected line: {out_line}'])
201
202    return bad
203
204def check_cfg_file(fname, adjust_cfg):
205    """Check that a config file has been adjusted according to adjust_cfg
206
207    Args:
208        fname (str): Filename of .config file to change
209        adjust_cfg (dict of str): Changes to make to .config file before
210                building:
211             key: str config to change, without the CONFIG_ prefix, e.g.
212                 FRED
213             value: str change to make (C is config option without prefix):
214                 C to enable C
215                 ~C to disable C
216                 C=val to set the value of C (val must have quotes if C is
217                     a string Kconfig)
218
219    Returns:
220        str: None if OK, else an error string listing the problems
221    """
222    lines = tools.read_file(fname, binary=False).splitlines()
223    bad_cfgs = check_cfg_lines(lines, adjust_cfg)
224    if bad_cfgs:
225        out = [f'{cfg:20}  {line}' for cfg, line in bad_cfgs]
226        content = '\\n'.join(out)
227        return f'''
228Some CONFIG adjustments did not take effect. This may be because
229the request CONFIGs do not exist or conflict with others.
230
231Failed adjustments:
232
233{content}
234'''
235    return None
236