]> git.proxmox.com Git - mirror_ubuntu-kernels.git/blob - debian/scripts/misc/kconfig/annotations.py
UBUNTU: [Packaging] annotations: unify same rule across all flavour within the same...
[mirror_ubuntu-kernels.git] / debian / scripts / misc / kconfig / annotations.py
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3 # python module to manage Ubuntu kernel .config and annotations
4 # Copyright © 2022 Canonical Ltd.
5
6 import json
7 import re
8 import shutil
9 import tempfile
10 from ast import literal_eval
11 from os.path import dirname, abspath
12
13 class Config(object):
14 def __init__(self, fname: str, arch: str = None, flavour: str = None):
15 """
16 Basic configuration file object
17 """
18 self.fname = fname
19 raw_data = self._load(fname)
20 self._parse(raw_data)
21
22 def _load(self, fname: str) -> str:
23 with open(fname, 'rt') as fd:
24 data = fd.read()
25 return data.rstrip()
26
27 def __str__(self):
28 """ Return a JSON representation of the config """
29 return json.dumps(self.config, indent=4)
30
31 class KConfig(Config):
32 """
33 Parse a .config file, individual config options can be accessed via
34 .config[<CONFIG_OPTION>]
35 """
36 def _parse(self, data: str) -> dict:
37 self.config = {}
38 for line in data.splitlines():
39 m = re.match(r'^# (CONFIG_.*) is not set$', line)
40 if m:
41 self.config[m.group(1)] = literal_eval("'n'")
42 continue
43 m = re.match(r'^(CONFIG_[A-Za-z0-9_]+)=(.*)$', line)
44 if m:
45 self.config[m.group(1)] = literal_eval("'" + m.group(2) + "'")
46 continue
47
48 class Annotation(Config):
49 """
50 Parse body of annotations file
51 """
52 def _parse_body(self, data: str):
53 # Skip comments
54 data = re.sub(r'(?m)^\s*#.*\n?', '', data)
55
56 # Convert multiple spaces to single space to simplifly parsing
57 data = re.sub(r' *', ' ', data)
58
59 # Handle includes (recursively)
60 for line in data.splitlines():
61 m = re.match(r'^include\s+"?([^"]*)"?', line)
62 if m:
63 self.include.append(m.group(1))
64 include_fname = dirname(abspath(self.fname)) + '/' + m.group(1)
65 include_data = self._load(include_fname)
66 self._parse_body(include_data)
67 else:
68 # Skip empty, non-policy and non-note lines
69 if re.match('.* policy<', line) or re.match('.* note<', line):
70 try:
71 # Parse single policy or note rule
72 conf = line.split(' ')[0]
73 if conf in self.config:
74 entry = self.config[conf]
75 else:
76 entry = {'policy': {}}
77 m = re.match(r'.*policy<(.*)>', line)
78 if m:
79 entry['policy'] |= literal_eval(m.group(1))
80 else:
81 m = re.match(r'.*note<(.*?)>', line)
82 if m:
83 entry['note'] = "'" + m.group(1).replace("'", '') + "'"
84 else:
85 raise Exception('syntax error')
86 self.config[conf] = entry
87 except Exception as e:
88 raise Exception(str(e) + f', line = {line}')
89
90 """
91 Parse main annotations file, individual config options can be accessed via
92 self.config[<CONFIG_OPTION>]
93 """
94 def _parse(self, data: str) -> dict:
95 self.config = {}
96 self.arch = []
97 self.flavour = []
98 self.flavour_dep = {}
99 self.include = []
100 self.header = ''
101
102 # Parse header (only main header will considered, headers in includes
103 # will be treated as comments)
104 for line in data.splitlines():
105 if re.match(r'^#.*', line):
106 m = re.match(r'^# ARCH: (.*)', line)
107 if m:
108 self.arch = list(m.group(1).split(' '))
109 m = re.match(r'^# FLAVOUR: (.*)', line)
110 if m:
111 self.flavour = list(m.group(1).split(' '))
112 m = re.match(r'^# FLAVOUR_DEP: (.*)', line)
113 if m:
114 self.flavour_dep = eval(m.group(1))
115 self.header += line + "\n"
116 else:
117 break
118
119 # Parse body (handle includes recursively)
120 self._parse_body(data)
121
122 def _remove_entry(self, config : str):
123 if 'policy' in self.config[config]:
124 del self.config[config]['policy']
125 if 'note' in self.config[config]:
126 del self.config[config]['note']
127 if not self.config[config]:
128 del self.config[config]
129
130 def remove(self, config : str, arch: str = None, flavour: str = None):
131 if config not in self.config:
132 return
133 if arch is not None:
134 if flavour is not None:
135 flavour = f'{arch}-{flavour}'
136 else:
137 flavour = arch
138 del self.config[config]['policy'][flavour]
139 if not self.config[config]['policy']:
140 self._remove_entry(config)
141 else:
142 self._remove_entry(config)
143
144 def set(self, config : str, arch: str = None, flavour: str = None,
145 value : str = None, note : str = None):
146 if value is not None:
147 if config not in self.config:
148 self.config[config] = { 'policy': {} }
149 if arch is not None:
150 if flavour is not None:
151 flavour = f'{arch}-{flavour}'
152 else:
153 flavour = arch
154 self.config[config]['policy'][flavour] = value
155 else:
156 for arch in self.arch:
157 self.config[config]['policy'][arch] = value
158 if note is not None:
159 self.config[config]['note'] = "'" + note.replace("'", '') + "'"
160
161 def update(self, c: KConfig, arch: str, flavour: str = None, configs: list = []):
162 """ Merge configs from a Kconfig object into Annotation object """
163
164 # Determine if we need to import all configs or a single config
165 if not configs:
166 configs = c.config.keys()
167 configs |= self.search_config(arch=arch, flavour=flavour).keys()
168
169 # Import configs from the Kconfig object into Annotations
170 if flavour is not None:
171 flavour = arch + f'-{flavour}'
172 else:
173 flavour = arch
174 for conf in configs:
175 if conf in c.config:
176 val = c.config[conf]
177 else:
178 val = '-'
179 if conf in self.config:
180 if 'policy' in self.config[conf]:
181 self.config[conf]['policy'][flavour] = val
182 else:
183 self.config[conf]['policy'] = {flavour: val}
184 else:
185 self.config[conf] = {'policy': {flavour: val}}
186
187 def _compact(self):
188 # Try to remove redundant settings: if the config value of a flavour is
189 # the same as the one of the main arch simply drop it.
190 for conf in self.config.copy():
191 if 'policy' not in self.config[conf]:
192 continue
193 for flavour in self.flavour:
194 if flavour not in self.config[conf]['policy']:
195 continue
196 m = re.match(r'^(.*?)-(.*)$', flavour)
197 if not m:
198 continue
199 arch = m.group(1)
200 if arch in self.config[conf]['policy']:
201 if self.config[conf]['policy'][flavour] == self.config[conf]['policy'][arch]:
202 del self.config[conf]['policy'][flavour]
203 continue
204 if flavour not in self.flavour_dep:
205 continue
206 generic = self.flavour_dep[flavour]
207 if generic in self.config[conf]['policy']:
208 if self.config[conf]['policy'][flavour] == self.config[conf]['policy'][generic]:
209 del self.config[conf]['policy'][flavour]
210 continue
211 # Remove rules for flavours / arches that are not supported (not
212 # listed in the annotations header).
213 for flavour in self.config[conf]['policy'].copy():
214 if flavour not in list(set(self.arch + self.flavour)):
215 del self.config[conf]['policy'][flavour]
216 # Drop empty rules
217 if not self.config[conf]['policy']:
218 del self.config[conf]
219 else:
220 # Compact same value across all flavour within the same arch
221 for arch in self.arch:
222 arch_flavours = [i for i in self.flavour if i.startswith(arch)]
223 value = None
224 for flavour in arch_flavours:
225 if flavour not in self.config[conf]['policy']:
226 break
227 elif value is None:
228 value = self.config[conf]['policy'][flavour]
229 elif value != self.config[conf]['policy'][flavour]:
230 break
231 else:
232 for flavour in arch_flavours:
233 del self.config[conf]['policy'][flavour]
234 self.config[conf]['policy'][arch] = value
235
236 def save(self, fname: str):
237 """ Save annotations data to the annotation file """
238 # Compact annotations structure
239 self._compact()
240
241 # Save annotations to disk
242 with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as tmp:
243 # Write header
244 tmp.write(self.header + '\n')
245
246 # Write includes
247 for i in self.include:
248 tmp.write(f'include "{i}"\n')
249 if self.include:
250 tmp.write("\n")
251
252 # Write config annotations and notes
253 tmp.flush()
254 shutil.copy(tmp.name, fname)
255 tmp_a = Annotation(fname)
256
257 # Only save local differences (preserve includes)
258 for conf in sorted(self.config):
259 old_val = tmp_a.config[conf] if conf in tmp_a.config else None
260 new_val = self.config[conf]
261 # If new_val is a subset of old_val, skip it
262 if old_val and 'policy' in old_val and 'policy' in new_val:
263 if old_val['policy'] == old_val['policy'] | new_val['policy']:
264 continue
265 if 'policy' in new_val:
266 val = dict(sorted(new_val['policy'].items()))
267 line = f"{conf : <47} policy<{val}>"
268 tmp.write(line + "\n")
269 if 'note' in new_val:
270 val = new_val['note']
271 line = f"{conf : <47} note<{val}>"
272 tmp.write(line + "\n\n")
273
274 # Replace annotations with the updated version
275 tmp.flush()
276 shutil.move(tmp.name, fname)
277
278 def search_config(self, config: str = None, arch: str = None, flavour: str = None) -> dict:
279 """ Return config value of a specific config option or architecture """
280 if flavour is None:
281 flavour = 'generic'
282 flavour = f'{arch}-{flavour}'
283 if flavour in self.flavour_dep:
284 generic = self.flavour_dep[flavour]
285 else:
286 generic = flavour
287 if config is None and arch is None:
288 # Get all config options for all architectures
289 return self.config
290 elif config is None and arch is not None:
291 # Get config options of a specific architecture
292 ret = {}
293 for c in self.config:
294 if not 'policy' in self.config[c]:
295 continue
296 if flavour in self.config[c]['policy']:
297 ret[c] = self.config[c]['policy'][flavour]
298 elif generic != flavour and generic in self.config[c]['policy']:
299 ret[c] = self.config[c]['policy'][generic]
300 elif arch in self.config[c]['policy']:
301 ret[c] = self.config[c]['policy'][arch]
302 return ret
303 elif config is not None and arch is None:
304 # Get a specific config option for all architectures
305 return self.config[config] if config in self.config else None
306 elif config is not None and arch is not None:
307 # Get a specific config option for a specific architecture
308 if config in self.config:
309 if 'policy' in self.config[config]:
310 if flavour in self.config[config]['policy']:
311 return {config: self.config[config]['policy'][flavour]}
312 elif generic != flavour and generic in self.config[config]['policy']:
313 return {config: self.config[config]['policy'][generic]}
314 elif arch in self.config[config]['policy']:
315 return {config: self.config[config]['policy'][arch]}
316 return None
317
318 @staticmethod
319 def to_config(data: dict) -> str:
320 """ Convert annotations data to .config format """
321 s = ''
322 for c in data:
323 v = data[c]
324 if v == 'n':
325 s += f"# {c} is not set\n"
326 elif v == '-':
327 pass
328 else:
329 s += f"{c}={v}\n"
330 return s.rstrip()