Coverage for rust2rpm/conf/ini.py: 92%
188 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-25 18:52 +0100
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-25 18:52 +0100
1"""Module containing functionality for the deprecated INI-based configuration format."""
3import sys
4from configparser import ConfigParser, ExtendedInterpolation, SectionProxy
5from dataclasses import dataclass
6from pathlib import Path
7from typing import Any
9from rust2rpm import log
10from rust2rpm.sysinfo import TARGET_NAMES
12from .common import ConfError
13from .toml import TomlConf
15VALID_INI_KEYS = [
16 "summary",
17 "supported-arches",
18 "all-features",
19 "unwanted-features",
20 "enabled-features",
21 "buildrequires",
22 "testrequires",
23 "lib.requires",
24 "bin.requires",
25]
28def to_list(s: str, *, sort: bool = True) -> list[str]:
29 """Parse a multi-line value into a list of strings."""
30 f = sorted if sort else list
31 return f(filter(None, (elem.strip() for elem in s.splitlines())))
34def _validate_ini_conf_with_features(merged: SectionProxy, validate_features: set[str]):
35 # validate configuration file
36 valid_keys = VALID_INI_KEYS.copy()
38 # the "default" feature is always implicitly defined
39 valid_keys.append("lib+default.requires")
40 valid_keys.extend(f"lib+{feature}.requires" for feature in validate_features)
42 # check setting keys
43 for key in merged:
44 if key not in valid_keys: 44 ↛ 45line 44 didn't jump to line 45 because the condition on line 44 was never true
45 msg = f"Invalid key: {key!r}"
46 raise ConfError(msg)
48 # check settings values
49 if unwanted_features := merged.get("unwanted-features"):
50 unwanted_features = to_list(unwanted_features)
51 for unwanted_feature in unwanted_features:
52 if unwanted_feature not in validate_features: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 msg = f'Unrecognized "unwanted" feature: {unwanted_feature!r}'
54 raise ConfError(msg)
56 if enabled_features := merged.get("enabled-features"):
57 enabled_features = to_list(enabled_features)
58 for enabled_feature in enabled_features:
59 if enabled_feature not in validate_features: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true
60 msg = f'Unrecognized "enabled" feature: {enabled_feature!r}'
61 raise ConfError(msg)
64def _validate_ini_conf_without_features(merged: SectionProxy) -> set[str]:
65 # validate configuration file
66 valid_keys = VALID_INI_KEYS.copy()
68 # determine features that are mentioned in the configuration file
69 mentioned_features = set()
70 for key in merged:
71 if key.startswith("lib+") and key.endswith(".requires"):
72 mentioned_features.add(key.removeprefix("lib+").removesuffix(".requires"))
74 # check setting keys
75 for key in merged:
76 # cannot validate against actual crate features:
77 # just validate against *mentioned* features and check for valid syntax
78 if key not in valid_keys:
79 if key in [f"lib+{feature}.requires" for feature in mentioned_features]: 79 ↛ 81line 79 didn't jump to line 81 because the condition on line 79 was always true
80 continue
81 msg = f"Invalid key: {key!r}"
82 raise ConfError(msg)
84 return mentioned_features
87@dataclass(frozen=True)
88class IniConf:
89 """rust2rpm configuration loaded from the deprecated rust2rpm.conf file format."""
91 summary: str | None = None
92 """Override for the generated RPM Summary tag."""
94 supported_arches: list[str] | None = None
95 """List of architectures supported by the package.
97 For crates, this is used to conditionally run `%build` and `%check` scriptlets.
98 For other projects, it is used as the value of an `ExclusiveArch` tag.
99 """
101 all_features: bool | None = None
102 """Flag that controls whether the `--all-features` flag is passed to cargo."""
104 unwanted_features: list[str] | None = None
105 """List of features for which the generation of the corresponding subpackage is suppressed."""
107 enabled_features: list[str] | None = None
108 """List of features that are enabled and passed with `--features` to cargo."""
110 buildrequires: list[str] | None = None
111 """List of additional `BuildRequires`."""
113 testrequires: list[str] | None = None
114 """List of additional `BuildRequires` that only apply when building the package with tests."""
116 bin_requires: list[str] | None = None
117 """List of additional `Requires` for the built binary subpackage."""
119 lib_requires: dict[str | None, list[str]] | None = None
120 """List of additional `Requires` for built library subpackages."""
122 @staticmethod
123 def _from_configparser(merged: SectionProxy, validate_features: set[str] | None) -> "IniConf":
124 # validate configuration
125 if validate_features is not None:
126 _validate_ini_conf_with_features(merged, validate_features)
127 known_features = validate_features
128 else:
129 mentioned_features = _validate_ini_conf_without_features(merged)
130 known_features = mentioned_features
132 # parse configuration
133 summary = value if (value := merged.get("summary")) else None
134 supported_arches = to_list(value, sort=False) if (value := merged.get("supported-arches")) else None
136 all_features = value if (value := merged.getboolean("all-features")) and value is not None else None
137 unwanted_features = to_list(value) if (value := merged.get("unwanted-features")) else None
138 enabled_features = to_list(value) if (value := merged.get("enabled-features")) else None
140 buildrequires = to_list(value) if (value := merged.get("buildrequires")) else None
141 testrequires = to_list(value) if (value := merged.get("testrequires")) else None
142 bin_requires = to_list(value) if (value := merged.get("bin.requires")) else None
144 lib_requires: dict[str | None, list[str]] | None = None
145 if value := merged.get("lib.requires"):
146 lib_requires = {}
147 lib_requires[None] = to_list(value) # type: ignore[index]
149 for feature in known_features:
150 if value := merged.get(f"lib+{feature}.requires"):
151 if lib_requires is None:
152 lib_requires = {}
153 lib_requires[feature] = to_list(value) # type: ignore[index]
155 return IniConf(
156 summary,
157 supported_arches,
158 all_features,
159 unwanted_features,
160 enabled_features,
161 buildrequires,
162 testrequires,
163 bin_requires,
164 lib_requires,
165 )
167 @staticmethod
168 def load(filenames: list[str], target: str, validate_features: set[str] | None) -> "IniConf":
169 """Load and validate rust2rpm configuration for the given target.
171 Arguments:
172 filenames: List of file paths that are attempted to be read.
173 target: Name of the spec file generation target.
174 validate_features: Optional set of valid feature names.
175 Used to validate some settings.
177 Returns:
178 Loaded and validated rust2rpm configuration.
180 Raises:
181 `FileNotFoundError` when none of the expected file paths existed.
182 `FileExistsError` when more than one config file was found.
183 `ConfError` if the configuration file failed validation.
185 """
186 conf = ConfigParser(interpolation=ExtendedInterpolation())
187 confs = conf.read(filenames)
189 if len(confs) == 0:
190 raise FileNotFoundError
192 if len(confs) > 1: # pragma nocover
193 raise FileExistsError
195 # clean up configuration files with deprecated names
196 if ".rust2rpm.conf" in confs: # pragma nocover
197 Path(".rust2rpm.conf").rename("rust2rpm.conf")
198 log.info("Renamed deprecated, hidden .rust2rpm.conf file to rust2rpm.conf.")
200 if "_rust2rpm.conf" in confs: # pragma nocover
201 Path("_rust2rpm.conf").rename("rust2rpm.conf")
202 log.info("Renamed deprecated _rust2rpm.conf file to rust2rpm.conf.")
204 # merge target-specific configuration with default configuration
205 if target not in conf:
206 conf.add_section(target)
207 merged = conf[target]
209 # check section names
210 for section in conf:
211 if section != "DEFAULT" and section not in TARGET_NAMES: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 msg = f"Invalid section: {section!r}"
213 raise ConfError(msg)
215 return IniConf._from_configparser(merged, validate_features)
217 def upgrade(self) -> TomlConf: # noqa: PLR0912
218 """Upgrade from old to new configuration file format."""
219 settings: dict[str, Any] = {}
221 package: dict[str, Any] = {}
222 if self.summary:
223 package["summary"] = self.summary
224 if self.supported_arches:
225 package["supported-arches"] = self.supported_arches
226 if package:
227 settings["package"] = package
229 features: dict[str, Any] = {}
230 if self.all_features is not None:
231 features["enable-all"] = self.all_features
232 if self.enabled_features:
233 features["enable"] = self.enabled_features
234 if self.unwanted_features:
235 features["hide"] = self.unwanted_features
236 if features:
237 settings["features"] = features
239 requires: dict[str, Any] = {}
240 if self.buildrequires:
241 requires["build"] = self.buildrequires
242 if self.testrequires:
243 requires["test"] = self.testrequires
244 if self.bin_requires:
245 requires["bin"] = self.bin_requires
246 if self.lib_requires:
247 for key, value in self.lib_requires.items():
248 if key is None:
249 requires["lib"] = value
250 else:
251 if "features" not in requires:
252 requires["features"] = {}
253 requires["features"][key] = value
254 if requires:
255 settings["requires"] = requires
257 # feature names were already validated
258 return TomlConf.from_data(settings, None)
260 def migrate(self, validate_features: set[str] | None) -> str:
261 """Migrate from old to new configuration file format.
263 Arguments:
264 validate_features: Optional set of valid feature names.
265 Used to validate some settings.
267 Returns:
268 Configuration in rust2rpm.toml format equivalent to the
269 settings in the original config file.
271 """
272 # check if the written config would be valid
273 old = self.upgrade()
275 lines = []
277 def write_array(name: str, values: list[str] | None):
278 if values is None:
279 return
280 lines.append(f"{name} = [")
281 lines.extend([f' "{value}",' for value in values])
282 lines.append("]")
284 if self.summary or self.supported_arches:
285 lines.append("[package]")
286 if summary := self.summary:
287 lines.append(f'summary = "{summary}"')
288 write_array("supported-arches", self.supported_arches)
289 lines.append("")
291 if self.all_features or self.unwanted_features or self.enabled_features:
292 lines.append("[features]")
293 if self.all_features is True:
294 lines.append("enable-all = true")
295 write_array("enable", self.enabled_features)
296 write_array("hide", self.unwanted_features)
297 lines.append("")
299 if self.buildrequires or self.testrequires or self.bin_requires or self.lib_requires:
300 lines.append("[requires]")
301 write_array("build", self.buildrequires)
302 write_array("test", self.testrequires)
303 write_array("bin", self.bin_requires)
304 if lib_requires := self.lib_requires:
305 write_array("lib", lib_requires.pop(None, None))
306 for feature, feature_deps in sorted(lib_requires.items()):
307 write_array(f"features.{feature}", feature_deps)
308 lines.append("")
310 toml = "\n".join(lines)
312 new = TomlConf.from_str(toml, validate_features)
314 # check if contents are equivalent
315 if old != new: # pragma nocover: no known failure cases
316 msg = "Failed to convert rust2rpm.conf to rust2rpm.toml automatically."
317 raise ConfError(msg)
319 return toml
322def load_config_ini(paths: list[str], target: str, validate_features: set[str] | None = None) -> IniConf | None:
323 """Load rust2rpm configuration from the deprecated rust2rpm.conf file format.
325 Arguments:
326 paths: List of accepted file paths (at most one of them should point to
327 an existing file).
328 target: Spec file generation target.
329 validate_features: Optional set of valid feature names.
330 Used to validate some settings.
332 Returns:
333 Configuration loaded from a file in INI format, or `None` if no file was found.
335 Raises:
336 `SystemExit` on fatal errors.
338 """
339 try:
340 distconf = IniConf.load(paths, target, validate_features)
342 except FileNotFoundError:
343 return None
345 except FileExistsError:
346 log.error(
347 "More than one *rust2rpm.conf file is present in this directory. "
348 "Ensure that there is only one, and that it has the correct contents.",
349 )
350 sys.exit(1)
352 except ConfError as exc:
353 log.error("Invalid rust2rpm configuration file:")
354 log.error(str(exc))
355 sys.exit(1)
357 else:
358 return distconf