1#!/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. 13 14Using --twister-out <filename> an argument file for twister script will 15be generated which would point to test and sample roots available in modules 16that can be included during a twister run. This allows testing code 17maintained in modules in addition to what is available in the main Zephyr tree. 18''' 19 20import argparse 21import hashlib 22import os 23import re 24import subprocess 25import sys 26import yaml 27import pykwalify.core 28from pathlib import Path, PurePath 29from collections import namedtuple 30 31try: 32 from yaml import CSafeLoader as SafeLoader 33except ImportError: 34 from yaml import SafeLoader 35 36METADATA_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: 44 name: 45 required: false 46 type: str 47 build: 48 required: false 49 type: map 50 mapping: 51 cmake: 52 required: false 53 type: str 54 kconfig: 55 required: false 56 type: str 57 cmake-ext: 58 required: false 59 type: bool 60 default: false 61 kconfig-ext: 62 required: false 63 type: bool 64 default: false 65 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 79 depends: 80 required: false 81 type: seq 82 sequence: 83 - type: str 84 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 94 snippet_root: 95 required: false 96 type: str 97 soc_root: 98 required: false 99 type: str 100 arch_root: 101 required: false 102 type: str 103 module_ext_root: 104 required: false 105 type: str 106 sca_root: 107 required: false 108 type: str 109 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 124 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 155 security: 156 required: false 157 type: map 158 mapping: 159 external-references: 160 required: false 161 type: seq 162 sequence: 163 - type: str 164 package-managers: 165 required: false 166 type: map 167 mapping: 168 pip: 169 required: false 170 type: map 171 mapping: 172 requirement-files: 173 required: false 174 type: seq 175 sequence: 176 - type: str 177 runners: 178 required: false 179 type: seq 180 sequence: 181 - type: map 182 mapping: 183 file: 184 required: true 185 type: str 186''' 187 188MODULE_YML_PATH = PurePath('zephyr/module.yml') 189# Path to the blobs folder 190MODULE_BLOBS_PATH = PurePath('zephyr/blobs') 191BLOB_PRESENT = 'A' 192BLOB_NOT_PRESENT = 'D' 193BLOB_OUTDATED = 'M' 194 195schema = yaml.load(METADATA_SCHEMA, Loader=SafeLoader) 196 197 198def validate_setting(setting, module_path, filename=None): 199 if setting is not None: 200 if filename is not None: 201 checkfile = Path(module_path) / setting / filename 202 else: 203 checkfile = Path(module_path) / setting 204 if not checkfile.resolve().is_file(): 205 return False 206 return True 207 208 209def process_module(module): 210 module_path = PurePath(module) 211 212 # The input is a module if zephyr/module.{yml,yaml} is a valid yaml file 213 # or if both zephyr/CMakeLists.txt and zephyr/Kconfig are present. 214 215 for module_yml in [module_path / MODULE_YML_PATH, 216 module_path / MODULE_YML_PATH.with_suffix('.yaml')]: 217 if Path(module_yml).is_file(): 218 with Path(module_yml).open('r', encoding='utf-8') as f: 219 meta = yaml.load(f.read(), Loader=SafeLoader) 220 221 try: 222 pykwalify.core.Core(source_data=meta, schema_data=schema)\ 223 .validate() 224 except pykwalify.errors.SchemaError as e: 225 sys.exit('ERROR: Malformed "build" section in file: {}\n{}' 226 .format(module_yml.as_posix(), e)) 227 228 meta['name'] = meta.get('name', module_path.name) 229 meta['name-sanitized'] = re.sub('[^a-zA-Z0-9]', '_', meta['name']) 230 return meta 231 232 if Path(module_path.joinpath('zephyr/CMakeLists.txt')).is_file() and \ 233 Path(module_path.joinpath('zephyr/Kconfig')).is_file(): 234 return {'name': module_path.name, 235 'name-sanitized': re.sub('[^a-zA-Z0-9]', '_', module_path.name), 236 'build': {'cmake': 'zephyr', 'kconfig': 'zephyr/Kconfig'}} 237 238 return None 239 240 241def process_cmake(module, meta): 242 section = meta.get('build', dict()) 243 module_path = PurePath(module) 244 module_yml = module_path.joinpath('zephyr/module.yml') 245 246 cmake_extern = section.get('cmake-ext', False) 247 if cmake_extern: 248 return('\"{}\":\"{}\":\"{}\"\n' 249 .format(meta['name'], 250 module_path.as_posix(), 251 "${ZEPHYR_" + meta['name-sanitized'].upper() + "_CMAKE_DIR}")) 252 253 cmake_setting = section.get('cmake', None) 254 if not validate_setting(cmake_setting, module, 'CMakeLists.txt'): 255 sys.exit('ERROR: "cmake" key in {} has folder value "{}" which ' 256 'does not contain a CMakeLists.txt file.' 257 .format(module_yml.as_posix(), cmake_setting)) 258 259 cmake_path = os.path.join(module, cmake_setting or 'zephyr') 260 cmake_file = os.path.join(cmake_path, 'CMakeLists.txt') 261 if os.path.isfile(cmake_file): 262 return('\"{}\":\"{}\":\"{}\"\n' 263 .format(meta['name'], 264 module_path.as_posix(), 265 Path(cmake_path).resolve().as_posix())) 266 else: 267 return('\"{}\":\"{}\":\"\"\n' 268 .format(meta['name'], 269 module_path.as_posix())) 270 271 272def process_sysbuildcmake(module, meta): 273 section = meta.get('build', dict()) 274 module_path = PurePath(module) 275 module_yml = module_path.joinpath('zephyr/module.yml') 276 277 cmake_extern = section.get('sysbuild-cmake-ext', False) 278 if cmake_extern: 279 return('\"{}\":\"{}\":\"{}\"\n' 280 .format(meta['name'], 281 module_path.as_posix(), 282 "${SYSBUILD_" + meta['name-sanitized'].upper() + "_CMAKE_DIR}")) 283 284 cmake_setting = section.get('sysbuild-cmake', None) 285 if not validate_setting(cmake_setting, module, 'CMakeLists.txt'): 286 sys.exit('ERROR: "cmake" key in {} has folder value "{}" which ' 287 'does not contain a CMakeLists.txt file.' 288 .format(module_yml.as_posix(), cmake_setting)) 289 290 if cmake_setting is None: 291 return "" 292 293 cmake_path = os.path.join(module, cmake_setting or 'zephyr') 294 cmake_file = os.path.join(cmake_path, 'CMakeLists.txt') 295 if os.path.isfile(cmake_file): 296 return('\"{}\":\"{}\":\"{}\"\n' 297 .format(meta['name'], 298 module_path.as_posix(), 299 Path(cmake_path).resolve().as_posix())) 300 else: 301 return('\"{}\":\"{}\":\"\"\n' 302 .format(meta['name'], 303 module_path.as_posix())) 304 305 306def process_settings(module, meta): 307 section = meta.get('build', dict()) 308 build_settings = section.get('settings', None) 309 out_text = "" 310 311 if build_settings is not None: 312 for root in ['board', 'dts', 'snippet', 'soc', 'arch', 'module_ext', 'sca']: 313 setting = build_settings.get(root+'_root', None) 314 if setting is not None: 315 root_path = PurePath(module) / setting 316 out_text += f'"{root.upper()}_ROOT":' 317 out_text += f'"{root_path.as_posix()}"\n' 318 319 return out_text 320 321 322def get_blob_status(path, sha256): 323 if not path.is_file(): 324 return BLOB_NOT_PRESENT 325 with path.open('rb') as f: 326 m = hashlib.sha256() 327 m.update(f.read()) 328 if sha256.lower() == m.hexdigest(): 329 return BLOB_PRESENT 330 else: 331 return BLOB_OUTDATED 332 333 334def process_blobs(module, meta): 335 blobs = [] 336 mblobs = meta.get('blobs', None) 337 if not mblobs: 338 return blobs 339 340 blobs_path = Path(module) / MODULE_BLOBS_PATH 341 for blob in mblobs: 342 blob['module'] = meta.get('name', None) 343 blob['abspath'] = blobs_path / Path(blob['path']) 344 blob['status'] = get_blob_status(blob['abspath'], blob['sha256']) 345 blobs.append(blob) 346 347 return blobs 348 349 350def kconfig_module_opts(name_sanitized, blobs, taint_blobs): 351 snippet = [f'config ZEPHYR_{name_sanitized.upper()}_MODULE', 352 ' bool', 353 ' default y'] 354 355 if taint_blobs: 356 snippet += [' select TAINT_BLOBS'] 357 358 if blobs: 359 snippet += [f'\nconfig ZEPHYR_{name_sanitized.upper()}_MODULE_BLOBS', 360 ' bool'] 361 if taint_blobs: 362 snippet += [' default y'] 363 364 return snippet 365 366 367def kconfig_snippet(meta, path, kconfig_file=None, blobs=False, taint_blobs=False, sysbuild=False): 368 name = meta['name'] 369 name_sanitized = meta['name-sanitized'] 370 371 snippet = [f'menu "{name} ({path.as_posix()})"'] 372 373 snippet += [f'osource "{kconfig_file.resolve().as_posix()}"' if kconfig_file 374 else f'osource "$(SYSBUILD_{name_sanitized.upper()}_KCONFIG)"' if sysbuild is True 375 else f'osource "$(ZEPHYR_{name_sanitized.upper()}_KCONFIG)"'] 376 377 snippet += kconfig_module_opts(name_sanitized, blobs, taint_blobs) 378 379 snippet += ['endmenu\n'] 380 381 return '\n'.join(snippet) 382 383 384def process_kconfig(module, meta): 385 blobs = process_blobs(module, meta) 386 taint_blobs = any(b['status'] != BLOB_NOT_PRESENT for b in blobs) 387 section = meta.get('build', dict()) 388 module_path = PurePath(module) 389 module_yml = module_path.joinpath('zephyr/module.yml') 390 kconfig_extern = section.get('kconfig-ext', False) 391 if kconfig_extern: 392 return kconfig_snippet(meta, module_path, blobs=blobs, taint_blobs=taint_blobs) 393 394 kconfig_setting = section.get('kconfig', None) 395 if not validate_setting(kconfig_setting, module): 396 sys.exit('ERROR: "kconfig" key in {} has value "{}" which does ' 397 'not point to a valid Kconfig file.' 398 .format(module_yml, kconfig_setting)) 399 400 kconfig_file = os.path.join(module, kconfig_setting or 'zephyr/Kconfig') 401 if os.path.isfile(kconfig_file): 402 return kconfig_snippet(meta, module_path, Path(kconfig_file), 403 blobs=blobs, taint_blobs=taint_blobs) 404 else: 405 name_sanitized = meta['name-sanitized'] 406 snippet = kconfig_module_opts(name_sanitized, blobs, taint_blobs) 407 return '\n'.join(snippet) + '\n' 408 409 410def process_sysbuildkconfig(module, meta): 411 section = meta.get('build', dict()) 412 module_path = PurePath(module) 413 module_yml = module_path.joinpath('zephyr/module.yml') 414 kconfig_extern = section.get('sysbuild-kconfig-ext', False) 415 if kconfig_extern: 416 return kconfig_snippet(meta, module_path, sysbuild=True) 417 418 kconfig_setting = section.get('sysbuild-kconfig', None) 419 if not validate_setting(kconfig_setting, module): 420 sys.exit('ERROR: "kconfig" key in {} has value "{}" which does ' 421 'not point to a valid Kconfig file.' 422 .format(module_yml, kconfig_setting)) 423 424 if kconfig_setting is not None: 425 kconfig_file = os.path.join(module, kconfig_setting) 426 if os.path.isfile(kconfig_file): 427 return kconfig_snippet(meta, module_path, Path(kconfig_file)) 428 429 name_sanitized = meta['name-sanitized'] 430 return (f'config ZEPHYR_{name_sanitized.upper()}_MODULE\n' 431 f' bool\n' 432 f' default y\n') 433 434 435def process_twister(module, meta): 436 437 out = "" 438 tests = meta.get('tests', []) 439 samples = meta.get('samples', []) 440 boards = meta.get('boards', []) 441 442 for pth in tests + samples: 443 if pth: 444 dir = os.path.join(module, pth) 445 out += '-T\n{}\n'.format(PurePath(os.path.abspath(dir)) 446 .as_posix()) 447 448 for pth in boards: 449 if pth: 450 dir = os.path.join(module, pth) 451 out += '--board-root\n{}\n'.format(PurePath(os.path.abspath(dir)) 452 .as_posix()) 453 454 return out 455 456 457def _create_meta_project(project_path): 458 def git_revision(path): 459 rc = subprocess.Popen(['git', 'rev-parse', '--is-inside-work-tree'], 460 stdout=subprocess.PIPE, 461 stderr=subprocess.PIPE, 462 cwd=path).wait() 463 if rc == 0: 464 # A git repo. 465 popen = subprocess.Popen(['git', 'rev-parse', 'HEAD'], 466 stdout=subprocess.PIPE, 467 stderr=subprocess.PIPE, 468 cwd=path) 469 stdout, stderr = popen.communicate() 470 stdout = stdout.decode('utf-8') 471 472 if not (popen.returncode or stderr): 473 revision = stdout.rstrip() 474 475 rc = subprocess.Popen(['git', 'diff-index', '--quiet', 'HEAD', 476 '--'], 477 stdout=None, 478 stderr=None, 479 cwd=path).wait() 480 if rc: 481 return revision + '-dirty', True 482 return revision, False 483 return None, False 484 485 def git_remote(path): 486 popen = subprocess.Popen(['git', 'remote'], 487 stdout=subprocess.PIPE, 488 stderr=subprocess.PIPE, 489 cwd=path) 490 stdout, stderr = popen.communicate() 491 stdout = stdout.decode('utf-8') 492 493 remotes_name = [] 494 if not (popen.returncode or stderr): 495 remotes_name = stdout.rstrip().split('\n') 496 497 remote_url = None 498 499 # If more than one remote, do not return any remote 500 if len(remotes_name) == 1: 501 remote = remotes_name[0] 502 popen = subprocess.Popen(['git', 'remote', 'get-url', remote], 503 stdout=subprocess.PIPE, 504 stderr=subprocess.PIPE, 505 cwd=path) 506 stdout, stderr = popen.communicate() 507 stdout = stdout.decode('utf-8') 508 509 if not (popen.returncode or stderr): 510 remote_url = stdout.rstrip() 511 512 return remote_url 513 514 def git_tags(path, revision): 515 if not revision or len(revision) == 0: 516 return None 517 518 popen = subprocess.Popen(['git', '-P', 'tag', '--points-at', revision], 519 stdout=subprocess.PIPE, 520 stderr=subprocess.PIPE, 521 cwd=path) 522 stdout, stderr = popen.communicate() 523 stdout = stdout.decode('utf-8') 524 525 tags = None 526 if not (popen.returncode or stderr): 527 tags = stdout.rstrip().splitlines() 528 529 return tags 530 531 workspace_dirty = False 532 path = PurePath(project_path).as_posix() 533 534 revision, dirty = git_revision(path) 535 workspace_dirty |= dirty 536 remote = git_remote(path) 537 tags = git_tags(path, revision) 538 539 meta_project = {'path': path, 540 'revision': revision} 541 542 if remote: 543 meta_project['remote'] = remote 544 545 if tags: 546 meta_project['tags'] = tags 547 548 return meta_project, workspace_dirty 549 550 551def _get_meta_project(meta_projects_list, project_path): 552 projects = [ prj for prj in meta_projects_list[1:] if prj["path"] == project_path ] 553 554 return projects[0] if len(projects) == 1 else None 555 556 557def process_meta(zephyr_base, west_projs, modules, extra_modules=None, 558 propagate_state=False): 559 # Process zephyr_base, projects, and modules and create a dictionary 560 # with meta information for each input. 561 # 562 # The dictionary will contain meta info in the following lists: 563 # - zephyr: path and revision 564 # - modules: name, path, and revision 565 # - west-projects: path and revision 566 # 567 # returns the dictionary with said lists 568 569 meta = {'zephyr': None, 'modules': None, 'workspace': None} 570 571 zephyr_project, zephyr_dirty = _create_meta_project(zephyr_base) 572 zephyr_off = zephyr_project.get("remote") is None 573 574 workspace_dirty = zephyr_dirty 575 workspace_extra = extra_modules is not None 576 workspace_off = zephyr_off 577 578 if zephyr_off: 579 zephyr_project['revision'] += '-off' 580 581 meta['zephyr'] = zephyr_project 582 meta['workspace'] = {} 583 584 if west_projs is not None: 585 from west.manifest import MANIFEST_REV_BRANCH 586 projects = west_projs['projects'] 587 meta_projects = [] 588 589 manifest_path = projects[0].posixpath 590 591 # Special treatment of manifest project 592 # Git information (remote/revision) are not provided by west for the Manifest (west.yml) 593 # To mitigate this, we check if we don't use the manifest from the zephyr repository or an other project. 594 # If it's from zephyr, reuse zephyr information 595 # If it's from an other project, ignore it, it will be added later 596 # If it's not found, we extract data manually (remote/revision) from the directory 597 598 manifest_project = None 599 manifest_dirty = False 600 manifest_off = False 601 602 if zephyr_base == manifest_path: 603 manifest_project = zephyr_project 604 manifest_dirty = zephyr_dirty 605 manifest_off = zephyr_off 606 elif not [ prj for prj in projects[1:] if prj.posixpath == manifest_path ]: 607 manifest_project, manifest_dirty = _create_meta_project( 608 projects[0].posixpath) 609 manifest_off = manifest_project.get("remote") is None 610 if manifest_off: 611 manifest_project["revision"] += "-off" 612 613 if manifest_project: 614 workspace_off |= manifest_off 615 workspace_dirty |= manifest_dirty 616 meta_projects.append(manifest_project) 617 618 # Iterates on all projects except the first one (manifest) 619 for project in projects[1:]: 620 meta_project, dirty = _create_meta_project(project.posixpath) 621 workspace_dirty |= dirty 622 meta_projects.append(meta_project) 623 624 off = False 625 if not meta_project.get("remote") or project.sha(MANIFEST_REV_BRANCH) != meta_project['revision'].removesuffix("-dirty"): 626 off = True 627 if not meta_project.get('remote') or project.url != meta_project['remote']: 628 # Force manifest URL and set commit as 'off' 629 meta_project['url'] = project.url 630 off = True 631 632 if off: 633 meta_project['revision'] += '-off' 634 workspace_off |= off 635 636 # If manifest is in project, updates related variables 637 if project.posixpath == manifest_path: 638 manifest_dirty |= dirty 639 manifest_off |= off 640 manifest_project = meta_project 641 642 meta.update({'west': {'manifest': west_projs['manifest_path'], 643 'projects': meta_projects}}) 644 meta['workspace'].update({'off': workspace_off}) 645 646 # Iterates on all modules 647 meta_modules = [] 648 for module in modules: 649 # Check if modules is not in projects 650 # It allows to have the "-off" flag since `modules` variable` does not provide URL/remote 651 meta_module = _get_meta_project(meta_projects, module.project) 652 653 if not meta_module: 654 meta_module, dirty = _create_meta_project(module.project) 655 workspace_dirty |= dirty 656 657 meta_module['name'] = module.meta.get('name') 658 659 if module.meta.get('security'): 660 meta_module['security'] = module.meta.get('security') 661 meta_modules.append(meta_module) 662 663 meta['modules'] = meta_modules 664 665 meta['workspace'].update({'dirty': workspace_dirty, 666 'extra': workspace_extra}) 667 668 if propagate_state: 669 zephyr_revision = zephyr_project['revision'] 670 if workspace_dirty and not zephyr_dirty: 671 zephyr_revision += '-dirty' 672 if workspace_extra: 673 zephyr_revision += '-extra' 674 if workspace_off and not zephyr_off: 675 zephyr_revision += '-off' 676 zephyr_project.update({'revision': zephyr_revision}) 677 678 if west_projs is not None: 679 manifest_revision = manifest_project['revision'] 680 if workspace_dirty and not manifest_dirty: 681 manifest_revision += '-dirty' 682 if workspace_extra: 683 manifest_revision += '-extra' 684 if workspace_off and not manifest_off: 685 manifest_revision += '-off' 686 manifest_project.update({'revision': manifest_revision}) 687 688 return meta 689 690 691def west_projects(manifest=None): 692 manifest_path = None 693 projects = [] 694 # West is imported here, as it is optional 695 # (and thus maybe not installed) 696 # if user is providing a specific modules list. 697 try: 698 from west.manifest import Manifest 699 except ImportError: 700 # West is not installed, so don't return any projects. 701 return None 702 703 # If west *is* installed, we need all of the following imports to 704 # work. West versions that are excessively old may fail here: 705 # west.configuration.MalformedConfig was 706 # west.manifest.MalformedConfig until west v0.14.0, for example. 707 # These should be hard errors. 708 from west.manifest import \ 709 ManifestImportFailed, MalformedManifest, ManifestVersionError 710 from west.configuration import MalformedConfig 711 from west.util import WestNotFound 712 from west.version import __version__ as WestVersion 713 714 from packaging import version 715 try: 716 if not manifest: 717 manifest = Manifest.from_file() 718 if version.parse(WestVersion) >= version.parse('0.9.0'): 719 projects = [p for p in manifest.get_projects([]) 720 if manifest.is_active(p)] 721 else: 722 projects = manifest.get_projects([]) 723 manifest_path = manifest.path 724 return {'manifest_path': manifest_path, 'projects': projects} 725 except (ManifestImportFailed, MalformedManifest, 726 ManifestVersionError, MalformedConfig) as e: 727 sys.exit(f'ERROR: {e}') 728 except WestNotFound: 729 # Only accept WestNotFound, meaning we are not in a west 730 # workspace. Such setup is allowed, as west may be installed 731 # but the project is not required to use west. 732 pass 733 return None 734 735 736def parse_modules(zephyr_base, manifest=None, west_projs=None, modules=None, 737 extra_modules=None): 738 739 if modules is None: 740 west_projs = west_projs or west_projects(manifest) 741 modules = ([p.posixpath for p in west_projs['projects']] 742 if west_projs else []) 743 744 if extra_modules is None: 745 extra_modules = [] 746 747 Module = namedtuple('Module', ['project', 'meta', 'depends']) 748 749 all_modules_by_name = {} 750 # dep_modules is a list of all modules that has an unresolved dependency 751 dep_modules = [] 752 # start_modules is a list modules with no depends left (no incoming edge) 753 start_modules = [] 754 # sorted_modules is a topological sorted list of the modules 755 sorted_modules = [] 756 757 for project in modules + extra_modules: 758 # Avoid including Zephyr base project as module. 759 if project == zephyr_base: 760 continue 761 762 meta = process_module(project) 763 if meta: 764 depends = meta.get('build', {}).get('depends', []) 765 all_modules_by_name[meta['name']] = Module(project, meta, depends) 766 767 elif project in extra_modules: 768 sys.exit(f'{project}, given in ZEPHYR_EXTRA_MODULES, ' 769 'is not a valid zephyr module') 770 771 for module in all_modules_by_name.values(): 772 if not module.depends: 773 start_modules.append(module) 774 else: 775 dep_modules.append(module) 776 777 # This will do a topological sort to ensure the modules are ordered 778 # according to dependency settings. 779 while start_modules: 780 node = start_modules.pop(0) 781 sorted_modules.append(node) 782 node_name = node.meta['name'] 783 to_remove = [] 784 for module in dep_modules: 785 if node_name in module.depends: 786 module.depends.remove(node_name) 787 if not module.depends: 788 start_modules.append(module) 789 to_remove.append(module) 790 for module in to_remove: 791 dep_modules.remove(module) 792 793 if dep_modules: 794 # If there are any modules with unresolved dependencies, then the 795 # modules contains unmet or cyclic dependencies. Error out. 796 error = 'Unmet or cyclic dependencies in modules:\n' 797 for module in dep_modules: 798 error += f'{module.project} depends on: {module.depends}\n' 799 sys.exit(error) 800 801 return sorted_modules 802 803 804def main(): 805 parser = argparse.ArgumentParser(description=''' 806 Process a list of projects and create Kconfig / CMake include files for 807 projects which are also a Zephyr module''', allow_abbrev=False) 808 809 parser.add_argument('--kconfig-out', 810 help="""File to write with resulting KConfig import 811 statements.""") 812 parser.add_argument('--twister-out', 813 help="""File to write with resulting twister 814 parameters.""") 815 parser.add_argument('--cmake-out', 816 help="""File to write with resulting <name>:<path> 817 values to use for including in CMake""") 818 parser.add_argument('--sysbuild-kconfig-out', 819 help="""File to write with resulting KConfig import 820 statements.""") 821 parser.add_argument('--sysbuild-cmake-out', 822 help="""File to write with resulting <name>:<path> 823 values to use for including in CMake""") 824 parser.add_argument('--meta-out', 825 help="""Write a build meta YaML file containing a list 826 of Zephyr modules and west projects. 827 If a module or project is also a git repository 828 the current SHA revision will also be written.""") 829 parser.add_argument('--meta-state-propagate', action='store_true', 830 help="""Propagate state of modules and west projects 831 to the suffix of the Zephyr SHA and if west is 832 used, to the suffix of the manifest SHA""") 833 parser.add_argument('--settings-out', 834 help="""File to write with resulting <name>:<value> 835 values to use for including in CMake""") 836 parser.add_argument('-m', '--modules', nargs='+', 837 help="""List of modules to parse instead of using `west 838 list`""") 839 parser.add_argument('-x', '--extra-modules', nargs='+', 840 help='List of extra modules to parse') 841 parser.add_argument('-z', '--zephyr-base', 842 help='Path to zephyr repository') 843 args = parser.parse_args() 844 845 kconfig = "" 846 cmake = "" 847 sysbuild_kconfig = "" 848 sysbuild_cmake = "" 849 settings = "" 850 twister = "" 851 852 west_projs = west_projects() 853 modules = parse_modules(args.zephyr_base, None, west_projs, 854 args.modules, args.extra_modules) 855 856 for module in modules: 857 kconfig += process_kconfig(module.project, module.meta) 858 cmake += process_cmake(module.project, module.meta) 859 sysbuild_kconfig += process_sysbuildkconfig( 860 module.project, module.meta) 861 sysbuild_cmake += process_sysbuildcmake(module.project, module.meta) 862 settings += process_settings(module.project, module.meta) 863 twister += process_twister(module.project, module.meta) 864 865 if args.kconfig_out: 866 with open(args.kconfig_out, 'w', encoding="utf-8") as fp: 867 fp.write(kconfig) 868 869 if args.cmake_out: 870 with open(args.cmake_out, 'w', encoding="utf-8") as fp: 871 fp.write(cmake) 872 873 if args.sysbuild_kconfig_out: 874 with open(args.sysbuild_kconfig_out, 'w', encoding="utf-8") as fp: 875 fp.write(sysbuild_kconfig) 876 877 if args.sysbuild_cmake_out: 878 with open(args.sysbuild_cmake_out, 'w', encoding="utf-8") as fp: 879 fp.write(sysbuild_cmake) 880 881 if args.settings_out: 882 with open(args.settings_out, 'w', encoding="utf-8") as fp: 883 fp.write('''\ 884# WARNING. THIS FILE IS AUTO-GENERATED. DO NOT MODIFY! 885# 886# This file contains build system settings derived from your modules. 887# 888# Modules may be set via ZEPHYR_MODULES, ZEPHYR_EXTRA_MODULES, 889# and/or the west manifest file. 890# 891# See the Modules guide for more information. 892''') 893 fp.write(settings) 894 895 if args.twister_out: 896 with open(args.twister_out, 'w', encoding="utf-8") as fp: 897 fp.write(twister) 898 899 if args.meta_out: 900 meta = process_meta(args.zephyr_base, west_projs, modules, 901 args.extra_modules, args.meta_state_propagate) 902 903 with open(args.meta_out, 'w', encoding="utf-8") as fp: 904 # Ignore references and insert data instead 905 yaml.Dumper.ignore_aliases = lambda self, data: True 906 fp.write(yaml.dump(meta)) 907 908 909if __name__ == "__main__": 910 main() 911