Coverage for src/debputy/dh_migration/migration.py: 7%

192 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import json 

2import os 

3import re 

4import subprocess 

5from itertools import chain 

6from typing import Optional, List, Callable, Set 

7 

8from debian.deb822 import Deb822 

9 

10from debputy.debhelper_emulation import CannotEmulateExecutableDHConfigFile 

11from debputy.dh_migration.migrators import MIGRATORS 

12from debputy.dh_migration.migrators_impl import ( 

13 read_dh_addon_sequences, 

14 MIGRATION_TARGET_DH_DEBPUTY, 

15 MIGRATION_TARGET_DH_DEBPUTY_RRR, 

16) 

17from debputy.dh_migration.models import ( 

18 FeatureMigration, 

19 AcceptableMigrationIssues, 

20 UnsupportedFeature, 

21 ConflictingChange, 

22) 

23from debputy.highlevel_manifest import HighLevelManifest 

24from debputy.manifest_parser.exceptions import ManifestParseException 

25from debputy.plugin.api import VirtualPath 

26from debputy.util import _error, _warn, _info, escape_shell, assume_not_none 

27 

28 

29def _print_migration_summary( 

30 migrations: List[FeatureMigration], 

31 compat: int, 

32 min_compat_level: int, 

33 required_plugins: Set[str], 

34 requested_plugins: Optional[Set[str]], 

35) -> None: 

36 warning_count = 0 

37 

38 for migration in migrations: 

39 if not migration.anything_to_do: 

40 continue 

41 underline = "-" * len(migration.tagline) 

42 if migration.warnings: 

43 _warn(f"Summary for migration: {migration.tagline}") 

44 _warn(f"-----------------------{underline}") 

45 _warn(" /!\\ ATTENTION /!\\") 

46 warning_count += len(migration.warnings) 

47 for warning in migration.warnings: 

48 _warn(f" * {warning}") 

49 

50 if compat < min_compat_level: 

51 if warning_count: 

52 _warn("") 

53 _warn("Supported debhelper compat check") 

54 _warn("--------------------------------") 

55 warning_count += 1 

56 _warn( 

57 f"The migration tool assumes debhelper compat {min_compat_level}+ semantics, but this package" 

58 f" is using compat {compat}. Consider upgrading the package to compat {min_compat_level}" 

59 " first." 

60 ) 

61 

62 if required_plugins: 

63 if requested_plugins is None: 

64 warning_count += 1 

65 needed_plugins = ", ".join(f"debputy-plugin-{n}" for n in required_plugins) 

66 if warning_count: 

67 _warn("") 

68 _warn("Missing debputy plugin check") 

69 _warn("----------------------------") 

70 _warn( 

71 f"The migration tool could not read d/control and therefore cannot tell if all the required" 

72 f" plugins have been requested. Please ensure that the package Build-Depends on: {needed_plugins}" 

73 ) 

74 else: 

75 missing_plugins = required_plugins - requested_plugins 

76 if missing_plugins: 

77 warning_count += 1 

78 needed_plugins = ", ".join( 

79 f"debputy-plugin-{n}" for n in missing_plugins 

80 ) 

81 if warning_count: 

82 _warn("") 

83 _warn("Missing debputy plugin check") 

84 _warn("----------------------------") 

85 _warn( 

86 f"The migration tool asserted that the following `debputy` plugins would be required, which" 

87 f" are not explicitly requested. Please add the following to Build-Depends: {needed_plugins}" 

88 ) 

89 

90 if warning_count: 

91 _warn("") 

92 _warn( 

93 f"/!\\ Total number of warnings or manual migrations required: {warning_count}" 

94 ) 

95 

96 

97def _dh_compat_level() -> Optional[int]: 

98 try: 

99 res = subprocess.check_output( 

100 ["dh_assistant", "active-compat-level"], stderr=subprocess.DEVNULL 

101 ) 

102 except subprocess.CalledProcessError: 

103 compat = None 

104 else: 

105 try: 

106 compat = json.loads(res)["declared-compat-level"] 

107 except RuntimeError: 

108 compat = None 

109 else: 

110 if not isinstance(compat, int): 

111 compat = None 

112 return compat 

113 

114 

115def _requested_debputy_plugins(debian_dir: VirtualPath) -> Optional[Set[str]]: 

116 ctrl_file = debian_dir.get("control") 

117 if not ctrl_file: 

118 return None 

