master
py 164 lines 4.88 KB
Raw
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())