Coverage for rust2rpm/cli.py: 100%

106 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2024-11-25 18:52 +0100

1"""Module containing command-line argument parsing logic.""" 

2 

3import argparse 

4from argparse import ArgumentParser, RawTextHelpFormatter 

5from dataclasses import KW_ONLY, dataclass 

6from enum import StrEnum, auto 

7from pathlib import Path 

8 

9from rust2rpm.sysinfo import TARGET_NAMES, Target, get_default_target 

10 

11 

12class VendorMode(StrEnum): 

13 """Description of different modes for using vendored dependencies.""" 

14 

15 OFF = auto() 

16 """Generate a spec file without any references to a vendor tarball.""" 

17 

18 AUTO = auto() 

19 """Generate a spec file for use with a vendor tarball, and create a 

20 tarball with vendored dependencies with default settings.""" 

21 

22 MANUAL = auto() 

23 """Generate a spec file for use with a vendor tarball, but require 

24 manual assembly of vendored sources into a tarball.""" 

25 

26 

27VENDOR_MODES = [mode.value for mode in VendorMode] 

28 

29 

30def get_parser() -> ArgumentParser: 

31 """Construct an `argparse.ArgumentParser` for parsing command-line arguments.""" 

32 parser = ArgumentParser("rust2rpm", formatter_class=RawTextHelpFormatter) 

33 

34 # ==================== # 

35 # positional arguments # 

36 # ==================== # 

37 

38 # pkgid: str | None = None 

39 parser.add_argument( 

40 "pkgid", 

41 nargs="?", 

42 help="""\ 

43crate (crate on crates.io) 

44crate@version (pkgid on crates.io) 

45@version (auto-detect crate name)""", 

46 ) 

47 

48 # ==================== # 

49 # options with a value # 

50 # ==================== # 

51 

52 # config_file: str | None 

53 parser.add_argument( 

54 "-C", 

55 "--config-file", 

56 action="store", 

57 default=None, 

58 dest="config_file", 

59 help="Path to rust2rpm.toml config file (defaults to ./rust2rpm.toml)", 

60 ) 

61 

62 # output_directory: str | None 

63 parser.add_argument( 

64 "-o", 

65 "--output-directory", 

66 action="store", 

67 default=None, 

68 dest="output_directory", 

69 help="Target directory for any created files", 

70 ) 

71 

72 # path: str | None = None 

73 parser.add_argument( 

74 "--path", 

75 action="store", 

76 default=None, 

77 dest="path", 

78 help="Path to local project directory or Cargo.toml file to check instead of crates.io", 

79 ) 

80 

81 # target: str | None = None 

82 parser.add_argument( 

83 "-t", 

84 "--target", 

85 action="store", 

86 choices=TARGET_NAMES, 

87 default=None, 

88 dest="target", 

89 help="Distribution target", 

90 ) 

91 

92 # vendor: VendorMode = VendorMode.OFF 

93 parser.add_argument( 

94 "-V", 

95 "--vendor", 

96 action="store", 

97 choices=VENDOR_MODES, 

98 default=None, 

99 dest="vendor", 

100 help="Write a spec for building with vendored dependencies (default=off)", 

101 ) 

102 

103 # ==================== # 

104 # flag-only options # 

105 # ==================== # 

106 

107 # rpmautospec: bool | None = None 

108 parser.add_argument( 

109 "-a", 

110 "--rpmautospec", 

111 action="store_true", 

112 default=None, 

113 dest="rpmautospec", 

114 help="Use autorelease and autochangelog features", 

115 ) 

116 parser.add_argument( 

117 "--no-rpmautospec", 

118 action="store_false", 

119 default=None, 

120 dest="rpmautospec", 

121 help="Do not use rpmautospec", 

122 ) 

123 

124 # auto_changelog_entry: bool = True 

125 parser.add_argument( 

126 "--no-auto-changelog-entry", 

127 action="store_false", 

128 dest="auto_changelog_entry", 

129 help=argparse.SUPPRESS, 

130 ) 

131 

132 # compat: bool = False 

