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

1"""Module containing functionality for preparing arguments for the "workspace" 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 

9from cargo2rpm.semver import Version 

10 

11from rust2rpm import __version__, log 

12from rust2rpm.conf import Patch, Source, TomlConf 

13from rust2rpm.project import LocalProject 

14from rust2rpm.sysinfo import Target 

15 

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) 

39 

40 

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 

43 

44 

45@dataclass(frozen=True) 

46class WorkspaceParameters(Parameters): 

47 """Collection of parameters for the "workspace" type template that is common for all targets.""" 

48 

49 _: KW_ONLY 

50 

51 _project: LocalProject 

52 _conf: TomlConf 

53 _target: Target 

54 

55 _make_changelog_entry: bool 

56 

57 # Convenience properties 

58 

59 @property 

60 def _metadata(self) -> Metadata: 

61 return self._project.metadata 

62 

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

66 

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) 

76 

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 

79 

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 

89 

90 return rpm_license_tag, rpm_license_comments 

91 

92 @cached_property 

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

94 feature_flags = conf_to_feature_flags(self._conf) 

95 

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 ) 

101 

102 return sorted(buildrequires), sorted(test_requires) 

103 

104 # Parameters specific to rust2rpm 

105 

106 @property 

107 def rust2rpm_version(self) -> str: 

108 """Current major version of rust2rpm.""" 

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

110 

111 @property 

112 def rust2rpm_target(self) -> str: 

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

114 return self._target 

115 

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 ] 

138 

139 # Parameters that control compiler flags 

140 

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 

145 

146 # Parameters for RPM package metadata 

147 

148 @property 

149 def rpm_name(self) -> str: 

150 """RPM source package Name.""" 

151 return self._project.rpm_name 

152 

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 

161 

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) 

166 

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

171 

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" 

176 

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" 

181 

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] 

186 

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] 

191 

192 @property 

193 def rpm_patch_file_automatic(self) -> str | None: 

194 """File name of the automatically generated patch.""" 

195 return None 

196 

197 @property 

198 def rpm_patch_file_manual(self) -> str | None: 

199 """File name of the manually generated patch.""" 

200 return None 

201 

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 

206 

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 

211 

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 

216 

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) 

221 

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) 

226 

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 

231 

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 

236 

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 

241 

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 

246 

247 # Parameters that control generation of subpackages 

248 

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

253 

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

258 

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) 

273 

274 # Parameters that allow injecting additional commands into scriptlets 

275 

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 

280 

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 

285 

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 

290 

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 

295 

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 

300 

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 

305 

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 

310 

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 

315 

316 # Parameters for RPM macros 

317 

318 @property 

319 def upstream_version(self) -> str | None: 

320 """Upstream project version.""" 

321 return self._project.version 

322 

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

327 

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

333 

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

338 

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) 

343 

344 # Parameters derived from rust2rpm.toml 

345 

346 @property 

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

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

349 return self._conf.requires.build 

350 

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 

355 

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 

360 

361 @property 

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

363 """List of supported architectures. 

364 

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 

371 

372 return conf_supported_arches 

373 

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 

378 

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) 

383 

384 # Parameters derived from command-line flags 

385 

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 

390 

391 

392@dataclass(frozen=True) 

393class WorkspaceParametersFedora(WorkspaceParameters, ParametersFedora): 

394 """Collection of parameters for the "workspace" type template that is specific to the "fedora" target.""" 

395 

396 

397@dataclass(frozen=True) 

398class WorkspaceParametersMageia(WorkspaceParameters, ParametersMageia): 

399 """Collection of parameters for the "workspace" type template that is specific to the "mageia" target.""" 

400 

401 @property 

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

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

404 return self._buildrequires[0] 

405 

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] 

410 

411 

412@dataclass(frozen=True) 

413class WorkspaceParametersOpenSUSE(WorkspaceParameters, ParametersOpenSUSE): 

414 """Collection of parameters for the "workspace" type template that is specific to the "opensuse" target.""" 

415 

416 @property 

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

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

419 return self._buildrequires[0] 

420 

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] 

425 

426 

427@dataclass(frozen=True) 

428class WorkspaceParametersPlain(WorkspaceParameters, ParametersPlain): 

429 """Collection of parameters for the "workspace" type template that is specific to the "plain" target.""" 

430 

431 @property 

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

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

434 return self._buildrequires[0] 

435 

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] 

440 

441 @property 

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

443 """Automatically generated RPM Requires for subpackages. 

444 

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

446 """ 

447 return {} 

448 

449 @property 

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

451 """Automatically generated RPM Provides for subpackages. 

452 

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

454 """ 

455 return {} 

456 

457 

458@dataclass(frozen=True) 

459class WorkspaceTemplate(Template): 

460 """Template for rendering spec files with the "workspace" template.""" 

461 

462 project: LocalProject 

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

464 

465 conf: TomlConf 

466 """Global rust2rpm configuration.""" 

467 

468 target: Target 

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

470 

471 date: time.struct_time | None 

472 """Optional timestamp for generated changelog entries. 

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

474 

475 packager: str | None 

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

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

478 

479 use_rpmautospec: bool 

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

481 the Release tag and %changelog.""" 

482 

483 make_changelog_entry: bool 

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

485 

486 def render(self) -> str: 

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

488 template = spec_file_template("workspace.spec") 

489 

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) 

502 

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 ) 

512 

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

514 

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

516 contents += "\n" 

517 

518 return contents