Coverage for rust2rpm/generator/crate.py: 95%
297 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 13:52 +0100
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 13:52 +0100
1"""Module containing functionality for preparing arguments for the "crate" type spec file template."""
3import time
4from dataclasses import KW_ONLY, dataclass
5from functools import cached_property
6from typing import cast
8from cargo2rpm import rpm
9from cargo2rpm.metadata import Metadata, Package
10from cargo2rpm.semver import Version
12from rust2rpm import __version__
13from rust2rpm.conf import Patch, Source, TomlConf
14from rust2rpm.metadata import get_required_features_for_binaries
15from rust2rpm.project import CrateProject
16from rust2rpm.sysinfo import Target
18from .common import (
19 RUST_PACKAGING_DEPS,
20 cargo_args_from_flags,
21 conf_to_bcond_check,
22 conf_to_bcond_check_comments,
23 conf_to_bin_rename_commands,
24 conf_to_cargo_test_commands,
25 conf_to_feature_flags,
26 make_rpm_summary,
27 min_rust_packaging_dep,
28 normalize_spdx_expr,
29 spec_file_template,
30)
31from .meta import (
32 InvalidMetadataError,
33 Parameters,
34 ParametersFedora,
35 ParametersMageia,
36 ParametersOpenSUSE,
37 ParametersPlain,
38 Template,
39 parameters_as_dict,
40)
43@dataclass(frozen=True)
44class CrateParameters(Parameters):
45 """Collection of parameters for the "crate" type template that is common for all targets."""
47 _: KW_ONLY
49 _project: CrateProject
50 _conf: TomlConf
51 _target: Target
53 _use_relative_license_paths: bool
54 _make_changelog_entry: bool
56 # Convenience properties
58 @property
59 def _metadata(self) -> Metadata:
60 return self._project.metadata
62 @property
63 def _package(self) -> Package:
64 return self._metadata.packages[0]
66 @cached_property
67 def _buildrequires(self) -> tuple[list[str], list[str]]:
68 feature_flags = conf_to_feature_flags(self._conf)
70 buildrequires_with_dev = rpm.buildrequires(self._package, feature_flags, with_dev_deps=True)
71 buildrequires_without_dev = rpm.buildrequires(self._package, feature_flags, with_dev_deps=False)
73 buildrequires = buildrequires_without_dev
74 test_requires = set.difference(buildrequires_with_dev, buildrequires_without_dev)
76 return sorted(buildrequires), sorted(test_requires)
78 # Parameters specific to rust2rpm
80 @property
81 def rust2rpm_version(self) -> str:
82 """Current major version of rust2rpm."""
83 return __version__.split(".")[0]
85 @property
86 def rust2rpm_target(self) -> str:
87 """Target platform (fedora, epel8, mageia, opensuse, plain)."""
88 return self._target
90 @property
91 def rust_packaging_dep(self) -> str:
92 """Dependency string for RPM Rust packaging tools."""
93 return RUST_PACKAGING_DEPS[
94 min_rust_packaging_dep(
95 self._package,
96 self._target,
97 self._project.vendor_tarball,
98 is_bin=self.rpm_binary_package,
99 is_cdylib=self.rpm_cdylib_package,
100 cargo_install_lib=self.cargo_install_lib,
101 cargo_install_bin=self.cargo_install_bin,
102 )
103 ]
105 # Parameters that control compiler flags
107 @property
108 def build_rustflags_debuginfo(self) -> int | None:
109 """Controls the level of debuginfo generated by rustc."""
110 return self._conf.package.debuginfo_level
112 # Parameters for RPM package metadata
114 @property
115 def rpm_name(self) -> str:
116 """RPM source package Name (rust-{crate}{suffix})."""
117 return self._project.rpm_name
119 @property
120 def rpm_version(self) -> str:
121 """RPM package Version (translated to RPM format from SemVer)."""
122 return Version.parse(self._package.version).to_rpm()
124 @cached_property
125 def rpm_summary(self) -> str | None:
126 """RPM package summary (derived from package.description value from Cargo.toml)."""
127 return make_rpm_summary(self._conf, self._package)
129 @property
130 def rpm_description(self) -> str | None:
131 """RPM package description (derived from package.description value from Cargo.toml)."""
132 return self._conf.package.description or self._package.get_description()
134 @cached_property
135 def rpm_license(self) -> str | None:
136 """RPM License tag (derived from package.license value from Cargo.toml)."""
137 if license_str := self._package.license: 137 ↛ 140line 137 didn't jump to line 140
138 return normalize_spdx_expr(license_str)
140 msg = (
141 "No license specified in crate metadata. The 'package.license' property MUST be "
142 "set in crate metadata, otherwise RPM packaging macros will not work properly. "
143 "To resolve this issue, patch Cargo.toml to set the correct license expression "
144 "in SPDX format."
145 )
146 raise InvalidMetadataError(msg)
148 @property
149 def rpm_license_comments(self) -> str | None:
150 """Additional information returned by license string translation."""
151 return None
153 @property
154 def rpm_patch_file_automatic(self) -> str | None:
155 """File name of the automatically generated patch."""
156 return patch.name if (patch := self._project.auto_patch) else None
158 @property
159 def rpm_patch_file_manual(self) -> str | None:
160 """File name of the manually generated patch."""
161 return patch.name if (patch := self._project.manual_patch) else None
163 @property
164 def rpm_patch_file_comments(self) -> list[str]:
165 """Additional lines of comments for the manually generated patch file."""
166 return self._conf.package.cargo_toml_patch_comment_lines
168 @property
169 def rpm_license_files(self) -> list[str]:
170 """List of the license files which were detected in crate sources."""
171 return self._project.license_files
173 @property
174 def rpm_doc_files(self) -> list[str]:
175 """List of the documentation files which were detected in crate sources."""
176 return self._project.doc_files
178 @property
179 def rpm_bcond_check(self) -> int:
180 """Flag to switch default value of the "check" bcond."""
181 return conf_to_bcond_check(self._conf)
183 @property
184 def rpm_bcond_check_comments(self) -> list[str]:
185 """Comments associated with a disabled "check" bcond."""
186 return conf_to_bcond_check_comments(self._conf)
188 @property
189 def rpm_vendor_source(self) -> str | None:
190 """File name of the vendor tarball in case vendored sources are used."""
191 return self._project.vendor_tarball
193 @property
194 def rpm_extra_sources(self) -> list[Source]:
195 """Additional source files with number and comments."""
196 return self._conf.package.extra_sources
198 @property
199 def rpm_extra_patches(self) -> list[Patch]:
200 """Additional patch files with number and comments."""
201 return self._conf.package.extra_patches
203 @property
204 def rpm_extra_files(self) -> list[str]:
205 """Additional files to be included in the built package (only applies if there is a non-devel package)."""
206 return self._conf.package.extra_files
208 @property
209 def rpm_exclude_crate_files(self) -> list[str]:
210 """List of files / directories to be excluded from installed crate sources."""
211 return self._conf.package.exclude_crate_files
213 # Parameters that control generation of subpackages
215 @property
216 def rpm_binary_package(self) -> bool:
217 """True if package ships any binary targets (bin targets)."""
218 return self._metadata.is_bin()
220 @property
221 def rpm_cdylib_package(self) -> bool:
222 """True if package ships any shared libraries (cdylib targets)."""
223 return self._metadata.is_cdylib()
225 @property
226 def rpm_library_package(self) -> bool:
227 """True if package shipts a library interface (lib target)."""
228 return self._metadata.is_lib()
230 @property
231 def rpm_binary_package_name(self) -> str:
232 """Override name of the binary subpackage (default: %{crate})."""
233 return self._conf.package.bin_package_name or "%{crate}"
235 @property
236 def rpm_binary_names(self) -> list[str]:
237 """List of the names of executables which are built from the crate."""
238 # enforce consistent order
239 if bin_renames := self._conf.package.bin_renames: 239 ↛ 240line 239 didn't jump to line 240 because the condition on line 239 was never true
240 binaries = []
241 for old_name in self._metadata.get_binaries():
242 if new_name := bin_renames.get(old_name):
243 binaries.append(new_name)
244 else:
245 binaries.append(old_name)
246 else:
247 binaries = list(self._metadata.get_binaries())
248 return sorted(binaries)
250 @property
251 def crate_features(self) -> list[str | None]:
252 """List of names of features for which sub-packages are generated."""
253 features: list[str | None] = cast(list[str | None], sorted(self._package.get_feature_names()))
255 # enforce consistent order:
256 # - None is always the first element,
257 # - "default" is always the second element,
258 # - other feature names appear at index >=2 in sorted order
260 if "default" in features:
261 features.remove("default")
262 features.insert(0, None)
263 features.insert(1, "default")
265 if features_hide := self._conf.features.hide:
266 for feature in features_hide:
267 features.remove(feature)
269 return features
271 @property
272 def cargo_install_lib(self) -> bool:
273 """Prevent installation of library sources if False (default: True)."""
274 return (self._conf.package.cargo_install_lib is not False) and (self._project.vendor_tarball is None)
276 @property
277 def cargo_install_bin(self) -> bool:
278 """Prevent installation of binary targets if False (default: True)."""
279 return (self._conf.package.cargo_install_bin is not False) and (
280 not self._project.compat if self.rpm_binary_package else True
281 )
283 # Parameters that allow injecting additional commands into scriptlets
285 @property
286 def rpm_prep_pre(self) -> list[str]:
287 """Additional commands that are injected before %cargo_prep."""
288 return self._conf.scripts.prep.pre
290 @property
291 def rpm_prep_post(self) -> list[str]:
292 """Additional commands that are injected after %cargo_prep."""
293 return self._conf.scripts.prep.post
295 @property
296 def rpm_build_pre(self) -> list[str]:
297 """Additional commands that are injected before %cargo_build."""
298 return self._conf.scripts.build.pre
300 @property
301 def rpm_build_post(self) -> list[str]:
302 """Additional commands that are injected after %cargo_build."""
303 return self._conf.scripts.build.post
305 @property
306 def rpm_install_pre(self) -> list[str]:
307 """Additional commands that are injected before %cargo_install."""
308 return self._conf.scripts.install.pre
310 @property
311 def rpm_install_post(self) -> list[str]:
312 """Additional commands that are injected after %cargo_install."""
313 return self._conf.scripts.install.post
315 @property
316 def rpm_check_pre(self) -> list[str]:
317 """Additional commands that are injected before %cargo_check."""
318 return self._conf.scripts.check.pre
320 @property
321 def rpm_check_post(self) -> list[str]:
322 """Additional commands that are injected after %cargo_check."""
323 return self._conf.scripts.check.post
325 # Parameters for crate metadata
327 @property
328 def crate_name(self) -> str:
329 """Crate name (from Cargo.toml metadata)."""
330 return self._project.name
332 @property
333 def crate_version(self) -> str:
334 """Crate version (from Cargo.toml metadata, SemVer format, not normalized)."""
335 return self._project.version
337 @property
338 def crate_license(self) -> str | None:
339 """Crate license (from Cargo.toml metadata, not normalized)."""
340 return self._package.license
342 # Parameters for RPM macros
344 @property
345 def rpm_autosetup_args(self) -> str:
346 """Additional arguments for the %autosetup macro."""
347 return " -a1" if self._project.vendor_tarball else ""
349 @property
350 def cargo_args(self) -> str:
351 """Additional arguments for %cargo_build, %cargo_install, etc."""
352 feature_flags = conf_to_feature_flags(self._conf)
353 required_features = get_required_features_for_binaries(self._package)
354 features_enabled_by_default = self._package.get_enabled_features_transitive(feature_flags)[0]
355 return cargo_args_from_flags(feature_flags, required_features, features_enabled_by_default)
357 @property
358 def cargo_prep_args(self) -> str:
359 """Additional arguments for %cargo_prep."""
360 return " -v vendor" if self._project.vendor_tarball else ""
362 @property
363 def cargo_test_commands(self) -> list[str]:
364 """List of customized and pre-formatted %cargo_test commands."""
365 return conf_to_cargo_test_commands(self._conf, self.cargo_args)
367 # Parameters derived from rust2rpm.toml
369 @property
370 def conf_buildrequires(self) -> list[str]:
371 """List of additionally specified RPM BuildRequires."""
372 return self._conf.requires.build
374 @property
375 def conf_test_requires(self) -> list[str]:
376 """List of additionally specified RPM BuildRequires that are gated by an "%if %{with check}"" conditional."""
377 return self._conf.requires.test
379 @property
380 def conf_bin_requires(self) -> list[str]:
381 """List of additionally specified RPM Requires for the binary package."""
382 return self._conf.requires.bin
384 @property
385 def conf_lib_requires(self) -> dict[str, list[str]]:
386 """Map from feature names to lists of additional RPM Requires for library packages."""
387 conf_lib_requires = {}
389 for feature in self.crate_features:
390 if feature is None:
391 conf_key = "lib"
392 conf_lib_requires[conf_key] = self._conf.requires.lib
394 else:
395 conf_key = f"lib+{feature}"
396 if requires_features := self._conf.requires.features:
397 conf_lib_requires[conf_key] = requires_features.get(feature) or []
398 else:
399 conf_lib_requires[conf_key] = []
401 return conf_lib_requires
403 @property
404 def conf_supported_arches(self) -> str | None:
405 """List of supported architectures.
407 Results in "%ifarch" conditionals around %cargo_build and %cargo_test macros.
408 """
409 if supported_arches := self._conf.package.supported_arches: 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true
410 conf_supported_arches = " ".join(supported_arches)
411 else:
412 conf_supported_arches = None
414 return conf_supported_arches
416 @property
417 def conf_suppress_cdylib_install_fixme(self) -> bool:
418 """Toggle suppression of the FIXME comment for installing cdylib crate targets."""
419 return self._conf.package.suppress_cdylib_install_fixme is True
421 @property
422 def rpm_bin_renames(self) -> list[str]:
423 """List of "mv" commands to rename binaries installed in %{_bindir}."""
424 return conf_to_bin_rename_commands(self._conf)
426 # Parameters derived from command-line flags
428 @property
429 def use_relative_license_paths(self) -> bool:
430 """Toggle between relative and absolute paths as arguments for the %license macro."""
431 return self._use_relative_license_paths
433 @property
434 def make_changelog_entry(self) -> bool:
435 """Toggle inclusion of a changelog entry in generated spec files."""
436 return self._make_changelog_entry
439@dataclass(frozen=True)
440class CrateParametersFedora(CrateParameters, ParametersFedora):
441 """Collection of parameters for the "crate" type template that is specific to the "fedora" target."""
444@dataclass(frozen=True)
445class CrateParametersMageia(CrateParameters, ParametersMageia):
446 """Collection of parameters for the "crate" type template that is specific to the "mageia" target."""
448 @property
449 def rpm_buildrequires(self) -> list[str]:
450 """Automatically generated RPM BuildRequires for crate dependencies."""
451 return self._buildrequires[0]
453 @property
454 def rpm_test_requires(self) -> list[str]:
455 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
456 return self._buildrequires[1]
459@dataclass(frozen=True)
460class CrateParametersOpenSUSE(CrateParameters, ParametersOpenSUSE):
461 """Collection of parameters for the "crate" type template that is specific to the "opensuse" target."""
463 @property
464 def rpm_buildrequires(self) -> list[str]:
465 """Automatically generated RPM BuildRequires for crate dependencies."""
466 return self._buildrequires[0]
468 @property
469 def rpm_test_requires(self) -> list[str]:
470 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
471 return self._buildrequires[1]
474@dataclass(frozen=True)
475class CrateParametersPlain(CrateParameters, ParametersPlain):
476 """Collection of parameters for the "crate" type template that is specific to the "plain" target."""
478 @property
479 def rpm_buildrequires(self) -> list[str]:
480 """Automatically generated RPM BuildRequires for crate dependencies."""
481 return self._buildrequires[0]
483 @property
484 def rpm_test_requires(self) -> list[str]:
485 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
486 return self._buildrequires[1]
488 @property
489 def rpm_requires(self) -> dict[str | None, list[str]]:
490 """Automatically generated RPM Requires for subpackages."""
491 return {feature: sorted(rpm.requires(self._package, feature)) for feature in self.crate_features}
493 @property
494 def rpm_provides(self) -> dict[str | None, str]:
495 """Automatically generated RPM Provides for subpackages."""
496 return {feature: rpm.provides(self._package, feature) for feature in self.crate_features}
499@dataclass(frozen=True)
500class CrateTemplate(Template):
501 """Template for rendering spec files with the "crate" template."""
503 project: CrateProject
504 """Project for which the spec file is rendered."""
506 conf: TomlConf
507 """Global rust2rpm configuration."""
509 target: Target
510 """Target for which the spec file is rendered."""
512 date: time.struct_time | None
513 """Optional timestamp for generated changelog entries.
514 Falls back to the current time if not specified."""
516 packager: str | None
517 """Optional user identification for generated changelog entries.
518 Falls back to dummy values if not specified."""
520 use_relative_license_paths: bool
521 """Predicate that controls whether relative or absolute paths are used
522 for %license files in %files listings."""
524 use_rpmautospec: bool
525 """Predicate that controls whether rpmautospec is used for
526 the Release tag and %changelog."""
528 make_changelog_entry: bool
529 """Predicate that controls whether a changelog entry is automatically generated."""
531 def render(self) -> str:
532 """Render spec file from template with the computed parameters."""
533 template = spec_file_template("crate.spec")
535 match self.target:
536 case Target.FEDORA:
537 parameter_cls = CrateParametersFedora # type: ignore[assignment]
538 case Target.MAGEIA:
539 parameter_cls = CrateParametersMageia # type: ignore[assignment]
540 case Target.OPENSUSE:
541 parameter_cls = CrateParametersOpenSUSE # type: ignore[assignment]
542 case Target.PLAIN:
543 parameter_cls = CrateParametersPlain # type: ignore[assignment]
544 case x: # pragma nocover
545 msg = f"{x} is not a supported target for project-type packages"
546 raise ValueError(msg)
548 parameters = parameter_cls(
549 _project=self.project,
550 _conf=self.conf,
551 _target=self.target,
552 _use_relative_license_paths=self.use_relative_license_paths,
553 _make_changelog_entry=self.make_changelog_entry,
554 _date=self.date,
555 _packager=self.packager,
556 _use_rpmautospec=self.use_rpmautospec,
557 )
559 contents = template.render(**parameters_as_dict(parameters))
561 if not contents.endswith("\n"):
562 contents += "\n"
564 return contents