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