xref: /qemu/scripts/rust/rustc_args.py (revision f8a113701dd2d28f3bedb216e59125ddcb77fd05)
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            if not (strict_lints and lint in STRICT_LINTS):
108                lint_list.append(LintFlag(flags=[flag, prefix + lint], priority=priority))
109
110    if strict_lints:
111        for lint in STRICT_LINTS:
112            lint_list.append(LintFlag(flags=["-D", lint], priority=1000000))
113
114    lint_list.sort(key=lambda x: x.priority)
115    for lint in lint_list:
116        yield from lint.flags
117
118
119def generate_cfg_flags(header: str, cargo_toml: CargoTOML) -> Iterable[str]:
120    """Converts defines from config[..].h headers to rustc --cfg flags."""
121
122    with open(header, encoding="utf-8") as cfg:
123        config = [l.split()[1:] for l in cfg if l.startswith("#define")]
124
125    cfg_list = []
126    for cfg in config:
127        name = cfg[0]
128        if f'cfg({name})' not in cargo_toml.check_cfg:
129            continue
130        if len(cfg) >= 2 and cfg[1] != "1":
131            continue
132        cfg_list.append("--cfg")
133        cfg_list.append(name)
134    return cfg_list
135
136
137def main() -> None:
138    parser = argparse.ArgumentParser()
139    parser.add_argument("-v", "--verbose", action="store_true")
140    parser.add_argument(
141        "--config-headers",
142        metavar="CONFIG_HEADER",
143        action="append",
144        dest="config_headers",
145        help="paths to any configuration C headers (*.h files), if any",
146        required=False,
147        default=[],
148    )
149    parser.add_argument(
150        metavar="TOML_FILE",
151        action="store",
152        dest="cargo_toml",
153        help="path to Cargo.toml file",
154        nargs='?',
155    )
156    parser.add_argument(
157        "--workspace",
158        metavar="DIR",
159        action="store",
160        dest="workspace",
161        help="path to root of the workspace",
162        required=False,
163        default=None,
164    )
165    parser.add_argument(
166        "--features",
167        action="store_true",
168        dest="features",
169        help="generate --check-cfg arguments for features",
170        required=False,
171        default=None,
172    )
173    parser.add_argument(
174        "--lints",
175        action="store_true",
176        dest="lints",
177        help="generate arguments from [lints] table",
178        required=False,
179        default=None,
180    )
181    parser.add_argument(
182        "--rustc-version",
183        metavar="VERSION",
184        dest="rustc_version",
185        action="store",
186        help="version of rustc",
187        required=False,
188        default="1.0.0",
189    )
190    parser.add_argument(
191        "--strict-lints",
192        action="store_true",
193        dest="strict_lints",
194        help="apply stricter checks (for nightly Rust)",
195        default=False,
196    )
197    args = parser.parse_args()
198    if args.verbose:
199        logging.basicConfig(level=logging.DEBUG)
200    logging.debug("args: %s", args)
201
202    rustc_version = tuple((int(x) for x in args.rustc_version.split('.')[0:2]))
203    if args.workspace:
204        workspace_cargo_toml = Path(args.workspace, "Cargo.toml").resolve()
205        cargo_toml = CargoTOML(args.cargo_toml, str(workspace_cargo_toml))
206    else:
207        cargo_toml = CargoTOML(args.cargo_toml, None)
208
209    if args.lints:
210        for tok in generate_lint_flags(cargo_toml, args.strict_lints):
211            print(tok)
212
213    if rustc_version >= (1, 80):
214        if args.lints:
215            print("--check-cfg")
216            print("cfg(test)")
217            for cfg in sorted(cargo_toml.check_cfg):
218                print("--check-cfg")
219                print(cfg)
220        if args.features:
221            for feature in cargo_toml.get_table("features"):
222                if feature != "default":
223                    print("--check-cfg")
224                    print(f'cfg(feature,values("{feature}"))')
225
226    for header in args.config_headers:
227        for tok in generate_cfg_flags(header, cargo_toml):
228            print(tok)
229
230
231if __name__ == "__main__":
232    main()
233