Coverage for rust2rpm/generator/workspace.py: 91%
260 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 "workspace" 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
9from cargo2rpm.semver import Version
11from rust2rpm import __version__, log
12from rust2rpm.conf import Patch, Source, TomlConf
13from rust2rpm.project import LocalProject
14from rust2rpm.sysinfo import Target
16from .common import (
17 RUST_PACKAGING_DEPS,
18 cargo_args_from_flags,
19 conf_to_bcond_check,
20 conf_to_bcond_check_comments,
21 conf_to_bin_rename_commands,
22 conf_to_cargo_test_commands,
23 conf_to_feature_flags,
24 make_rpm_summary,
25 min_rust_packaging_dep,
26 normalize_spdx_expr,
27 spec_file_template,
28)
29from .meta import (
30 InvalidMetadataError,
31 Parameters,
32 ParametersFedora,
33 ParametersMageia,
34 ParametersOpenSUSE,
35 ParametersPlain,
36 Template,
37 parameters_as_dict,
38)
41def _license_is_composite(expr: str) -> bool:
42 return (" " in expr and " WITH " not in expr and "(" not in expr and ")" not in expr) or "(" in expr or ")" in expr
45@dataclass(frozen=True)
46class WorkspaceParameters(Parameters):
47 """Collection of parameters for the "workspace" type template that is common for all targets."""
49 _: KW_ONLY
51 _project: LocalProject
52 _conf: TomlConf
53 _target: Target
55 _make_changelog_entry: bool
57 # Convenience properties
59 @property
60 def _metadata(self) -> Metadata:
61 return self._project.metadata
63 @cached_property
64 def _rpm_license_and_comments(self) -> tuple[str | None, str | None]:
65 license_strs = list({package.license for package in self._metadata.packages if package.license})
67 if len(license_strs) == 0: 67 ↛ 68line 67 didn't jump to line 68
68 msg = (
69 "None of the crates in this workspace project specify a license in crate "
70 "metadata. The 'package.license' property MUST be set in at least one workspace "
71 "member, otherwise RPM packaging macros will not work properly. To resolve this "
72 "issue, set the 'package.license' property in at least one workspace member to "
73 "the correct license expression in SPDX format with a patch."
74 )
75 raise InvalidMetadataError(msg)
77 if len(license_strs) == 1: 77 ↛ 81line 77 didn't jump to line 81 because the condition on line 77 was always true
78 rpm_license_tag, rpm_license_comments = normalize_spdx_expr(license_strs[0]), None
80 else:
81 license_strs = [
82 license_str if not _license_is_composite(license_str) else f"({license_str})"
83 for license_str in license_strs
84 if license_str is not None
85 ]
86 license_strs.sort()
87 license_str = " AND ".join(license_strs)
88 rpm_license_tag, rpm_license_comments = normalize_spdx_expr(license_str), None
90 return rpm_license_tag, rpm_license_comments
92 @cached_property
93 def _buildrequires(self) -> tuple[list[str], list[str]]:
94 feature_flags = conf_to_feature_flags(self._conf)
96 buildrequires = rpm.workspace_buildrequires(self._metadata, feature_flags, with_dev_deps=False)
97 test_requires = set.difference(
98 rpm.workspace_buildrequires(self._metadata, feature_flags, with_dev_deps=True),
99 rpm.workspace_buildrequires(self._metadata, feature_flags, with_dev_deps=False),
100 )
102 return sorted(buildrequires), sorted(test_requires)
104 # Parameters specific to rust2rpm
106 @property
107 def rust2rpm_version(self) -> str:
108 """Current major version of rust2rpm."""
109 return __version__.split(".")[0]
111 @property
112 def rust2rpm_target(self) -> str:
113 """Target platform (fedora, epel8, mageia, opensuse, plain)."""
114 return self._target
116 @property
117 def rust_packaging_dep(self) -> str:
118 """Dependency string for RPM Rust packaging tools."""
119 return RUST_PACKAGING_DEPS[
120 max(
121 (
122 24,
123 max(
124 min_rust_packaging_dep(
125 package,
126 self._target,
127 self._project.vendor_tarball,
128 is_bin=package.is_bin(),
129 is_cdylib=package.is_cdylib(),
130 cargo_install_lib=True,
131 cargo_install_bin=True,
132 )
133 for package in self._metadata.packages
134 ),
135 ),
136 )
137 ]
139 # Parameters that control compiler flags
141 @property
142 def build_rustflags_debuginfo(self) -> int | None:
143 """Controls the level of debuginfo generated by rustc."""
144 return self._conf.package.debuginfo_level
146 # Parameters for RPM package metadata
148 @property
149 def rpm_name(self) -> str:
150 """RPM source package Name."""
151 return self._project.rpm_name
153 @property
154 def rpm_version(self) -> str:
155 """RPM package Version (translated to RPM format from SemVer)."""
156 try:
157 return Version.parse(self._project.version).to_rpm()
158 except ValueError:
159 log.warn(f"Version {self._project.version!r} is not valid according to SemVer.")
160 return self._project.version
162 @cached_property
163 def rpm_summary(self) -> str | None:
164 """RPM package summary (derived from package.description value from Cargo.toml)."""
165 return make_rpm_summary(self._conf, self._project.main_package)
167 @property
168 def rpm_description(self) -> str | None:
169 """RPM package description (derived from package.description value from Cargo.toml)."""
170 return self._conf.package.description or self._project.main_package.get_description()
172 @property
173 def rpm_url(self) -> str:
174 """RPM URL tag (derived from the rust2rpm.toml setting or Cargo.toml metadata)."""
175 return self._conf.package.url or "# FIXME"
177 @property
178 def rpm_source_url(self) -> str:
179 """RPM Source tag (derive from the rust2rpm.toml setting)."""
180 return self._conf.package.source_url or "# FIXME"
182 @property
183 def rpm_license(self) -> str | None:
184 """RPM License tag (derived from package.license value from Cargo.toml)."""
185 return self._rpm_license_and_comments[0]
187 @property
188 def rpm_license_comments(self) -> str | None:
189 """Additional information returned by license string translation."""
190 return self._rpm_license_and_comments[1]
192 @property
193 def rpm_patch_file_automatic(self) -> str | None:
194 """File name of the automatically generated patch."""
195 return None
197 @property
198 def rpm_patch_file_manual(self) -> str | None:
199 """File name of the manually generated patch."""
200 return None
202 @property
203 def rpm_patch_file_comments(self) -> list[str]:
204 """Additional lines of comments for the manually generated patch file."""
205 return self._conf.package.cargo_toml_patch_comment_lines
207 @property
208 def rpm_license_files(self) -> list[str]:
209 """List of the license files which were detected in crate sources."""
210 return self._project.license_files
212 @property
213 def rpm_doc_files(self) -> list[str]:
214 """List of the documentation files which were detected in crate sources."""
215 return self._project.doc_files
217 @property
218 def rpm_bcond_check(self) -> int:
219 """Flag to switch default value of the "check" bcond."""
220 return conf_to_bcond_check(self._conf)
222 @property
223 def rpm_bcond_check_comments(self) -> list[str]:
224 """Comments associated with a disabled "check" bcond."""
225 return conf_to_bcond_check_comments(self._conf)
227 @property
228 def rpm_vendor_source(self) -> str | None:
229 """File name of the vendor tarball in case vendored sources are used."""
230 return self._project.vendor_tarball
232 @property
233 def rpm_extra_sources(self) -> list[Source]:
234 """Additional source files with number and comments."""
235 return self._conf.package.extra_sources
237 @property
238 def rpm_extra_patches(self) -> list[Patch]:
239 """Additional patch files with number and comments."""
240 return self._conf.package.extra_patches
242 @property
243 def rpm_extra_files(self) -> list[str]:
244 """Additional files to be included in the built package."""
245 return self._conf.package.extra_files
247 # Parameters that control generation of subpackages
249 @property
250 def rpm_binary_package(self) -> bool:
251 """True if package ships any binary targets (bin targets)."""
252 return self._metadata.is_bin()
254 @property
255 def rpm_cdylib_package(self) -> bool:
256 """True if package ships any shared libraries (cdylib targets)."""
257 return self._metadata.is_cdylib()
259 @property
260 def rpm_binary_names(self) -> list[str]:
261 """List of the names of executables which are built from the crate."""
262 # enforce consistent order
263 if bin_renames := self._conf.package.bin_renames: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 binaries = []
265 for old_name in self._metadata.get_binaries():
266 if new_name := bin_renames.get(old_name):
267 binaries.append(new_name)
268 else:
269 binaries.append(old_name)
270 else:
271 binaries = list(self._metadata.get_binaries())
272 return sorted(binaries)
274 # Parameters that allow injecting additional commands into scriptlets
276 @property
277 def rpm_prep_pre(self) -> list[str]:
278 """Additional commands that are injected before %cargo_prep."""
279 return self._conf.scripts.prep.pre
281 @property
282 def rpm_prep_post(self) -> list[str]:
283 """Additional commands that are injected after %cargo_prep."""
284 return self._conf.scripts.prep.post
286 @property
287 def rpm_build_pre(self) -> list[str]:
288 """Additional commands that are injected before %cargo_build."""
289 return self._conf.scripts.build.pre
291 @property
292 def rpm_build_post(self) -> list[str]:
293 """Additional commands that are injected after %cargo_build."""
294 return self._conf.scripts.build.post
296 @property
297 def rpm_install_pre(self) -> list[str]:
298 """Additional commands that are injected before %cargo_install."""
299 return self._conf.scripts.install.pre
301 @property
302 def rpm_install_post(self) -> list[str]:
303 """Additional commands that are injected after %cargo_install."""
304 return self._conf.scripts.install.post
306 @property
307 def rpm_check_pre(self) -> list[str]:
308 """Additional commands that are injected before %cargo_check."""
309 return self._conf.scripts.check.pre
311 @property
312 def rpm_check_post(self) -> list[str]:
313 """Additional commands that are injected after %cargo_check."""
314 return self._conf.scripts.check.post
316 # Parameters for RPM macros
318 @property
319 def upstream_version(self) -> str | None:
320 """Upstream project version."""
321 return self._project.version
323 @property
324 def rpm_autosetup_args(self) -> str:
325 """Additional arguments for the %autosetup macro."""
326 return " -a1" if self._project.vendor_tarball else ""
328 @property
329 def cargo_args(self) -> str:
330 """Additional arguments for %cargo_build, %cargo_install, etc."""
331 feature_flags = conf_to_feature_flags(self._conf)
332 return cargo_args_from_flags(feature_flags, set(), set())
334 @property
335 def cargo_prep_args(self) -> str:
336 """Additional arguments for %cargo_prep."""
337 return " -v vendor" if self._project.vendor_tarball else ""
339 @property
340 def cargo_test_commands(self) -> list[str]:
341 """List of customized and pre-formatted %cargo_test commands."""
342 return conf_to_cargo_test_commands(self._conf, self.cargo_args)
344 # Parameters derived from rust2rpm.toml
346 @property
347 def conf_buildrequires(self) -> list[str]:
348 """List of additionally specified RPM BuildRequires."""
349 return self._conf.requires.build
351 @property
352 def conf_test_requires(self) -> list[str]:
353 """List of additionally specified RPM BuildRequires that are gated by an "%if %{with check}"" conditional."""
354 return self._conf.requires.test
356 @property
357 def conf_bin_requires(self) -> list[str]:
358 """List of additionally specified RPM Requires for the binary package."""
359 return self._conf.requires.bin
361 @property
362 def conf_supported_arches(self) -> str | None:
363 """List of supported architectures.
365 Results in an ExclusiveArch tag with the specified values.
366 """
367 if supported_arches := self._conf.package.supported_arches: 367 ↛ 368line 367 didn't jump to line 368 because the condition on line 367 was never true
368 conf_supported_arches = " ".join(supported_arches)
369 else:
370 conf_supported_arches = None
372 return conf_supported_arches
374 @property
375 def conf_suppress_cdylib_install_fixme(self) -> bool:
376 """Toggle suppression of the FIXME comment for installing cdylib crate targets."""
377 return self._conf.package.suppress_cdylib_install_fixme is True
379 @property
380 def rpm_bin_renames(self) -> list[str]:
381 """List of "mv" commands to rename binaries installed in %{_bindir}."""
382 return conf_to_bin_rename_commands(self._conf)
384 # Parameters derived from command-line flags
386 @property
387 def make_changelog_entry(self) -> bool:
388 """Toggle inclusion of a changelog entry in generated spec files."""
389 return self._make_changelog_entry
392@dataclass(frozen=True)
393class WorkspaceParametersFedora(WorkspaceParameters, ParametersFedora):
394 """Collection of parameters for the "workspace" type template that is specific to the "fedora" target."""
397@dataclass(frozen=True)
398class WorkspaceParametersMageia(WorkspaceParameters, ParametersMageia):
399 """Collection of parameters for the "workspace" type template that is specific to the "mageia" target."""
401 @property
402 def rpm_buildrequires(self) -> list[str]:
403 """Automatically generated RPM BuildRequires for crate dependencies."""
404 return self._buildrequires[0]
406 @property
407 def rpm_test_requires(self) -> list[str]:
408 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
409 return self._buildrequires[1]
412@dataclass(frozen=True)
413class WorkspaceParametersOpenSUSE(WorkspaceParameters, ParametersOpenSUSE):
414 """Collection of parameters for the "workspace" type template that is specific to the "opensuse" target."""
416 @property
417 def rpm_buildrequires(self) -> list[str]:
418 """Automatically generated RPM BuildRequires for crate dependencies."""
419 return self._buildrequires[0]
421 @property
422 def rpm_test_requires(self) -> list[str]:
423 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
424 return self._buildrequires[1]
427@dataclass(frozen=True)
428class WorkspaceParametersPlain(WorkspaceParameters, ParametersPlain):
429 """Collection of parameters for the "workspace" type template that is specific to the "plain" target."""
431 @property
432 def rpm_buildrequires(self) -> list[str]:
433 """Automatically generated RPM BuildRequires for crate dependencies."""
434 return self._buildrequires[0]
436 @property
437 def rpm_test_requires(self) -> list[str]:
438 """Automatically generated test-only RPM BuildRequires for crate dependencies."""
439 return self._buildrequires[1]
441 @property
442 def rpm_requires(self) -> dict[str | None, list[str]]:
443 """Automatically generated RPM Requires for subpackages.
445 This returns an empty dictionary because this value is unused in the "project" template.
446 """
447 return {}
449 @property
450 def rpm_provides(self) -> dict[str | None, str]:
451 """Automatically generated RPM Provides for subpackages.
453 This returns an empty dictionary because this value is unused in the "project" template.
454 """
455 return {}
458@dataclass(frozen=True)
459class WorkspaceTemplate(Template):
460 """Template for rendering spec files with the "workspace" template."""
462 project: LocalProject
463 """Project for which the spec file is rendered."""
465 conf: TomlConf
466 """Global rust2rpm configuration."""
468 target: Target
469 """Target for which the spec file is rendered."""
471 date: time.struct_time | None
472 """Optional timestamp for generated changelog entries.
473 Falls back to the current time if not specified."""
475 packager: str | None
476 """Optional user identification for generated changelog entries.
477 Falls back to dummy values if not specified."""
479 use_rpmautospec: bool
480 """Predicate that controls whether rpmautospec is used for
481 the Release tag and %changelog."""
483 make_changelog_entry: bool
484 """Predicate that controls whether a changelog entry is automatically generated."""
486 def render(self) -> str:
487 """Render spec file from template with the computed parameters."""
488 template = spec_file_template("workspace.spec")
490 match self.target:
491 case Target.FEDORA:
492 parameter_cls = WorkspaceParametersFedora # type: ignore[assignment]
493 case Target.MAGEIA:
494 parameter_cls = WorkspaceParametersMageia # type: ignore[assignment]
495 case Target.OPENSUSE:
496 parameter_cls = WorkspaceParametersOpenSUSE # type: ignore[assignment]
497 case Target.PLAIN:
498 parameter_cls = WorkspaceParametersPlain # type: ignore[assignment]
499 case x: # pragma nocover
500 msg = f"{x} is not a supported target for workspace-type packages"
501 raise ValueError(msg)
503 parameters = parameter_cls(
504 _project=self.project,
505 _conf=self.conf,
506 _target=self.target,
507 _make_changelog_entry=self.make_changelog_entry,
508 _date=self.date,
509 _packager=self.packager,
510 _use_rpmautospec=self.use_rpmautospec,
511 )
513 contents = template.render(**parameters_as_dict(parameters))
515 if not contents.endswith("\n"):
516 contents += "\n"
518 return contents