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

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

2 

3import time 

4from dataclasses import KW_ONLY, dataclass 

5from functools import cached_property 

6 

7from cargo2rpm import rpm 

8from cargo2rpm.metadata import Metadata, Package 

9from cargo2rpm.semver import Version 

10 

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 

16 

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) 

40 

41 

42@dataclass(frozen=True) 

43class ProjectParameters(Parameters): 

44 """Collection of parameters for the "project" type template that is common for all targets.""" 

45 

46 _: KW_ONLY 

47 

48 _project: LocalProject 

49 _conf: TomlConf 

50 _target: Target 

51 

52 _make_changelog_entry: bool 

53 

54 # Convenience properties 

55 

56 @property 

57 def _metadata(self) -> Metadata: 

58 return self._project.metadata 

59 

60 @property 

61 def _package(self) -> Package: 

62 return self._metadata.packages[0] 

63 

64 @cached_property 

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

66 feature_flags = conf_to_feature_flags(self._conf) 

67 

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) 

70 

71 buildrequires = buildrequires_without_dev 

72 test_requires = set.difference(buildrequires_with_dev, buildrequires_without_dev) 

73 

74 return sorted(buildrequires), sorted(test_requires) 

75 

76 # Parameters specific to rust2rpm 

77 

78 @property 

79 def rust2rpm_version(self) -> str: 

80 """Current major version of rust2rpm.""" 

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

82 

83 @property 

84 def rust2rpm_target(self) -> str: 

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

86 return self._target 

87 

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 ] 

102 

103 # Parameters that control compiler flags 

104 

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 

109 

110 # Parameters for RPM package metadata 

111 

112 @property 

113 def rpm_name(self) -> str: 

114 """RPM source package Name.""" 

115 return self._project.rpm_name 

116 

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

121 

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) 

126 

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

131 

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" 

136 

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" 

141 

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) 

147 

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) 

155 

156 @property 

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

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

159 return None 

160 

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 

165 

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 

170 

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 

175 

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 

180 

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 

185 

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) 

190 

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) 

195 

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 

200 

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 

205 

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 

210 

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 

215 

216 # Parameters that control generation of subpackages 

217 

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

222 

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

227 

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) 

242 

243 # Parameters that allow injecting additional commands into scriptlets 

244 

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 

249 

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 

254 

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 

259 

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 

264 

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 

269 

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 

274 

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 

279 

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 

284 

285 # Parameters for crate metadata 

286 

287 @property 

288 def upstream_version(self) -> str: 

289 """Upstream version (from Cargo.toml metadata, SemVer format, not normalized).""" 

290 return self._package.version 

291 

292 @property 

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

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

295 return self._package.license 

296 

297 # Parameters for RPM macros 

298 

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

303 

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) 

311 

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

316 

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) 

321 

322 # Parameters derived from rust2rpm.toml 

323 

324 @property 

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

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

327 return self._conf.requires.build 

328 

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 

333 

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 

338 

339 @property 

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

341 """List of supported architectures. 

342 

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 

349 

350 return conf_supported_arches 

351 

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 

356 

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) 

361 

362 # Parameters derived from command-line flags 

363 

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 

368 

369 

370@dataclass(frozen=True) 

371class ProjectParametersFedora(ProjectParameters, ParametersFedora): 

372 """Collection of parameters for the "project" type template that is specific to the "fedora" target.""" 

373 

374 

375@dataclass(frozen=True) 

376class ProjectParametersMageia(ProjectParameters, ParametersMageia): 

377 """Collection of parameters for the "project" type template that is specific to the "mageia" target.""" 

378 

379 @property 

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

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

382 return self._buildrequires[0] 

383 

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] 

388 

389 

390@dataclass(frozen=True) 

391class ProjectParametersOpenSUSE(ProjectParameters, ParametersOpenSUSE): 

392 """Collection of parameters for the "project" type template that is specific to the "opensuse" target.""" 

393 

394 @property 

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

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

397 return self._buildrequires[0] 

398 

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] 

403 

404 

405@dataclass(frozen=True) 

406class ProjectParametersPlain(ProjectParameters, ParametersPlain): 

407 """Collection of parameters for the "project" type template that is specific to the "plain" target.""" 

408 

409 @property 

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

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

412 return self._buildrequires[0] 

413 

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] 

418 

419 @property 

420 def rpm_requires(self) -> dict[str | None, list[str]]: 

421 """Automatically generated RPM Requires for subpackages. 

422 

423 This returns an empty dictionary because this value is unused in the "project" template. 

424 """ 

425 return {} 

426 

427 @property 

428 def rpm_provides(self) -> dict[str | None, str]: 

429 """Automatically generated RPM Provides for subpackages. 

430 

431 This returns an empty dictionary because this value is unused in the "project" template. 

432 """ 

433 return {} 

434 

435 

436@dataclass(frozen=True) 

437class ProjectTemplate(Template): 

438 """Template for rendering spec files with the "project" template.""" 

439 

440 project: LocalProject 

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

442 

443 conf: TomlConf 

444 """Global rust2rpm configuration.""" 

445 

446 target: Target 

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

448 

449 date: time.struct_time | None 

450 """Optional timestamp for generated changelog entries. 

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

452 

453 packager: str | None 

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

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

456 

457 use_rpmautospec: bool 

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

459 the Release tag and %changelog.""" 

460 

461 make_changelog_entry: bool 

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

463 

464 def render(self) -> str: 

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

466 template = spec_file_template("project.spec") 

467 

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) 

480 

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 ) 

490 

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

492 

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

494 contents += "\n" 

495 

496 return contents