| 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import argparse |
| 4 | import os |
| 5 | import sys |
| 6 | from typing import Optional |
| 7 | |
| 8 | |
| 9 | def print_array(name: str, values: list[str]) -> None: |
| 10 | if len(values) == 0: |
| 11 | return |
| 12 | list = ", ".join(values) |
| 13 | print(f" .{name} = ((const char*[]){{ {list}, NULL }}),") |
| 14 | |
| 15 | def parse_line(line: str) -> tuple[str, str]: |
| 16 | kind = "" |
| 17 | data = "" |
| 18 | get_kind = False |
| 19 | get_data = False |
| 20 | for item in line.split(): |
| 21 | if item == "MODINFO_START": |
| 22 | get_kind = True |
| 23 | continue |
| 24 | if item.startswith("MODINFO_END"): |
| 25 | get_data = False |
| 26 | continue |
| 27 | if get_kind: |
| 28 | kind = item |
| 29 | get_kind = False |
| 30 | get_data = True |
| 31 | continue |
| 32 | if get_data: |
| 33 | data += " " + item |
| 34 | continue |
| 35 | return (kind, data) |
| 36 | |
| 37 | def parse_modinfo(name: str, lines: list[str], enabled: set[str]) -> Optional[dict]: |
| 38 | """Parse a modinfo file and return module metadata, or None if disabled.""" |
| 39 | arch = "" |
| 40 | objs = [] |
| 41 | deps = [] |
| 42 | opts = [] |
| 43 | for line in lines: |
| 44 | if "MODINFO_START" in line: |
| 45 | (kind, data) = parse_line(line) |
| 46 | if kind == 'obj': |
| 47 | objs.append(data) |
| 48 | elif kind == 'dep': |
| 49 | deps.append(data) |
| 50 | elif kind == 'opts': |
| 51 | opts.append(data) |
| 52 | elif kind == 'arch': |
| 53 | arch = data |
| 54 | elif kind == 'kconfig': |
| 55 | # don't add a module which dependency is not enabled |
| 56 | # in kconfig |
| 57 | if data.strip() not in enabled: |
| 58 | return None |
| 59 | else: |
| 60 | print("unknown:", kind) |
| 61 | exit(1) |
| 62 | |
| 63 | return { |
| 64 | 'name': name, |
| 65 | 'arch': arch, |
| 66 | 'objs': objs, |
| 67 | 'deps': deps, |
| 68 | 'opts': opts, |
| 69 | 'dep_names': {dep.strip('" ') for dep in deps} |
| 70 | } |
| 71 | |
| 72 | def generate(modinfo: str, mod: Optional[dict], |
| 73 | skip_reason: Optional[str]) -> None: |
| 74 | """Generate C code for a module.""" |
| 75 | print(f" /* {modinfo} */") |
| 76 | if mod is None: |
| 77 | if skip_reason == "missing_deps": |
| 78 | print(" /* module has missing dependencies. */") |
| 79 | else: |
| 80 | print(" /* module isn't enabled in Kconfig. */") |
| 81 | print("/* },{ */") |
| 82 | return |
| 83 | |
| 84 | print(f' .name = "{mod["name"]}",') |
| 85 | if mod['arch'] != "": |
| 86 | print(f" .arch = {mod['arch']},") |
| 87 | print_array("objs", mod['objs']) |
| 88 | print_array("deps", mod['deps']) |
| 89 | print_array("opts", mod['opts']) |
| 90 | print("},{") |
| 91 | |
| 92 | def print_pre() -> None: |
| 93 | print("/* generated by scripts/modinfo-generate.py */") |
| 94 | print("#include \"qemu/osdep.h\"") |
| 95 | print("#include \"qemu/module.h\"") |
| 96 | print("const QemuModinfo qemu_modinfo[] = {{") |
| 97 | |
| 98 | def print_post() -> None: |
| 99 | print(" /* end of list */") |
| 100 | print("}};") |
| 101 | |
| 102 | def main() -> None: |
| 103 | parser = argparse.ArgumentParser( |
| 104 | description='Generate C code for QEMU module info' |
| 105 | ) |
| 106 | parser.add_argument('--devices', |
| 107 | help='path to config-device.mak') |
| 108 | parser.add_argument('--skip-missing-deps', action='store_true', |
| 109 | help='warn if a dependency is missing and continue') |
| 110 | parser.add_argument('modinfo', nargs='+', |
| 111 | help='modinfo files to process') |
| 112 | args = parser.parse_args() |
| 113 | |
| 114 | # get all devices enabled in kconfig, from *-config-device.mak |
| 115 | enabled = set() |
| 116 | if args.devices: |
| 117 | with open(args.devices) as file: |
| 118 | for line in file.readlines(): |
| 119 | config = line.split('=') |
| 120 | if config[1].rstrip() == 'y': |
| 121 | enabled.add(config[0][7:]) # remove CONFIG_ |
| 122 | |
| 123 | # all_modules: modinfo path -> (basename, parsed module or None, skip_reason) |
| 124 | all_modules = {} |
| 125 | for modinfo in args.modinfo: |
| 126 | with open(modinfo) as f: |
| 127 | lines = f.readlines() |
| 128 | (basename, _) = os.path.splitext(modinfo) |
| 129 | mod = parse_modinfo(basename, lines, enabled) |
| 130 | skip_reason = "kconfig" if mod is None else None |
| 131 | all_modules[modinfo] = (basename, mod, skip_reason) |
| 132 | |
| 133 | # Collect all available module names |
| 134 | available = {basename for basename, mod, _ in all_modules.values() |
| 135 | if mod is not None} |
| 136 | |
| 137 | # Collect all dependencies |
| 138 | all_deps = set() |
| 139 | for basename, mod, _ in all_modules.values(): |
| 140 | if mod is not None: |
| 141 | all_deps.update(mod['dep_names']) |
| 142 | |
| 143 | # Check for missing dependencies |
| 144 | missing = all_deps.difference(available) |
| 145 | for dep in missing: |
| 146 | print(f"Dependency {dep} cannot be satisfied", file=sys.stderr) |
| 147 | |
| 148 | if missing and not args.skip_missing_deps: |
| 149 | exit(1) |
| 150 | |
| 151 | # When skipping missing deps, iteratively remove modules with |
| 152 | # unsatisfiable dependencies |
| 153 | if args.skip_missing_deps and missing: |
| 154 | changed = True |
| 155 | while changed: |
| 156 | changed = False |
| 157 | for modinfo, (basename, mod, skip_reason) in list(all_modules.items()): |
| 158 | if mod is None: |
| 159 | continue |
| 160 | if not mod['dep_names'].issubset(available): |
| 161 | available.discard(basename) |
| 162 | all_modules[modinfo] = (basename, None, "missing_deps") |
| 163 | changed = True |
| 164 | |
| 165 | # generate output |
| 166 | print_pre() |
| 167 | for modinfo in args.modinfo: |
| 168 | (basename, mod, skip_reason) = all_modules[modinfo] |
| 169 | generate(modinfo, mod, skip_reason) |
| 170 | print_post() |
| 171 | |
| 172 | if __name__ == "__main__": |
| 173 | main() |