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

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

2 

3import time 

4from dataclasses import KW_ONLY, dataclass 

5from functools import cached_property 

6 

7from cargo2rpm.metadata import FeatureFlags, Metadata, Package 

8from cargo2rpm.semver import Version 

9 

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 

14 

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) 

32 

33 

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. 

40 

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. 

45 

46 Returns: 

47 Formatted string with cargo arguments. 

48 

49 """ 

50 if feature_flags.all_features: 

51 cargo_args = " --all-features" 

52 

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) 

58 

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) 

64 

65 cargo_args_list = ["--no-default-features"] if feature_flags.no_default_features else [] 

66 

67 if len(enabled_features) > 0: 

68 cargo_args_list.append(f"--features {','.join(enabled_features)}") 

69 

70 cargo_args = " " + " ".join(cargo_args_list) if cargo_args_list else "" 

71 

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

75 

76 cargo_args_list.append(f"--features {','.join(feature_flags.features)}") 

77 cargo_args = " " + " ".join(cargo_args_list) 

78 

79 elif feature_flags.no_default_features: 

80 cargo_args = " --no-default-features" 

81 

82 else: 

83 cargo_args = "" 

84 

85 return cargo_args 

86 

87 

88def cargo_test_commands(conf: TomlConf) -> list[str]: 

89 """Format %cargo_test arguments for rust-toolset macros on RHEL 8. 

90 

91 Arguments: 

92 conf: Global rust2rpm configuration. 

93 

94 Returns: 

95 List of formatted string with %cargo_test arguments. 

96 

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) 

102 

103 skip_flags = [] 

104 for name in skip: 

105 skip_flags.extend(["--skip", name]) 

106 else: 

107 skip_flags = [] 

108 

109 skip_exact_flag = ["--exact"] if conf.tests.skip_exact is True else [] 

110 

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 

119 

120 elif skip_flags: 

121 args = [" " + " ".join(["--", *skip_exact_flag, *skip_flags])] 

122 

123 else: 

124 args = [] 

125 

126 return [f"%cargo_test{arg}" for arg in args] 

127 

128 

129@dataclass(frozen=True) 

130class CrateParametersEpelEight(ParametersEpelEight, Parameters): 

131 """Collection of parameters for the "epel8" type template.""" 

132 

133 _: KW_ONLY 

134 

135 _project: CrateProject 

136 _conf: TomlConf 

137 

138 _make_changelog_entry: bool 

139 

140 # Convenience properties 

141 

142 @property 

143 def _metadata(self) -> Metadata: 

144 return self._project.metadata 

145 

146 @property 

147 def _package(self) -> Package: 

148 return self._metadata.packages[0] 

149 

150 # Parameters specific to rust2rpm 

151 

152 @property 

153 def rust2rpm_version(self) -> str: 

154 """Current major version of rust2rpm.""" 

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

156 

157 @property 

158 def rust_packaging_dep(self) -> str: 

159 """Dependency string for RPM Rust packaging tools.""" 

160 return RUST_PACKAGING_DEPS[0] 

161 

162 # Parameters that control compiler flags 

163 

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 

168 

169 # Parameters for RPM package metadata 

170 

171 @property 

172 def rpm_name(self) -> str: 

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

174 return self._project.rpm_name 

175 

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

180 

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) 

185 

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

190 

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) 

196 

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) 

204 

205 @property 

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

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

208 return None 

209 

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 

214 

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 

219 

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 

224 

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 

229 

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 

234 

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 

239 

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) 

244 

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 

249 

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 

254 

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 

259 

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 

264 

265 # Parameters that control generation of subpackages 

266 

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

271 

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) 

286 

287 # Parameters that allow injecting additional commands into scriptlets 

288 

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 

293 

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 

298 

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 

303 

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 

308 

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 

313 

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 

318 

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 

323 

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 

328 

329 # Parameters for crate metadata 

330 

331 @property 

332 def crate_name(self) -> str: 

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

334 return self._project.name 

335 

336 @property 

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

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

339 return self._package.license 

340 

341 # Parameters for RPM macros 

342 

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) 

350 

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) 

355 

356 # Parameters derived from rust2rpm.toml 

357 

358 @property 

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

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

361 return self._conf.requires.build 

362 

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 

367 

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 

372 

373 @property 

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

375 """List of supported architectures. 

376 

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 

383 

384 return conf_supported_arches 

385 

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) 

390 

391 # Parameters derived from command-line flags 

392 

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 

397 

398 

399@dataclass(frozen=True) 

400class EpelEightTemplate(Template): 

401 """Template for rendering spec files with the "epel8" template.""" 

402 

403 project: CrateProject 

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

405 

406 conf: TomlConf 

407 """Global rust2rpm configuration.""" 

408 

409 date: time.struct_time | None 

410 """Optional timestamp for generated changelog entries. 

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

412 

413 packager: str | None 

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

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

416 

417 use_rpmautospec: bool 

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

419 the Release tag and %changelog.""" 

420 

421 make_changelog_entry: bool 

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

423 

424 def render(self) -> str: 

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

426 template = spec_file_template("epel8.spec") 

427 

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 ) 

436 

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

438 

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" 

441 

442 return contents