133 parser.add_argument( 

134 "--compat", 

135 action="store_true", 

136 dest="compat", 

137 help="Create a compat package with appropriate version suffix", 

138 ) 

139 

140 # ignore_missing_license_files: bool = False 

141 parser.add_argument( 

142 "-I", 

143 "--ignore-missing-license-files", 

144 action="store_true", 

145 dest="ignore_missing_license_files", 

146 help=argparse.SUPPRESS, 

147 ) 

148 

149 # existence_check: bool = True 

150 parser.add_argument( 

151 "--no-existence-check", 

152 action="store_false", 

153 dest="existence_check", 

154 help="Do not check whether the package already exists in dist-git", 

155 ) 

156 

157 # offline: bool = False 

158 parser.add_argument( 

159 "-O", 

160 "--offline", 

161 action="store_true", 

162 dest="offline", 

163 help="Skip network queries to crates.io and / or src.fedoraproject.org", 

164 ) 

165 

166 # patch: bool = False 

167 parser.add_argument( 

168 "-p", 

169 "--patch", 

170 action="store_true", 

171 dest="patch", 

172 help="Do manual patching of Cargo.toml", 

173 ) 

174 

175 # patch_foreign: bool = True 

176 parser.add_argument( 

177 "--no-patch-foreign", 

178 action="store_false", 

179 dest="patch_foreign", 

180 help="Do not automatically drop foreign dependencies in Cargo.toml", 

181 ) 

182 

183 # relative_license_paths: bool = False 

184 parser.add_argument( 

185 "--relative-license-paths", 

186 action="store_true", 

187 dest="relative_license_paths", 

188 help=argparse.SUPPRESS, 

189 ) 

190 

191 # reuse_patch: bool = False 

192 parser.add_argument( 

193 "-r", 

194 "--reuse-patch", 

195 action="store_true", 

196 dest="reuse_patch", 

197 help="Attempt to re-apply existing Cargo.toml patch", 

198 ) 

199 

200 # store_crate: bool = False 

201 parser.add_argument( 

202 "-s", 

203 "--store-crate", 

204 action="store_true", 

205 dest="store_crate", 

206 help="Store crate in current directory", 

207 ) 

208 

209 # validate_only: bool = False 

210 parser.add_argument( 

211 "-v", 

212 "--validate-only", 

213 action="store_true", 

214 dest="validate_only", 

215 help="Validate rust2rpm.toml config file and exit", 

216 ) 

217 

218 return parser 

219 

220 

221@dataclass 

222class Options: 

223 """Collection of boolean or flag-like command-line arguments.""" 

224 

225 _: KW_ONLY 

226 # keep sorted alphabetically 

227 

228 auto_changelog_entry: bool = True 

229 """Include an auto-generated changelog entry in rendered spec files 

230 (default: ON).""" 

231 

232 compat: bool = False 

233 """Apply *"compat"* version suffis to the RPM `Name` and file name of the 

234 written spec file (default: OFF). 

235 """ 

236 

237 existence_check: bool = True 

238 """Check whether the package that a spec file is being generated for already 

239 exists in the distribution repositories (default: ON).""" 

240 

241 ignore_missing_license_files: bool = False 

242 """Do not treat it as a hard error if the heuristics for detecting license 

243 files in project sources don't detect any files (default: OFF).""" 

244 

245 offline: bool = False 

246 """Skip any operations that would require accessing the network and fail if 

247 any requested operation would require internet access default: OFF).""" 

248 

249 patch: bool = False 

250 """Open an editor for interactively patching `Cargo.toml` metadata. This is 

251 the only supported way of editing `Cargo.toml` since it is the only way to 

252 have metadata changes affect the generated spec file (default: OFF).""" 

253 

254 patch_foreign: bool = True 

255 """Automatically generate a patch that strips "foreign" dependencies from 

256 `Cargo.toml` (i.e. dependencies for non-Linux targets; default: ON).""" 

257 

258 relative_license_paths: bool = False 

259 """Use relative paths instead of absolute paths for `%license` files in 

260 `%files` lists in spec files. This usually causes built RPM packages to 

261 contain two copies of these files (default: OFF).""" 

