blob: 8e526d7e81969bbbfd2bddcb31c2d85e0c172b93 [file] [log] [blame]
#!/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())