#!/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 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())