262 

263 reuse_patch: bool = False 

264 """Attempt to re-apply existing `Cargo.toml` patch. Falls back to opening an 

265 editor if the `patch` flag is set (default: OFF).""" 

266 

267 rpmautospec: bool | None = None 

268 """Override automatically detected use of rpmautospec in an existing spec 

269 file (default: fall back to autodetection).""" 

270 

271 store_crate: bool = False 

272 """Copy the `.crate` file from the download cache into the output directory 

273 after downloading and processing it. This option has no effect when using 

274 local sources (default: OFF).""" 

275 

276 validate_only: bool = False 

277 """Only attempt to load and validate a `rust2rpm.toml` configuration file 

278 and exit (default: OFF).""" 

279 

280 vendor: VendorMode = VendorMode.OFF 

281 """Use vendored dependencies and / or create a tarball containing vendored 

282 dependencies (default: OFF).""" 

283 

284 

285@dataclass(frozen=True) 

286class CliArgs: 

287 """Parsed and processed command-line arguments.""" 

288 

289 pkgid: str | None 

290 """Project identifier consisting of name and version (both optional) in the 

291 format `<name>`, `<name>@<version>`, `@<version>`, or `None`.""" 

292 

293 output_directory: Path | None 

294 """Optional output directory (default: current working directory).""" 

295 

296 path: Path | None 

297 """Optional path to a local `.crate` file, local `Cargo.toml` file, or a 

298 directory containing a `Cargo.toml` file for operating on local sources.""" 

299 

300 config_file: Path | None 

301 """Optional explicit path to a `rust2rpm.toml` configuration file 

302 (default: `rust2rpm.toml` in current working directory, if it exists).""" 

303 

304 target: Target 

305 """Target to generate spec file for (default: based on current system).""" 

306 

307 options: Options 

308 """Collection of other flags and flag-like command-line arguments.""" 

309 

310 @staticmethod 

311 def parse(args: list[str] | None = None) -> "CliArgs": 

312 """Parse a list of arguments into `CliArgs`.""" 

313 parser = get_parser() 

314 parsed = parser.parse_args(args) 

315 

316 output_directory = Path(parsed.output_directory) if parsed.output_directory else None 

317 path = Path(parsed.path) if parsed.path else None 

318 config_file = Path(parsed.config_file) if parsed.config_file else None 

319 target = Target(parsed.target) if parsed.target else Target(get_default_target()) 

320 vendor = VendorMode(parsed.vendor) if parsed.vendor else VendorMode.OFF 

321 

322 options = Options( 

323 auto_changelog_entry=parsed.auto_changelog_entry, 

324 compat=parsed.compat, 

325 existence_check=parsed.existence_check, 

326 ignore_missing_license_files=parsed.ignore_missing_license_files, 

327 offline=parsed.offline, 

328 patch=parsed.patch, 

329 patch_foreign=parsed.patch_foreign, 

330 relative_license_paths=parsed.relative_license_paths, 

331 reuse_patch=parsed.reuse_patch, 

332 rpmautospec=parsed.rpmautospec, 

333 store_crate=parsed.store_crate, 

334 validate_only=parsed.validate_only, 

335 vendor=vendor, 

336 ) 

337 

338 return CliArgs( 

339 pkgid=parsed.pkgid, 

340 output_directory=output_directory, 

341 path=path, 

342 config_file=config_file, 

343 target=target, 

344 options=options, 

345 ) 

346 

347 @property 

348 def name(self) -> str | None: 

349 """The optional `<name>` part of the `pkgid` argument.""" 

350 if self.pkgid is None: 

351 return None 

352 

353 if "@" in self.pkgid: 

354 name, _version = self.pkgid.split("@") 

355 return name if name != "" else None 

356 

357 return self.pkgid 

358 

359 @property 

360 def version(self) -> str | None: 

361 """The optional `<version>` part of the `pkgid` argument.""" 

362 if self.pkgid is None: 

363 return None 

364 

365 if "@" in self.pkgid: 

366 _name, version = self.pkgid.split("@") 

367 return version if version != "" else None 

368 

369 return None