Coverage for rust2rpm/generator/epel8.py: 92%
223 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 "epel8" type spec file template."""
3import time
4from dataclasses import KW_ONLY, dataclass
5from functools import cached_property
7from cargo2rpm.metadata import FeatureFlags, Metadata, Package
8from cargo2rpm.semver import Version
10from rust2rpm import __version__
11from rust2rpm.conf import ConfError, Patch, Source, TomlConf
12from rust2rpm.metadata import get_required_features_for_binaries
13from rust2rpm.project import CrateProject
15from .common import (
16 RUST_PACKAGING_DEPS,
17 conf_to_bcond_check,
18 conf_to_bcond_check_comments,
19 conf_to_bin_rename_commands,
20 conf_to_feature_flags,
21 make_rpm_summary,
22 normalize_spdx_expr,
23 spec_file_template,
24)
25from .meta import (
26 InvalidMetadataError,
27 Parameters,
28 ParametersEpelEight,
29 Template,
30 parameters_as_dict,
31)
34def cargo_args_from_flags(
35 feature_flags: FeatureFlags,
36 required_features: set[str],
37 enabled_by_default: set[str],
38) -> str:
39 """Format cargo arguments for rust-toolset macros on RHEL 8.
41 Arguments:
42 feature_flags: Feature flags from configuration.
43 required_features: Features required by binary targets.
44 enabled_by_default: Transitive closure of the "default" feature.
46 Returns:
47 Formatted string with cargo arguments.
49 """
50 if feature_flags.all_features:
51 cargo_args = " --all-features"
53 elif required_features:
54 # remove required features that are already part of the enabled feature set
55 for f in enabled_by_default:
56 if f in required_features:
57 required_features.remove(f)
59 # merge, de-duplicate, and re-sort lists of enabled features
60 if features_enable := feature_flags.features:
61 enabled_features = sorted(set.union(set(required_features), set(features_enable)))
62 else:
63 enabled_features = sorted(required_features)
65 cargo_args_list = ["--no-default-features"] if feature_flags.no_default_features else []
67 if len(enabled_features) > 0:
68 cargo_args_list.append(f"--features {','.join(enabled_features)}")
70 cargo_args = " " + " ".join(cargo_args_list) if cargo_args_list else ""
72 elif feature_flags.features:
73 # list of features is already sorted when parsing the configuration file
74 cargo_args_list = ["--no-default-features"] if feature_flags.no_default_features else []
76 cargo_args_list.append(f"--features {','.join(feature_flags.features)}")
77 cargo_args = " " + " ".join(cargo_args_list)
79 elif feature_flags.no_default_features:
80 cargo_args = " --no-default-features"
82 else:
83 cargo_args = ""
85 return cargo_args
88def cargo_test_commands(conf: TomlConf) -> list[str]:
89 """Format %cargo_test arguments for rust-toolset macros on RHEL 8.
91 Arguments:
92 conf: Global rust2rpm configuration.
94 Returns:
95 List of formatted string with %cargo_test arguments.
97 """
98 if skip := conf.tests.skip:
99 if not isinstance(skip, list): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 msg = "EPEL8 target does not support specifying per-test-target skips."
101 raise ConfError(msg)
103 skip_flags = []
104 for name in skip:
105 skip_flags.extend(["--skip", name])
106 else:
107 skip_flags = []
109 skip_exact_flag = ["--exact"] if conf.tests.skip_exact is True else []
111 if (run := conf.tests.run) and not ("all" in run or "none" in run):
112 multi = []
113 for kind in run:
114 if skip_flags: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 multi.append(" " + " ".join([f"--{kind}", "--", *skip_exact_flag, *skip_flags]))
116 else:
117 multi.append(" " + " ".join([f"--{kind}"]))
118 args = multi
120 elif skip_flags:
121 args = [" " + " ".join(["--", *skip_exact_flag, *skip_flags])]
123 else:
124 args = []
126 return [f"%cargo_test{arg}" for arg in args]
129@dataclass(frozen=True)
130class CrateParametersEpelEight(ParametersEpelEight, Parameters):
131 """Collection of parameters for the "epel8" type template."""
133 _: KW_ONLY
135 _project: CrateProject
136 _conf: TomlConf
138 _make_changelog_entry: bool
140 # Convenience properties
142 @property
143 def _metadata(self) -> Metadata:
144 return self._project.metadata
146 @property
147 def _package(self) -> Package:
148 return self._metadata.packages[0]
150 # Parameters specific to rust2rpm
152 @property
153 def rust2rpm_version(self) -> str:
154 """Current major version of rust2rpm."""
155 return __version__.split(".")[0]
157 @property
158 def rust_packaging_dep(self) -> str:
159 """Dependency string for RPM Rust packaging tools."""
160 return RUST_PACKAGING_DEPS[0]
162 # Parameters that control compiler flags
164 @property
165 def build_rustflags_debuginfo(self) -> int | None:
166 """Controls the level of debuginfo generated by rustc."""
167 return self._conf.package.debuginfo_level
169 # Parameters for RPM package metadata
171 @property
172 def rpm_name(self) -> str:
173 """RPM source package Name (rust-{crate}{suffix})."""
174 return self._project.rpm_name
176 @property
177 def rpm_version(self) -> str:
178 """RPM package Version (translated to RPM format from SemVer)."""
179 return Version.parse(self._package.version).to_rpm()
181 @cached_property
182 def rpm_summary(self) -> str | None:
183 """RPM package summary (derived from package.description value from Cargo.toml)."""
184 return make_rpm_summary(self._conf, self._package)
186 @property
187 def rpm_description(self) -> str | None:
188 """RPM package description (derived from package.description value from Cargo.toml)."""
189 return self._conf.package.description or self._package.get_description()
191 @cached_property
192 def rpm_license(self) -> str | None:
193 """RPM License tag (derived from package.license value from Cargo.toml)."""
194 if license_str := self._package.license: 194 ↛ 197line 194 didn't jump to line 197
195 return normalize_spdx_expr(license_str)
197 msg = (
198 "No license specified in crate metadata. The 'package.license' property MUST be "
199 "set in crate metadata, otherwise RPM packaging macros will not work properly. "
200 "To resolve this issue, patch Cargo.toml to set the correct license expression "
201 "in SPDX format."
202 )
203 raise InvalidMetadataError(msg)
205 @property
206 def rpm_license_comments(self) -> str | None:
207 """Additional information returned by license string translation."""
208 return None
210 @property
211 def rpm_patch_file_automatic(self) -> str | None:
212 """File name of the automatically generated patch."""
213 return patch.name if (patch := self._project.auto_patch) else None
215 @property
216 def rpm_patch_file_manual(self) -> str | None:
217 """File name of the manually generated patch."""
218 return patch.name if (patch := self._project.manual_patch) else None
220 @property
221 def rpm_patch_file_comments(self) -> list[str]:
222 """Additional lines of comments for the manually generated patch file."""
223 return self._conf.package.cargo_toml_patch_comment_lines
225 @property
226 def rpm_license_files(self) -> list[str]:
227 """List of the license files which were detected in crate sources."""
228 return self._project.license_files
230 @property
231 def rpm_doc_files(self) -> list[str]:
232 """List of the documentation files which were detected in crate sources."""
233 return self._project.doc_files
235 @property
236 def rpm_bcond_check(self) -> bool:
237 """Flag to switch default value of the "check" bcond."""
238 return conf_to_bcond_check(self._conf) == 1
240 @property
241 def rpm_bcond_check_comments(self) -> list[str]:
242 """Comments associated with a disabled "check" bcond."""
243 return conf_to_bcond_check_comments(self._conf)
245 @property
246 def rpm_vendor_source(self) -> str | None:
247 """File name of the vendor tarball in case vendored sources are used."""
248 return self._project.vendor_tarball
250 @property
251 def rpm_extra_sources(self) -> list[Source]:
252 """Additional source files with number and comments."""
253 return self._conf.package.extra_sources
255 @property
256 def rpm_extra_patches(self) -> list[Patch]:
257 """Additional patch files with number and comments."""
258 return self._conf.package.extra_patches
260 @property
261 def rpm_extra_files(self) -> list[str]:
262 """Additional files to be included (only applies if there is a non-devel package)."""
263 return self._conf.package.extra_files
265 # Parameters that control generation of subpackages
267 @property
268 def rpm_binary_package_name(self) -> str:
269 """Override name of the binary subpackage (default: %{crate})."""
270 return self._conf.package.bin_package_name or "%{crate}"
272 @property
273 def rpm_binary_names(self) -> list[str]:
274 """List of the names of executables which are built from the crate."""
275 # enforce consistent order
276 if bin_renames := self._conf.package.bin_renames: 276 ↛ 277line 276 didn't jump to line 277 because the condition on line 276 was never true
277 binaries = []
278 for old_name in self._metadata.get_binaries():
279 if new_name := bin_renames.get(old_name):
280 binaries.append(new_name)
281 else:
282 binaries.append(old_name)
283 else:
284 binaries = list(self._metadata.get_binaries())
285 return sorted(binaries)
287 # Parameters that allow injecting additional commands into scriptlets
289 @property
290 def rpm_prep_pre(self) -> list[str]:
291 """Additional commands that are injected before %cargo_prep."""
292 return self._conf.scripts.prep.pre
294 @property
295 def rpm_prep_post(self) -> list[str]:
296 """Additional commands that are injected after %cargo_prep."""
297 return self._conf.scripts.prep.post
299 @property
300 def rpm_build_pre(self) -> list[str]:
301 """Additional commands that are injected before %cargo_build."""
302 return self._conf.scripts.build.pre
304 @property
305 def rpm_build_post(self) -> list[str]:
306 """Additional commands that are injected after %cargo_build."""
307 return self._conf.scripts.build.post
309 @property
310 def rpm_install_pre(self) -> list[str]:
311 """Additional commands that are injected before %cargo_install."""
312 return self._conf.scripts.install.pre
314 @property
315 def rpm_install_post(self) -> list[str]:
316 """Additional commands that are injected after %cargo_install."""
317 return self._conf.scripts.install.post
319 @property
320 def rpm_check_pre(self) -> list[str]:
321 """Additional commands that are injected before %cargo_check."""
322 return self._conf.scripts.check.pre
324 @property
325 def rpm_check_post(self) -> list[str]:
326 """Additional commands that are injected after %cargo_check."""
327 return self._conf.scripts.check.post
329 # Parameters for crate metadata
331 @property
332 def crate_name(self) -> str:
333 """Crate name (from Cargo.toml metadata)."""
334 return self._project.name
336 @property
337 def crate_license(self) -> str | None:
338 """Crate license (from Cargo.toml metadata, not normalized)."""
339 return self._package.license
341 # Parameters for RPM macros
343 @property
344 def cargo_args(self) -> str:
345 """Additional arguments for %cargo_build, %cargo_install, etc."""
346 feature_flags = conf_to_feature_flags(self._conf)
347 required_features = get_required_features_for_binaries(self._package)
348 features_enabled_by_default = self._package.get_enabled_features_transitive(feature_flags)[0]
349 return cargo_args_from_flags(feature_flags, required_features, features_enabled_by_default)
351 @property
352 def cargo_test_commands(self) -> list[str]:
353 """List of customized and pre-formatted %cargo_test commands."""
354 return cargo_test_commands(self._conf)
356 # Parameters derived from rust2rpm.toml
358 @property
359 def conf_buildrequires(self) -> list[str]:
360 """List of additionally specified RPM BuildRequires."""
361 return self._conf.requires.build
363 @property
364 def conf_test_requires(self) -> list[str]:
365 """List of additionally specified RPM BuildRequires that are gated by an "%if %{with check}"" conditional."""
366 return self._conf.requires.test
368 @property
369 def conf_bin_requires(self) -> list[str]:
370 """List of additionally specified RPM Requires for the binary package."""
371 return self._conf.requires.bin
373 @property
374 def conf_supported_arches(self) -> str | None:
375 """List of supported architectures.
377 Results in an ExclusiveArch tag with the specified values.
378 """
379 if supported_arches := self._conf.package.supported_arches: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
380 conf_supported_arches = " ".join(supported_arches)
381 else:
382 conf_supported_arches = None
384 return conf_supported_arches
386 @property
387 def rpm_bin_renames(self) -> list[str]:
388 """List of "mv" commands to rename binaries installed in %{_bindir}."""
389 return conf_to_bin_rename_commands(self._conf)
391 # Parameters derived from command-line flags
393 @property
394 def make_changelog_entry(self) -> bool:
395 """Toggle inclusion of a changelog entry in generated spec files."""
396 return self._make_changelog_entry
399@dataclass(frozen=True)
400class EpelEightTemplate(Template):
401 """Template for rendering spec files with the "epel8" template."""
403 project: CrateProject
404 """Project for which the spec file is rendered."""
406 conf: TomlConf
407 """Global rust2rpm configuration."""
409 date: time.struct_time | None
410 """Optional timestamp for generated changelog entries.
411 Falls back to the current time if not specified."""
413 packager: str | None
414 """Optional user identification for generated changelog entries.
415 Falls back to dummy values if not specified."""
417 use_rpmautospec: bool
418 """Predicate that controls whether rpmautospec is used for
419 the Release tag and %changelog."""
421 make_changelog_entry: bool
422 """Predicate that controls whether a changelog entry is automatically generated."""
424 def render(self) -> str:
425 """Render spec file from template with the computed parameters."""
426 template = spec_file_template("epel8.spec")
428 parameters = CrateParametersEpelEight(
429 _project=self.project,
430 _conf=self.conf,
431 _make_changelog_entry=self.make_changelog_entry,
432 _date=self.date,
433 _packager=self.packager,
434 _use_rpmautospec=self.use_rpmautospec,
435 )
437 contents = template.render(**parameters_as_dict(parameters))
439 if not contents.endswith("\n"): 439 ↛ 442line 439 didn't jump to line 442 because the condition on line 439 was always true
440 contents += "\n"
442 return contents