blob: ed22b777d624ceb0e5a7199b669933d597ea31e7 [file] [log] [blame]
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +01001#!/usr/bin/env python3
2#
3# Copyright (c) 2019, Nordic Semiconductor ASA
4#
5# SPDX-License-Identifier: Apache-2.0
6
7'''Tool for parsing a list of projects to determine if they are Zephyr
8projects. If no projects are given then the output from `west list` will be
9used as project list.
10
11Include file is generated for Kconfig using --kconfig-out.
12A <name>:<path> text file is generated for use with CMake using --cmake-out.
Anas Nashif286a9ed2019-12-11 10:13:23 -050013
Anas Nashif54713982020-12-07 14:52:10 -050014Using --twister-out <filename> an argument file for twister script will
Anas Nashif286a9ed2019-12-11 10:13:23 -050015be generated which would point to test and sample roots available in modules
Anas Nashif54713982020-12-07 14:52:10 -050016that can be included during a twister run. This allows testing code
Anas Nashif286a9ed2019-12-11 10:13:23 -050017maintained in modules in addition to what is available in the main Zephyr tree.
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010018'''
19
20import argparse
21import os
Torsten Rasmussen3d880832021-01-19 12:01:38 +010022import re
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010023import sys
24import yaml
25import pykwalify.core
Torsten Rasmussenb3da9ef2019-12-13 09:42:13 +010026from pathlib import Path, PurePath
Torsten Rasmussen3917ee52020-05-18 22:34:49 +020027from collections import namedtuple
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010028
29METADATA_SCHEMA = '''
30## A pykwalify schema for basic validation of the structure of a
31## metadata YAML file.
32##
33# The zephyr/module.yml file is a simple list of key value pairs to be used by
34# the build system.
35type: map
36mapping:
Torsten Rasmussenf24f8832021-01-07 15:27:53 +010037 name:
38 required: false
39 type: str
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010040 build:
Anas Nashif286a9ed2019-12-11 10:13:23 -050041 required: false
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010042 type: map
43 mapping:
44 cmake:
45 required: false
46 type: str
47 kconfig:
48 required: false
49 type: str
Torsten Rasmussen3673e282020-12-17 11:27:42 +010050 cmake-ext:
51 required: false
52 type: bool
53 default: false
54 kconfig-ext:
55 required: false
56 type: bool
57 default: false
Torsten Rasmussen3917ee52020-05-18 22:34:49 +020058 depends:
59 required: false
60 type: seq
61 sequence:
62 - type: str
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +020063 settings:
64 required: false
65 type: map
66 mapping:
67 board_root:
68 required: false
69 type: str
70 dts_root:
71 required: false
72 type: str
73 soc_root:
74 required: false
75 type: str
76 arch_root:
77 required: false
78 type: str
Torsten Rasmussen3673e282020-12-17 11:27:42 +010079 module_ext_root:
80 required: false
81 type: str
Anas Nashif286a9ed2019-12-11 10:13:23 -050082 tests:
83 required: false
84 type: seq
85 sequence:
86 - type: str
87 samples:
88 required: false
89 type: seq
90 sequence:
91 - type: str
92 boards:
93 required: false
94 type: seq
95 sequence:
96 - type: str
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010097'''
98
99schema = yaml.safe_load(METADATA_SCHEMA)
100
101
102def validate_setting(setting, module_path, filename=None):
103 if setting is not None:
104 if filename is not None:
105 checkfile = os.path.join(module_path, setting, filename)
106 else:
107 checkfile = os.path.join(module_path, setting)
108 if not os.path.isfile(checkfile):
109 return False
110 return True
111
112
Anas Nashif286a9ed2019-12-11 10:13:23 -0500113def process_module(module):
Torsten Rasmussenb3da9ef2019-12-13 09:42:13 +0100114 module_path = PurePath(module)
115 module_yml = module_path.joinpath('zephyr/module.yml')
Anas Nashif286a9ed2019-12-11 10:13:23 -0500116
Sebastian Bøebb95dce2019-12-19 15:49:22 +0100117 # The input is a module if zephyr/module.yml is a valid yaml file
118 # or if both zephyr/CMakeLists.txt and zephyr/Kconfig are present.
119
Torsten Rasmussenb3da9ef2019-12-13 09:42:13 +0100120 if Path(module_yml).is_file():
121 with Path(module_yml).open('r') as f:
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100122 meta = yaml.safe_load(f.read())
123
124 try:
125 pykwalify.core.Core(source_data=meta, schema_data=schema)\
126 .validate()
127 except pykwalify.errors.SchemaError as e:
Ulf Magnusson50b9b122019-09-07 14:41:01 +0200128 sys.exit('ERROR: Malformed "build" section in file: {}\n{}'
Torsten Rasmussenb3da9ef2019-12-13 09:42:13 +0100129 .format(module_yml.as_posix(), e))
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100130
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100131 meta['name'] = meta.get('name', module_path.name)
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100132 meta['name-sanitized'] = re.sub('[^a-zA-Z0-9]', '_', meta['name'])
Anas Nashif286a9ed2019-12-11 10:13:23 -0500133 return meta
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100134
Sebastian Bøebb95dce2019-12-19 15:49:22 +0100135 if Path(module_path.joinpath('zephyr/CMakeLists.txt')).is_file() and \
136 Path(module_path.joinpath('zephyr/Kconfig')).is_file():
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100137 return {'name': module_path.name,
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100138 'name-sanitized': re.sub('[^a-zA-Z0-9]', '_', module_path.name),
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100139 'build': {'cmake': 'zephyr', 'kconfig': 'zephyr/Kconfig'}}
Sebastian Bøebb95dce2019-12-19 15:49:22 +0100140
Anas Nashif286a9ed2019-12-11 10:13:23 -0500141 return None
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100142
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100143
Anas Nashif286a9ed2019-12-11 10:13:23 -0500144def process_cmake(module, meta):
145 section = meta.get('build', dict())
146 module_path = PurePath(module)
147 module_yml = module_path.joinpath('zephyr/module.yml')
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100148
149 cmake_extern = section.get('cmake-ext', False)
150 if cmake_extern:
151 return('\"{}\":\"{}\":\"{}\"\n'
Andrzej Głąbek02819482021-01-19 07:39:47 +0100152 .format(meta['name'],
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100153 module_path.as_posix(),
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100154 "${ZEPHYR_" + meta['name-sanitized'].upper() + "_CMAKE_DIR}"))
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100155
Anas Nashif286a9ed2019-12-11 10:13:23 -0500156 cmake_setting = section.get('cmake', None)
157 if not validate_setting(cmake_setting, module, 'CMakeLists.txt'):
158 sys.exit('ERROR: "cmake" key in {} has folder value "{}" which '
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200159 'does not contain a CMakeLists.txt file.'
160 .format(module_yml.as_posix(), cmake_setting))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500161
162 cmake_path = os.path.join(module, cmake_setting or 'zephyr')
163 cmake_file = os.path.join(cmake_path, 'CMakeLists.txt')
164 if os.path.isfile(cmake_file):
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200165 return('\"{}\":\"{}\":\"{}\"\n'
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100166 .format(meta['name'],
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200167 module_path.as_posix(),
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200168 Path(cmake_path).resolve().as_posix()))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500169 else:
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200170 return('\"{}\":\"{}\":\"\"\n'
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100171 .format(meta['name'],
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200172 module_path.as_posix()))
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200173
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100174
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200175def process_settings(module, meta):
176 section = meta.get('build', dict())
177 build_settings = section.get('settings', None)
178 out_text = ""
179
180 if build_settings is not None:
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100181 for root in ['board', 'dts', 'soc', 'arch', 'module_ext']:
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200182 setting = build_settings.get(root+'_root', None)
183 if setting is not None:
184 root_path = PurePath(module) / setting
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100185 out_text += f'"{root.upper()}_ROOT":'
186 out_text += f'"{root_path.as_posix()}"\n'
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200187
188 return out_text
189
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200190
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100191def kconfig_snippet(meta, path, kconfig_file=None):
192 name = meta['name']
193 name_sanitized = meta['name-sanitized']
194
Andrzej Głąbek02819482021-01-19 07:39:47 +0100195 snippet = (f'menu "{name} ({path})"',
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100196 f'osource "{kconfig_file.resolve().as_posix()}"' if kconfig_file
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100197 else f'osource "$(ZEPHYR_{name_sanitized.upper()}_KCONFIG)"',
198 f'config ZEPHYR_{name_sanitized.upper()}_MODULE',
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100199 ' bool',
200 ' default y',
201 'endmenu\n')
202 return '\n'.join(snippet)
203
204
Anas Nashif286a9ed2019-12-11 10:13:23 -0500205def process_kconfig(module, meta):
206 section = meta.get('build', dict())
207 module_path = PurePath(module)
208 module_yml = module_path.joinpath('zephyr/module.yml')
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100209 kconfig_extern = section.get('kconfig-ext', False)
210 if kconfig_extern:
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100211 return kconfig_snippet(meta, module_path)
Anas Nashif286a9ed2019-12-11 10:13:23 -0500212
213 kconfig_setting = section.get('kconfig', None)
214 if not validate_setting(kconfig_setting, module):
215 sys.exit('ERROR: "kconfig" key in {} has value "{}" which does '
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200216 'not point to a valid Kconfig file.'
217 .format(module_yml, kconfig_setting))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500218
219 kconfig_file = os.path.join(module, kconfig_setting or 'zephyr/Kconfig')
220 if os.path.isfile(kconfig_file):
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100221 return kconfig_snippet(meta, module_path, Path(kconfig_file))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500222 else:
223 return ""
224
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100225
Anas Nashif54713982020-12-07 14:52:10 -0500226def process_twister(module, meta):
Anas Nashif286a9ed2019-12-11 10:13:23 -0500227
228 out = ""
229 tests = meta.get('tests', [])
230 samples = meta.get('samples', [])
231 boards = meta.get('boards', [])
232
233 for pth in tests + samples:
234 if pth:
235 dir = os.path.join(module, pth)
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200236 out += '-T\n{}\n'.format(PurePath(os.path.abspath(dir))
237 .as_posix())
Anas Nashif286a9ed2019-12-11 10:13:23 -0500238
239 for pth in boards:
240 if pth:
241 dir = os.path.join(module, pth)
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200242 out += '--board-root\n{}\n'.format(PurePath(os.path.abspath(dir))
243 .as_posix())
Anas Nashif286a9ed2019-12-11 10:13:23 -0500244
245 return out
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100246
247
248def main():
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100249 parser = argparse.ArgumentParser(description='''
250 Process a list of projects and create Kconfig / CMake include files for
251 projects which are also a Zephyr module''')
252
253 parser.add_argument('--kconfig-out',
Anas Nashif286a9ed2019-12-11 10:13:23 -0500254 help="""File to write with resulting KConfig import
255 statements.""")
Anas Nashif54713982020-12-07 14:52:10 -0500256 parser.add_argument('--twister-out',
257 help="""File to write with resulting twister
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200258 parameters.""")
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100259 parser.add_argument('--cmake-out',
Anas Nashif286a9ed2019-12-11 10:13:23 -0500260 help="""File to write with resulting <name>:<path>
261 values to use for including in CMake""")
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200262 parser.add_argument('--settings-out',
263 help="""File to write with resulting <name>:<value>
264 values to use for including in CMake""")
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100265 parser.add_argument('-m', '--modules', nargs='+',
Anas Nashif286a9ed2019-12-11 10:13:23 -0500266 help="""List of modules to parse instead of using `west
267 list`""")
Martí Bolívar34346c42020-01-29 12:38:58 -0800268 parser.add_argument('-x', '--extra-modules', nargs='+', default=[],
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100269 help='List of extra modules to parse')
Torsten Rasmussen39cd4c82020-02-19 12:01:39 +0100270 parser.add_argument('-z', '--zephyr-base',
271 help='Path to zephyr repository')
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100272 args = parser.parse_args()
273
274 if args.modules is None:
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200275 # West is imported here, as it is optional
276 # (and thus maybe not installed)
Torsten Rasmussenef3c5e52020-06-08 21:09:15 +0200277 # if user is providing a specific modules list.
278 from west.manifest import Manifest
279 from west.util import WestNotFound
Torsten Rasmussen0cafde62021-01-07 13:17:41 +0100280 from west.version import __version__ as WestVersion
281 from packaging import version
Torsten Rasmussenef3c5e52020-06-08 21:09:15 +0200282 try:
283 manifest = Manifest.from_file()
Torsten Rasmussen0cafde62021-01-07 13:17:41 +0100284 if version.parse(WestVersion) >= version.parse('0.9.0'):
285 projects = [p.posixpath for p in manifest.get_projects([])
286 if manifest.is_active(p)]
287 else:
288 projects = [p.posixpath for p in manifest.get_projects([])]
Torsten Rasmussenef3c5e52020-06-08 21:09:15 +0200289 except WestNotFound:
290 # Only accept WestNotFound, meaning we are not in a west
291 # workspace. Such setup is allowed, as west may be installed
292 # but the project is not required to use west.
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100293 projects = []
Carles Cufi02dea922020-07-20 14:09:12 +0200294 else:
295 projects = args.modules.copy()
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100296
Martí Bolívar34346c42020-01-29 12:38:58 -0800297 projects += args.extra_modules
298 extra_modules = set(args.extra_modules)
Anas Nashif286a9ed2019-12-11 10:13:23 -0500299
300 kconfig = ""
301 cmake = ""
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200302 settings = ""
Anas Nashif54713982020-12-07 14:52:10 -0500303 twister = ""
Anas Nashif286a9ed2019-12-11 10:13:23 -0500304
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200305 Module = namedtuple('Module', ['project', 'meta', 'depends'])
306 # dep_modules is a list of all modules that has an unresolved dependency
307 dep_modules = []
308 # start_modules is a list modules with no depends left (no incoming edge)
309 start_modules = []
310 # sorted_modules is a topological sorted list of the modules
311 sorted_modules = []
312
Anas Nashif286a9ed2019-12-11 10:13:23 -0500313 for project in projects:
314 # Avoid including Zephyr base project as module.
Torsten Rasmussenef3c5e52020-06-08 21:09:15 +0200315 if project == args.zephyr_base:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500316 continue
317
318 meta = process_module(project)
319 if meta:
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200320 section = meta.get('build', dict())
321 deps = section.get('depends', [])
322 if not deps:
323 start_modules.append(Module(project, meta, []))
324 else:
325 dep_modules.append(Module(project, meta, deps))
Martí Bolívar34346c42020-01-29 12:38:58 -0800326 elif project in extra_modules:
327 sys.exit(f'{project}, given in ZEPHYR_EXTRA_MODULES, '
328 'is not a valid zephyr module')
Anas Nashif286a9ed2019-12-11 10:13:23 -0500329
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200330 # This will do a topological sort to ensure the modules are ordered
331 # according to dependency settings.
332 while start_modules:
333 node = start_modules.pop(0)
334 sorted_modules.append(node)
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100335 node_name = node.meta['name']
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200336 to_remove = []
337 for module in dep_modules:
338 if node_name in module.depends:
339 module.depends.remove(node_name)
340 if not module.depends:
341 start_modules.append(module)
342 to_remove.append(module)
343 for module in to_remove:
344 dep_modules.remove(module)
345
346 if dep_modules:
347 # If there are any modules with unresolved dependencies, then the
348 # modules contains unmet or cyclic dependencies. Error out.
349 error = 'Unmet or cyclic dependencies in modules:\n'
350 for module in dep_modules:
351 error += f'{module.project} depends on: {module.depends}\n'
352 sys.exit(error)
353
354 for module in sorted_modules:
355 kconfig += process_kconfig(module.project, module.meta)
356 cmake += process_cmake(module.project, module.meta)
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200357 settings += process_settings(module.project, module.meta)
Anas Nashif54713982020-12-07 14:52:10 -0500358 twister += process_twister(module.project, module.meta)
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200359
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100360 if args.kconfig_out:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500361 with open(args.kconfig_out, 'w', encoding="utf-8") as fp:
362 fp.write(kconfig)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100363
364 if args.cmake_out:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500365 with open(args.cmake_out, 'w', encoding="utf-8") as fp:
366 fp.write(cmake)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100367
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200368 if args.settings_out:
369 with open(args.settings_out, 'w', encoding="utf-8") as fp:
370 fp.write(settings)
371
Anas Nashif54713982020-12-07 14:52:10 -0500372 if args.twister_out:
373 with open(args.twister_out, 'w', encoding="utf-8") as fp:
374 fp.write(twister)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100375
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200376
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100377if __name__ == "__main__":
378 main()