Coverage for rust2rpm/generator/project.py: 95%
255 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 "project" type spec file template."""
3import time
4from dataclasses import KW_ONLY, dataclass
5from functools import cached_property
7from cargo2rpm import rpm
8from cargo2rpm.metadata import Metadata, Package
9from cargo2rpm.semver import Version
11from rust2rpm import __version__
12from rust2rpm.conf import Patch, Source, TomlConf
13from rust2rpm.metadata import get_required_features_for_binaries
14from rust2rpm.project import LocalProject
15from rust2rpm.sysinfo import Target
17from .common import (
18 RUST_PACKAGING_DEPS,
19 cargo_args_from_flags,
20 conf_to_bcond_check,
21 conf_to_bcond_check_comments,
22 conf_to_bin_rename_commands,
23 conf_to_cargo_test_commands,
24 conf_to_feature_flags,
25 make_rpm_summary,
26 min_rust_packaging_dep,
27 normalize_spdx_expr,
28 spec_file_template,
29)
30from .meta import (
31 InvalidMetadataError,
32 Parameters,
33 ParametersFedora,
34 ParametersMageia,
35 ParametersOpenSUSE,
36 ParametersPlain,
37 Template,
38 parameters_as_dict,
39)
42@dataclass(frozen=True)
43class ProjectParameters(Parameters):
44 """Collection of parameters for the "project" type template that is common for all targets."""
46 _: KW_ONLY
48 _project: LocalProject
49 _conf: TomlConf
50 _target: Target
52 _make_changelog_entry: bool
54 # Convenience properties
56 @property
57 def _metadata(self) -> Metadata:
58 return self._project.metadata
60 @property
61 def _package(self) -> Package:
62 return self._metadata.packages[0]
64 @cached_property
65 def _buildrequires(self) -> tuple[list[str], list[str]]:
66 feature_flags = conf_to_feature_flags(self._conf)
68 buildrequires_with_dev = rpm.buildrequires(self._package, feature_flags, with_dev_deps=True)
69 buildrequires_without_dev = rpm.buildrequires(self._package, feature_flags, with_dev_deps=False)
71 buildrequires = buildrequires_without_dev
72 test_requires = set.difference(buildrequires_with_dev, buildrequires_without_dev)
74 return sorted(buildrequires), sorted(test_requires)
76 # Parameters specific to rust2rpm
78 @property
79 def rust2rpm_version(self) -> str:
80 """Current major version of rust2rpm."""
81 return __version__.split(".")[0]
83 @property
84 def rust2rpm_target(self) -> str:
85 """Target platform (fedora, epel8, mageia, opensuse, plain)."""
86 return self._target
88 @property
89 def rust_packaging_dep(self) -> str:
90 """Dependency string for RPM Rust packaging tools."""
91 return RUST_PACKAGING_DEPS[
92 min_rust_packaging_dep(
93 self._package,
94 self._target,
95 self._project.vendor_tarball,
96 is_bin=self.rpm_binary_package,
97 is_cdylib=self.rpm_cdylib_package,
98 cargo_install_lib=False,
99 cargo_install_bin=True,
100 )
101 ]
103 # Parameters that control compiler flags
105 @property
106 def build_rustflags_debuginfo(self) -> int | None:
107 """Controls the level of debuginfo generated by rustc."""
108 return self._conf.package.debuginfo_level
110 # Parameters for RPM package metadata
112 @property
113 def rpm_name(self) -> str:
114 """RPM source package Name."""
115 return self._project.rpm_name
117 @property
118 def rpm_version(self) -> str:
119 """RPM package Version (translated to RPM format from SemVer)."""
120 return Version.parse(self._package.version).to_rpm()
122 @cached_property
123 def rpm_summary(self) -> str | None:
124 """RPM package summary (derived from package.description value from Cargo.toml)."""
125 return make_rpm_summary(self._conf, self._package)
127 @property
128 def rpm_description(self) -> str | None:
129 """RPM package description (derived from package.description value from Cargo.toml)."""
130 return self._conf.package.description or self._package.get_description()
132 @property
133 def rpm_url(self) -> str:
134 """RPM URL tag (derived from the rust2rpm.toml setting or Cargo.toml metadata)."""
135 return self._conf.package.url or self._package.repository or self._package.homepage or "# FIXME"
137 @property
138 def rpm_source_url(self) -> str:
139 """RPM Source tag (derive from the rust2rpm.toml setting)."""
140 return self._conf.package.source_url or "# FIXME"
142 @cached_property
143 def rpm_license(self) -> str | None:
144 """RPM License tag (derived from package.license value from Cargo.toml)."""
145 if license_str := self._package.license: 145 ↛ 148line 145 didn't jump to line 148
146 return normalize_spdx_expr(license_str)
148 msg = (
149 "No license specified in crate metadata. The 'package.license' property MUST be "
150 "set in crate metadata, otherwise RPM packaging macros will not work properly. "
151 "To resolve this issue, patch Cargo.toml to set the correct license expression "
152 "in SPDX format."
153 )
154 raise InvalidMetadataError(msg)
156 @property
157 def rpm_license_comments(self) -> str | None:
158 """Additional information returned by license string translation."""
159 return None
161 @property
162 def rpm_patch_file_automatic(self) -> str | None:
163 """File name of the automatically generated patch."""
164 return patch.name if (patch := self._project.auto_patch) else None
166 @property
167 def rpm_patch_file_manual(self) -> str | None:
168 """File name of the manually generated patch."""
169 return patch.name if (patch := self._project.manual_patch) else None
171 @property
172 def rpm_patch_file_comments(self) -> list[str]:
173 """Additional lines of comments for the manually generated patch file."""
174 return self._conf.package.cargo_toml_patch_comment_lines
176 @property
177 def rpm_license_files(self) -> list[str]:
178 """List of the license files which were detected in crate sources."""
179 return self._project.license_files
181 @property
182 def rpm_doc_files(self) -> list[str]:
183 """List of the documentation files which were detected in crate sources."""
184 return self._project.doc_files
186 @property
187 def rpm_bcond_check(self) -> int:
188 """Flag to switch default value of the "check" bcond."""
189 return conf_to_bcond_check(self._conf)
191 @property
192 def rpm_bcond_check_comments(self) -> list[str]:
193 """Comments associated with a disabled "check" bcond."""
194 return conf_to_bcond_check_comments(self._conf)
196 @property
197 def rpm_vendor_source(self) -> str | None:
198 """File name of the vendor tarball in case vendored sources are used."""
199 return self._project.vendor_tarball
201 @property
202 def rpm_extra_sources(self) -> list[Source]:
203 """Additional source files with number and comments."""
204 return self._conf.package.extra_sources
206 @property
207 def rpm_extra_patches(self) -> list[Patch]:
208 """Additional patch files with number and comments."""
209 return self._conf.package.extra_patches
211 @property
212 def rpm_extra_files(self) -> list[str]:
213 """Additional files to be included in the built package."""
214 return self._conf.package.extra_files
216 # Parameters that control generation of subpackages
218 @property
219 def rpm_binary_package(self) -> bool:
220 """True if package ships any binary targets (bin targets)."""
221 return self._metadata.is_bin()
223 @property
224 def rpm_cdylib_package(self) -> bool:
225 """True if package ships any shared libraries (cdylib targets)."""
226 return self._metadata.is_cdylib()
228 @property
229 def rpm_binary_names(self) -> list[str]:
230 """List of the names of executables which are built from the crate."""
231 # enforce consistent order
232 if bin_renames := self._conf.package.bin_renames: 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true
233 binaries = []
234 for old_name in self._metadata.get_binaries():
235 if new_name := bin_renames.get(old_name):
236 binaries.append(new_name)
237 else:
238 binaries.append(old_name)
239 else:
240 binaries = list(self._metadata.get_binaries())
241 return sorted(binaries)
243 # Parameters that allow injecting additional commands into scriptlets
245 @property
246 def rpm_prep_pre(self) -> list[str]:
247 """Additional commands that are injected before %cargo_prep."""
248 return self._conf.scripts.prep.pre
250 @property
251 def rpm_prep_post(self) -> list[str]:
252 """Additional commands that are injected after %cargo_prep."""
253 return self._conf.scripts.prep.post
255 @property
256 def rpm_build_pre(self) -> list[str]:
257 """Additional commands that are injected before %cargo_build."""
258 return self._conf.scripts.build.pre
260 @property
261 def rpm_build_post(self) -> list[str]:
262 """Additional commands that are injected after %cargo_build."""
263 return self._conf.scripts.build.post
265 @property
266 def rpm_install_pre(self) -> list[str]:
267 """Additional commands that are injected before %cargo_install."""
268 return self._conf.scripts.install.pre
270 @property
271 def rpm_install_post(self) -> list[str]:
272 """Additional commands that are injected after %cargo_install."""
273 return self._conf.scripts.install.post
275 @property
276 def rpm_check_pre(self) -> list[str]:
277 """Additional commands that are injected before %cargo_check."""
278 return self._conf.scripts.check.pre
280 @property
281 def rpm_check_post(self) -> list[str]:
282 """Additional commands that are injected after %cargo_check."""
283 return self._conf.scripts.check.post
285 # Parameters for crate metadata
287 @property
288 def upstream_version(self) -> str:
289 """Upstream version (from Cargo.toml metadata, SemVer format, not normalized)."""
290 return self._package.version
292 @property
293 def crate_license(self) -> str | None:
294 """Crate license (from Cargo.toml metadata, not normalized)."""
295 return self._package.license
297 # Parameters for RPM macros
299 @property
300 def rpm_autosetup_args(self) -> str:
301 """Additional arguments for the %autosetup macro."""
302 return " -a1" if self._project.vendor_tarball else ""
304 @property
305 def cargo_args(self) -> str:
306 """Additional arguments for %cargo_build, %cargo_install, etc."""
307 feature_flags = conf_to_feature_flags(self._conf)
308 required_features = get_required_features_for_binaries(self._package)
309 features_enabled_by_default = self._package.get_enabled_features_transitive(feature_flags)[0]
310 return cargo_args_from_flags(feature_flags, required_features, features_enabled_by_default)
312 @property
313 def cargo_prep_args(self) -> str:
314 """Additional arguments for %cargo_prep."""
315 return " -v vendor" if self._project.vendor_tarball else ""
317 @property
318 def cargo_test_commands(self) -> list[str]:
319 """List of customized and pre-formatted %cargo_test commands."""
320 return conf_to_cargo_test_commands(self._conf, self.cargo_args)
322 # Parameters derived from rust2rpm.toml
324 @property
325 def conf_buildrequires(self) -> list[str]:
326 """List of additionally specified RPM BuildRequires."""
327 return self._conf.requires.build
329 @property
330 def conf_test_requires(self) -> list[str]:
331 """List of additionally specified RPM BuildRequires that are gated by an "%if %{with check}"" conditional."""
332 return self._conf.requires.test
334 @property
335 def conf_bin_requires(self) -> list[str]:
336 """List of additionally specified RPM Requires for the binary package."""
337 return self._conf.requires.bin
339 @property
340 def conf_supported_arches(self) -> str | None:
341 """List of supported architectures.
343 Results in an ExclusiveArch tag with the specified values.
344 """
345 if supported_arches := self._conf.package.supported_arches: 345 ↛ 346line 345 didn't jump to line 346 because the condition on line 345 was never true
346 conf_supported_arches = " ".join(supported_arches)
347 else:
348 conf_supported_arches = None
350 return conf_supported_arches
352 @property
353 def conf_suppress_cdylib_install_fixme(self) -> bool:
354 """Toggle suppression of the FIXME comment for installing cdylib crate targets."""
355 return self._conf.package.suppress_cdylib_install_fixme is True
357 @property
358 def rpm_bin_renames(self) -> list[str]:
359 """List of "mv" commands to rename binaries installed in %{_bindir}."""
360 return conf_to_bin_rename_commands(self._conf)
362 # Parameters derived from command-line flags
364 @property
365 def make_changelog_entry(self) -> bool:
366 """Toggle inclusion of a changelog entry in generated spec files."""
367 return self._make_changelog_entry
370@dataclass(frozen=True)
371class ProjectParametersFedora(ProjectParameters, ParametersFedora):
372 """Collection of parameters for the "project" type template that is specific to the "fedora" target."""
375@dataclass(frozen=True)
376class ProjectParametersMageia(ProjectParameters, ParametersMageia):
377 """Collection of parameters for the "project" type template that is specific to the "mageia" target."""
379 @property
380 def rpm_buildrequires(self) -> list[str]:
381 """Automatically generated RPM BuildRequires for crate dependencies."""
382 return self._buildrequires[0]
384 @property
385 def rpm_test_requires(self) -> list[str]:
386 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
387 return self._buildrequires[1]
390@dataclass(frozen=True)
391class ProjectParametersOpenSUSE(ProjectParameters, ParametersOpenSUSE):
392 """Collection of parameters for the "project" type template that is specific to the "opensuse" target."""
394 @property
395 def rpm_buildrequires(self) -> list[str]:
396 """Automatically generated RPM BuildRequires for crate dependencies."""
397 return self._buildrequires[0]
399 @property
400 def rpm_test_requires(self) -> list[str]:
401 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
402 return self._buildrequires[1]
405@dataclass(frozen=True)
406class ProjectParametersPlain(ProjectParameters, ParametersPlain):
407 """Collection of parameters for the "project" type template that is specific to the "plain" target."""
409 @property
410 def rpm_buildrequires(self) -> list[str]:
411 """Automatically generated RPM BuildRequires for crate dependencies."""
412 return self._buildrequires[0]
414 @property
415 def rpm_test_requires(self) -> list[str]:
416 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
417 return self._buildrequires[1]
419 @property
420 def rpm_requires(self) -> dict[str | None, list[str]]:
421 """Automatically generated RPM Requires for subpackages.
423 This returns an empty dictionary because this value is unused in the "project" template.
424 """
425 return {}
427 @property
428 def rpm_provides(self) -> dict[str | None, str]:
429 """Automatically generated RPM Provides for subpackages.
431 This returns an empty dictionary because this value is unused in the "project" template.
432 """
433 return {}
436@dataclass(frozen=True)
437class ProjectTemplate(Template):
438 """Template for rendering spec files with the "project" template."""
440 project: LocalProject
441 """Project for which the spec file is rendered."""
443 conf: TomlConf
444 """Global rust2rpm configuration."""
446 target: Target
447 """Target for which the spec file is rendered."""
449 date: time.struct_time | None
450 """Optional timestamp for generated changelog entries.
451 Falls back to the current time if not specified."""
453 packager: str | None
454 """Optional user identification for generated changelog entries.
455 Falls back to dummy values if not specified."""
457 use_rpmautospec: bool
458 """Predicate that controls whether rpmautospec is used for
459 the Release tag and %changelog."""
461 make_changelog_entry: bool
462 """Predicate that controls whether a changelog entry is automatically generated."""
464 def render(self) -> str:
465 """Render spec file from template with the computed parameters."""
466 template = spec_file_template("project.spec")
468 match self.target:
469 case Target.FEDORA:
470 parameter_cls = ProjectParametersFedora # type: ignore[assignment]
471 case Target.MAGEIA:
472 parameter_cls = ProjectParametersMageia # type: ignore[assignment]
473 case Target.OPENSUSE:
474 parameter_cls = ProjectParametersOpenSUSE # type: ignore[assignment]
475 case Target.PLAIN:
476 parameter_cls = ProjectParametersPlain # type: ignore[assignment]
477 case x: # pragma nocover
478 msg = f"{x} is not a supported target for project-type packages"
479 raise ValueError(msg)
481 parameters = parameter_cls(
482 _project=self.project,
483 _conf=self.conf,
484 _target=self.target,
485 _make_changelog_entry=self.make_changelog_entry,
486 _date=self.date,
487 _packager=self.packager,
488 _use_rpmautospec=self.use_rpmautospec,
489 )
491 contents = template.render(**parameters_as_dict(parameters))
493 if not contents.endswith("\n"):
494 contents += "\n"
496 return contents