blob: c6043154bdcefe603662d6e60f2efbb0f3ffd617 [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
Carles Cufib662bc92022-08-24 15:42:55 +020021import hashlib
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010022import os
Torsten Rasmussen3d880832021-01-19 12:01:38 +010023import re
Torsten Rasmussenfffaf052021-10-12 23:08:36 +020024import subprocess
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010025import sys
26import yaml
27import pykwalify.core
Torsten Rasmussenb3da9ef2019-12-13 09:42:13 +010028from pathlib import Path, PurePath
Torsten Rasmussen3917ee52020-05-18 22:34:49 +020029from collections import namedtuple
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010030
Lukasz Mrugalab2f43212024-04-19 10:37:20 +000031try:
32 from yaml import CSafeLoader as SafeLoader
33except ImportError:
34 from yaml import SafeLoader
35
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010036METADATA_SCHEMA = '''
37## A pykwalify schema for basic validation of the structure of a
38## metadata YAML file.
39##
40# The zephyr/module.yml file is a simple list of key value pairs to be used by
41# the build system.
42type: map
43mapping:
Torsten Rasmussenf24f8832021-01-07 15:27:53 +010044 name:
45 required: false
46 type: str
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010047 build:
Anas Nashif286a9ed2019-12-11 10:13:23 -050048 required: false
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +010049 type: map
50 mapping:
51 cmake:
52 required: false
53 type: str
54 kconfig:
55 required: false
56 type: str
Torsten Rasmussen3673e282020-12-17 11:27:42 +010057 cmake-ext:
58 required: false
59 type: bool
60 default: false
61 kconfig-ext:
62 required: false
63 type: bool
64 default: false
Jamie McCraedf9027a2023-02-20 10:00:38 +000065 sysbuild-cmake:
66 required: false
67 type: str
68 sysbuild-kconfig:
69 required: false
70 type: str
71 sysbuild-cmake-ext:
72 required: false
73 type: bool
74 default: false
75 sysbuild-kconfig-ext:
76 required: false
77 type: bool
78 default: false
Torsten Rasmussen3917ee52020-05-18 22:34:49 +020079 depends:
80 required: false
81 type: seq
82 sequence:
83 - type: str
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +020084 settings:
85 required: false
86 type: map
87 mapping:
88 board_root:
89 required: false
90 type: str
91 dts_root:
92 required: false
93 type: str
Marti Bolivar3f282da02023-01-07 15:25:10 -080094 snippet_root:
95 required: false
96 type: str
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +020097 soc_root:
98 required: false
99 type: str
100 arch_root:
101 required: false
102 type: str
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100103 module_ext_root:
104 required: false
105 type: str
Torsten Rasmussencb690ec2022-11-30 12:14:35 +0100106 sca_root:
107 required: false
108 type: str
Anas Nashif286a9ed2019-12-11 10:13:23 -0500109 tests:
110 required: false
111 type: seq
112 sequence:
113 - type: str
114 samples:
115 required: false
116 type: seq
117 sequence:
118 - type: str
119 boards:
120 required: false
121 type: seq
122 sequence:
123 - type: str
Carles Cufi336aa9d2022-08-04 19:08:22 +0200124 blobs:
125 required: false
126 type: seq
127 sequence:
128 - type: map
129 mapping:
130 path:
131 required: true
132 type: str
133 sha256:
134 required: true
135 type: str
136 type:
137 required: true
138 type: str
139 enum: ['img', 'lib']
140 version:
141 required: true
142 type: str
143 license-path:
144 required: true
145 type: str
146 url:
147 required: true
148 type: str
149 description:
150 required: true
151 type: str
152 doc-url:
153 required: false
154 type: str
Thomas Gagneret0d053182024-01-18 17:31:46 +0100155 security:
156 required: false
157 type: map
158 mapping:
159 external-references:
160 required: false
161 type: seq
162 sequence:
163 - type: str
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100164'''
165
Carles Cufi57e5cd42022-08-11 17:37:34 +0200166MODULE_YML_PATH = PurePath('zephyr/module.yml')
Carles Cufi336aa9d2022-08-04 19:08:22 +0200167# Path to the blobs folder
168MODULE_BLOBS_PATH = PurePath('zephyr/blobs')
Carles Cufib662bc92022-08-24 15:42:55 +0200169BLOB_PRESENT = 'A'
170BLOB_NOT_PRESENT = 'D'
171BLOB_OUTDATED = 'M'
Carles Cufi57e5cd42022-08-11 17:37:34 +0200172
Lukasz Mrugalab2f43212024-04-19 10:37:20 +0000173schema = yaml.load(METADATA_SCHEMA, Loader=SafeLoader)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100174
175
176def validate_setting(setting, module_path, filename=None):
177 if setting is not None:
178 if filename is not None:
Jack Rosenthalf9593092023-08-09 16:24:14 -0600179 checkfile = Path(module_path) / setting / filename
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100180 else:
Jack Rosenthalf9593092023-08-09 16:24:14 -0600181 checkfile = Path(module_path) / setting
182 if not checkfile.resolve().is_file():
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100183 return False
184 return True
185
186
Anas Nashif286a9ed2019-12-11 10:13:23 -0500187def process_module(module):
Torsten Rasmussenb3da9ef2019-12-13 09:42:13 +0100188 module_path = PurePath(module)
Anas Nashif286a9ed2019-12-11 10:13:23 -0500189
Carles Cufi57e5cd42022-08-11 17:37:34 +0200190 # The input is a module if zephyr/module.{yml,yaml} is a valid yaml file
Sebastian Bøebb95dce2019-12-19 15:49:22 +0100191 # or if both zephyr/CMakeLists.txt and zephyr/Kconfig are present.
192
Carles Cufi57e5cd42022-08-11 17:37:34 +0200193 for module_yml in [module_path / MODULE_YML_PATH,
194 module_path / MODULE_YML_PATH.with_suffix('.yaml')]:
Alexander Mihajlovic099c1262022-03-31 08:04:45 +0200195 if Path(module_yml).is_file():
196 with Path(module_yml).open('r') as f:
Lukasz Mrugalab2f43212024-04-19 10:37:20 +0000197 meta = yaml.load(f.read(), Loader=SafeLoader)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100198
Alexander Mihajlovic099c1262022-03-31 08:04:45 +0200199 try:
200 pykwalify.core.Core(source_data=meta, schema_data=schema)\
201 .validate()
202 except pykwalify.errors.SchemaError as e:
203 sys.exit('ERROR: Malformed "build" section in file: {}\n{}'
204 .format(module_yml.as_posix(), e))
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100205
Alexander Mihajlovic099c1262022-03-31 08:04:45 +0200206 meta['name'] = meta.get('name', module_path.name)
207 meta['name-sanitized'] = re.sub('[^a-zA-Z0-9]', '_', meta['name'])
208 return meta
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100209
Sebastian Bøebb95dce2019-12-19 15:49:22 +0100210 if Path(module_path.joinpath('zephyr/CMakeLists.txt')).is_file() and \
211 Path(module_path.joinpath('zephyr/Kconfig')).is_file():
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100212 return {'name': module_path.name,
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100213 'name-sanitized': re.sub('[^a-zA-Z0-9]', '_', module_path.name),
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100214 'build': {'cmake': 'zephyr', 'kconfig': 'zephyr/Kconfig'}}
Sebastian Bøebb95dce2019-12-19 15:49:22 +0100215
Anas Nashif286a9ed2019-12-11 10:13:23 -0500216 return None
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100217
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100218
Anas Nashif286a9ed2019-12-11 10:13:23 -0500219def process_cmake(module, meta):
220 section = meta.get('build', dict())
221 module_path = PurePath(module)
222 module_yml = module_path.joinpath('zephyr/module.yml')
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100223
224 cmake_extern = section.get('cmake-ext', False)
225 if cmake_extern:
226 return('\"{}\":\"{}\":\"{}\"\n'
Andrzej Głąbek02819482021-01-19 07:39:47 +0100227 .format(meta['name'],
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100228 module_path.as_posix(),
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100229 "${ZEPHYR_" + meta['name-sanitized'].upper() + "_CMAKE_DIR}"))
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100230
Anas Nashif286a9ed2019-12-11 10:13:23 -0500231 cmake_setting = section.get('cmake', None)
232 if not validate_setting(cmake_setting, module, 'CMakeLists.txt'):
233 sys.exit('ERROR: "cmake" key in {} has folder value "{}" which '
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200234 'does not contain a CMakeLists.txt file.'
235 .format(module_yml.as_posix(), cmake_setting))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500236
237 cmake_path = os.path.join(module, cmake_setting or 'zephyr')
238 cmake_file = os.path.join(cmake_path, 'CMakeLists.txt')
239 if os.path.isfile(cmake_file):
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200240 return('\"{}\":\"{}\":\"{}\"\n'
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100241 .format(meta['name'],
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200242 module_path.as_posix(),
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200243 Path(cmake_path).resolve().as_posix()))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500244 else:
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200245 return('\"{}\":\"{}\":\"\"\n'
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100246 .format(meta['name'],
Torsten Rasmussenab7ec172020-08-25 13:32:33 +0200247 module_path.as_posix()))
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200248
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100249
Jamie McCraedf9027a2023-02-20 10:00:38 +0000250def process_sysbuildcmake(module, meta):
251 section = meta.get('build', dict())
252 module_path = PurePath(module)
253 module_yml = module_path.joinpath('zephyr/module.yml')
254
255 cmake_extern = section.get('sysbuild-cmake-ext', False)
256 if cmake_extern:
257 return('\"{}\":\"{}\":\"{}\"\n'
258 .format(meta['name'],
259 module_path.as_posix(),
260 "${SYSBUILD_" + meta['name-sanitized'].upper() + "_CMAKE_DIR}"))
261
262 cmake_setting = section.get('sysbuild-cmake', None)
263 if not validate_setting(cmake_setting, module, 'CMakeLists.txt'):
264 sys.exit('ERROR: "cmake" key in {} has folder value "{}" which '
265 'does not contain a CMakeLists.txt file.'
266 .format(module_yml.as_posix(), cmake_setting))
267
268 if cmake_setting is None:
269 return ""
270
271 cmake_path = os.path.join(module, cmake_setting or 'zephyr')
272 cmake_file = os.path.join(cmake_path, 'CMakeLists.txt')
273 if os.path.isfile(cmake_file):
274 return('\"{}\":\"{}\":\"{}\"\n'
275 .format(meta['name'],
276 module_path.as_posix(),
277 Path(cmake_path).resolve().as_posix()))
278 else:
279 return('\"{}\":\"{}\":\"\"\n'
280 .format(meta['name'],
281 module_path.as_posix()))
282
283
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200284def process_settings(module, meta):
285 section = meta.get('build', dict())
286 build_settings = section.get('settings', None)
287 out_text = ""
288
289 if build_settings is not None:
Marti Bolivar3f282da02023-01-07 15:25:10 -0800290 for root in ['board', 'dts', 'snippet', 'soc', 'arch', 'module_ext', 'sca']:
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200291 setting = build_settings.get(root+'_root', None)
292 if setting is not None:
293 root_path = PurePath(module) / setting
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100294 out_text += f'"{root.upper()}_ROOT":'
295 out_text += f'"{root_path.as_posix()}"\n'
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200296
297 return out_text
298
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200299
Carles Cufib662bc92022-08-24 15:42:55 +0200300def get_blob_status(path, sha256):
301 if not path.is_file():
302 return BLOB_NOT_PRESENT
303 with path.open('rb') as f:
304 m = hashlib.sha256()
305 m.update(f.read())
306 if sha256.lower() == m.hexdigest():
307 return BLOB_PRESENT
308 else:
309 return BLOB_OUTDATED
310
311
312def process_blobs(module, meta):
313 blobs = []
314 mblobs = meta.get('blobs', None)
315 if not mblobs:
316 return blobs
317
318 blobs_path = Path(module) / MODULE_BLOBS_PATH
319 for blob in mblobs:
320 blob['module'] = meta.get('name', None)
321 blob['abspath'] = blobs_path / Path(blob['path'])
322 blob['status'] = get_blob_status(blob['abspath'], blob['sha256'])
323 blobs.append(blob)
324
325 return blobs
326
327
Johan Hedberg5562bb92024-08-22 11:24:36 +0300328def kconfig_module_opts(name_sanitized, blobs, taint_blobs):
329 snippet = [f'config ZEPHYR_{name_sanitized.upper()}_MODULE',
330 ' bool',
331 ' default y']
332
333 if taint_blobs:
334 snippet += [' select TAINT_BLOBS']
335
336 if blobs:
337 snippet += [f'\nconfig ZEPHYR_{name_sanitized.upper()}_MODULE_BLOBS',
338 ' bool']
339 if taint_blobs:
340 snippet += [' default y']
341
342 return snippet
343
344
345def kconfig_snippet(meta, path, kconfig_file=None, blobs=False, taint_blobs=False, sysbuild=False):
Torsten Rasmussen3d880832021-01-19 12:01:38 +0100346 name = meta['name']
347 name_sanitized = meta['name-sanitized']
348
Johan Hedberg5562bb92024-08-22 11:24:36 +0300349 snippet = [f'menu "{name} ({path.as_posix()})"']
Carles Cufi035dffc2022-08-24 16:56:01 +0200350
Johan Hedberg5562bb92024-08-22 11:24:36 +0300351 snippet += [f'osource "{kconfig_file.resolve().as_posix()}"' if kconfig_file
352 else f'osource "$(SYSBUILD_{name_sanitized.upper()}_KCONFIG)"' if sysbuild is True
353 else f'osource "$(ZEPHYR_{name_sanitized.upper()}_KCONFIG)"']
354
355 snippet += kconfig_module_opts(name_sanitized, blobs, taint_blobs)
356
357 snippet += ['endmenu\n']
358
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100359 return '\n'.join(snippet)
360
361
Anas Nashif286a9ed2019-12-11 10:13:23 -0500362def process_kconfig(module, meta):
Carles Cufi035dffc2022-08-24 16:56:01 +0200363 blobs = process_blobs(module, meta)
Christophe Dufazaae9326c2024-07-15 15:48:13 +0200364 taint_blobs = any(b['status'] != BLOB_NOT_PRESENT for b in blobs)
Anas Nashif286a9ed2019-12-11 10:13:23 -0500365 section = meta.get('build', dict())
366 module_path = PurePath(module)
367 module_yml = module_path.joinpath('zephyr/module.yml')
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100368 kconfig_extern = section.get('kconfig-ext', False)
369 if kconfig_extern:
Johan Hedberg5562bb92024-08-22 11:24:36 +0300370 return kconfig_snippet(meta, module_path, blobs=blobs, taint_blobs=taint_blobs)
Anas Nashif286a9ed2019-12-11 10:13:23 -0500371
372 kconfig_setting = section.get('kconfig', None)
373 if not validate_setting(kconfig_setting, module):
374 sys.exit('ERROR: "kconfig" key in {} has value "{}" which does '
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200375 'not point to a valid Kconfig file.'
376 .format(module_yml, kconfig_setting))
Anas Nashif286a9ed2019-12-11 10:13:23 -0500377
378 kconfig_file = os.path.join(module, kconfig_setting or 'zephyr/Kconfig')
379 if os.path.isfile(kconfig_file):
Carles Cufi035dffc2022-08-24 16:56:01 +0200380 return kconfig_snippet(meta, module_path, Path(kconfig_file),
Johan Hedberg5562bb92024-08-22 11:24:36 +0300381 blobs=blobs, taint_blobs=taint_blobs)
Anas Nashif286a9ed2019-12-11 10:13:23 -0500382 else:
Flavio Ceolin2e5c3712023-05-10 21:40:04 +0000383 name_sanitized = meta['name-sanitized']
Johan Hedberg5562bb92024-08-22 11:24:36 +0300384 snippet = kconfig_module_opts(name_sanitized, blobs, taint_blobs)
385 return '\n'.join(snippet) + '\n'
Anas Nashif286a9ed2019-12-11 10:13:23 -0500386
Torsten Rasmussen3673e282020-12-17 11:27:42 +0100387
Jamie McCraedf9027a2023-02-20 10:00:38 +0000388def process_sysbuildkconfig(module, meta):
389 section = meta.get('build', dict())
390 module_path = PurePath(module)
391 module_yml = module_path.joinpath('zephyr/module.yml')
392 kconfig_extern = section.get('sysbuild-kconfig-ext', False)
393 if kconfig_extern:
394 return kconfig_snippet(meta, module_path, sysbuild=True)
395
396 kconfig_setting = section.get('sysbuild-kconfig', None)
397 if not validate_setting(kconfig_setting, module):
398 sys.exit('ERROR: "kconfig" key in {} has value "{}" which does '
399 'not point to a valid Kconfig file.'
400 .format(module_yml, kconfig_setting))
401
Jamie McCraeacd14f82024-04-29 09:40:24 +0100402 if kconfig_setting is not None:
403 kconfig_file = os.path.join(module, kconfig_setting)
404 if os.path.isfile(kconfig_file):
405 return kconfig_snippet(meta, module_path, Path(kconfig_file))
Jamie McCraedf9027a2023-02-20 10:00:38 +0000406
Jamie McCraeacd14f82024-04-29 09:40:24 +0100407 name_sanitized = meta['name-sanitized']
408 return (f'config ZEPHYR_{name_sanitized.upper()}_MODULE\n'
409 f' bool\n'
410 f' default y\n')
Jamie McCraedf9027a2023-02-20 10:00:38 +0000411
412
Anas Nashif54713982020-12-07 14:52:10 -0500413def process_twister(module, meta):
Anas Nashif286a9ed2019-12-11 10:13:23 -0500414
415 out = ""
416 tests = meta.get('tests', [])
417 samples = meta.get('samples', [])
418 boards = meta.get('boards', [])
419
420 for pth in tests + samples:
421 if pth:
422 dir = os.path.join(module, pth)
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200423 out += '-T\n{}\n'.format(PurePath(os.path.abspath(dir))
424 .as_posix())
Anas Nashif286a9ed2019-12-11 10:13:23 -0500425
426 for pth in boards:
427 if pth:
428 dir = os.path.join(module, pth)
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200429 out += '--board-root\n{}\n'.format(PurePath(os.path.abspath(dir))
430 .as_posix())
Anas Nashif286a9ed2019-12-11 10:13:23 -0500431
432 return out
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100433
434
Thomas Gagneret0d053182024-01-18 17:31:46 +0100435def _create_meta_project(project_path):
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200436 def git_revision(path):
437 rc = subprocess.Popen(['git', 'rev-parse', '--is-inside-work-tree'],
438 stdout=subprocess.PIPE,
439 stderr=subprocess.PIPE,
440 cwd=path).wait()
441 if rc == 0:
442 # A git repo.
443 popen = subprocess.Popen(['git', 'rev-parse', 'HEAD'],
444 stdout=subprocess.PIPE,
445 stderr=subprocess.PIPE,
446 cwd=path)
447 stdout, stderr = popen.communicate()
448 stdout = stdout.decode('utf-8')
449
450 if not (popen.returncode or stderr):
451 revision = stdout.rstrip()
452
453 rc = subprocess.Popen(['git', 'diff-index', '--quiet', 'HEAD',
454 '--'],
455 stdout=None,
456 stderr=None,
457 cwd=path).wait()
458 if rc:
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100459 return revision + '-dirty', True
460 return revision, False
461 return None, False
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200462
Thomas Gagneret0d053182024-01-18 17:31:46 +0100463 def git_remote(path):
464 popen = subprocess.Popen(['git', 'remote'],
465 stdout=subprocess.PIPE,
466 stderr=subprocess.PIPE,
467 cwd=path)
468 stdout, stderr = popen.communicate()
469 stdout = stdout.decode('utf-8')
470
471 remotes_name = []
472 if not (popen.returncode or stderr):
473 remotes_name = stdout.rstrip().split('\n')
474
475 remote_url = None
476
477 # If more than one remote, do not return any remote
478 if len(remotes_name) == 1:
479 remote = remotes_name[0]
480 popen = subprocess.Popen(['git', 'remote', 'get-url', remote],
481 stdout=subprocess.PIPE,
482 stderr=subprocess.PIPE,
483 cwd=path)
484 stdout, stderr = popen.communicate()
485 stdout = stdout.decode('utf-8')
486
487 if not (popen.returncode or stderr):
488 remote_url = stdout.rstrip()
489
490 return remote_url
491
492 def git_tags(path, revision):
493 if not revision or len(revision) == 0:
494 return None
495
496 popen = subprocess.Popen(['git', '-P', 'tag', '--points-at', revision],
497 stdout=subprocess.PIPE,
498 stderr=subprocess.PIPE,
499 cwd=path)
500 stdout, stderr = popen.communicate()
501 stdout = stdout.decode('utf-8')
502
503 tags = None
504 if not (popen.returncode or stderr):
505 tags = stdout.rstrip().splitlines()
506
507 return tags
508
509 workspace_dirty = False
510 path = PurePath(project_path).as_posix()
511
512 revision, dirty = git_revision(path)
513 workspace_dirty |= dirty
514 remote = git_remote(path)
515 tags = git_tags(path, revision)
516
517 meta_project = {'path': path,
518 'revision': revision}
519
520 if remote:
521 meta_project['remote'] = remote
522
523 if tags:
524 meta_project['tags'] = tags
525
526 return meta_project, workspace_dirty
527
528
529def _get_meta_project(meta_projects_list, project_path):
530 projects = [ prj for prj in meta_projects_list[1:] if prj["path"] == project_path ]
531
532 return projects[0] if len(projects) == 1 else None
533
534
535def process_meta(zephyr_base, west_projs, modules, extra_modules=None,
536 propagate_state=False):
537 # Process zephyr_base, projects, and modules and create a dictionary
538 # with meta information for each input.
539 #
540 # The dictionary will contain meta info in the following lists:
541 # - zephyr: path and revision
542 # - modules: name, path, and revision
543 # - west-projects: path and revision
544 #
545 # returns the dictionary with said lists
546
547 meta = {'zephyr': None, 'modules': None, 'workspace': None}
548
549 zephyr_project, zephyr_dirty = _create_meta_project(zephyr_base)
550 zephyr_off = zephyr_project.get("remote") is None
551
552 workspace_dirty = zephyr_dirty
553 workspace_extra = extra_modules is not None
554 workspace_off = zephyr_off
555
556 if zephyr_off:
557 zephyr_project['revision'] += '-off'
558
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100559 meta['zephyr'] = zephyr_project
560 meta['workspace'] = {}
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200561
Carles Cufia580b3d2022-08-09 11:13:54 +0200562 if west_projs is not None:
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100563 from west.manifest import MANIFEST_REV_BRANCH
Carles Cufia580b3d2022-08-09 11:13:54 +0200564 projects = west_projs['projects']
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200565 meta_projects = []
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100566
Thomas Gagneret0d053182024-01-18 17:31:46 +0100567 manifest_path = projects[0].posixpath
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100568
Thomas Gagneret0d053182024-01-18 17:31:46 +0100569 # Special treatment of manifest project
570 # Git information (remote/revision) are not provided by west for the Manifest (west.yml)
571 # To mitigate this, we check if we don't use the manifest from the zephyr repository or an other project.
572 # If it's from zephyr, reuse zephyr information
573 # If it's from an other project, ignore it, it will be added later
574 # If it's not found, we extract data manually (remote/revision) from the directory
575
576 manifest_project = None
577 manifest_dirty = False
578 manifest_off = False
579
580 if zephyr_base == manifest_path:
581 manifest_project = zephyr_project
582 manifest_dirty = zephyr_dirty
583 manifest_off = zephyr_off
584 elif not [ prj for prj in projects[1:] if prj.posixpath == manifest_path ]:
585 manifest_project, manifest_dirty = _create_meta_project(
586 projects[0].posixpath)
587 manifest_off = manifest_project.get("remote") is None
588 if manifest_off:
589 manifest_project["revision"] += "-off"
590
591 if manifest_project:
592 workspace_off |= manifest_off
593 workspace_dirty |= manifest_dirty
594 meta_projects.append(manifest_project)
595
596 # Iterates on all projects except the first one (manifest)
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100597 for project in projects[1:]:
Thomas Gagneret0d053182024-01-18 17:31:46 +0100598 meta_project, dirty = _create_meta_project(project.posixpath)
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100599 workspace_dirty |= dirty
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200600 meta_projects.append(meta_project)
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100601
Thomas Gagneret0d053182024-01-18 17:31:46 +0100602 off = False
603 if not meta_project.get("remote") or project.sha(MANIFEST_REV_BRANCH) != meta_project['revision'].removesuffix("-dirty"):
604 off = True
605 if not meta_project.get('remote') or project.url != meta_project['remote']:
606 # Force manifest URL and set commit as 'off'
607 meta_project['url'] = project.url
608 off = True
609
610 if off:
611 meta_project['revision'] += '-off'
612 workspace_off |= off
613
614 # If manifest is in project, updates related variables
615 if project.posixpath == manifest_path:
616 manifest_dirty |= dirty
617 manifest_off |= off
618 manifest_project = meta_project
619
Carles Cufia580b3d2022-08-09 11:13:54 +0200620 meta.update({'west': {'manifest': west_projs['manifest_path'],
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100621 'projects': meta_projects}})
622 meta['workspace'].update({'off': workspace_off})
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200623
Thomas Gagneret0d053182024-01-18 17:31:46 +0100624 # Iterates on all modules
625 meta_modules = []
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200626 for module in modules:
Thomas Gagneret0d053182024-01-18 17:31:46 +0100627 # Check if modules is not in projects
628 # It allows to have the "-off" flag since `modules` variable` does not provide URL/remote
629 meta_module = _get_meta_project(meta_projects, module.project)
630
631 if not meta_module:
632 meta_module, dirty = _create_meta_project(module.project)
633 workspace_dirty |= dirty
634
635 meta_module['name'] = module.meta.get('name')
636
637 if module.meta.get('security'):
638 meta_module['security'] = module.meta.get('security')
639 meta_modules.append(meta_module)
640
641 meta['modules'] = meta_modules
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200642
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100643 meta['workspace'].update({'dirty': workspace_dirty,
644 'extra': workspace_extra})
645
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100646 if propagate_state:
Thomas Gagneret0d053182024-01-18 17:31:46 +0100647 zephyr_revision = zephyr_project['revision']
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100648 if workspace_dirty and not zephyr_dirty:
649 zephyr_revision += '-dirty'
650 if workspace_extra:
651 zephyr_revision += '-extra'
Thomas Gagneret0d053182024-01-18 17:31:46 +0100652 if workspace_off and not zephyr_off:
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100653 zephyr_revision += '-off'
654 zephyr_project.update({'revision': zephyr_revision})
655
Carles Cufia580b3d2022-08-09 11:13:54 +0200656 if west_projs is not None:
Thomas Gagneret0d053182024-01-18 17:31:46 +0100657 manifest_revision = manifest_project['revision']
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100658 if workspace_dirty and not manifest_dirty:
659 manifest_revision += '-dirty'
660 if workspace_extra:
661 manifest_revision += '-extra'
Thomas Gagneret0d053182024-01-18 17:31:46 +0100662 if workspace_off and not manifest_off:
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100663 manifest_revision += '-off'
664 manifest_project.update({'revision': manifest_revision})
665
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200666 return meta
667
668
Thomas Gagneret0d053182024-01-18 17:31:46 +0100669def west_projects(manifest=None):
Carles Cufia580b3d2022-08-09 11:13:54 +0200670 manifest_path = None
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200671 projects = []
672 # West is imported here, as it is optional
673 # (and thus maybe not installed)
674 # if user is providing a specific modules list.
Tristan Honscheiddbbe8022022-05-06 11:47:47 -0600675 try:
Martí Bolívar251f2692023-05-19 11:22:01 -0700676 from west.manifest import Manifest
Tristan Honscheiddbbe8022022-05-06 11:47:47 -0600677 except ImportError:
678 # West is not installed, so don't return any projects.
679 return None
680
Martí Bolívar251f2692023-05-19 11:22:01 -0700681 # If west *is* installed, we need all of the following imports to
682 # work. West versions that are excessively old may fail here:
683 # west.configuration.MalformedConfig was
684 # west.manifest.MalformedConfig until west v0.14.0, for example.
685 # These should be hard errors.
686 from west.manifest import \
687 ManifestImportFailed, MalformedManifest, ManifestVersionError
688 from west.configuration import MalformedConfig
689 from west.util import WestNotFound
690 from west.version import __version__ as WestVersion
691
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200692 from packaging import version
693 try:
Carles Cufia580b3d2022-08-09 11:13:54 +0200694 if not manifest:
695 manifest = Manifest.from_file()
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200696 if version.parse(WestVersion) >= version.parse('0.9.0'):
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100697 projects = [p for p in manifest.get_projects([])
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200698 if manifest.is_active(p)]
699 else:
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100700 projects = manifest.get_projects([])
Carles Cufia580b3d2022-08-09 11:13:54 +0200701 manifest_path = manifest.path
702 return {'manifest_path': manifest_path, 'projects': projects}
Martí Bolívarce2a7d92023-05-18 13:21:34 -0700703 except (ManifestImportFailed, MalformedManifest,
704 ManifestVersionError, MalformedConfig) as e:
705 sys.exit(f'ERROR: {e}')
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200706 except WestNotFound:
707 # Only accept WestNotFound, meaning we are not in a west
708 # workspace. Such setup is allowed, as west may be installed
709 # but the project is not required to use west.
710 pass
711 return None
712
713
Carles Cufia580b3d2022-08-09 11:13:54 +0200714def parse_modules(zephyr_base, manifest=None, west_projs=None, modules=None,
715 extra_modules=None):
716
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200717 if modules is None:
Carles Cufia580b3d2022-08-09 11:13:54 +0200718 west_projs = west_projs or west_projects(manifest)
719 modules = ([p.posixpath for p in west_projs['projects']]
720 if west_projs else [])
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100721
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200722 if extra_modules is None:
723 extra_modules = []
Anas Nashif286a9ed2019-12-11 10:13:23 -0500724
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200725 Module = namedtuple('Module', ['project', 'meta', 'depends'])
Damian Krolika5051482022-03-01 18:36:16 +0100726
727 all_modules_by_name = {}
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200728 # dep_modules is a list of all modules that has an unresolved dependency
729 dep_modules = []
730 # start_modules is a list modules with no depends left (no incoming edge)
731 start_modules = []
732 # sorted_modules is a topological sorted list of the modules
733 sorted_modules = []
734
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200735 for project in modules + extra_modules:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500736 # Avoid including Zephyr base project as module.
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200737 if project == zephyr_base:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500738 continue
739
740 meta = process_module(project)
741 if meta:
Damian Krolika5051482022-03-01 18:36:16 +0100742 depends = meta.get('build', {}).get('depends', [])
743 all_modules_by_name[meta['name']] = Module(project, meta, depends)
744
Martí Bolívar34346c42020-01-29 12:38:58 -0800745 elif project in extra_modules:
746 sys.exit(f'{project}, given in ZEPHYR_EXTRA_MODULES, '
747 'is not a valid zephyr module')
Anas Nashif286a9ed2019-12-11 10:13:23 -0500748
Damian Krolika5051482022-03-01 18:36:16 +0100749 for module in all_modules_by_name.values():
750 if not module.depends:
751 start_modules.append(module)
752 else:
753 dep_modules.append(module)
754
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200755 # This will do a topological sort to ensure the modules are ordered
756 # according to dependency settings.
757 while start_modules:
758 node = start_modules.pop(0)
759 sorted_modules.append(node)
Torsten Rasmussenf24f8832021-01-07 15:27:53 +0100760 node_name = node.meta['name']
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200761 to_remove = []
762 for module in dep_modules:
763 if node_name in module.depends:
764 module.depends.remove(node_name)
765 if not module.depends:
766 start_modules.append(module)
767 to_remove.append(module)
768 for module in to_remove:
769 dep_modules.remove(module)
770
771 if dep_modules:
772 # If there are any modules with unresolved dependencies, then the
773 # modules contains unmet or cyclic dependencies. Error out.
774 error = 'Unmet or cyclic dependencies in modules:\n'
775 for module in dep_modules:
776 error += f'{module.project} depends on: {module.depends}\n'
777 sys.exit(error)
778
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200779 return sorted_modules
780
781
782def main():
783 parser = argparse.ArgumentParser(description='''
784 Process a list of projects and create Kconfig / CMake include files for
Jamie McCraeec704442023-01-04 16:08:36 +0000785 projects which are also a Zephyr module''', allow_abbrev=False)
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200786
787 parser.add_argument('--kconfig-out',
788 help="""File to write with resulting KConfig import
789 statements.""")
790 parser.add_argument('--twister-out',
791 help="""File to write with resulting twister
792 parameters.""")
793 parser.add_argument('--cmake-out',
794 help="""File to write with resulting <name>:<path>
795 values to use for including in CMake""")
Jamie McCraedf9027a2023-02-20 10:00:38 +0000796 parser.add_argument('--sysbuild-kconfig-out',
797 help="""File to write with resulting KConfig import
798 statements.""")
799 parser.add_argument('--sysbuild-cmake-out',
800 help="""File to write with resulting <name>:<path>
801 values to use for including in CMake""")
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200802 parser.add_argument('--meta-out',
803 help="""Write a build meta YaML file containing a list
804 of Zephyr modules and west projects.
805 If a module or project is also a git repository
806 the current SHA revision will also be written.""")
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100807 parser.add_argument('--meta-state-propagate', action='store_true',
808 help="""Propagate state of modules and west projects
809 to the suffix of the Zephyr SHA and if west is
810 used, to the suffix of the manifest SHA""")
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200811 parser.add_argument('--settings-out',
812 help="""File to write with resulting <name>:<value>
813 values to use for including in CMake""")
814 parser.add_argument('-m', '--modules', nargs='+',
815 help="""List of modules to parse instead of using `west
816 list`""")
817 parser.add_argument('-x', '--extra-modules', nargs='+',
818 help='List of extra modules to parse')
819 parser.add_argument('-z', '--zephyr-base',
820 help='Path to zephyr repository')
821 args = parser.parse_args()
822
823 kconfig = ""
824 cmake = ""
Jamie McCraedf9027a2023-02-20 10:00:38 +0000825 sysbuild_kconfig = ""
826 sysbuild_cmake = ""
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200827 settings = ""
828 twister = ""
829
Carles Cufia580b3d2022-08-09 11:13:54 +0200830 west_projs = west_projects()
831 modules = parse_modules(args.zephyr_base, None, west_projs,
832 args.modules, args.extra_modules)
Marcin Niestroj248d25d2021-05-08 16:56:34 +0200833
834 for module in modules:
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200835 kconfig += process_kconfig(module.project, module.meta)
836 cmake += process_cmake(module.project, module.meta)
Thomas Gagneret0d053182024-01-18 17:31:46 +0100837 sysbuild_kconfig += process_sysbuildkconfig(
838 module.project, module.meta)
Jamie McCraedf9027a2023-02-20 10:00:38 +0000839 sysbuild_cmake += process_sysbuildcmake(module.project, module.meta)
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200840 settings += process_settings(module.project, module.meta)
Anas Nashif54713982020-12-07 14:52:10 -0500841 twister += process_twister(module.project, module.meta)
Torsten Rasmussen3917ee52020-05-18 22:34:49 +0200842
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100843 if args.kconfig_out:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500844 with open(args.kconfig_out, 'w', encoding="utf-8") as fp:
845 fp.write(kconfig)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100846
847 if args.cmake_out:
Anas Nashif286a9ed2019-12-11 10:13:23 -0500848 with open(args.cmake_out, 'w', encoding="utf-8") as fp:
849 fp.write(cmake)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100850
Jamie McCraedf9027a2023-02-20 10:00:38 +0000851 if args.sysbuild_kconfig_out:
852 with open(args.sysbuild_kconfig_out, 'w', encoding="utf-8") as fp:
853 fp.write(sysbuild_kconfig)
854
855 if args.sysbuild_cmake_out:
856 with open(args.sysbuild_cmake_out, 'w', encoding="utf-8") as fp:
857 fp.write(sysbuild_cmake)
858
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200859 if args.settings_out:
860 with open(args.settings_out, 'w', encoding="utf-8") as fp:
Martí Bolívar361956b2021-08-09 10:46:17 -0700861 fp.write('''\
862# WARNING. THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!
863#
864# This file contains build system settings derived from your modules.
865#
866# Modules may be set via ZEPHYR_MODULES, ZEPHYR_EXTRA_MODULES,
867# and/or the west manifest file.
868#
869# See the Modules guide for more information.
870''')
Torsten Rasmussen25d57ba2020-07-07 17:29:56 +0200871 fp.write(settings)
872
Anas Nashif54713982020-12-07 14:52:10 -0500873 if args.twister_out:
874 with open(args.twister_out, 'w', encoding="utf-8") as fp:
875 fp.write(twister)
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100876
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200877 if args.meta_out:
Carles Cufia580b3d2022-08-09 11:13:54 +0200878 meta = process_meta(args.zephyr_base, west_projs, modules,
Torsten Rasmussen1a519932021-11-04 18:35:50 +0100879 args.extra_modules, args.meta_state_propagate)
Torsten Rasmussen64ec6ee2021-11-04 14:28:21 +0100880
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200881 with open(args.meta_out, 'w', encoding="utf-8") as fp:
Thomas Gagneret0d053182024-01-18 17:31:46 +0100882 # Ignore references and insert data instead
883 yaml.Dumper.ignore_aliases = lambda self, data: True
Torsten Rasmussenfffaf052021-10-12 23:08:36 +0200884 fp.write(yaml.dump(meta))
885
Torsten Rasmussen25c3f8e2020-08-25 14:02:04 +0200886
Torsten Rasmussenbd7569f2019-03-19 10:38:18 +0100887if __name__ == "__main__":
888 main()