summaryrefslogtreecommitdiff
path: root/tools/buildman/cfgutil.py
blob: ab74a8ef0622e2e217a73abbdb813a3f02a8454f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# SPDX-License-Identifier: GPL-2.0+
# Copyright 2022 Google LLC
# Written by Simon Glass <sjg@chromium.org>
#

"""Utility functions for dealing with Kconfig .confing files"""

import re

from patman import tools

RE_LINE = re.compile(r'(# )?CONFIG_([A-Z0-9_]+)(=(.*)| is not set)')
RE_CFG = re.compile(r'(~?)(CONFIG_)?([A-Z0-9_]+)(=.*)?')

def make_cfg_line(opt, adj):
    """Make a new config line for an option

    Args:
        opt (str): Option to process, without CONFIG_ prefix
        adj (str): Adjustment to make (C is config option without prefix):
             C to enable C
             ~C to disable C
             C=val to set the value of C (val must have quotes if C is
                 a string Kconfig)

    Returns:
        str: New line to use, one of:
            CONFIG_opt=y               - option is enabled
            # CONFIG_opt is not set    - option is disabled
            CONFIG_opt=val             - option is getting a new value (val is
                in quotes if this is a string)
    """
    if adj[0] == '~':
        return f'# CONFIG_{opt} is not set'
    if '=' in adj:
        return f'CONFIG_{adj}'
    return f'CONFIG_{opt}=y'

def adjust_cfg_line(line, adjust_cfg, done=None):
    """Make an adjustment to a single of line from a .config file

    This processes a .config line, producing a new line if a change for this
    CONFIG is requested in adjust_cfg

    Args:
        line (str): line to process, e.g. '# CONFIG_FRED is not set' or
            'CONFIG_FRED=y' or 'CONFIG_FRED=0x123' or 'CONFIG_FRED="fred"'
        adjust_cfg (dict of str): Changes to make to .config file before
                building:
             key: str config to change, without the CONFIG_ prefix, e.g.
                 FRED
             value: str change to make (C is config option without prefix):
                 C to enable C
                 ~C to disable C
                 C=val to set the value of C (val must have quotes if C is
                     a string Kconfig)
        done (set of set): Adds the config option to this set if it is changed
            in some way. This is used to track which ones have been processed.
            None to skip.

    Returns:
        tuple:
            str: New string for this line (maybe unchanged)
            str: Adjustment string that was used
    """
    out_line = line
    m_line = RE_LINE.match(line)
    adj = None
    if m_line:
        _, opt, _, _ = m_line.groups()
        adj = adjust_cfg.get(opt)
        if adj:
            out_line = make_cfg_line(opt, adj)
            if done is not None:
                done.add(opt)

    return out_line, adj

def adjust_cfg_lines(lines, adjust_cfg):
    """Make adjustments to a list of lines from a .config file

    Args:
        lines (list of str): List of lines to process
        adjust_cfg (dict of str): Changes to make to .config file before
                building:
             key: str config to change, without the CONFIG_ prefix, e.g.
                 FRED
             value: str change to make (C is config option without prefix):
                 C to enable C
                 ~C to disable C
                 C=val to set the value of C (val must have quotes if C is
                     a string Kconfig)

    Returns:
        list of str: New list of lines resulting from the processing
    """
    out_lines = []
    done = set()
    for line in lines:
        out_line, _ = adjust_cfg_line(line, adjust_cfg, done)
        out_lines.append(out_line)

    for opt in adjust_cfg:
        if opt not in done:
            adj = adjust_cfg.get(opt)
            out_line = make_cfg_line(opt, adj)
            out_lines.append(out_line)

    return out_lines

def adjust_cfg_file(fname, adjust_cfg):
    """Make adjustments to a .config file

    Args:
        fname (str): Filename of .config file to change
        adjust_cfg (dict of str): Changes to make to .config file before
                building:
             key: str config to change, without the CONFIG_ prefix, e.g.
                 FRED
             value: str change to make (C is config option without prefix):
                 C to enable C
                 ~C to disable C
                 C=val to set the value of C (val must have quotes if C is
                     a string Kconfig)
    """
    lines = tools.read_file(fname, binary=False).splitlines()
    out_lines = adjust_cfg_lines(lines, adjust_cfg)
    out = '\n'.join(out_lines) + '\n'
    tools.write_file(fname, out, binary=False)

def convert_list_to_dict(adjust_cfg_list):
    """Convert a list of config changes into the dict used by adjust_cfg_file()

    Args:
        adjust_cfg_list (list of str): List of changes to make to .config file
            before building. Each is one of (where C is the config option with
            or without the CONFIG_ prefix)

                C to enable C
                ~C to disable C
                C=val to set the value of C (val must have quotes if C is
                    a string Kconfig

    Returns:
        dict of str: Changes to make to .config file before building:
             key: str config to change, without the CONFIG_ prefix, e.g. FRED
             value: str change to make (C is config option without prefix):
                 C to enable C
                 ~C to disable C
                 C=val to set the value of C (val must have quotes if C is
                     a string Kconfig)

    Raises:
        ValueError: if an item in adjust_cfg_list has invalid syntax
    """
    result = {}
    for cfg in adjust_cfg_list or []:
        m_cfg = RE_CFG.match(cfg)
        if not m_cfg:
            raise ValueError(f"Invalid CONFIG adjustment '{cfg}'")
        negate, _, opt, val = m_cfg.groups()
        result[opt] = f'%s{opt}%s' % (negate or '', val or '')

    return result

def check_cfg_lines(lines, adjust_cfg):
    """Check that lines do not conflict with the requested changes

    If a line enables a CONFIG which was requested to be disabled, etc., then
    this is an error. This function finds such errors.

    Args:
        lines (list of str): List of lines to process
        adjust_cfg (dict of str): Changes to make to .config file before
                building:
             key: str config to change, without the CONFIG_ prefix, e.g.
                 FRED
             value: str change to make (C is config option without prefix):
                 C to enable C
                 ~C to disable C
                 C=val to set the value of C (val must have quotes if C is
                     a string Kconfig)

    Returns:
        list of tuple: list of errors, each a tuple:
            str: cfg adjustment requested
            str: line of the config that conflicts
    """
    bad = []
    done = set()
    for line in lines:
        out_line, adj = adjust_cfg_line(line, adjust_cfg, done)
        if out_line != line:
            bad.append([adj, line])

    for opt in adjust_cfg:
        if opt not in done:
            adj = adjust_cfg.get(opt)
            out_line = make_cfg_line(opt, adj)
            bad.append([adj, f'Missing expected line: {out_line}'])

    return bad

def check_cfg_file(fname, adjust_cfg):
    """Check that a config file has been adjusted according to adjust_cfg

    Args:
        fname (str): Filename of .config file to change
        adjust_cfg (dict of str): Changes to make to .config file before
                building:
             key: str config to change, without the CONFIG_ prefix, e.g.
                 FRED
             value: str change to make (C is config option without prefix):
                 C to enable C
                 ~C to disable C
                 C=val to set the value of C (val must have quotes if C is
                     a string Kconfig)

    Returns:
        str: None if OK, else an error string listing the problems
    """
    lines = tools.read_file(fname, binary=False).splitlines()
    bad_cfgs = check_cfg_lines(lines, adjust_cfg)
    if bad_cfgs:
        out = [f'{cfg:20}  {line}' for cfg, line in bad_cfgs]
        content = '\\n'.join(out)
        return f'''
Some CONFIG adjustments did not take effect. This may be because
the request CONFIGs do not exist or conflict with others.

Failed adjustments:

{content}
'''
    return None