119 

120 dep_regex = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII) 

121 plugins = set() 

122 

123 with ctrl_file.open() as fd: 

124 ctrl = list(Deb822.iter_paragraphs(fd)) 

125 source_paragraph = ctrl[0] if ctrl else {} 

126 

127 for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"): 

128 field = source_paragraph.get(f) 

129 if not field: 

130 continue 

131 

132 for dep_clause in (d.strip() for d in field.split(",")): 

133 match = dep_regex.match(dep_clause.strip()) 

134 if not match: 

135 continue 

136 dep = match.group(1) 

137 if not dep.startswith("debputy-plugin-"): 

138 continue 

139 plugins.add(dep[15:]) 

140 return plugins 

141 

142 

143def _check_migration_target( 

144 debian_dir: VirtualPath, 

145 migration_target: Optional[str], 

146) -> str: 

147 r = read_dh_addon_sequences(debian_dir) 

148 if r is None and migration_target is None: 

149 _error("debian/control is missing and no migration target was provided") 

150 bd_sequences, dr_sequences = r 

151 all_sequences = bd_sequences | dr_sequences 

152 

153 has_zz_debputy = "zz-debputy" in all_sequences or "debputy" in all_sequences 

154 has_zz_debputy_rrr = "zz-debputy-rrr" in all_sequences 

155 has_any_existing = has_zz_debputy or has_zz_debputy_rrr 

156 

157 if migration_target == "dh-sequence-zz-debputy-rrr" and has_zz_debputy: 

158 _error("Cannot migrate from (zz-)debputy to zz-debputy-rrr") 

159 

160 if has_zz_debputy_rrr and not has_zz_debputy: 

161 resolved_migration_target = MIGRATION_TARGET_DH_DEBPUTY_RRR 

162 else: 

163 resolved_migration_target = MIGRATION_TARGET_DH_DEBPUTY 

164 

165 if migration_target is not None: 

166 resolved_migration_target = migration_target 

167 

168 if has_any_existing: 

169 _info( 

170 f'Using "{resolved_migration_target}" as migration target based on the packaging' 

171 ) 

172 else: 

173 _info( 

174 f'Using "{resolved_migration_target}" as default migration target. Use --migration-target to choose!' 

175 ) 

176 

177 return resolved_migration_target 

178 

179 

180def migrate_from_dh( 

181 manifest: HighLevelManifest, 

182 acceptable_migration_issues: AcceptableMigrationIssues, 

183 permit_destructive_changes: Optional[bool], 

184 migration_target: Optional[str], 

185 manifest_parser_factory: Callable[[str], HighLevelManifest], 

186) -> None: 

187 migrations = [] 

188 compat = _dh_compat_level() 

189 if compat is None: 

190 _error( 

191 'Cannot detect declared compat level (try running "dh_assistant active-compat-level")' 

192 ) 

193 

194 debian_dir = manifest.debian_dir 

195 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

196 

197 resolved_migration_target = _check_migration_target(debian_dir, migration_target) 

198 

199 try: 

200 for migrator in MIGRATORS[resolved_migration_target]: 

201 feature_migration = FeatureMigration(migrator.__name__) 

202 migrator( 

203 debian_dir, 

204 manifest, 

205 acceptable_migration_issues, 

206 feature_migration, 

207 resolved_migration_target, 

208 ) 

209 migrations.append(feature_migration) 

210 except CannotEmulateExecutableDHConfigFile as e: 

211 _error( 

212 f"Unable to process the executable dh config file {e.config_file().fs_path}: {e.message()}" 

213 ) 

214 except UnsupportedFeature as e: 

215 msg = ( 

216 f"Unable to migrate automatically due to missing features in debputy. The feature is:" 

217 f"\n\n * {e.message}" 

218 ) 

219 keys = e.issue_keys 

220 if keys: 

221 primary_key = keys[0] 

222 alt_keys = "" 

223 if len(keys) > 1: 

224 alt_keys = ( 

225 f' Alternatively you can also use one of: {", ".join(keys[1:])}. Please note that some' 

226 " of these may cover more cases." 

227 ) 

228 msg += ( 

229 f"\n\nUse --acceptable-migration-issues={primary_key} to convert this into a warning and try again." 

230 " However, you should only do that if you believe you can replace the functionality manually" 

231 f" or the usage is obsolete / can be removed. {alt_keys}" 

232 ) 

233 _error(msg) 

