| 1 | #!/usr/bin/env python3 |
| 2 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | |
| 4 | """ |
| 5 | Generate bindgen arguments from Cargo.toml metadata for QEMU's Rust FFI bindings. |
| 6 | |
| 7 | Author: Paolo Bonzini <pbonzini@redhat.com> |
| 8 | |
| 9 | Copyright (C) 2025 Red Hat, Inc. |
| 10 | |
| 11 | This script processes Cargo.toml file for QEMU's bindings crates (util-sys, |
| 12 | chardev-sys, qom-sys, etc.); it generates bindgen command lines that allow |
| 13 | easy customization and that export the right headers in each bindings crate. |
| 14 | |
| 15 | For detailed information, see docs/devel/rust.rst. |
| 16 | """ |
| 17 | |
| 18 | import os |
| 19 | import re |
| 20 | import sys |
| 21 | import argparse |
| 22 | from pathlib import Path |
| 23 | from dataclasses import dataclass |
| 24 | from typing import Iterable, List, Dict, Any |
| 25 | |
| 26 | try: |
| 27 | import tomllib |
| 28 | except ImportError: |
| 29 | import tomli as tomllib # type: ignore |
| 30 | |
| 31 | INCLUDE_RE = re.compile(r'^#include\s+"([^"]+)"') |
| 32 | OPTIONS = [ |
| 33 | "bitfield-enum", |
| 34 | "newtype-enum", |
| 35 | "newtype-global-enum", |
| 36 | "rustified-enum", |
| 37 | "rustified-non-exhaustive-enum", |
| 38 | "constified-enum", |
| 39 | "constified-enum-module", |
| 40 | "normal-alias", |
| 41 | "new-type-alias", |
| 42 | "new-type-alias-deref", |
| 43 | "bindgen-wrapper-union", |
| 44 | "manually-drop-union", |
| 45 | "blocklist-type", |
| 46 | "blocklist-function", |
| 47 | "blocklist-item", |
| 48 | "blocklist-file", |
| 49 | "blocklist-var", |
| 50 | "opaque-type", |
| 51 | "no-partialeq", |
| 52 | "no-copy", |
| 53 | "no-debug", |
| 54 | "no-default", |
| 55 | "no-hash", |
| 56 | "must-use-type", |
| 57 | "with-derive-custom", |
| 58 | "with-derive-custom-struct", |
| 59 | "with-derive-custom-enum", |
| 60 | "with-derive-custom-union", |
| 61 | "with-attribute-custom", |
| 62 | "with-attribute-custom-struct", |
| 63 | "with-attribute-custom-enum", |
| 64 | "with-attribute-custom-union", |
| 65 | ] |
| 66 | |
| 67 | |
| 68 | @dataclass |
| 69 | class BindgenInfo: |
| 70 | cmd_args: List[str] |
| 71 | inputs: List[str] |
| 72 | |
| 73 | |
| 74 | def extract_includes(lines: Iterable[str]) -> List[str]: |
| 75 | """Extract #include directives from a file.""" |
| 76 | includes: List[str] = [] |
| 77 | for line in lines: |
| 78 | match = INCLUDE_RE.match(line.strip()) |
| 79 | if match: |
| 80 | includes.append(match.group(1)) |
| 81 | return includes |
| 82 | |
| 83 | |
| 84 | def build_bindgen_args(metadata: Dict[str, Any]) -> List[str]: |
| 85 | """Build command line arguments from [package.metadata.bindgen].""" |
| 86 | args: List[str] = [] |
| 87 | for key, values in metadata.items(): |
| 88 | if key in OPTIONS: |
| 89 | flag = f"--{key}" |
| 90 | assert isinstance(values, list) |
| 91 | for value in values: |
| 92 | args.append(flag) |
| 93 | args.append(value) |
| 94 | |
| 95 | return args |
| 96 | |
| 97 | |
| 98 | def main() -> int: |
| 99 | parser = argparse.ArgumentParser( |
| 100 | description="Generate bindgen arguments from Cargo.toml metadata" |
| 101 | ) |
| 102 | parser.add_argument( |
| 103 | "directories", nargs="+", help="Directories containing Cargo.toml files" |
| 104 | ) |
| 105 | parser.add_argument( |
| 106 | "-I", |
| 107 | "--include-root", |
| 108 | default=None, |
| 109 | help="Base path for --allowlist-file/--blocklist-file", |
| 110 | ) |
| 111 | parser.add_argument("--source-dir", default=os.getcwd(), help="Source directory") |
| 112 | parser.add_argument("-o", "--output", required=True, help="Output file") |
| 113 | parser.add_argument("--dep-file", help="Dependency file to write") |
| 114 | args = parser.parse_args() |
| 115 | |
| 116 | prev_allowlist_files: Dict[str, object] = {} |
| 117 | bindgen_infos: Dict[str, BindgenInfo] = {} |
| 118 | |
| 119 | os.chdir(args.source_dir) |
| 120 | include_root = args.include_root or args.source_dir |
| 121 | for directory in args.directories: |
| 122 | cargo_path = Path(directory) / "Cargo.toml" |
| 123 | inputs = [str(Path(args.source_dir) / cargo_path)] |
| 124 | |
| 125 | with open(cargo_path, "rb") as f: |
| 126 | cargo_toml = tomllib.load(f) |
| 127 | |
| 128 | metadata = cargo_toml.get("package", {}).get("metadata", {}).get("bindgen", {}) |
| 129 | input_file = Path(directory) / metadata["header"] |
| 130 | inputs.append(str(Path(args.source_dir) / input_file)) |
| 131 | |
| 132 | cmd_args = build_bindgen_args(metadata) |
| 133 | |
| 134 | # Each include file is allowed for this file and blocked in the |
| 135 | # next ones |
| 136 | for blocklist_path in prev_allowlist_files: |
| 137 | cmd_args.extend(["--blocklist-file", blocklist_path]) |
| 138 | with open(input_file, "r", encoding="utf-8", errors="ignore") as f: |
| 139 | includes = extract_includes(f) |
| 140 | for allowlist_file in includes + metadata.get("additional-files", []): |
| 141 | allowlist_path = Path(include_root) / allowlist_file |
| 142 | cmd_args.extend(["--allowlist-file", str(allowlist_path)]) |
| 143 | prev_allowlist_files.setdefault(str(allowlist_path), True) |
| 144 | |
| 145 | bindgen_infos[directory] = BindgenInfo(cmd_args=cmd_args, inputs=inputs) |
| 146 | |
| 147 | # now write the output |
| 148 | with open(args.output, "w") as f: |
| 149 | for directory, info in bindgen_infos.items(): |
| 150 | args_sh = " ".join(info.cmd_args) |
| 151 | f.write(f"{directory}={args_sh}\n") |
| 152 | |
| 153 | if args.dep_file: |
| 154 | with open(args.dep_file, "w") as f: |
| 155 | deps: List[str] = [] |
| 156 | for info in bindgen_infos.values(): |
| 157 | deps += info.inputs |
| 158 | f.write(f"{os.path.basename(args.output)}: {' '.join(deps)}\n") |
| 159 | |
| 160 | return 0 |
| 161 | |
| 162 | |
| 163 | if __name__ == "__main__": |
| 164 | sys.exit(main()) |