xref: /qemu/scripts/rust/rustc_args.py (revision 15606965400b8f3038d6e85cfe5956d5a6ac33a1)
1#!/usr/bin/env python3
2
3"""Generate rustc arguments for meson rust builds.
4
5This program generates --cfg compile flags for the configuration headers passed
6as arguments.
7
8Copyright (c) 2024 Linaro Ltd.
9
10Authors:
11 Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
12
13This program is free software; you can redistribute it and/or modify
14it under the terms of the GNU General Public License as published by
15the Free Software Foundation; either version 2 of the License, or
16(at your option) any later version.
17
18This program is distributed in the hope that it will be useful,
19but WITHOUT ANY WARRANTY; without even the implied warranty of
20MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21GNU General Public License for more details.
22
23You should have received a copy of the GNU General Public License
24along with this program.  If not, see <http://www.gnu.org/licenses/>.
25"""
26
27import argparse
28from dataclasses import dataclass
29import logging
30from pathlib import Path
31from typing import Any, Iterable, List, Mapping, Optional, Set
32
33try:
34    import tomllib
35except ImportError:
36    import tomli as tomllib
37
38STRICT_LINTS = {"unknown_lints", "warnings"}
39
40
41class CargoTOML:
42    tomldata: Mapping[Any, Any]
43    workspace_data: Mapping[Any, Any]
44    check_cfg: Set[str]
45
46    def __init__(self, path: Optional[str], workspace: Optional[str]):
47        if path is not None:
48            with open(path, 'rb') as f:
49                self.tomldata = tomllib.load(f)
50        else:
51            self.tomldata = {"lints": {"workspace": True}}
52
53        if workspace is not None:
54            with open(workspace, 'rb') as f:
55                self.workspace_data = tomllib.load(f)
56            if "workspace" not in self.workspace_data:
57                self.workspace_data["workspace"] = {}
58
59        self.check_cfg = set(self.find_check_cfg())
60
61    def find_check_cfg(self) -> Iterable[str]:
62        toml_lints = self.lints
63        rust_lints = toml_lints.get("rust", {})
64        cfg_lint = rust_lints.get("unexpected_cfgs", {})
65        return cfg_lint.get("check-cfg", [])
66
67    @property
68    def lints(self) -> Mapping[Any, Any]:
69        return self.get_table("lints", True)
70
71    def get_table(self, key: str, can_be_workspace: bool = False) -> Mapping[Any, Any]:
72        table = self.tomldata.get(key, {})
73        if can_be_workspace and table.get("workspace", False) is True:
74            table = self.workspace_data["workspace"].get(key, {})
75
76        return table
77
78
79@dataclass
80class LintFlag:
81    flags: List[str]
82    priority: int
83
84
85def generate_lint_flags(cargo_toml: CargoTOML, strict_lints: bool) -> Iterable[str]:
86    """Converts Cargo.toml lints to rustc -A/-D/-F/-W flags."""
87
88    toml_lints = cargo_toml.lints
89
90    lint_list = []
91    for k, v in toml_lints.items():
92        prefix = "" if k == "rust" else k + "::"
93        for lint, data in v.items():
94            level = data if isinstance(data, str) else data["level"]
95            priority = 0 if isinstance(data, str) else data.get("priority", 0)
96            if level == "deny":
97                flag = "-D"
98            elif level == "allow":
99                flag = "-A"
100            elif level == "warn":
101                flag = "-W"
102            elif level == "forbid":
103                flag = "-F"
104            else:
105                raise Exception(f"invalid level {level} for {prefix}{lint}")
106
107            # This may change if QEMU ever invokes clippy-driver or rustdoc by
108            # hand.  For now, check the syntax but do not add non-rustc lints to
109            # the command line.
110            if k == "rust" and not (strict_lints and lint in STRICT_LINTS):
111                lint_list.append(LintFlag(flags=[flag, prefix + lint], priority=priority))
112
113    if strict_lints:
114        for lint in STRICT_LINTS:
115            lint_list.append(LintFlag(flags=["-D", lint], priority=1000000))
116
117    lint_list.sort(key=lambda x: x.priority)
118    for lint in lint_list:
119        yield from lint.flags
120
121
122def generate_cfg_flags(header: str, cargo_toml: CargoTOML) -> Iterable[str]:
123    """Converts defines from config[..].h headers to rustc --cfg flags."""
124
125    with open(header, encoding="utf-8") as cfg:
126        config = [l.split()[1:] for l in cfg if l.startswith("#define")]
127
128    cfg_list = []
129    for cfg in config:
130        name = cfg[0]
131        if f'cfg({name})' not in cargo_toml.check_cfg:
132            continue
133        if len(cfg) >= 2 and cfg[1] != "1":
134            continue
135        cfg_list.append("--cfg")
136        cfg_list.append(name)
137    return cfg_list
138
139
140def main() -> None:
141    parser = argparse.ArgumentParser()
142    parser.add_argument("-v", "--verbose", action="store_true")
143    parser.add_argument(
144        "--config-headers",
145        metavar="CONFIG_HEADER",
146        action="append",
147        dest="config_headers",
148        help="paths to any configuration C headers (*.h files), if any",
149        required=False,
150        default=[],
151    )
152    parser.add_argument(
153        metavar="TOML_FILE",
154        action="store",
155        dest="cargo_toml",
156        help="path to Cargo.toml file",
157        nargs='?',
158    )
159    parser.add_argument(
160        "--workspace",
161        metavar="DIR",
162        action="store",
163        dest="workspace",
164        help="path to root of the workspace",
165        required=False,
166        default=None,
167    )
168    parser.add_argument(
169        "--features",
170        action="store_true",
171        dest="features",
172        help="generate --check-cfg arguments for features",
173        required=False,
174        default=None,
175    )
176    parser.add_argument(
177        "--lints",
178        action="store_true",
179        dest="lints",
180        help="generate arguments from [lints] table",
181        required=False,
182        default=None,
183    )
184    parser.add_argument(
185        "--rustc-version",
186        metavar="VERSION",
187        dest="rustc_version",
188        action="store",
189        help="version of rustc",
190        required=False,
191        default="1.0.0",
192    )
193    parser.add_argument(
194        "--strict-lints",
195        action="store_true",
196        dest="strict_lints",
197        help="apply stricter checks (for nightly Rust)",
198        default=False,
199    )
200    args = parser.parse_args()
201    if args.verbose:
202        logging.basicConfig(level=logging.DEBUG)
203    logging.debug("args: %s", args)
204
205    rustc_version = tuple((int(x) for x in args.rustc_version.split('.')[0:2]))
206    if args.workspace:
207        workspace_cargo_toml = Path(args.workspace, "Cargo.toml").resolve()
208        cargo_toml = CargoTOML(args.cargo_toml, str(workspace_cargo_toml))
209    else:
210        cargo_toml = CargoTOML(args.cargo_toml, None)
211
212    if args.lints:
213        for tok in generate_lint_flags(cargo_toml, args.strict_lints):
214            print(tok)
215
216    if rustc_version >= (1, 80):
217        if args.lints:
218            print("--check-cfg")
219            print("cfg(test)")
220            for cfg in sorted(cargo_toml.check_cfg):
221                print("--check-cfg")
222                print(cfg)
223        if args.features:
224            for feature in cargo_toml.get_table("features"):
225                if feature != "default":
226                    print("--check-cfg")
227                    print(f'cfg(feature,values("{feature}"))')
228
229    for header in args.config_headers:
230        for tok in generate_cfg_flags(header, cargo_toml):
231            print(tok)
232
233
234if __name__ == "__main__":
235    main()
236