Coverage for rust2rpm/conf.py: 76%
495 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-27 15:21 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-27 15:21 +0100
1import configparser
2import os
3import sys
4import textwrap
5import tomllib
6from typing import Any, Optional
8from cargo2rpm.metadata import FeatureFlags
9import jsonschema
11from rust2rpm import log
14TOML_SCHEMA = {
15 "type": "object",
16 "properties": {
17 # package properties
18 "package": {
19 "type": "object",
20 "properties": {
21 # override package Summary
22 "summary": {
23 "type": "string",
24 },
25 # override package description
26 "description": {
27 "type": "string",
28 },
29 # override package project URL
30 "url": {
31 "type": "string",
32 },
33 # override package source URL
34 "source-url": {
35 "type": "string",
36 },
37 # supported target architectures
38 "supported-arches": {
39 "type": "array",
40 "items": {
41 "type": "string",
42 "uniqueItems": True,
43 },
44 },
45 # override binary subpackage name
46 "bin-package-name": {
47 "type": "string",
48 },
49 # override installation of binary targets
50 "cargo-install-bin": {
51 "type": "boolean",
52 },
53 # override installation of library sources
54 "cargo-install-lib": {
55 "type": "boolean",
56 },
57 # override default debuginfo level (2)
58 "debuginfo-level": {
59 "type": "integer",
60 "enum": [0, 1, 2],
61 },
62 # comments for manual Cargo.toml patches (rust2rpm -p)
63 "cargo-toml-patch-comments": {
64 "type": "array",
65 "items": {
66 "type": "string",
67 },
68 },
69 # additional source files
70 "extra-sources": {
71 "type": "array",
72 "items": {
73 "type": "object",
74 "properties": {
75 "file": {
76 "type": "string",
77 },
78 "number": {
79 "type": "integer",
80 },
81 "comments": {
82 "type": "array",
83 "items": {
84 "type": "string",
85 },
86 },
87 },
88 "additionalProperties": False,
89 "required": ["file", "number"],
90 },
91 },
92 # additional patch files
93 "extra-patches": {
94 "type": "array",
95 "items": {
96 "type": "object",
97 "properties": {
98 "file": {
99 "type": "string",
100 },
101 "number": {
102 "type": "integer",
103 },
104 "comments": {
105 "type": "array",
106 "items": {
107 "type": "string",
108 },
109 },
110 },
111 "additionalProperties": False,
112 "required": ["file", "number"],
113 },
114 },
115 # additional packaged files
116 "extra-files": {
117 "type": "array",
118 "items": {
119 "type": "string",
120 },
121 },
122 # rename binaries to avoid conflicts
123 "bin-renames": {
124 "type": "object",
125 "patternProperties": {
126 "^.*$": {
127 "type": "string",
128 },
129 },
130 "additionalProperties": False,
131 },
132 },
133 "additionalProperties": False,
134 },
135 # additional commands for scriptlets
136 "scripts": {
137 "type": "object",
138 "properties": {
139 "prep": {
140 "type": "object",
141 "properties": {
142 "pre": {
143 "type": "array",
144 "items": {
145 "type": "string",
146 },
147 },
148 "post": {
149 "type": "array",
150 "items": {
151 "type": "string",
152 },
153 },
154 },
155 "additionalProperties": False,
156 },
157 "build": {
158 "type": "object",
159 "properties": {
160 "pre": {
161 "type": "array",
162 "items": {
163 "type": "string",
164 },
165 },
166 "post": {
167 "type": "array",
168 "items": {
169 "type": "string",
170 },
171 },
172 },
173 "additionalProperties": False,
174 },
175 "install": {
176 "type": "object",
177 "properties": {
178 "pre": {
179 "type": "array",
180 "items": {
181 "type": "string",
182 },
183 },
184 "post": {
185 "type": "array",
186 "items": {
187 "type": "string",
188 },
189 },
190 },
191 "additionalProperties": False,
192 },
193 "check": {
194 "type": "object",
195 "properties": {
196 "pre": {
197 "type": "array",
198 "items": {
199 "type": "string",
200 },
201 },
202 "post": {
203 "type": "array",
204 "items": {
205 "type": "string",
206 },
207 },
208 },
209 "additionalProperties": False,
210 },
211 },
212 "additionalProperties": False,
213 },
214 # configure which tests are run
215 "tests": {
216 "type": "object",
217 "properties": {
218 "run": {
219 "type": "array",
220 "items": {
221 "type": "string",
222 "enum": ["none", "all", "lib", "bin", "doc", "bins", "tests"],
223 "uniqueItems": True,
224 },
225 },
226 "skip": {
227 "type": "array",
228 "items": {
229 "type": "string",
230 "uniqueItems": True,
231 },
232 },
233 "skip-exact": {
234 "type": "boolean",
235 },
236 "comments": {
237 "type": "array",
238 "items": {
239 "type": "string",
240 },
241 },
242 },
243 "additionalProperties": False,
244 "required": ["comments"],
245 },
246 # feature flags passed to cargo
247 "features": {
248 "type": "object",
249 "properties": {
250 # pass "--all-features"
251 "enable-all": {
252 "type": "boolean",
253 },
254 # pass "--features foo,bar"
255 "enable": {
256 "type": "array",
257 "items": {
258 "type": "string",
259 "uniqueItems": True,
260 },
261 },
262 # pass "--no-default-features"
263 "disable-default": {
264 "type": "boolean",
265 },
266 # hide feature subpackages
267 "hide": {
268 "type": "array",
269 "items": {
270 "type": "string",
271 "uniqueItems": True,
272 },
273 },
274 },
275 "additionalProperties": False,
276 },
277 # additional RPM Build/Requires
278 "requires": {
279 "type": "object",
280 "properties": {
281 # additional BuildRequires
282 "build": {
283 "type": "array",
284 "items": {
285 "type": "string",
286 "uniqueItems": True,
287 },
288 },
289 # additional BuildRequires gated by the "check" bconf
290 "test": {
291 "type": "array",
292 "items": {
293 "type": "string",
294 "uniqueItems": True,
295 },
296 },
297 # additional Requires for the -devel subpackage
298 "lib": {
299 "type": "array",
300 "items": {
301 "type": "string",
302 "uniqueItems": True,
303 },
304 },
305 # additional Requires for the binary subpackage
306 "bin": {
307 "type": "array",
308 "items": {
309 "type": "string",
310 "uniqueItems": True,
311 },
312 },
313 # additional Requires for feature subpackages
314 "features": {
315 "type": "object",
316 "patternProperties": {
317 "^.*$": {
318 "type": "array",
319 "items": {
320 "type": "string",
321 "uniqueItems": True,
322 },
323 },
324 },
325 "additionalProperties": False,
326 },
327 },
328 "additionalProperties": False,
329 },
330 },
331 "additionalProperties": False,
332}
335def to_list(s, sort=True):
336 return list(sorted(filter(None, (elem.strip() for elem in s.splitlines()))))
339class ConfError(ValueError):
340 pass
343class IniConf:
344 def __init__(
345 self,
346 *,
347 summary: Optional[str] = None,
348 supported_arches: Optional[list[str]] = None,
349 all_features: Optional[bool] = None,
350 unwanted_features: Optional[list[str]] = None,
351 enabled_features: Optional[list[str]] = None,
352 buildrequires: Optional[list[str]] = None,
353 testrequires: Optional[list[str]] = None,
354 bin_requires: Optional[list[str]] = None,
355 lib_requires: Optional[dict[Optional[str], list[str]]] = None,
356 conf_file: bool = False,
357 ):
358 self.summary: Optional[str] = summary
359 self.supported_arches: list[str] = supported_arches or list()
360 self.all_features: Optional[bool] = all_features
361 self.unwanted_features: list[str] = unwanted_features or list()
362 self.enabled_features: list[str] = enabled_features or list()
363 self.buildrequires: list[str] = buildrequires or list()
364 self.testrequires: list[str] = testrequires or list()
365 self.bin_requires: list[str] = bin_requires or list()
366 self.lib_requires: dict[Optional[str], list[str]] = lib_requires or dict()
367 self.conf_file: bool = conf_file
369 def __eq__(self, other):
370 if not isinstance(other, IniConf):
371 return False # pragma nocover
373 # order of items in the list is consistent:
374 # lists are sorted when parsing rust2rpm.conf files
375 return (
376 self.summary == other.summary
377 and self.supported_arches == other.supported_arches
378 and self.all_features == other.all_features
379 and self.unwanted_features == other.unwanted_features
380 and self.enabled_features == other.enabled_features
381 and self.buildrequires == other.buildrequires
382 and self.testrequires == other.testrequires
383 and self.bin_requires == other.bin_requires
384 and self.lib_requires == other.lib_requires
385 )
387 @staticmethod
388 def load(filenames: list[str], target: str, features: Optional[set[str]]) -> "IniConf":
389 conf = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
390 confs = conf.read(filenames)
392 if len(confs) > 1: # pragma nocover
393 raise FileExistsError
395 # clean up configuration files with deprecated names
396 if ".rust2rpm.conf" in confs: # pragma nocover
397 os.rename(".rust2rpm.conf", "rust2rpm.conf")
398 log.info("Renamed deprecated, hidden .rust2rpm.conf file to rust2rpm.conf.")
400 if "_rust2rpm.conf" in confs: # pragma nocover
401 os.rename("_rust2rpm.conf", "rust2rpm.conf")
402 log.info("Renamed deprecated _rust2rpm.conf file to rust2rpm.conf.")
404 # merge target-specific configuration with default configuration
405 if target not in conf:
406 conf.add_section(target)
407 merged = conf[target]
409 # validate configuration file
410 valid_targets = ["fedora", "epel8", "mageia", "opensuse", "plain"]
411 valid_keys = [
412 "summary",
413 "supported-arches",
414 "all-features",
415 "unwanted-features",
416 "enabled-features",
417 "buildrequires",
418 "testrequires",
419 "lib.requires",
420 "bin.requires",
421 ]
423 if features is not None: 423 ↛ 432line 423 didn't jump to line 432 because the condition on line 423 was always true
424 # the "default" feature is always implicitly defined
425 if "default" not in features: 425 ↛ 428line 425 didn't jump to line 428 because the condition on line 425 was always true
426 features.add("default")
428 for feature in features:
429 valid_keys.append(f"lib+{feature}.requires")
431 # check section names
432 for section in conf.keys():
433 if section != "DEFAULT" and section not in valid_targets: 433 ↛ 434line 433 didn't jump to line 434 because the condition on line 433 was never true
434 raise ConfError(f"Invalid section: {section!r}")
436 # check setting keys
437 for key in merged.keys():
438 if key not in valid_keys: 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true
439 raise ConfError(f"Invalid key: {key!r}")
441 # parse configuration and validate settings
442 settings = dict()
444 if summary := merged.get("summary"): 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true
445 settings["summary"] = summary
447 if supported_arches := merged.get("supported-arches"): 447 ↛ 448line 447 didn't jump to line 448 because the condition on line 447 was never true
448 settings["supported_arches"] = to_list(supported_arches, False)
450 if merged.get("all-features"):
451 all_features = merged.getboolean("all-features")
452 settings["all_features"] = all_features
454 if unwanted_features := merged.get("unwanted-features"):
455 settings["unwanted_features"] = to_list(unwanted_features)
457 if features is not None: 457 ↛ 462line 457 didn't jump to line 462 because the condition on line 457 was always true
458 for unwanted_feature in settings["unwanted_features"]:
459 if unwanted_feature not in features: 459 ↛ 460line 459 didn't jump to line 460 because the condition on line 459 was never true
460 raise ConfError(f'Unrecognized "unwanted" feature: {unwanted_feature!r}')
462 if enabled_features := merged.get("enabled-features"):
463 settings["enabled_features"] = to_list(enabled_features)
465 if features is not None: 465 ↛ 470line 465 didn't jump to line 470 because the condition on line 465 was always true
466 for enabled_feature in settings["enabled_features"]:
467 if enabled_feature not in features: 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true
468 raise ConfError(f'Unrecognized "enabled" feature: {enabled_feature!r}')
470 if buildrequires := merged.get("buildrequires"):
471 settings["buildrequires"] = to_list(buildrequires)
472 if testrequires := merged.get("testrequires"): 472 ↛ 473line 472 didn't jump to line 473 because the condition on line 472 was never true
473 settings["testrequires"] = to_list(testrequires)
474 if bin_requires := merged.get("bin.requires"): 474 ↛ 475line 474 didn't jump to line 475 because the condition on line 474 was never true
475 settings["bin_requires"] = to_list(bin_requires)
477 if lib_requires := merged.get("lib.requires"):
478 if "lib_requires" not in settings.keys(): 478 ↛ 480line 478 didn't jump to line 480 because the condition on line 478 was always true
479 settings["lib_requires"] = dict()
480 settings["lib_requires"][None] = to_list(lib_requires) # type: ignore
482 if features is not None: 482 ↛ 489line 482 didn't jump to line 489 because the condition on line 482 was always true
483 for feature in features:
484 if lib_feature_requires := merged.get(f"lib+{feature}.requires"):
485 if "lib_requires" not in settings.keys(): 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true
486 settings["lib_requires"] = dict()
487 settings["lib_requires"][feature] = to_list(lib_feature_requires) # type: ignore
489 settings["conf_file"] = len(confs) == 1
491 return IniConf(**settings) # type: ignore
494def conf_comments_to_spec_comments(comments: Optional[list[str]]) -> list[str]:
495 if not comments:
496 return []
498 lines = []
499 for comment in comments:
500 lines.extend(
501 textwrap.wrap(
502 comment,
503 width=80,
504 initial_indent="# * ",
505 subsequent_indent="# ",
506 break_long_words=False,
507 break_on_hyphens=False,
508 )
509 )
511 return lines
514class Source:
515 def __init__(self, data: dict):
516 self._data = data
518 @property
519 def file(self) -> str:
520 return self._data["file"]
522 @property
523 def number(self) -> int:
524 return self._data["number"]
526 @property
527 def comments(self) -> list[str]:
528 return self._data.get("comments") or list()
530 @property
531 def comment_lines(self) -> list[str]:
532 return conf_comments_to_spec_comments(self.comments)
534 @property
535 def whitespace(self) -> str:
536 return " " * (16 - (len("Source") + len(str(self.number)) + 1))
539class Patch:
540 def __init__(self, data: dict):
541 self._data = data
543 @property
544 def file(self) -> str:
545 return self._data["file"]
547 @property
548 def number(self) -> int:
549 return self._data["number"]
551 @property
552 def comments(self) -> list[str]:
553 return self._data.get("comments") or list()
555 @property
556 def comment_lines(self) -> list[str]:
557 return conf_comments_to_spec_comments(self.comments)
559 @property
560 def whitespace(self) -> str:
561 return " " * (16 - (len("Patch") + len(str(self.number)) + 1))
564class TomlConf:
565 def __init__(self, data: Optional[dict] = None):
566 self._data = data or dict()
568 def __repr__(self) -> str:
569 return repr(self._data)
571 def __eq__(self, other) -> bool:
572 if not isinstance(other, TomlConf):
573 return False # pragma nocover
575 return self._data == other._data
577 @property
578 def _package(self) -> Optional[dict]:
579 return self._data.get("package")
581 @property
582 def package_summary(self) -> Optional[str]:
583 if package := self._package: 583 ↛ 584line 583 didn't jump to line 584 because the condition on line 583 was never true
584 return package.get("summary")
585 else:
586 return None
588 @property
589 def package_description(self) -> Optional[str]:
590 if package := self._package: 590 ↛ 591line 590 didn't jump to line 591 because the condition on line 590 was never true
591 return package.get("description")
592 else:
593 return None
595 @property
596 def package_url(self) -> Optional[str]:
597 if package := self._package: 597 ↛ 598line 597 didn't jump to line 598 because the condition on line 597 was never true
598 return package.get("url")
599 else:
600 return None
602 @property
603 def package_source_url(self) -> Optional[str]:
604 if package := self._package: 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true
605 return package.get("source-url")
606 else:
607 return None
609 @property
610 def package_supported_arches(self) -> Optional[list[str]]:
611 if package := self._package: 611 ↛ 612line 611 didn't jump to line 612 because the condition on line 611 was never true
612 return package.get("supported-arches")
613 else:
614 return None
616 @property
617 def package_bin_package_name(self) -> Optional[str]:
618 if package := self._package: 618 ↛ 619line 618 didn't jump to line 619 because the condition on line 618 was never true
619 return package.get("bin-package-name")
620 else:
621 return None
623 @property
624 def package_cargo_install_bin(self) -> Optional[bool]:
625 if package := self._package: 625 ↛ 626line 625 didn't jump to line 626 because the condition on line 625 was never true
626 return package.get("cargo-install-bin")
627 else:
628 return None
630 @property
631 def package_cargo_install_lib(self) -> Optional[bool]:
632 if package := self._package: 632 ↛ 633line 632 didn't jump to line 633 because the condition on line 632 was never true
633 return package.get("cargo-install-lib")
634 else:
635 return None
637 @property
638 def package_debuginfo_level(self) -> Optional[int]:
639 if package := self._package: 639 ↛ 640line 639 didn't jump to line 640 because the condition on line 639 was never true
640 return package.get("debuginfo-level")
641 else:
642 return None
644 @property
645 def package_cargo_toml_patch_comments(self) -> Optional[list[str]]:
646 if package := self._package: 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true
647 return package.get("cargo-toml-patch-comments")
648 else:
649 return None
651 @property
652 def package_cargo_toml_patch_comment_lines(self) -> Optional[list[str]]:
653 return conf_comments_to_spec_comments(self.package_cargo_toml_patch_comments)
655 @property
656 def package_extra_sources(self) -> Optional[list[Source]]:
657 if package := self._package: 657 ↛ 658line 657 didn't jump to line 658 because the condition on line 657 was never true
658 if sources := package.get("extra-sources"):
659 return [Source(source) for source in sources]
660 else:
661 return None
662 else:
663 return None
665 @property
666 def package_extra_patches(self) -> Optional[list[Patch]]:
667 if package := self._package: 667 ↛ 668line 667 didn't jump to line 668 because the condition on line 667 was never true
668 if patches := package.get("extra-patches"):
669 return [Patch(patch) for patch in patches]
670 else:
671 return None
672 else:
673 return None
675 @property
676 def package_extra_files(self) -> Optional[list[str]]:
677 if package := self._package: 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true
678 return package.get("extra-files")
679 else:
680 return None
682 @property
683 def package_bin_renames(self) -> Optional[dict[str, str]]:
684 if package := self._package: 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true
685 return package.get("bin-renames")
686 else:
687 return None
689 @property
690 def _scripts(self) -> Optional[dict[str, dict[str, list[str]]]]:
691 return self._data.get("scripts")
693 @property
694 def _scripts_prep(self) -> Optional[dict[str, list[str]]]:
695 if scripts := self._scripts: 695 ↛ 696line 695 didn't jump to line 696 because the condition on line 695 was never true
696 return scripts.get("prep")
697 else:
698 return None
700 @property
701 def scripts_prep_pre(self) -> Optional[list[str]]:
702 if scripts_prep := self._scripts_prep: 702 ↛ 703line 702 didn't jump to line 703 because the condition on line 702 was never true
703 return scripts_prep.get("pre")
704 else:
705 return None
707 @property
708 def scripts_prep_post(self) -> Optional[list[str]]:
709 if scripts_prep := self._scripts_prep: 709 ↛ 710line 709 didn't jump to line 710 because the condition on line 709 was never true
710 return scripts_prep.get("post")
711 else:
712 return None
714 @property
715 def _scripts_build(self) -> Optional[dict[str, list[str]]]:
716 if scripts := self._scripts: 716 ↛ 717line 716 didn't jump to line 717 because the condition on line 716 was never true
717 return scripts.get("build")
718 else:
719 return None
721 @property
722 def scripts_build_pre(self) -> Optional[list[str]]:
723 if scripts_build := self._scripts_build: 723 ↛ 724line 723 didn't jump to line 724 because the condition on line 723 was never true
724 return scripts_build.get("pre")
725 else:
726 return None
728 @property
729 def scripts_build_post(self) -> Optional[list[str]]:
730 if scripts_build := self._scripts_build: 730 ↛ 731line 730 didn't jump to line 731 because the condition on line 730 was never true
731 return scripts_build.get("post")
732 else:
733 return None
735 @property
736 def _scripts_install(self) -> Optional[dict[str, list[str]]]:
737 if scripts := self._scripts: 737 ↛ 738line 737 didn't jump to line 738 because the condition on line 737 was never true
738 return scripts.get("install")
739 else:
740 return None
742 @property
743 def scripts_install_pre(self) -> Optional[list[str]]:
744 if scripts_install := self._scripts_install: 744 ↛ 745line 744 didn't jump to line 745 because the condition on line 744 was never true
745 return scripts_install.get("pre")
746 else:
747 return None
749 @property
750 def scripts_install_post(self) -> Optional[list[str]]:
751 if scripts_install := self._scripts_install: 751 ↛ 752line 751 didn't jump to line 752 because the condition on line 751 was never true
752 return scripts_install.get("post")
753 else:
754 return None
756 @property
757 def _scripts_check(self) -> Optional[dict[str, list[str]]]:
758 if scripts := self._scripts: 758 ↛ 759line 758 didn't jump to line 759 because the condition on line 758 was never true
759 return scripts.get("check")
760 else:
761 return None
763 @property
764 def scripts_check_pre(self) -> Optional[list[str]]:
765 if scripts_check := self._scripts_check: 765 ↛ 766line 765 didn't jump to line 766 because the condition on line 765 was never true
766 return scripts_check.get("pre")
767 else:
768 return None
770 @property
771 def scripts_check_post(self) -> Optional[list[str]]:
772 if scripts_check := self._scripts_check: 772 ↛ 773line 772 didn't jump to line 773 because the condition on line 772 was never true
773 return scripts_check.get("post")
774 else:
775 return None
777 @property
778 def _tests(self) -> Optional[dict[str, Any]]:
779 return self._data.get("tests")
781 @property
782 def tests_run(self) -> Optional[list[str]]:
783 if tests := self._tests:
784 return tests.get("run")
785 return None
787 @property
788 def tests_skip(self) -> Optional[list[str]]:
789 if tests := self._tests:
790 return tests.get("skip")
791 return None
793 @property
794 def tests_skip_exact(self) -> Optional[bool]:
795 if tests := self._tests:
796 return tests.get("skip-exact")
797 return None
799 @property
800 def tests_comments(self) -> Optional[list[str]]:
801 if tests := self._tests:
802 return tests.get("comments")
803 return None
805 @property
806 def tests_comment_lines(self) -> Optional[list[str]]:
807 return conf_comments_to_spec_comments(self.tests_comments)
809 @property
810 def _features(self) -> Optional[dict]:
811 return self._data.get("features")
813 @property
814 def features_enable_all(self) -> Optional[bool]:
815 if features := self._features:
816 return features.get("enable-all")
817 else:
818 return None
820 @property
821 def features_enable(self) -> Optional[list[str]]:
822 if features := self._features:
823 return features.get("enable")
824 else:
825 return None
827 @property
828 def features_disable_default(self) -> Optional[bool]:
829 if features := self._features:
830 return features.get("disable-default")
831 else:
832 return None
834 @property
835 def features_hide(self) -> Optional[list[str]]:
836 if features := self._features:
837 return features.get("hide")
838 else:
839 return None
841 @property
842 def _requires(self) -> Optional[dict]:
843 return self._data.get("requires")
845 @property
846 def requires_build(self) -> Optional[list[str]]:
847 if requires := self._requires:
848 return requires.get("build")
849 else:
850 return None
852 @property
853 def requires_test(self) -> Optional[list[str]]:
854 if requires := self._requires:
855 return requires.get("test")
856 else:
857 return None
859 @property
860 def requires_lib(self) -> Optional[list[str]]:
861 if requires := self._requires:
862 return requires.get("lib")
863 else:
864 return None
866 @property
867 def requires_bin(self) -> Optional[list[str]]:
868 if requires := self._requires:
869 return requires.get("bin")
870 else:
871 return None
873 @property
874 def requires_features(self) -> Optional[dict[str, list[str]]]:
875 if requires := self._requires:
876 return requires.get("features")
877 else:
878 return None
880 @staticmethod
881 def load(path: str, features: Optional[set[str]]) -> "TomlConf":
882 with open(path, "rb") as file:
883 toml = tomllib.load(file)
885 jsonschema.validate(toml, TOML_SCHEMA)
887 conf = TomlConf(toml)
889 # the "default" feature is always implicitly defined
890 if features and "default" not in features:
891 features.add("default")
893 # validate the "tests.run" setting:
894 # both "all" and "none" cannot be combimed with other values
895 if tests_run := conf.tests_run:
896 if "all" in tests_run: 896 ↛ 897line 896 didn't jump to line 897 because the condition on line 896 was never true
897 if len(tests_run) != 1:
898 raise ConfError(f"Invalid set of tests to run: {tests_run!r}")
899 if "none" in tests_run:
900 if len(tests_run) != 1: 900 ↛ 901line 900 didn't jump to line 901 because the condition on line 900 was never true
901 raise ConfError(f"Invalid set of tests to run: {tests_run!r}")
903 # validate the "features.enable-all" and "features.disable-default" settings:
904 # both cannot be enabled at the same time
905 if conf.features_enable_all is True and conf.features_disable_default is True: 905 ↛ 906line 905 didn't jump to line 906 because the condition on line 905 was never true
906 raise ConfError("Conflicting settings for features: 'enable-all' and 'disable-default'")
908 if features is not None: 908 ↛ 931line 908 didn't jump to line 931 because the condition on line 908 was always true
909 # validate the "features.enable" setting:
910 # list elements must be valid feature names
911 if enable_list := conf.features_enable:
912 for enabled in enable_list:
913 if enabled not in features: 913 ↛ 914line 913 didn't jump to line 914 because the condition on line 913 was never true
914 raise ConfError(f'Unrecognized "enabled" feature: {enabled!r}')
916 # validate the "features.hide" setting:
917 # list elements must be valid feature names
918 if hide_list := conf.features_hide:
919 for hidden in hide_list:
920 if hidden not in features: 920 ↛ 921line 920 didn't jump to line 921 because the condition on line 920 was never true
921 raise ConfError(f'Unrecognized "hidden" feature: {hidden!r}')
923 # validate the "requires.features" setting
924 # dictionary keys must be valid feature names
925 if feature_requires := conf.requires_features:
926 for feature in feature_requires.keys():
927 if feature not in features: 927 ↛ 928line 927 didn't jump to line 928 because the condition on line 927 was never true
928 raise ConfError(f'Unrecognized "Requires" for feature: {feature!r}')
930 # warn when conflicting settings are used
931 if (conf.features_enable_all is True) and (conf.features_hide is not None and len(conf.features_hide) > 0):
932 log.warn(
933 "Conflicting settings for features: "
934 "All features are enabled for the build but some feature subpackages are hidden. "
935 "This is likely an error."
936 )
938 return conf
940 @staticmethod
941 def from_conf(conf: IniConf) -> "TomlConf":
942 settings: dict[str, Any] = dict()
944 package: dict[str, Any] = dict()
945 if conf.summary: 945 ↛ 946line 945 didn't jump to line 946 because the condition on line 945 was never true
946 package["summary"] = conf.summary
947 if conf.supported_arches: 947 ↛ 948line 947 didn't jump to line 948 because the condition on line 947 was never true
948 package["supported-arches"] = conf.supported_arches
949 if package: 949 ↛ 950line 949 didn't jump to line 950 because the condition on line 949 was never true
950 settings["package"] = package
952 features: dict[str, Any] = dict()
953 if conf.all_features is not None:
954 features["enable-all"] = conf.all_features
955 if conf.enabled_features:
956 features["enable"] = conf.enabled_features
957 if conf.unwanted_features:
958 features["hide"] = conf.unwanted_features
959 if features:
960 settings["features"] = features
962 requires: dict[str, Any] = dict()
963 if conf.buildrequires:
964 requires["build"] = conf.buildrequires
965 if conf.testrequires: 965 ↛ 966line 965 didn't jump to line 966 because the condition on line 965 was never true
966 requires["test"] = conf.testrequires
967 if conf.bin_requires: 967 ↛ 968line 967 didn't jump to line 968 because the condition on line 967 was never true
968 requires["bin"] = conf.bin_requires
969 if conf.lib_requires:
970 for key, value in conf.lib_requires.items():
971 if key is None:
972 requires["lib"] = value
973 else:
974 if "features" not in requires.keys():
975 requires["features"] = dict()
976 requires["features"][key] = value
977 if requires:
978 settings["requires"] = requires
980 jsonschema.validate(settings, TOML_SCHEMA)
982 return TomlConf(settings)
984 def to_bcond_check(self) -> bool:
985 if tests_run := self.tests_run:
986 return "none" not in tests_run
987 else:
988 return True
990 def to_feature_flags(self) -> FeatureFlags:
991 return FeatureFlags(
992 all_features=self.features_enable_all is True,
993 no_default_features=self.features_disable_default is True,
994 features=list(sorted(set(self.features_enable) if self.features_enable else set())),
995 )
997 def to_cargo_test_args(self) -> list[str]:
998 if skip := self.tests_skip:
999 skip_flags = []
1000 for name in skip:
1001 skip_flags.extend(["--skip", name])
1002 else:
1003 skip_flags = []
1005 if self.tests_skip_exact is True:
1006 skip_exact_flag = ["--exact"]
1007 else:
1008 skip_exact_flag = []
1010 if run := self.tests_run:
1011 if not ("all" in run or "none" in run):
1012 multi = []
1013 for kind in run:
1014 if skip_flags: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the condition on line 1014 was never true
1015 multi.append(" " + " ".join(["--", f"--{kind}", "--"] + skip_exact_flag + skip_flags))
1016 else:
1017 multi.append(" " + " ".join(["--", f"--{kind}"]))
1018 return multi
1020 if skip_flags:
1021 return [" " + " ".join(["--", "--"] + skip_exact_flag + skip_flags)]
1022 else:
1023 return []
1026def load_config(features: Optional[set[str]], target: str) -> TomlConf:
1027 # attempt to load rust2rpm.toml
1028 try:
1029 tomlconf = TomlConf.load("rust2rpm.toml", features)
1031 except FileNotFoundError as _exc:
1032 tomlconf = None
1034 except tomllib.TOMLDecodeError as exc:
1035 log.error("Cannot read rust2rpm.toml file (TOML syntax error):")
1036 log.error(str(exc))
1037 sys.exit(1)
1039 except jsonschema.ValidationError as exc:
1040 if not exc.path:
1041 log.error("Invalid rust2rpm.toml file (unknown setting or table):")
1042 log.error(exc.message)
1043 else:
1044 err_path = ""
1045 for elem in exc.path:
1046 if isinstance(elem, int):
1047 err_path += f"[{elem}]"
1048 else:
1049 err_path += f"/{elem}"
1050 log.error(f"Invalid rust2rpm.toml file (invalid setting at {err_path!r}):")
1051 log.error(exc.message)
1052 sys.exit(1)
1054 except ConfError as exc:
1055 log.error("Invalid rust2rpm.toml file:")
1056 log.error(str(exc))
1057 sys.exit(1)
1059 if tomlconf:
1060 return tomlconf
1062 # fall back to rust2rpm.conf and translate
1063 try:
1064 # known file names (only in the current working directory)
1065 filenames = [".rust2rpm.conf", "_rust2rpm.conf", "rust2rpm.conf"]
1066 distconf = IniConf.load(filenames, target, features)
1068 except FileExistsError:
1069 log.error(
1070 "More than one *rust2rpm.conf file is present in this directory. "
1071 + "Ensure that there is only one, and that it has the correct contents."
1072 )
1073 sys.exit(1)
1075 except ConfError as exc:
1076 log.error("Invalid rust2rpm configuration file:")
1077 log.error(str(exc))
1078 sys.exit(1)
1080 if distconf.conf_file:
1081 log.warn(
1082 "The rust2rpm.conf file format is deprecated and support for it will "
1083 + "be removed in a future release of rust2rpm. Please migrate settings "
1084 + "to the new TOML based format (rust2rpm.toml)."
1085 )
1087 tomlconf = TomlConf.from_conf(distconf)
1088 return tomlconf