Coverage for rust2rpm/metadata.py: 73%

64 statements  

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

2 

3from cargo2rpm.metadata import Metadata, Package 

4from cargo2rpm.semver import Comparator, VersionReq 

5from cargo2rpm.semver import Op as Operator 

6 

7from rust2rpm import log 

8 

9 

10class WorkspaceError(Exception): 

11 """Raised if the 'main' package of a workspace cannot be determined.""" 

12 

13 

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. 

16 

17 Arguments: 

18 package: Crate metadata. 

19 

20 Returns: 

21 Union of the required features of all `bin` and `cdylib` targets. 

22 

23 """ 

24 required_features = set() 

25 

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) 

31 

32 return required_features 

33 

34 

35def guess_main_package(metadata: Metadata, hint: str | None, guess: str | None) -> Package: 

36 """Guess the "main" member of a cargo workspace. 

37 

38 This function uses some heuristics to try to determine the "main" 

39 member of a cargo workspace: 

40 

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. 

47 

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. 

53 

54 Returns: 

55 Metadata for the "main" workspace member. 

56 

57 Raises: 

58 `WorkspaceError` is raised in cases where the "main" package cannot be 

59 determined heuristically or the supplied hint was invalid. 

60 

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 

66 

67 msg = "The supplied hint for the name of the 'main' crate of the did not match any workspace member." 

68 raise WorkspaceError(msg) 

69 

70 with_bin = [] 

71 with_cdylib = [] 

72 

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) 

79 

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] 

82 

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] 

85 

86 if guess: 

87 for package in metadata.packages: 

88 if package.name == guess: 

89 return package 

90 

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) 

96 

97 

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+. 

100 

101 Arguments: 

102 features: Mapping from feature names to feature dependencies. 

103 

104 Returns: 

105 `True` if the new feature syntax is used, and `False` otherwise. 

106 

107 """ 

108 for name, deps in features.items(): 

109 for dep in deps: 

110 if "?/" in dep: 

111 return True 

112 

113 if dep.startswith("dep:"): 

114 if len(deps) != 1: 

115 return True 

116 if dep.removeprefix("dep:") != name: 

117 return True 

118 

119 return False 

120 

121 

122def warn_if_package_uses_restrictive_dependencies(package: Package): 

123 """Log a warning if crate metadata contains restrictive version ranges for dependencies. 

124 

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. 

130 

131 Arguments: 

132 package: Metadata for a single crate. 

133 

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

141 

142 

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 

147 

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 

151 

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 

155 

156 return False