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