| #!/usr/bin/env python3 |
| # SPDX-License-Identifier: GPL-2.0-or-later |
| |
| """ |
| Generate bindgen arguments from Cargo.toml metadata for QEMU's Rust FFI bindings. |
| |
| Author: Paolo Bonzini <pbonzini@redhat.com> |
| |
| Copyright (C) 2025 Red Hat, Inc. |
| |
| This script processes Cargo.toml file for QEMU's bindings crates (util-sys, |
| chardev-sys, qom-sys, etc.); it generates bindgen command lines that allow |
| easy customization and that export the right headers in each bindings crate. |
| |
| For detailed information, see docs/devel/rust.rst. |
| """ |
| |
| import os |
| import re |
| import sys |
| import argparse |
| from pathlib import Path |
| from dataclasses import dataclass |
| from typing import Iterable, List, Dict, Any |
| |
| try: |
| import tomllib |
| except ImportError: |
| import tomli as tomllib # type: ignore |
| |
| INCLUDE_RE = re.compile(r'^#include\s+"([^"]+)"') |
| OPTIONS = [ |
| "bitfield-enum", |
| "newtype-enum", |
| "newtype-global-enum", |
| "rustified-enum", |
| "rustified-non-exhaustive-enum", |
| "constified-enum", |
| "constified-enum-module", |
| "normal-alias", |
| "new-type-alias", |
| "new-type-alias-deref", |
| "bindgen-wrapper-union", |
| "manually-drop-union", |
| "blocklist-type", |
| "blocklist-function", |
| "blocklist-item", |
| "blocklist-file", |
| "blocklist-var", |
| "opaque-type", |
| "no-partialeq", |
| "no-copy", |
| "no-debug", |
| "no-default", |
| "no-hash", |
| "must-use-type", |
| "with-derive-custom", |
| "with-derive-custom-struct", |
| "with-derive-custom-enum", |
| "with-derive-custom-union", |
| "with-attribute-custom", |
| "with-attribute-custom-struct", |
| "with-attribute-custom-enum", |
| "with-attribute-custom-union", |
| ] |
| |
| |
| @dataclass |
| class BindgenInfo: |
| cmd_args: List[str] |
| inputs: List[str] |
| |
| |
| def extract_includes(lines: Iterable[str]) -> List[str]: |
| """Extract #include directives from a file.""" |
| includes: List[str] = [] |
| for line in lines: |
| match = INCLUDE_RE.match(line.strip()) |
| if match: |
| includes.append(match.group(1)) |
| return includes |
| |
| |
| def build_bindgen_args(metadata: Dict[str, Any]) -> List[str]: |
| """Build command line arguments from [package.metadata.bindgen].""" |
| args: List[str] = [] |
| for key, values in metadata.items(): |
| if key in OPTIONS: |
| flag = f"--{key}" |
| assert isinstance(values, list) |
| for value in values: |
| args.append(flag) |
| args.append(value) |
| |
| return args |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser( |
| description="Generate bindgen arguments from Cargo.toml metadata" |
| ) |
| parser.add_argument( |
| "directories", nargs="+", help="Directories containing Cargo.toml files" |
| ) |
| parser.add_argument( |
| "-I", |
| "--include-root", |
| default=None, |
| help="Base path for --allowlist-file/--blocklist-file", |
| ) |
| parser.add_argument("--source-dir", default=os.getcwd(), help="Source directory") |
| parser.add_argument("-o", "--output", required=True, help="Output file") |
| parser.add_argument("--dep-file", help="Dependency file to write") |
| args = parser.parse_args() |
| |
| prev_allowlist_files: Dict[str, object] = {} |
| bindgen_infos: Dict[str, BindgenInfo] = {} |
| |
| os.chdir(args.source_dir) |
| include_root = args.include_root or args.source_dir |
| for directory in args.directories: |
| cargo_path = Path(directory) / "Cargo.toml" |
| inputs = [str(Path(args.source_dir) / cargo_path)] |
| |
| with open(cargo_path, "rb") as f: |
| cargo_toml = tomllib.load(f) |
| |
| metadata = cargo_toml.get("package", {}).get("metadata", {}).get("bindgen", {}) |
| input_file = Path(directory) / metadata["header"] |
| inputs.append(str(Path(args.source_dir) / input_file)) |
| |
| cmd_args = build_bindgen_args(metadata) |
| |
| # Each include file is allowed for this file and blocked in the |
| # next ones |
| for blocklist_path in prev_allowlist_files: |
| cmd_args.extend(["--blocklist-file", blocklist_path]) |
| with open(input_file, "r", encoding="utf-8", errors="ignore") as f: |
| includes = extract_includes(f) |
| for allowlist_file in includes + metadata.get("additional-files", []): |
| allowlist_path = Path(include_root) / allowlist_file |
| cmd_args.extend(["--allowlist-file", str(allowlist_path)]) |
| prev_allowlist_files.setdefault(str(allowlist_path), True) |
| |
| bindgen_infos[directory] = BindgenInfo(cmd_args=cmd_args, inputs=inputs) |
| |
| # now write the output |
| with open(args.output, "w") as f: |
| for directory, info in bindgen_infos.items(): |
| args_sh = " ".join(info.cmd_args) |
| f.write(f"{directory}={args_sh}\n") |
| |
| if args.dep_file: |
| with open(args.dep_file, "w") as f: |
| deps: List[str] = [] |
| for info in bindgen_infos.values(): |
| deps += info.inputs |
| f.write(f"{os.path.basename(args.output)}: {' '.join(deps)}\n") |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |