]>
Commit | Line | Data |
---|---|---|
3c08e8b8 MY |
1 | #!/usr/bin/env python |
2 | # | |
3 | # Author: Masahiro Yamada <[email protected]> | |
4 | # | |
5 | # SPDX-License-Identifier: GPL-2.0+ | |
6 | # | |
7 | ||
8 | """ | |
9 | Converter from Kconfig and MAINTAINERS to boards.cfg | |
10 | ||
11 | Run 'tools/genboardscfg.py' to create boards.cfg file. | |
12 | ||
13 | Run 'tools/genboardscfg.py -h' for available options. | |
14 | """ | |
15 | ||
16 | import errno | |
17 | import fnmatch | |
18 | import glob | |
19 | import optparse | |
20 | import os | |
21 | import re | |
22 | import shutil | |
23 | import subprocess | |
24 | import sys | |
25 | import tempfile | |
26 | import time | |
27 | ||
28 | BOARD_FILE = 'boards.cfg' | |
29 | CONFIG_DIR = 'configs' | |
30 | REFORMAT_CMD = [os.path.join('tools', 'reformat.py'), | |
31 | '-i', '-d', '-', '-s', '8'] | |
32 | SHOW_GNU_MAKE = 'scripts/show-gnu-make' | |
33 | SLEEP_TIME=0.03 | |
34 | ||
35 | COMMENT_BLOCK = '''# | |
36 | # List of boards | |
37 | # Automatically generated by %s: don't edit | |
38 | # | |
ca418dd7 | 39 | # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers |
3c08e8b8 MY |
40 | |
41 | ''' % __file__ | |
42 | ||
43 | ### helper functions ### | |
44 | def get_terminal_columns(): | |
45 | """Get the width of the terminal. | |
46 | ||
47 | Returns: | |
48 | The width of the terminal, or zero if the stdout is not | |
49 | associated with tty. | |
50 | """ | |
51 | try: | |
52 | return shutil.get_terminal_size().columns # Python 3.3~ | |
53 | except AttributeError: | |
54 | import fcntl | |
55 | import termios | |
56 | import struct | |
57 | arg = struct.pack('hhhh', 0, 0, 0, 0) | |
58 | try: | |
59 | ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg) | |
60 | except IOError as exception: | |
3c08e8b8 MY |
61 | # If 'Inappropriate ioctl for device' error occurs, |
62 | # stdout is probably redirected. Return 0. | |
63 | return 0 | |
64 | return struct.unpack('hhhh', ret)[1] | |
65 | ||
66 | def get_devnull(): | |
67 | """Get the file object of '/dev/null' device.""" | |
68 | try: | |
69 | devnull = subprocess.DEVNULL # py3k | |
70 | except AttributeError: | |
71 | devnull = open(os.devnull, 'wb') | |
72 | return devnull | |
73 | ||
74 | def check_top_directory(): | |
75 | """Exit if we are not at the top of source directory.""" | |
76 | for f in ('README', 'Licenses'): | |
77 | if not os.path.exists(f): | |
31e2141d | 78 | sys.exit('Please run at the top of source directory.') |
3c08e8b8 MY |
79 | |
80 | def get_make_cmd(): | |
81 | """Get the command name of GNU Make.""" | |
82 | process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE) | |
83 | ret = process.communicate() | |
84 | if process.returncode: | |
31e2141d | 85 | sys.exit('GNU Make not found') |
3c08e8b8 MY |
86 | return ret[0].rstrip() |
87 | ||
88 | ### classes ### | |
89 | class MaintainersDatabase: | |
90 | ||
91 | """The database of board status and maintainers.""" | |
92 | ||
93 | def __init__(self): | |
94 | """Create an empty database.""" | |
95 | self.database = {} | |
96 | ||
97 | def get_status(self, target): | |
98 | """Return the status of the given board. | |
99 | ||
100 | Returns: | |
101 | Either 'Active' or 'Orphan' | |
102 | """ | |
b8828e8f MY |
103 | if not target in self.database: |
104 | print >> sys.stderr, "WARNING: no status info for '%s'" % target | |
105 | return '-' | |
106 | ||
3c08e8b8 MY |
107 | tmp = self.database[target][0] |
108 | if tmp.startswith('Maintained'): | |
109 | return 'Active' | |
110 | elif tmp.startswith('Orphan'): | |
111 | return 'Orphan' | |
112 | else: | |
b8828e8f MY |
113 | print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" % |
114 | (tmp, target)) | |
115 | return '-' | |
3c08e8b8 MY |
116 | |
117 | def get_maintainers(self, target): | |
118 | """Return the maintainers of the given board. | |
119 | ||
120 | If the board has two or more maintainers, they are separated | |
121 | with colons. | |
122 | """ | |
b8828e8f MY |
123 | if not target in self.database: |
124 | print >> sys.stderr, "WARNING: no maintainers for '%s'" % target | |
125 | return '' | |
126 | ||
3c08e8b8 MY |
127 | return ':'.join(self.database[target][1]) |
128 | ||
129 | def parse_file(self, file): | |
130 | """Parse the given MAINTAINERS file. | |
131 | ||
132 | This method parses MAINTAINERS and add board status and | |
133 | maintainers information to the database. | |
134 | ||
135 | Arguments: | |
136 | file: MAINTAINERS file to be parsed | |
137 | """ | |
138 | targets = [] | |
139 | maintainers = [] | |
140 | status = '-' | |
141 | for line in open(file): | |
142 | tag, rest = line[:2], line[2:].strip() | |
143 | if tag == 'M:': | |
144 | maintainers.append(rest) | |
145 | elif tag == 'F:': | |
146 | # expand wildcard and filter by 'configs/*_defconfig' | |
147 | for f in glob.glob(rest): | |
148 | front, match, rear = f.partition('configs/') | |
149 | if not front and match: | |
150 | front, match, rear = rear.rpartition('_defconfig') | |
151 | if match and not rear: | |
152 | targets.append(front) | |
153 | elif tag == 'S:': | |
154 | status = rest | |
9c2d60c3 | 155 | elif line == '\n': |
3c08e8b8 MY |
156 | for target in targets: |
157 | self.database[target] = (status, maintainers) | |
158 | targets = [] | |
159 | maintainers = [] | |
160 | status = '-' | |
161 | if targets: | |
162 | for target in targets: | |
163 | self.database[target] = (status, maintainers) | |
164 | ||
165 | class DotConfigParser: | |
166 | ||
167 | """A parser of .config file. | |
168 | ||
169 | Each line of the output should have the form of: | |
170 | Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers | |
171 | Most of them are extracted from .config file. | |
172 | MAINTAINERS files are also consulted for Status and Maintainers fields. | |
173 | """ | |
174 | ||
175 | re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"') | |
176 | re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"') | |
177 | re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"') | |
178 | re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"') | |
179 | re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"') | |
180 | re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"') | |
181 | re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"') | |
182 | re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc), | |
183 | ('vendor', re_vendor), ('board', re_board), | |
184 | ('config', re_config), ('options', re_options)) | |
185 | must_fields = ('arch', 'config') | |
186 | ||
187 | def __init__(self, build_dir, output, maintainers_database): | |
188 | """Create a new .config perser. | |
189 | ||
190 | Arguments: | |
191 | build_dir: Build directory where .config is located | |
192 | output: File object which the result is written to | |
193 | maintainers_database: An instance of class MaintainersDatabase | |
194 | """ | |
195 | self.dotconfig = os.path.join(build_dir, '.config') | |
196 | self.output = output | |
197 | self.database = maintainers_database | |
198 | ||
199 | def parse(self, defconfig): | |
200 | """Parse .config file and output one-line database for the given board. | |
201 | ||
202 | Arguments: | |
203 | defconfig: Board (defconfig) name | |
204 | """ | |
205 | fields = {} | |
206 | for line in open(self.dotconfig): | |
207 | if not line.startswith('CONFIG_SYS_'): | |
208 | continue | |
209 | for (key, pattern) in self.re_list: | |
210 | m = pattern.match(line) | |
211 | if m and m.group(1): | |
212 | fields[key] = m.group(1) | |
213 | break | |
214 | ||
215 | # sanity check of '.config' file | |
216 | for field in self.must_fields: | |
217 | if not field in fields: | |
13246f48 MY |
218 | print >> sys.stderr, ( |
219 | "WARNING: '%s' is not defined in '%s'. Skip." % | |
220 | (field, defconfig)) | |
221 | return | |
3c08e8b8 | 222 | |
ca418dd7 | 223 | # fix-up for aarch64 |
3c08e8b8 MY |
224 | if fields['arch'] == 'arm' and 'cpu' in fields: |
225 | if fields['cpu'] == 'armv8': | |
226 | fields['arch'] = 'aarch64' | |
3c08e8b8 MY |
227 | |
228 | target, match, rear = defconfig.partition('_defconfig') | |
229 | assert match and not rear, \ | |
230 | '%s : invalid defconfig file name' % defconfig | |
231 | ||
232 | fields['status'] = self.database.get_status(target) | |
233 | fields['maintainers'] = self.database.get_maintainers(target) | |
234 | ||
235 | if 'options' in fields: | |
236 | options = fields['config'] + ':' + \ | |
237 | fields['options'].replace(r'\"', '"') | |
238 | elif fields['config'] != target: | |
239 | options = fields['config'] | |
240 | else: | |
241 | options = '-' | |
242 | ||
243 | self.output.write((' '.join(['%s'] * 9) + '\n') % | |
244 | (fields['status'], | |
245 | fields['arch'], | |
246 | fields.get('cpu', '-'), | |
247 | fields.get('soc', '-'), | |
248 | fields.get('vendor', '-'), | |
249 | fields.get('board', '-'), | |
250 | target, | |
251 | options, | |
252 | fields['maintainers'])) | |
253 | ||
254 | class Slot: | |
255 | ||
256 | """A slot to store a subprocess. | |
257 | ||
258 | Each instance of this class handles one subprocess. | |
259 | This class is useful to control multiple processes | |
260 | for faster processing. | |
261 | """ | |
262 | ||
263 | def __init__(self, output, maintainers_database, devnull, make_cmd): | |
264 | """Create a new slot. | |
265 | ||
266 | Arguments: | |
267 | output: File object which the result is written to | |
268 | maintainers_database: An instance of class MaintainersDatabase | |
269 | """ | |
270 | self.occupied = False | |
271 | self.build_dir = tempfile.mkdtemp() | |
272 | self.devnull = devnull | |
273 | self.make_cmd = make_cmd | |
274 | self.parser = DotConfigParser(self.build_dir, output, | |
275 | maintainers_database) | |
276 | ||
277 | def __del__(self): | |
278 | """Delete the working directory""" | |
d6538d22 MY |
279 | if not self.occupied: |
280 | while self.ps.poll() == None: | |
281 | pass | |
3c08e8b8 MY |
282 | shutil.rmtree(self.build_dir) |
283 | ||
284 | def add(self, defconfig): | |
285 | """Add a new subprocess to the slot. | |
286 | ||
287 | Fails if the slot is occupied, that is, the current subprocess | |
288 | is still running. | |
289 | ||
290 | Arguments: | |
291 | defconfig: Board (defconfig) name | |
292 | ||
293 | Returns: | |
294 | Return True on success or False on fail | |
295 | """ | |
296 | if self.occupied: | |
297 | return False | |
298 | o = 'O=' + self.build_dir | |
299 | self.ps = subprocess.Popen([self.make_cmd, o, defconfig], | |
300 | stdout=self.devnull) | |
301 | self.defconfig = defconfig | |
302 | self.occupied = True | |
303 | return True | |
304 | ||
305 | def poll(self): | |
306 | """Check if the subprocess is running and invoke the .config | |
307 | parser if the subprocess is terminated. | |
308 | ||
309 | Returns: | |
310 | Return True if the subprocess is terminated, False otherwise | |
311 | """ | |
312 | if not self.occupied: | |
313 | return True | |
314 | if self.ps.poll() == None: | |
315 | return False | |
13246f48 MY |
316 | if self.ps.poll() == 0: |
317 | self.parser.parse(self.defconfig) | |
318 | else: | |
319 | print >> sys.stderr, ("WARNING: failed to process '%s'. skip." % | |
320 | self.defconfig) | |
3c08e8b8 MY |
321 | self.occupied = False |
322 | return True | |
323 | ||
324 | class Slots: | |
325 | ||
326 | """Controller of the array of subprocess slots.""" | |
327 | ||
328 | def __init__(self, jobs, output, maintainers_database): | |
329 | """Create a new slots controller. | |
330 | ||
331 | Arguments: | |
332 | jobs: A number of slots to instantiate | |
333 | output: File object which the result is written to | |
334 | maintainers_database: An instance of class MaintainersDatabase | |
335 | """ | |
336 | self.slots = [] | |
337 | devnull = get_devnull() | |
338 | make_cmd = get_make_cmd() | |
339 | for i in range(jobs): | |
340 | self.slots.append(Slot(output, maintainers_database, | |
341 | devnull, make_cmd)) | |
342 | ||
343 | def add(self, defconfig): | |
344 | """Add a new subprocess if a vacant slot is available. | |
345 | ||
346 | Arguments: | |
347 | defconfig: Board (defconfig) name | |
348 | ||
349 | Returns: | |
350 | Return True on success or False on fail | |
351 | """ | |
352 | for slot in self.slots: | |
353 | if slot.add(defconfig): | |
354 | return True | |
355 | return False | |
356 | ||
357 | def available(self): | |
358 | """Check if there is a vacant slot. | |
359 | ||
360 | Returns: | |
361 | Return True if a vacant slot is found, False if all slots are full | |
362 | """ | |
363 | for slot in self.slots: | |
364 | if slot.poll(): | |
365 | return True | |
366 | return False | |
367 | ||
368 | def empty(self): | |
369 | """Check if all slots are vacant. | |
370 | ||
371 | Returns: | |
372 | Return True if all slots are vacant, False if at least one slot | |
373 | is running | |
374 | """ | |
375 | ret = True | |
376 | for slot in self.slots: | |
377 | if not slot.poll(): | |
378 | ret = False | |
379 | return ret | |
380 | ||
381 | class Indicator: | |
382 | ||
383 | """A class to control the progress indicator.""" | |
384 | ||
385 | MIN_WIDTH = 15 | |
386 | MAX_WIDTH = 70 | |
387 | ||
388 | def __init__(self, total): | |
389 | """Create an instance. | |
390 | ||
391 | Arguments: | |
392 | total: A number of boards | |
393 | """ | |
394 | self.total = total | |
395 | self.cur = 0 | |
396 | width = get_terminal_columns() | |
397 | width = min(width, self.MAX_WIDTH) | |
398 | width -= self.MIN_WIDTH | |
399 | if width > 0: | |
400 | self.enabled = True | |
401 | else: | |
402 | self.enabled = False | |
403 | self.width = width | |
404 | ||
405 | def inc(self): | |
406 | """Increment the counter and show the progress bar.""" | |
407 | if not self.enabled: | |
408 | return | |
409 | self.cur += 1 | |
410 | arrow_len = self.width * self.cur // self.total | |
411 | msg = '%4d/%d [' % (self.cur, self.total) | |
412 | msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']' | |
413 | sys.stdout.write('\r' + msg) | |
414 | sys.stdout.flush() | |
415 | ||
416 | def __gen_boards_cfg(jobs): | |
417 | """Generate boards.cfg file. | |
418 | ||
419 | Arguments: | |
420 | jobs: The number of jobs to run simultaneously | |
421 | ||
422 | Note: | |
5b12b7a1 | 423 | The incomplete boards.cfg is left over when an error (including |
3c08e8b8 MY |
424 | the termination by the keyboard interrupt) occurs on the halfway. |
425 | """ | |
426 | check_top_directory() | |
427 | print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs) | |
428 | ||
429 | # All the defconfig files to be processed | |
430 | defconfigs = [] | |
431 | for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR): | |
432 | dirpath = dirpath[len(CONFIG_DIR) + 1:] | |
433 | for filename in fnmatch.filter(filenames, '*_defconfig'): | |
04b43f32 MY |
434 | if fnmatch.fnmatch(filename, '.*'): |
435 | continue | |
3c08e8b8 MY |
436 | defconfigs.append(os.path.join(dirpath, filename)) |
437 | ||
438 | # Parse all the MAINTAINERS files | |
439 | maintainers_database = MaintainersDatabase() | |
440 | for (dirpath, dirnames, filenames) in os.walk('.'): | |
441 | if 'MAINTAINERS' in filenames: | |
442 | maintainers_database.parse_file(os.path.join(dirpath, | |
443 | 'MAINTAINERS')) | |
444 | ||
445 | # Output lines should be piped into the reformat tool | |
446 | reformat_process = subprocess.Popen(REFORMAT_CMD, stdin=subprocess.PIPE, | |
447 | stdout=open(BOARD_FILE, 'w')) | |
448 | pipe = reformat_process.stdin | |
449 | pipe.write(COMMENT_BLOCK) | |
450 | ||
451 | indicator = Indicator(len(defconfigs)) | |
452 | slots = Slots(jobs, pipe, maintainers_database) | |
453 | ||
454 | # Main loop to process defconfig files: | |
455 | # Add a new subprocess into a vacant slot. | |
456 | # Sleep if there is no available slot. | |
457 | for defconfig in defconfigs: | |
458 | while not slots.add(defconfig): | |
459 | while not slots.available(): | |
460 | # No available slot: sleep for a while | |
461 | time.sleep(SLEEP_TIME) | |
462 | indicator.inc() | |
463 | ||
464 | # wait until all the subprocesses finish | |
465 | while not slots.empty(): | |
466 | time.sleep(SLEEP_TIME) | |
467 | print '' | |
468 | ||
469 | # wait until the reformat tool finishes | |
470 | reformat_process.communicate() | |
471 | if reformat_process.returncode != 0: | |
31e2141d | 472 | sys.exit('"%s" failed' % REFORMAT_CMD[0]) |
3c08e8b8 MY |
473 | |
474 | def gen_boards_cfg(jobs): | |
475 | """Generate boards.cfg file. | |
476 | ||
477 | The incomplete boards.cfg is deleted if an error (including | |
478 | the termination by the keyboard interrupt) occurs on the halfway. | |
479 | ||
480 | Arguments: | |
481 | jobs: The number of jobs to run simultaneously | |
482 | """ | |
483 | try: | |
484 | __gen_boards_cfg(jobs) | |
485 | except: | |
486 | # We should remove incomplete boards.cfg | |
487 | try: | |
488 | os.remove(BOARD_FILE) | |
489 | except OSError as exception: | |
490 | # Ignore 'No such file or directory' error | |
491 | if exception.errno != errno.ENOENT: | |
492 | raise | |
493 | raise | |
494 | ||
495 | def main(): | |
496 | parser = optparse.OptionParser() | |
497 | # Add options here | |
498 | parser.add_option('-j', '--jobs', | |
499 | help='the number of jobs to run simultaneously') | |
500 | (options, args) = parser.parse_args() | |
501 | if options.jobs: | |
502 | try: | |
503 | jobs = int(options.jobs) | |
504 | except ValueError: | |
31e2141d | 505 | sys.exit('Option -j (--jobs) takes a number') |
3c08e8b8 MY |
506 | else: |
507 | try: | |
508 | jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'], | |
509 | stdout=subprocess.PIPE).communicate()[0]) | |
510 | except (OSError, ValueError): | |
511 | print 'info: failed to get the number of CPUs. Set jobs to 1' | |
512 | jobs = 1 | |
513 | gen_boards_cfg(jobs) | |
514 | ||
515 | if __name__ == '__main__': | |
516 | main() |