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