Coverage for rust2rpm/__main__.py: 67%
252 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 21:21 +0100
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 21:21 +0100
1import sys
2from pathlib import Path
4import requests
5from cargo2rpm.semver import Version
7from rust2rpm import log
8from rust2rpm.cli import CliArgs, Options
9from rust2rpm.conf import TomlConf, load_config_ini, load_config_toml
10from rust2rpm.cratesio import NoVersionsError
11from rust2rpm.distgit import get_package_info
12from rust2rpm.generator import (
13 CrateTemplate,
14 EpelEightTemplate,
15 InvalidMetadataError,
16 ProjectTemplate,
17 WorkspaceTemplate,
18)
19from rust2rpm.metadata import WorkspaceError, warn_if_package_uses_restrictive_dependencies
20from rust2rpm.patching import PatchError
21from rust2rpm.project import CrateProject, InvalidVersionError, LocalProject, OfflineError, Project
22from rust2rpm.sysinfo import Target
23from rust2rpm.utils import detect_packager, detect_rpmautospec, guess_crate_name
26def validate_extra_sources(conf: TomlConf, vendor_tarball: str | None):
27 if extra_sources := conf.package.extra_sources:
28 min_source_number = 2 if vendor_tarball else 1
29 safe_source_number = 2
31 for extra_source in extra_sources:
32 if extra_source.number == 0:
33 log.error(
34 f"Extra source {extra_source.file!r} conflicts with the primary tarball. "
35 f"Use a number larger than {min_source_number} instead.",
36 )
37 sys.exit(1)
38 if extra_source.number == 1 and vendor_tarball: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 log.error(
40 f"Extra source {extra_source.file!r} conflicts with the vendor tarball. "
41 f"Use a number larger than {min_source_number} instead.",
42 )
43 sys.exit(1)
45 # no conflict now, but maybe later
46 if extra_source.number < safe_source_number: 46 ↛ 31line 46 didn't jump to line 31 because the condition on line 46 was always true
47 log.warn(
48 f"Extra source {extra_source.file!r} might conflict with other files in the future. "
49 "Using source numbers >= 2 is recommended.",
50 )
53def validate_extra_patches(conf: TomlConf, *, auto_patch: bool, manual_patch: bool):
54 if extra_patches := conf.package.extra_patches:
55 min_patch_number = 0
56 if auto_patch: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 min_patch_number += 1
58 if manual_patch: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true
59 min_patch_number += 1
60 safe_patch_number = 2
62 for extra_patch in extra_patches:
63 if extra_patch.number == 0 and auto_patch: 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 log.error(
65 f"Extra patch {extra_patch.file!r} conflicts with the automatically generated patch. "
66 f"Use a number larger than {min_patch_number} instead.",
67 )
68 sys.exit(1)
69 if extra_patch.number == 0 and manual_patch: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 log.error(
71 f"Extra patch {extra_patch.file!r} conflicts with the manually created patch. "
72 f"Use a number larger than {min_patch_number} instead.",
73 )
74 sys.exit(1)
75 if extra_patch.number == 1 and auto_patch and manual_patch: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 log.error(
77 f"Extra patch {extra_patch.file!r} conflicts with the manually created patch. "
78 f"Use a number larger than {min_patch_number} instead.",
79 )
80 sys.exit(1)
82 # no conflict now, but maybe later
83 if extra_patch.number < safe_patch_number: 83 ↛ 62line 83 didn't jump to line 62 because the condition on line 83 was always true
84 log.warn(
85 f"Extra patch {extra_patch.file!r} might conflict with other patches in the future. "
86 "Using patch numbers >= 2 is recommended.",
87 )
90def load_config(
91 target: str,
92 *,
93 path: Path | None = None,
94 features: set[str] | None = None,
95 migrate: bool = False,
96) -> TomlConf:
97 ini_paths = [".rust2rpm.conf", "_rust2rpm.conf", "rust2rpm.conf"]
98 toml_path = path or Path("rust2rpm.toml")
100 # check if both new and old config format are present
101 if path is None:
102 for ini_path in ini_paths:
103 if Path(ini_path).exists() and toml_path.exists(): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 log.error(f"Both rust2rpm.toml and {ini_path!s} exist.")
105 sys.exit(1)
107 tomlconf = load_config_toml(toml_path, features)
108 if tomlconf is not None:
109 return tomlconf
111 if path and tomlconf is None: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 log.error("Configuration file not found: {path!s}")
113 sys.exit(1)
115 distconf = load_config_ini(ini_paths, target, features)
116 if distconf is not None: 116 ↛ 118line 116 didn't jump to line 118 because the condition on line 116 was never true
118 log.warn(
119 "The rust2rpm.conf file format is deprecated and support for it will be removed "
120 "removed in a future release of rust2rpm. The current settings have been "
121 "automatically migrated to the new TOML based format (rust2rpm.toml).",
122 )
124 # automatically migrate to TOML format unless given explicit path
125 if migrate and path is None:
126 with toml_path.open("w") as file:
127 file.write(distconf.migrate(features))
129 for ini_path in ini_paths:
130 if Path(ini_path).exists():
131 Path(ini_path).unlink()
133 return distconf.upgrade()
135 return TomlConf()
138def load_project(args: CliArgs, conf: TomlConf) -> Project:
139 project: Project
141 try:
142 if args.path:
143 if args.path.is_dir():
144 project = LocalProject.from_project_folder(
145 args.path,
146 args.name,
147 args.version,
148 args.output_directory,
149 conf,
150 args.options,
151 )
152 name = project.name
153 return project
155 if args.path.name == "Cargo.toml":
156 project = LocalProject.from_cargo_toml_path(
157 args.path,
158 args.name,
159 args.version,
160 args.output_directory,
161 conf,
162 args.options,
163 )
164 name = project.name
165 return project
167 if args.path.name.endswith(".crate"):
168 project = CrateProject.from_local_crate(
169 args.path,
170 args.output_directory,
171 conf,
172 args.options,
173 )
174 name = project.name
175 return project
177 msg = (
178 "Invalid path argument."
179 if args.path.exists()
180 else f"Invalid path argument: {args.path!s} is not a local file or directory"
181 )
182 log.error(msg)
183 sys.exit(1)
185 else: # pragma nocover: requires internet access
186 if guessed_name := args.name or guess_crate_name():
187 name = guessed_name
188 else:
189 log.error("Neither pkgid nor --path was specified and auto-detection failed.")
190 sys.exit(1)
192 project = CrateProject.from_crates_io(
193 name,
194 args.version,
195 args.output_directory,
196 conf,
197 args.options,
198 )
200 except (InvalidVersionError, NoVersionsError, OfflineError, PatchError, requests.RequestException) as exc:
201 log.error(str(exc))
202 sys.exit(1)
204 else:
205 return project
208def use_rpmautospec(args: CliArgs, spec_file: Path) -> bool:
209 return detect_rpmautospec(args.target, spec_file) if args.options.rpmautospec is None else args.options.rpmautospec
212def existence_check(project: Project, args: CliArgs, spec_file: Path): # pragma nocover: requires internet access
213 if (
214 args.target in {Target.FEDORA}
215 and args.options.existence_check
216 and not args.options.offline
217 and not spec_file.exists()
218 and (package_info := get_package_info(project.rpm_name))
219 ):
220 if args.options.compat:
221 version_suffix = project.rpm_name.removeprefix(project.name)
222 log.warn(
223 f"Version {version_suffix}.* of the crate {project.name!r} is already "
224 f"packaged for Fedora: {package_info['full_url']}",
225 )
226 else:
227 log.warn(f"Crate {project.name!r} is already packaged for Fedora: {package_info['full_url']}")
229 log.info("Re-run with --no-existence-check to create a new spec file from scratch.")
230 sys.exit(1)
233def license_file_check(project: Project, options: Options):
234 if not project.license_files and not options.ignore_missing_license_files: 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true
235 log.error(
236 "No license files were detected. "
237 "In almost all cases, this is an issue with the upstream project that should be reported.",
238 )
239 log.info(
240 "To temporarily ignore this issue, use the '--ignore-missing-license-files' / '-I' flag - "
241 "for example, when manually including license files from the upstream project, or for test builds.",
242 )
243 sys.exit(1)
246def handle_crate(project: CrateProject, args: CliArgs, conf: TomlConf) -> tuple[Path, str]:
247 package = project.metadata.packages[0]
248 spec_file = (args.output_directory or Path()) / f"{project.rpm_name}.spec"
249 packager = detect_packager()
251 license_file_check(project, args.options)
252 existence_check(project, args, spec_file)
254 # basic validation for extra sources and patches
255 validate_extra_sources(conf, project.vendor_tarball)
256 validate_extra_patches(
257 conf,
258 auto_patch=project.auto_patch is not None,
259 manual_patch=project.manual_patch is not None,
260 )
262 if build_meta := Version.parse(package.version).build: 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true
263 log.error(f"Crate version {package.version!r} contains build metadata: '+{build_meta}'")
264 log.error(f"This is not supported by rust2rpm; remove the '+{build_meta}' suffix.")
265 sys.exit(1)
267 if project.vendor_tarball and not (package.is_bin() or package.is_cdylib()): 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true
268 log.error("Building library-only crates with vendored dependencies is not supported.")
269 sys.exit(1)
271 if conf.package.cargo_toml_patch_comments and project.manual_patch is None: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 log.error(
273 "The rust2rpm.toml configuration file specifies comments for a Cargo.toml patch, "
274 "but Cargo.toml was not patched.",
275 )
276 sys.exit(1)
278 if conf.package.url: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 log.error(
280 "The rust2rpm.toml configuration file specifies an override for the package URL, "
281 "but this is not valid for crates packaged from crates.io.",
282 )
283 sys.exit(1)
285 if conf.package.source_url: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 log.error(
287 "The rust2rpm.toml configuration file specifies an override for the Source URL, "
288 "but this is not valid for crates packaged from crates.io.",
289 )
290 sys.exit(1)
292 warn_if_package_uses_restrictive_dependencies(package)
294 try:
295 generator = CrateTemplate(
296 project=project,
297 conf=conf,
298 target=args.target,
299 date=None,
300 packager=packager,
301 use_relative_license_paths=args.options.relative_license_paths,
302 use_rpmautospec=use_rpmautospec(args, spec_file),
303 make_changelog_entry=args.options.auto_changelog_entry,
304 )
305 spec_contents = generator.render()
307 except InvalidMetadataError as exc:
308 # this can happen for crates that do not specify a license
309 log.error(str(exc.args))
310 sys.exit(1)
312 except ValueError as exc:
313 # this can happen for weird version requirements in ancient crates
314 # that cannot be translated correctly into RPM dependencies
315 log.error(f"Could not generate spec file due to invalid version requirement: {exc!s}")
316 sys.exit(1)
318 return spec_file, spec_contents
321def handle_epel8(project: CrateProject, args: CliArgs, conf: TomlConf) -> tuple[Path, str]:
322 package = project.metadata.packages[0]
323 spec_file = (args.output_directory or Path()) / f"{project.rpm_name}.spec"
324 packager = detect_packager()
326 license_file_check(project, args.options)
327 existence_check(project, args, spec_file)
329 # basic validation for extra sources and patches
330 validate_extra_sources(conf, project.vendor_tarball)
331 validate_extra_patches(
332 conf,
333 auto_patch=project.auto_patch is not None,
334 manual_patch=project.manual_patch is not None,
335 )
337 if build_meta := Version.parse(package.version).build: 337 ↛ 338line 337 didn't jump to line 338 because the condition on line 337 was never true
338 log.error(f"Crate version {package.version!r} contains build metadata: '+{build_meta}'")
339 log.error(f"This is not supported by rust2rpm; remove the '+{build_meta}' suffix.")
340 sys.exit(1)
342 if project.vendor_tarball and not (package.is_bin() or package.is_cdylib()): 342 ↛ 343line 342 didn't jump to line 343 because the condition on line 342 was never true
343 log.error("Building library-only crates with vendored dependencies is not supported.")
344 sys.exit(1)
346 if conf.package.cargo_toml_patch_comments and project.manual_patch is None: 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true
347 log.error(
348 "The rust2rpm.toml configuration file specifies comments for a Cargo.toml patch, "
349 "but Cargo.toml was not patched.",
350 )
351 sys.exit(1)
353 if conf.package.url: 353 ↛ 354line 353 didn't jump to line 354 because the condition on line 353 was never true
354 log.error(
355 "The rust2rpm.toml configuration file specifies an override for the package URL, "
356 "but this is not valid for crates packaged from crates.io.",
357 )
358 sys.exit(1)
360 if conf.package.source_url: 360 ↛ 361line 360 didn't jump to line 361 because the condition on line 360 was never true
361 log.error(
362 "The rust2rpm.toml configuration file specifies an override for the Source URL, "
363 "but this is not valid for crates packaged from crates.io.",
364 )
365 sys.exit(1)
367 warn_if_package_uses_restrictive_dependencies(package)
369 try:
370 generator = EpelEightTemplate(
371 project=project,
372 conf=conf,
373 date=None,
374 packager=packager,
375 use_rpmautospec=use_rpmautospec(args, spec_file),
376 make_changelog_entry=args.options.auto_changelog_entry,
377 )
378 spec_contents = generator.render()
380 except InvalidMetadataError as exc:
381 # this can happen for crates that do not specify a license
382 log.error(str(exc.args))
383 sys.exit(1)
385 except ValueError as exc:
386 # this can happen for weird version requirements in ancient crates
387 # that cannot be translated correctly into RPM dependencies
388 log.error(f"Could not generate spec file due to invalid version requirement: {exc!s}")
389 sys.exit(1)
391 return spec_file, spec_contents
394def handle_local(project: LocalProject, args: CliArgs, conf: TomlConf) -> tuple[Path, str]:
395 package = project.metadata.packages[0]
396 spec_file = (args.output_directory or Path()) / f"{project.rpm_name}.spec"
397 packager = detect_packager()
399 license_file_check(project, args.options)
400 existence_check(project, args, spec_file)
402 # basic validation for extra sources and patches
403 validate_extra_sources(conf, project.vendor_tarball)
404 validate_extra_patches(
405 conf,
406 auto_patch=project.auto_patch is not None,
407 manual_patch=project.manual_patch is not None,
408 )
410 if build_meta := Version.parse(package.version).build: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 log.error(f"Crate version {package.version!r} contains build metadata: '+{build_meta}'")
412 log.error(f"This is not supported by rust2rpm; remove the '+{build_meta}' suffix.")
413 sys.exit(1)
415 if conf.package.cargo_toml_patch_comments and project.manual_patch is None: 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true
416 log.error(
417 "The rust2rpm.toml configuration file specifies comments for a Cargo.toml patch, "
418 "but Cargo.toml was not patched.",
419 )
420 sys.exit(1)
422 warn_if_package_uses_restrictive_dependencies(package)
424 try:
425 generator = ProjectTemplate(
426 project=project,
427 conf=conf,
428 target=args.target,
429 date=None,
430 packager=packager,
431 use_rpmautospec=use_rpmautospec(args, spec_file),
432 make_changelog_entry=args.options.auto_changelog_entry,
433 )
434 spec_contents = generator.render()
436 except InvalidMetadataError as exc:
437 # this can happen for crates that do not specify a license
438 log.error(str(exc.args))
439 sys.exit(1)
441 return spec_file, spec_contents
444def handle_workspace(project: LocalProject, args: CliArgs, conf: TomlConf) -> tuple[Path, str]:
445 spec_file = (args.output_directory or Path()) / f"{project.rpm_name}.spec"
446 packager = detect_packager()
448 license_file_check(project, args.options)
449 existence_check(project, args, spec_file)
451 # basic validation for extra sources and patches
452 validate_extra_sources(conf, project.vendor_tarball)
454 try:
455 generator = WorkspaceTemplate(
456 project=project,
457 conf=conf,
458 target=args.target,
459 date=None,
460 packager=packager,
461 use_rpmautospec=use_rpmautospec(args, spec_file),
462 make_changelog_entry=args.options.auto_changelog_entry,
463 )
464 spec_contents = generator.render()
466 except InvalidMetadataError as exc:
467 # this can happen for crates that do not specify a license
468 log.error(str(exc.args))
469 sys.exit(1)
471 except WorkspaceError as exc:
472 # this can happen for confusing workspace metadata or invalid hints
473 log.error(str(exc.args))
474 sys.exit(1)
476 return spec_file, spec_contents
479def print_deprecation_warnings(args: CliArgs):
480 if args.options.relative_license_paths is True: 480 ↛ 481line 480 didn't jump to line 481 because the condition on line 480 was never true
481 log.warn(
482 "The '--relative-license-paths' flag produces slightly unexpected results."
483 "It is deprecated and will be removed in a future version of rust2rpm.",
484 )
486 if args.options.auto_changelog_entry is False: 486 ↛ 487line 486 didn't jump to line 487 because the condition on line 486 was never true
487 log.warn("The '--no-auto-changelog-entry' is deprecated and will be removed in a future version of rust2rpm.")
490def print_migration_errors(args: CliArgs):
491 if (pkgid := args.pkgid) and ("/" in pkgid): 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true
492 log.error("Paths are no longer accepted as positional arguments. Use the '--path' argument instead.")
493 sys.exit(1)
496def main(argv: list[str] | None = None):
497 args = CliArgs.parse(argv)
498 print_deprecation_warnings(args)
499 print_migration_errors(args)
501 # validate config and exit early if config is invalid
502 conf = load_config(args.target, path=args.config_file, features=None, migrate=False)
503 if args.options.validate_only: 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true
504 sys.exit(0)
506 # force generation of a Cargo.toml patch if comments are configured
507 args.options.patch = args.options.patch or (
508 # only force opening an editor if comments are present but neither --patch nor --reuse-patch were specified
509 len(conf.package.cargo_toml_patch_comments) > 0
510 and not args.options.patch
511 and not args.options.reuse_patch
512 )
514 project = load_project(args, conf)
516 # validate config with known feature names
517 if isinstance(project, CrateProject):
518 feature_names = project.package.get_feature_names()
519 conf = load_config(args.target, path=args.config_file, features=feature_names, migrate=True)
520 else:
521 conf = load_config(args.target, path=args.config_file, features=None, migrate=True)
523 # special handling for epel8 target
524 if args.target == Target.EPEL8 and isinstance(project, CrateProject):
525 spec_file, spec_contents = handle_epel8(project, args, conf)
527 # package for a workspace project
528 elif isinstance(project, LocalProject) and project.metadata.is_workspace():
529 spec_file, spec_contents = handle_workspace(project, args, conf)
531 # package for non-crate, non-workspace project
532 elif isinstance(project, LocalProject):
533 spec_file, spec_contents = handle_local(project, args, conf)
535 # package for an actual Rust crate
536 elif isinstance(project, CrateProject):
537 spec_file, spec_contents = handle_crate(project, args, conf)
539 else: # pragma nocover
540 log.error("Invalid project - this should not happen.")
541 sys.exit(1)
543 # ensure output directory exists
544 if out_dir := args.output_directory: 544 ↛ 547line 544 didn't jump to line 547 because the condition on line 544 was always true
545 out_dir.mkdir(parents=True, exist_ok=True)
546 else:
547 out_dir = Path()
549 # write spec file
550 with spec_file.open("w") as file:
551 file.write(spec_contents)
552 log.success(f"Generated: {file.name}")
554 # write patch files
555 for patch in [project.auto_patch, project.manual_patch]:
556 if patch is not None:
557 with (out_dir / patch.name).open("w") as file:
558 file.write("\n".join(patch.contents) + "\n")
559 log.success(f"Generated: {file.name}")
562if __name__ == "__main__": # pragma nocover
563 try:
564 main()
565 except KeyboardInterrupt:
566 sys.exit(0)