xref: /openbmc/qemu/scripts/rust/rustc_args.py (revision 90868c3dcec755f567426d1fad64e7611053778e)
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
38
39class CargoTOML:
40    tomldata: Mapping[Any, Any]
41    workspace_data: Mapping[Any, Any]
42    check_cfg: Set[str]
43
44    def __init__(self, path: Optional[str], workspace: Optional[str]):
45        if path is not None:
46            with open(path, 'rb') as f:
47                self.tomldata = tomllib.load(f)
48        else:
49            self.tomldata = {"lints": {"workspace": True}}
50
51        if workspace is not None:
52            with open(workspace, 'rb') as f:
53                self.workspace_data = tomllib.load(f)
54            if "workspace" not in self.workspace_data:
55                self.workspace_data["workspace"] = {}
56
57        self.check_cfg = set(self.find_check_cfg())
58
59    def find_check_cfg(self) -> Iterable[str]:
60        toml_lints = self.lints
61        rust_lints = toml_lints.get("rust", {})
62        cfg_lint = rust_lints.get("unexpected_cfgs", {})
63        return cfg_lint.get("check-cfg", [])
64
65    @property
66    def lints(self) -> Mapping[Any, Any]:
67        return self.get_table("lints", True)
68
69    def get_table(self, key: str, can_be_workspace: bool = False) -> Mapping[Any, Any]:
70        table = self.tomldata.get(key, {})
71        if can_be_workspace and table.get("workspace", False) is True:
72            table = self.workspace_data["workspace"].get(key, {})
73
74        return table
75
76
77@dataclass
78class LintFlag:
79    flags: List[str]
80    priority: int
81
82
83def generate_lint_flags(cargo_toml: CargoTOML) -> Iterable[str]:
84    """Converts Cargo.toml lints to rustc -A/-D/-F/-W flags."""
85
86    toml_lints = cargo_toml.lints
87
88    lint_list = []
89    for k, v in toml_lints.items():
90        prefix = "" if k == "rust" else k + "::"
91        for lint, data in v.items():
92            level = data if isinstance(data, str) else data["level"]
93            priority = 0 if isinstance(data, str) else data.get("priority", 0)
94            if level == "deny":
95                flag = "-D"
96            elif level == "allow":
97                flag = "-A"
98            elif level == "warn":
99                flag = "-W"
100            elif level == "forbid":
101                flag = "-F"
102            else:
103                raise Exception(f"invalid level {level} for {prefix}{lint}")
104
105            # This may change if QEMU ever invokes clippy-driver or rustdoc by
106            # hand.  For now, check the syntax but do not add non-rustc lints to
107            # the command line.
108            if k == "rust":
109                lint_list.append(LintFlag(flags=[flag, prefix + lint], priority=priority))
110
111    lint_list.sort(key=lambda x: x.priority)
112    for lint in lint_list:
113        yield from lint.flags
114
115
116def generate_cfg_flags(header: str, cargo_toml: CargoTOML) -> Iterable[str]:
117    """Converts defines from config[..].h headers to rustc --cfg flags."""
118
119    with open(header, encoding="utf-8") as cfg:
120        config = [l.split()[1:] for l in cfg if l.startswith("#define")]
121
122    cfg_list = []
123    for cfg in config:
124        name = cfg[0]
125        if f'cfg({name})' not in cargo_toml.check_cfg:
126            continue
127        if len(cfg) >= 2 and cfg[1] != "1":
128            continue
129        cfg_list.append("--cfg")
130        cfg_list.append(name)
131    return cfg_list
132
133
134def main() -> None:
135    parser = argparse.ArgumentParser()
136    parser.add_argument("-v", "--verbose", action="store_true")
137    parser.add_argument(
138        "--config-headers",
139        metavar="CONFIG_HEADER",
140        action="append",
141        dest="config_headers",
142        help="paths to any configuration C headers (*.h files), if any",
143        required=False,
144        default=[],
145    )
146    parser.add_argument(
147        metavar="TOML_FILE",
148        action="store",
149        dest="cargo_toml",
150        help="path to Cargo.toml file",
151        nargs='?',
152    )
153    parser.add_argument(
154        "--workspace",
155        metavar="DIR",
156        action="store",
157        dest="workspace",
158        help="path to root of the workspace",
159        required=False,
160        default=None,
161    )
162    parser.add_argument(
163        "--features",
164        action="store_true",
165        dest="features",
166        help="generate --check-cfg arguments for features",
167        required=False,
168        default=None,
169    )
170    parser.add_argument(
171        "--lints",
172        action="store_true",
173        dest="lints",
174        help="generate arguments from [lints] table",
175        required=False,
176        default=None,
177    )
178    parser.add_argument(
179        "--rustc-version",
180        metavar="VERSION",
181        dest="rustc_version",
182        action="store",
183        help="version of rustc",
184        required=False,
185        default="1.0.0",
186    )
187    args = parser.parse_args()
188    if args.verbose:
189        logging.basicConfig(level=logging.DEBUG)
190    logging.debug("args: %s", args)
191
192    rustc_version = tuple((int(x) for x in args.rustc_version.split('.')[0:2]))
193    if args.workspace:
194        workspace_cargo_toml = Path(args.workspace, "Cargo.toml").resolve()
195        cargo_toml = CargoTOML(args.cargo_toml, str(workspace_cargo_toml))
196    else:
197        cargo_toml = CargoTOML(args.cargo_toml, None)
198
199    if args.lints:
200        for tok in generate_lint_flags(cargo_toml):
201            print(tok)
202
203    if rustc_version >= (1, 80):
204        if args.lints:
205            for cfg in sorted(cargo_toml.check_cfg):
206                print("--check-cfg")
207                print(cfg)
208        if args.features:
209            for feature in cargo_toml.get_table("features"):
210                if feature != "default":
211                    print("--check-cfg")
212                    print(f'cfg(feature,values("{feature}"))')
213
214    for header in args.config_headers:
215        for tok in generate_cfg_flags(header, cargo_toml):
216            print(tok)
217
218
219if __name__ == "__main__":
220    main()
221