Coverage for rust2rpm/metadata.py: 73%
64 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 13:52 +0100
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-26 13:52 +0100
1"""Module containing helper functionality for parsing / processing crate metadata."""
3from cargo2rpm.metadata import Metadata, Package
4from cargo2rpm.semver import Comparator, VersionReq
5from cargo2rpm.semver import Op as Operator
7from rust2rpm import log
10class WorkspaceError(Exception):
11 """Raised if the 'main' package of a workspace cannot be determined."""
14def get_required_features_for_binaries(package: Package) -> set[str]:
15 """Query crate metadata for the set of features that is required by binary targets.
17 Arguments:
18 package: Crate metadata.
20 Returns:
21 Union of the required features of all `bin` and `cdylib` targets.
23 """
24 required_features = set()
26 for target in package.targets:
27 if "bin" in target.kind and "bin" in target.crate_types and (reqs := target.required_features):
28 required_features.update(reqs)
29 if "cdylib" in target.kind and "cdylib" in target.crate_types and (reqs := target.required_features): 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 required_features.update(reqs)
32 return required_features
35def guess_main_package(metadata: Metadata, hint: str | None, guess: str | None) -> Package:
36 """Guess the "main" member of a cargo workspace.
38 This function uses some heuristics to try to determine the "main"
39 member of a cargo workspace:
41 - If there is a workspace member with a name that matches the supplied
42 hint, that workspace member is returned.
43 - If there is exactly one workspace member that contains `cdylib` targets,
44 that workspace member is returned.
45 - If there is exactly one workspace member that contains `bin` targets,
46 that workspace member is returned.
48 Arguments:
49 metadata: Metadata for the whole cargo workspace.
50 hint: Optional hint pointing at the "main" workspace member.
51 guess: Optional guess for the project name based on the name of the
52 parent directory.
54 Returns:
55 Metadata for the "main" workspace member.
57 Raises:
58 `WorkspaceError` is raised in cases where the "main" package cannot be
59 determined heuristically or the supplied hint was invalid.
61 """
62 if hint:
63 for package in metadata.packages: 63 ↛ 67line 63 didn't jump to line 67 because the loop on line 63 didn't complete
64 if package.name == hint:
65 return package
67 msg = "The supplied hint for the name of the 'main' crate of the did not match any workspace member."
68 raise WorkspaceError(msg)
70 with_bin = []
71 with_cdylib = []
73 for package in metadata.packages:
74 for target in package.targets:
75 if "bin" in target.kind and "bin" in target.crate_types:
76 with_bin.append(package)
77 if "cdylib" in target.kind and "cdylib" in target.crate_types: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true
78 with_cdylib.append(package)
80 if len(with_cdylib) == 1: 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true
81 return with_cdylib[0]
83 if len(with_bin) == 1: 83 ↛ 86line 83 didn't jump to line 86 because the condition on line 83 was always true
84 return with_bin[0]
86 if guess:
87 for package in metadata.packages:
88 if package.name == guess:
89 return package
91 msg = (
92 "Heuristic for determining the 'main' crate of the workspace failed. "
93 "Please supply the 'pkgid' argument to select the 'main' workspace member."
94 )
95 raise WorkspaceError(msg)
98def package_uses_rust_1_60_feature_syntax(features: dict[str, list[str]]) -> bool:
99 """Determine whether the specified feature set contains syntax only supported in Rust 1.60+.
101 Arguments:
102 features: Mapping from feature names to feature dependencies.
104 Returns:
105 `True` if the new feature syntax is used, and `False` otherwise.
107 """
108 for name, deps in features.items():
109 for dep in deps:
110 if "?/" in dep:
111 return True
113 if dep.startswith("dep:"):
114 if len(deps) != 1:
115 return True
116 if dep.removeprefix("dep:") != name:
117 return True
119 return False
122def warn_if_package_uses_restrictive_dependencies(package: Package):
123 """Log a warning if crate metadata contains restrictive version ranges for dependencies.
125 Dependencies with versions ranges that are more restrictive than
126 "SemVer-compatible" cause problems with packaging, and the reasons for their
127 presence are in almost all cases not valid reasons for downstream packaging
128 (i.e. MSRV concerns). This logs a warning if the given crate metadata
129 contains any such dependencies.
131 Arguments:
132 package: Metadata for a single crate.
134 """
135 for dependency in package.dependencies: 135 ↛ 136line 135 didn't jump to line 136 because the loop on line 135 never started
136 name = dependency.rename or dependency.name
137 req = VersionReq.parse(dependency.req)
138 for comp in req.comparators:
139 if _is_strict_dep(comp):
140 log.warn(f"Dependency on {name!r} stricter than necessary: {comp}")
143def _is_strict_dep(comp: Comparator) -> bool:
144 # dependencies like "~1.0" are stricter than SemVer
145 if comp.op == Operator.TILDE and comp.major > 0 and comp.minor is not None:
146 return True
148 # dependencies like "1.0.*" are stricter than SemVer
149 if comp.op == Operator.WILDCARD and comp.major > 0 and comp.minor is not None:
150 return True
152 # dependencies like "=1.2" are stricter than SemVer
153 if comp.op == Operator.EXACT and comp.major > 0 and comp.minor is not None and comp.patch is None: # noqa: SIM103
154 return True
156 return False