Coverage for rust2rpm/project/local.py: 96%

80 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2024-11-26 14:01 +0100

1"""Module containing functionality for loading projects from unpacked local sources.""" 

2 

3import contextlib 

4import shutil 

5import tempfile 

6from collections.abc import Generator 

7from dataclasses import dataclass 

8from pathlib import Path 

9from typing import Any 

10 

11from cargo2rpm.metadata import Metadata, Package 

12from cargo2rpm.semver import Version 

13 

14from rust2rpm import log 

15from rust2rpm.cli import Options 

16from rust2rpm.conf import TomlConf 

17from rust2rpm.inspect import get_doc_files, get_license_files 

18from rust2rpm.metadata import guess_main_package 

19from rust2rpm.patching import make_patches 

20from rust2rpm.vendor import generate_vendor_tarball 

21 

22from .meta import PatchFile, Project 

23from .shared import file_name_auto_patch, file_name_manual_patch 

24 

25 

26@contextlib.contextmanager 

27def temp_project_copy(path: Path) -> Generator[Path, Any, None]: 

28 """Provide a copy of the project sources in a temporary directory.""" 

29 with tempfile.TemporaryDirectory() as tmpdir: 

30 target = Path(tmpdir) / path.name 

31 yield shutil.copytree(path, target) 

32 

33 

34def guess_local_project_version_from_dir(dir_name: str) -> tuple[str | None, str | None]: 

35 """Guess name and version of a local project based on the name of the parent directory. 

36 

37 The heuristic used in this function does not work for pre-release versions, 

38 since a default format of `{name}-{version}` is assumed, but pre-releases 

39 contain a `-` character too. 

40 

41 Arguments: 

42 dir_name: Name of the directory. 

43 

44 Returns: 

45 Tuple of (name, version) strings, or (`None`, `None`) in case the 

46 directory name could not be parsed. 

47 

48 """ 

49 project = dir_name.rstrip("0123456789.").removesuffix("-") 

50 version = dir_name.removeprefix(f"{project}-") 

51 

52 try: 

53 Version.parse(version) 

54 except ValueError: 

55 return None, None 

56 else: 

57 return project, version 

58 

59 

60def guess_local_project_version_from_path(path: Path, version: str | None) -> tuple[str | None, str | None]: 

61 """Guess name and version of a local project based on the project path and version. 

62 

63 If the version is specified, it is stripped from the name of the parent 

64 directory, and the remaining string is assumed to the the project name. 

65 

66 If the version is not specified, the name of the parent directory is 

67 attempted to be parsed based on the assumption that it is named according to 

68 the common `{name}-{version}` format. 

69 

70 Arguments: 

71 path: Path to the project's root directory or to the Cargo.toml file at 

72 the project root. 

73 version: Optional version string. This is useful for pre-releases (which 

74 otherwise cannot be parsed correctly) or if the upstream project 

75 releases tarballs with non-SemVer version strings. 

76 

77 Returns: 

78 Tuple of (name, version) strings, or (`None`, `None`) in case the 

79 directory name could not be parsed. 

80 

81 """ 

82 dir_name = path.resolve().name if path.is_dir() else path.resolve().parent.name 

83 

84 if version: 84 ↛ 88line 84 didn't jump to line 88 because the condition on line 84 was always true

85 project = dir_name.removesuffix(f"-{version}") 

86 return project, version 

87 

88 return guess_local_project_version_from_dir(dir_name) 

89 

90 

91@dataclass(frozen=True) 

92class LocalProject(Project): 

93 """Description of a project based on locally unpacked sources.""" 

94 

95 main_package: Package 

96 """Heuristically determined "main" package of the cargo workspace. 

97 

98 In the case of local sources that contain a single crate (i.e. which are not 

99 cargo workspaces), this contains the metadata of this single crate. 

100 """ 

101 

102 @property 

103 def is_workspace(self) -> bool: 

104 """Predicate that is True for cargo workspaces and False for single crates.""" 

105 return len(self.metadata.packages) > 1 

106 

107 @property 

108 def rpm_name(self) -> str: 

109 """RPM source package name based on project metadata.""" 

110 return self.name 

111 

112 @staticmethod 

113 def from_cargo_toml_path( 

114 path: Path, 

115 name: str | None, 

116 version: str | None, 

117 out_dir: Path | None, 

118 conf: TomlConf, 

119 options: Options, 

120 ) -> "LocalProject": 