234 except ConflictingChange as e: 

235 _error( 

236 "The migration tool detected a conflict data being migrated and data already migrated / in the existing" 

237 "manifest." 

238 f"\n\n * {e.message}" 

239 "\n\nPlease review the situation and resolve the conflict manually." 

240 ) 

241 

242 # We start on compat 12 for arch:any due to the new dh_makeshlibs and dh_installinit default 

243 min_compat = 12 

244 min_compat = max( 

245 (m.assumed_compat for m in migrations if m.assumed_compat is not None), 

246 default=min_compat, 

247 ) 

248 

249 if compat < min_compat and "min-compat-level" not in acceptable_migration_issues: 

250 # The migration summary special-cases the compat mismatch and warns for us. 

251 _error( 

252 f"The migration tool assumes debhelper compat {min_compat} or later but the package is only on" 

253 f" compat {compat}. This may lead to incorrect result." 

254 f"\n\nUse --acceptable-migration-issues=min-compat-level to convert this into a warning and" 

255 f" try again, if you want to continue regardless." 

256 ) 

257 

258 requested_plugins = _requested_debputy_plugins(debian_dir) 

259 required_plugins: Set[str] = set() 

260 required_plugins.update( 

261 chain.from_iterable( 

262 m.required_plugins for m in migrations if m.required_plugins 

263 ) 

264 ) 

265 

266 _print_migration_summary( 

267 migrations, compat, min_compat, required_plugins, requested_plugins 

268 ) 

269 migration_count = sum((m.performed_changes for m in migrations), 0) 

270 

271 if not migration_count: 

272 _info( 

273 "debputy was not able to find any (supported) migrations that it could perform for you." 

274 ) 

275 return 

276 

277 if any(m.successful_manifest_changes for m in migrations): 

278 new_manifest_path = manifest.manifest_path + ".new" 

279 

280 with open(new_manifest_path, "w") as fd: 

281 mutable_manifest.write_to(fd) 

282 

283 try: 

284 _info("Verifying the generating manifest") 

285 manifest_parser_factory(new_manifest_path) 

286 except ManifestParseException as e: 

287 raise AssertionError( 

288 "Could not parse the manifest generated from the migrator" 

289 ) from e 

290 

291 if permit_destructive_changes: 

292 if os.path.isfile(manifest.manifest_path): 

293 os.rename(manifest.manifest_path, manifest.manifest_path + ".orig") 

294 os.rename(new_manifest_path, manifest.manifest_path) 

295 _info(f"Updated manifest {manifest.manifest_path}") 

296 else: 

297 _info( 

298 f'Created draft manifest "{new_manifest_path}" (rename to "{manifest.manifest_path}"' 

299 " to activate it)" 

300 ) 

301 else: 

302 _info("No manifest changes detected; skipping update of manifest.") 

303 

304 removals: int = sum((len(m.remove_paths_on_success) for m in migrations), 0) 

305 renames: int = sum((len(m.rename_paths_on_success) for m in migrations), 0) 

306 

307 if renames: 

308 if permit_destructive_changes: 

309 _info("Paths being renamed:") 

310 else: 

311 _info("Migration *would* rename the following paths:") 

312 for previous_path, new_path in ( 

313 p for m in migrations for p in m.rename_paths_on_success 

314 ): 

315 _info(f" mv {escape_shell(previous_path, new_path)}") 

316 

317 if removals: 

318 if permit_destructive_changes: 

319 _info("Removals:") 

320 else: 

321 _info("Migration *would* remove the following files:") 

322 for path in (p for m in migrations for p in m.remove_paths_on_success): 

323 _info(f" rm -f {escape_shell(path)}") 

324 

325 if permit_destructive_changes is None: 

326 print() 

327 _info( 

328 "If you would like to perform the migration, please re-run with --apply-changes." 

329 ) 

330 elif permit_destructive_changes: 

331 for previous_path, new_path in ( 

332 p for m in migrations for p in m.rename_paths_on_success 

333 ): 

334 os.rename(previous_path, new_path) 

335 for path in (p for m in migrations for p in m.remove_paths_on_success): 

336 os.unlink(path) 

337 

338 print() 

339 _info("Migrations performed successfully") 

340 print() 

341 _info( 

342 "Remember to validate the resulting binary packages after rebuilding with debputy" 

343 ) 

344 else: 

345 print() 

346 _info("No migrations performed as requested")