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