121 """Load project metadata based on the contents of an unpacked source tree. 

122 

123 Arguments: 

124 path: Path to the Cargo.toml file at the root of the project. 

125 name: Optional name hint for the project in case it cannot be 

126 guessed from the name of the parent directory. 

127 version: Optional version hint for the project in case it cannot be 

128 guessed from the name of the parent directory. 

129 out_dir: Output directory for any generated or downloaded files. 

130 conf: Global rust2rpm configuration. 

131 options: Options and flags derived from command-line arguments. 

132 

133 Returns: 

134 Project metadata based on the contents of the unpacked source tree. 

135 

136 """ 

137 project_root = path.parent 

138 doc_files = get_doc_files(project_root, conf) 

139 license_files = get_license_files(project_root, conf) 

140 

141 metadata = Metadata.from_cargo(str(path)) 

142 parent_dir = Path(project_root).parent 

143 

144 is_workspace = len(metadata.packages) > 1 

145 

146 if is_workspace: 

147 log.info("Skipping automatic creation of patches for cargo workspace.") 

148 

149 # fall back to the directory name for determining the name / version 

150 # of the project heuristically 

151 guessed_name, guessed_version = guess_local_project_version_from_path(path, version) 

152 

153 main_package = guess_main_package(metadata, name, guessed_name) 

154 

155 # prefer explicit arguments over guesses over conjectures 

156 name = name or guessed_name or main_package.name 

157 version = version or guessed_version or main_package.version 

158 

159 log.warn( 

160 f"Falling back to {name!r} as the main package. If this is not correct, " 

161 "specify the correct name in the 'pkgid' argument on the command line.", 

162 ) 

163 

164 vendor_tarball = generate_vendor_tarball(path, name, version, out_dir or parent_dir, options.vendor) 

165 

166 return LocalProject( 

167 name, 

168 version, 

169 metadata, 

170 license_files, 

171 doc_files, 

172 None, 

173 None, 

174 vendor_tarball, 

175 main_package, 

176 ) 

177 

178 # RET505: removing this else branch makes this less readable 

179 else: # noqa: RET505 

180 package = metadata.packages[0] 

181 

182 name = package.name 

183 version = package.version 

184 

185 reapply_path = (out_dir or Path()) / file_name_manual_patch(name) if options.reuse_patch else None 

186 

187 with temp_project_copy(project_root) as temp_root: 

188 temp_toml = temp_root / "Cargo.toml" 

189 

190 auto_diff, manual_diff = make_patches( 

191 name, 

192 package.version, 

193 temp_toml, 

194 reapply_path, 

195 options, 

196 ) 

197 

198 # ensure metadata is up-to-date with changes from patches 

199 metadata = Metadata.from_cargo(str(temp_toml)) 

200 

201 vendor_tarball = generate_vendor_tarball( 

202 temp_toml, 

203 name, 

204 version, 

205 out_dir or parent_dir, 

206 options.vendor, 

207 ) 

208 

209 auto_patch = PatchFile(file_name_auto_patch(name), diff) if (diff := auto_diff) else None 

210 manual_patch = PatchFile(file_name_manual_patch(name), diff) if (diff := manual_diff) else None 

211 

212 return LocalProject( 

213 name, 

214 version, 

215 metadata, 

216 license_files, 

217 doc_files, 

218 auto_patch, 

219 manual_patch, 

220 vendor_tarball, 

221 package, 

222 ) 

223 

224 @staticmethod 

225 def from_project_folder( 

226 path: Path, 

227 name: str | None, 

228 version: str | None, 

229 out_dir: Path | None, 

230 conf: TomlConf, 

231 options: Options, 

232 ) -> "LocalProject": 

233 """Load project metadata based on the contents of an unpacked source tree. 

234 

235 This method is a thin wrapper around `LocalProject.from_cargo_toml_path`. 

236 

237 Arguments: 

238 path: Path to the root directory of the unpacked sources. 

239 name: Optional name hint for the project in case it cannot be 

240 guessed from the name of the parent directory. 

241 version: Optional version hint for the project in case it cannot be 

242 guessed from the name of the parent directory. 

243 out_dir: Output directory for any generated or downloaded files. 

244 conf: Global rust2rpm configuration. 

245 options: Options and flags derived from command-line arguments. 

246 

247 Returns: 

248 Project metadata based on the contents of the unpacked source tree. 

249 

250 """ 

251 toml_path = path / "Cargo.toml" 

252 

253 return LocalProject.from_cargo_toml_path( 

254 toml_path, 

255 name, 

256 version, 

257 out_dir, 

258 conf, 

259 options, 

260 )