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

1"""Module containing functionality for preparing arguments for the "crate" type spec file template.""" 

2 

3import time 

4from dataclasses import KW_ONLY, dataclass 

5from functools import cached_property 

6from typing import cast 

7 

8from cargo2rpm import rpm 

9from cargo2rpm.metadata import Metadata, Package 

10from cargo2rpm.semver import Version 

11 

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 

17 

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) 

41 

42 

43@dataclass(frozen=True) 

44class CrateParameters(Parameters): 

45 """Collection of parameters for the "crate" type template that is common for all targets.""" 

46 

47 _: KW_ONLY 

48 

49 _project: CrateProject 

50 _conf: TomlConf 

51 _target: Target 

52 

53 _use_relative_license_paths: bool 

54 _make_changelog_entry: bool 

55 

56 # Convenience properties 

57 

58 @property 

59 def _metadata(self) -> Metadata: 

60 return self._project.metadata 

61 

62 @property 

63 def _package(self) -> Package: 

64 return self._metadata.packages[0] 

65 

66 @cached_property 

67 def _buildrequires(self) -> tuple[list[str], list[str]]: 

68 feature_flags = conf_to_feature_flags(self._conf) 

69 

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) 

72 

73 buildrequires = buildrequires_without_dev 

74 test_requires = set.difference(buildrequires_with_dev, buildrequires_without_dev) 

75 

76 return sorted(buildrequires), sorted(test_requires) 

77 

78 # Parameters specific to rust2rpm 

79 

80 @property 

81 def rust2rpm_version(self) -> str: 

82 """Current major version of rust2rpm.""" 

83 return __version__.split(".")[0] 

84 

85 @property 

86 def rust2rpm_target(self) -> str: 

87 """Target platform (fedora, epel8, mageia, opensuse, plain).""" 

88 return self._target 

89 

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 ] 

104 

105 # Parameters that control compiler flags 

106 

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 

111 

112 # Parameters for RPM package metadata 

113 

114 @property 

115 def rpm_name(self) -> str: 

116 """RPM source package Name (rust-{crate}{suffix}).""" 

117 return self._project.rpm_name 

118 

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() 

123 

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) 

128 

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() 

133 

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) 

139 

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) 

147 

148 @property 

149 def rpm_license_comments(self) -> str | None: 

150 """Additional information returned by license string translation.""" 

151 return None 

152 

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 

157 

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 

162 

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 

167 

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 

172 

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 

177 

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) 

182 

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) 

187 

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 

192 

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 

197 

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 

202 

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 

207 

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 

212 

213 # Parameters that control generation of subpackages 

214 

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() 

219 

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() 

224 

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() 

229 

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}" 

234 

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) 

249 

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())) 

254 

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 

259 

260 if "default" in features: 

261 features.remove("default") 

262 features.insert(0, None) 

263 features.insert(1, "default") 

264 

265 if features_hide := self._conf.features.hide: 

266 for feature in features_hide: 

267 features.remove(feature) 

268 

269 return features 

270 

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) 

275 

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 ) 

282 

283 # Parameters that allow injecting additional commands into scriptlets 

284 

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 

289 

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 

294 

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 

299 

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 

304 

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 

309 

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 

314 

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 

319 

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 

324 

325 # Parameters for crate metadata 

326 

327 @property 

328 def crate_name(self) -> str: 

329 """Crate name (from Cargo.toml metadata).""" 

330 return self._project.name 

331 

332 @property 

333 def crate_version(self) -> str: 

334 """Crate version (from Cargo.toml metadata, SemVer format, not normalized).""" 

335 return self._project.version 

336 

337 @property 

338 def crate_license(self) -> str | None: 

339 """Crate license (from Cargo.toml metadata, not normalized).""" 

340 return self._package.license 

341 

342 # Parameters for RPM macros 

343 

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 "" 

348 

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) 

356 

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 "" 

361 

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) 

366 

367 # Parameters derived from rust2rpm.toml 

368 

369 @property 

370 def conf_buildrequires(self) -> list[str]: 

371 """List of additionally specified RPM BuildRequires.""" 

372 return self._conf.requires.build 

373 

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 

378 

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 

383 

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 = {} 

388 

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 

393 

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] = [] 

400 

401 return conf_lib_requires 

402 

403 @property 

404 def conf_supported_arches(self) -> str | None: 

405 """List of supported architectures. 

406 

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 

413 

414 return conf_supported_arches 

415 

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 

420 

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) 

425 

426 # Parameters derived from command-line flags 

427 

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 

432 

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 

437 

438 

439@dataclass(frozen=True) 

440class CrateParametersFedora(CrateParameters, ParametersFedora): 

441 """Collection of parameters for the "crate" type template that is specific to the "fedora" target.""" 

442 

443 

444@dataclass(frozen=True) 

445class CrateParametersMageia(CrateParameters, ParametersMageia): 

446 """Collection of parameters for the "crate" type template that is specific to the "mageia" target.""" 

447 

448 @property 

449 def rpm_buildrequires(self) -> list[str]: 

450 """Automatically generated RPM BuildRequires for crate dependencies.""" 

451 return self._buildrequires[0] 

452 

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] 

457 

458 

459@dataclass(frozen=True) 

460class CrateParametersOpenSUSE(CrateParameters, ParametersOpenSUSE): 

461 """Collection of parameters for the "crate" type template that is specific to the "opensuse" target.""" 

462 

463 @property 

464 def rpm_buildrequires(self) -> list[str]: 

465 """Automatically generated RPM BuildRequires for crate dependencies.""" 

466 return self._buildrequires[0] 

467 

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] 

472 

473 

474@dataclass(frozen=True) 

475class CrateParametersPlain(CrateParameters, ParametersPlain): 

476 """Collection of parameters for the "crate" type template that is specific to the "plain" target.""" 

477 

478 @property 

479 def rpm_buildrequires(self) -> list[str]: 

480 """Automatically generated RPM BuildRequires for crate dependencies.""" 

481 return self._buildrequires[0] 

482 

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] 

487 

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} 

492 

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} 

497 

498 

499@dataclass(frozen=True) 

500class CrateTemplate(Template): 

501 """Template for rendering spec files with the "crate" template.""" 

502 

503 project: CrateProject 

504 """Project for which the spec file is rendered.""" 

505 

506 conf: TomlConf 

507 """Global rust2rpm configuration.""" 

508 

509 target: Target 

510 """Target for which the spec file is rendered.""" 

511 

512 date: time.struct_time | None 

513 """Optional timestamp for generated changelog entries. 

514 Falls back to the current time if not specified.""" 

515 

516 packager: str | None 

517 """Optional user identification for generated changelog entries. 

518 Falls back to dummy values if not specified.""" 

519 

520 use_relative_license_paths: bool 

521 """Predicate that controls whether relative or absolute paths are used 

522 for %license files in %files listings.""" 

523 

524 use_rpmautospec: bool 

525 """Predicate that controls whether rpmautospec is used for 

526 the Release tag and %changelog.""" 

527 

528 make_changelog_entry: bool 

529 """Predicate that controls whether a changelog entry is automatically generated.""" 

530 

531 def render(self) -> str: 

532 """Render spec file from template with the computed parameters.""" 

533 template = spec_file_template("crate.spec") 

534 

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) 

547 

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 ) 

558 

559 contents = template.render(**parameters_as_dict(parameters)) 

560 

561 if not contents.endswith("\n"): 

562 contents += "\n" 

563 

564 return contents