Coverage for src/debputy/plugin/debputy/metadata_detectors.py: 96%

228 statements  

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

1import itertools 

2import os 

3import re 

4import textwrap 

5from typing import Iterable, Iterator 

6 

7from debputy.plugin.api import ( 

8 VirtualPath, 

9 BinaryCtrlAccessor, 

10 PackageProcessingContext, 

11) 

12from debputy.plugin.debputy.paths import ( 

13 INITRAMFS_HOOK_DIR, 

14 SYSTEMD_TMPFILES_DIR, 

15 GSETTINGS_SCHEMA_DIR, 

16 SYSTEMD_SYSUSERS_DIR, 

17) 

18from debputy.plugin.debputy.types import DebputyCapability 

19from debputy.util import assume_not_none, _warn 

20 

21DPKG_ROOT = '"${DPKG_ROOT}"' 

22DPKG_ROOT_UNQUOTED = "${DPKG_ROOT}" 

23 

24KERNEL_MODULE_EXTENSIONS = tuple( 

25 f"{ext}{comp_ext}" 

26 for ext, comp_ext in itertools.product( 

27 (".o", ".ko"), 

28 ("", ".gz", ".bz2", ".xz"), 

29 ) 

30) 

31 

32 

33def detect_initramfs_hooks( 

34 fs_root: VirtualPath, 

35 ctrl: BinaryCtrlAccessor, 

36 _unused: PackageProcessingContext, 

37) -> None: 

38 hook_dir = fs_root.lookup(INITRAMFS_HOOK_DIR) 

39 if not hook_dir: 

40 return 

41 for _ in hook_dir.iterdir: 

42 # Only add the trigger if the directory is non-empty. It is unlikely to matter a lot, 

43 # but we do this to match debhelper. 

44 break 

45 else: 

46 return 

47 

48 ctrl.dpkg_trigger("activate-noawait", "update-initramfs") 

49 

50 

51def _all_tmpfiles_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]: 

52 seen_tmpfiles = set() 

53 tmpfiles_dirs = [ 

54 SYSTEMD_TMPFILES_DIR, 

55 "./etc/tmpfiles.d", 

56 ] 

57 for tmpfiles_dir_path in tmpfiles_dirs: 

58 tmpfiles_dir = fs_root.lookup(tmpfiles_dir_path) 

59 if not tmpfiles_dir: 

60 continue 

61 for path in tmpfiles_dir.iterdir: 

62 if ( 

63 not path.is_file 

64 or not path.name.endswith(".conf") 

65 or path.name in seen_tmpfiles 

66 ): 

67 continue 

68 seen_tmpfiles.add(path.name) 

69 yield path 

70 

71 

72def detect_systemd_tmpfiles( 

73 fs_root: VirtualPath, 

74 ctrl: BinaryCtrlAccessor, 

75 _unused: PackageProcessingContext, 

76) -> None: 

77 tmpfiles_confs = [ 

78 x.name for x in sorted(_all_tmpfiles_conf(fs_root), key=lambda x: x.name) 

79 ] 

80 if not tmpfiles_confs: 

81 return 

82 

83 tmpfiles_escaped = ctrl.maintscript.escape_shell_words(*tmpfiles_confs) 

84 

85 snippet = textwrap.dedent( 

86 f"""\ 

87 if [ -x "$(command -v systemd-tmpfiles)" ]; then 

88 systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {tmpfiles_escaped} || true 

89 fi 

90 """ 

91 ) 

92 

93 ctrl.maintscript.on_configure(snippet) 

94 

95 

96def _all_sysusers_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]: 

97 sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR) 

98 if not sysusers_dir: 

99 return 

100 for child in sysusers_dir.iterdir: 

101 if not child.name.endswith(".conf"): 

102 continue 

103 yield child 

104 

105 

106def detect_systemd_sysusers( 

107 fs_root: VirtualPath, 

108 ctrl: BinaryCtrlAccessor, 

109 _unused: PackageProcessingContext, 

110) -> None: 

111 sysusers_confs = [p.name for p in _all_sysusers_conf(fs_root)] 

112 if not sysusers_confs: 

113 return 

114 

115 sysusers_escaped = ctrl.maintscript.escape_shell_words(*sysusers_confs) 

116 

117 snippet = textwrap.dedent( 

118 f"""\ 

119 systemd-sysusers ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {sysusers_escaped} || true 

120 """ 

121 ) 

122 

123 ctrl.substvars.add_dependency( 

124 "misc:Depends", "systemd | systemd-standalone-sysusers | systemd-sysusers" 

125 ) 

126 ctrl.maintscript.on_configure(snippet) 

127 

128 

129def detect_icons( 

130 fs_root: VirtualPath, 

131 ctrl: BinaryCtrlAccessor, 

132 _unused: PackageProcessingContext, 

133) -> None: 

134 icons_root_dir = fs_root.lookup("./usr/share/icons") 

135 if not icons_root_dir: 

136 return 

137 icon_dirs = [] 

138 for subdir in icons_root_dir.iterdir: 

139 if subdir.name in ("gnome", "hicolor"): 

140 # dh_icons skips this for some reason. 

141 continue 

142 for p in subdir.all_paths(): 

143 if p.is_file and p.name.endswith((".png", ".svg", ".xpm", ".icon")): 

144 icon_dirs.append(subdir.absolute) 

145 break 

146 if not icon_dirs: 

147 return 

148 

149 icon_dir_list_escaped = ctrl.maintscript.escape_shell_words(*icon_dirs) 

150 

151 postinst_snippet = textwrap.dedent( 

152 f"""\ 

153 if command -v update-icon-caches >/dev/null; then 

154 update-icon-caches {icon_dir_list_escaped} 

155 fi 

156 """ 

157 ) 

158 

159 postrm_snippet = textwrap.dedent( 

160 f"""\ 

161 if command -v update-icon-caches >/dev/null; then 

162 update-icon-caches {icon_dir_list_escaped} 

163 fi 

164 """ 

165 ) 

166 

167 ctrl.maintscript.on_configure(postinst_snippet) 

168 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet) 

169 

170 

171def detect_gsettings_dependencies( 

172 fs_root: VirtualPath, 

173 ctrl: BinaryCtrlAccessor, 

174 _unused: PackageProcessingContext, 

175) -> None: 

176 gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR) 

177 if not gsettings_schema_dir: 

178 return 

179 

180 for path in gsettings_schema_dir.all_paths(): 

181 if path.is_file and path.name.endswith((".xml", ".override")): 

182 ctrl.substvars.add_dependency( 

183 "misc:Depends", "dconf-gsettings-backend | gsettings-backend" 

184 ) 

185 break 

186 

187 

188def detect_kernel_modules( 

189 fs_root: VirtualPath, 

190 ctrl: BinaryCtrlAccessor, 

191 _unused: PackageProcessingContext, 

192) -> None: 

193 for prefix in [".", "./usr"]: 

194 module_root_dir = fs_root.lookup(f"{prefix}/lib/modules") 

195 

196 if not module_root_dir: 

197 continue 

198 

199 module_version_dirs = [] 

200 

201 for module_version_dir in module_root_dir.iterdir: 

202 if not module_version_dir.is_dir: 

203 continue 

204 

205 for fs_path in module_version_dir.all_paths(): 

206 if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS): 

207 module_version_dirs.append(module_version_dir.name) 

208 break 

209 

210 for module_version in module_version_dirs: 

211 module_version_escaped = ctrl.maintscript.escape_shell_words(module_version) 

212 postinst_snippet = textwrap.dedent( 

213 f"""\ 

214 if [ -e /boot/System.map-{module_version_escaped} ]; then 

215 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true 

216 fi 

217 """ 

218 ) 

219 

220 postrm_snippet = textwrap.dedent( 

221 f"""\ 

222 if [ -e /boot/System.map-{module_version_escaped} ]; then 

223 depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true 

224 fi 

225 """ 

226 ) 

227 

228 ctrl.maintscript.on_configure(postinst_snippet) 

229 # TODO: This should probably be on removal. However, this is what debhelper did and we should 

230 # do the same until we are sure (not that it matters a lot). 

231 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet) 

232 

233 

234def detect_xfonts( 

235 fs_root: VirtualPath, 

236 ctrl: BinaryCtrlAccessor, 

237 context: PackageProcessingContext, 

238) -> None: 

239 xfonts_root_dir = fs_root.lookup("./usr/share/fonts/X11/") 

240 if not xfonts_root_dir: 

241 return 

242 

243 cmds = [] 

244 cmds_postinst = [] 

245 cmds_postrm = [] 

246 escape_shell_words = ctrl.maintscript.escape_shell_words 

247 package_name = context.binary_package.name 

248 

249 for xfonts_dir in xfonts_root_dir.iterdir: 

250 xfonts_dirname = xfonts_dir.name 

251 if not xfonts_dir.is_dir or xfonts_dirname.startswith("."): 

252 continue 

253 if fs_root.lookup(f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.scale"): 

254 cmds.append(escape_shell_words("update-fonts-scale", xfonts_dirname)) 

255 cmds.append( 

256 escape_shell_words("update-fonts-dir", "--x11r7-layout", xfonts_dirname) 

257 ) 

258 alias_file = fs_root.lookup( 

259 f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.alias" 

260 ) 

261 if alias_file: 

262 cmds_postinst.append( 

263 escape_shell_words( 

264 "update-fonts-alias", 

265 "--include", 

266 alias_file.absolute, 

267 xfonts_dirname, 

268 ) 

269 ) 

270 cmds_postrm.append( 

271 escape_shell_words( 

272 "update-fonts-alias", 

273 "--exclude", 

274 alias_file.absolute, 

275 xfonts_dirname, 

276 ) 

277 ) 

278 

279 if not cmds: 

280 return 

281 

282 postinst_snippet = textwrap.dedent( 

283 f"""\ 

284 if command -v update-fonts-dir >/dev/null; then 

285 {';'.join(itertools.chain(cmds, cmds_postinst))} 

286 fi 

287 """ 

288 ) 

289 

290 postrm_snippet = textwrap.dedent( 

291 f"""\ 

292 if [ -x "`command -v update-fonts-dir`" ]; then 

293 {';'.join(itertools.chain(cmds, cmds_postrm))} 

294 fi 

295 """ 

296 ) 

297 

298 ctrl.maintscript.unconditionally_in_script("postinst", postinst_snippet) 

299 ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet) 

300 ctrl.substvars.add_dependency("misc:Depends", "xfonts-utils") 

301 

302 

303# debputy does not support python2, so we do not list python / python2. 

304_PYTHON_PUBLIC_DIST_DIR_NAMES = re.compile(r"(?:pypy|python)3(?:[.]\d+)?") 

305 

306 

307def _public_python_dist_dirs(fs_root: VirtualPath) -> Iterator[VirtualPath]: 

308 usr_lib = fs_root.lookup("./usr/lib") 

309 root_dirs = [] 

310 if usr_lib: 

311 root_dirs.append(usr_lib) 

312 

313 dbg_root = fs_root.lookup("./usr/lib/debug/usr/lib") 

314 if dbg_root: 314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true

315 root_dirs.append(dbg_root) 

316 

317 for root_dir in root_dirs: 

318 python_dirs = ( 

319 path 

320 for path in root_dir.iterdir 

321 if path.is_dir and _PYTHON_PUBLIC_DIST_DIR_NAMES.match(path.name) 

322 ) 

323 for python_dir in python_dirs: 

324 dist_packages = python_dir.get("dist-packages") 

325 if not dist_packages: 

326 continue 

327 yield dist_packages 

328 

329 

330def _has_py_file_in_dir(d: VirtualPath) -> bool: 

331 return any(f.is_file and f.name.endswith(".py") for f in d.all_paths()) 331 ↛ exitline 331 didn't finish the generator expression on line 331

332 

333 

334def detect_pycompile_files( 

335 fs_root: VirtualPath, 

336 ctrl: BinaryCtrlAccessor, 

337 context: PackageProcessingContext, 

338) -> None: 

339 package = context.binary_package.name 

340 # TODO: Support configurable list of private dirs 

341 private_search_dirs = [ 

342 fs_root.lookup(os.path.join(d, package)) 

343 for d in [ 

344 "./usr/share", 

345 "./usr/share/games", 

346 "./usr/lib", 

347 f"./usr/lib/{context.binary_package.deb_multiarch}", 

348 "./usr/lib/games", 

349 ] 

350 ] 

351 private_search_dirs_with_py_files = [ 

352 p for p in private_search_dirs if p is not None and _has_py_file_in_dir(p) 

353 ] 

354 public_search_dirs_has_py_files = any( 

355 p is not None and _has_py_file_in_dir(p) 

356 for p in _public_python_dist_dirs(fs_root) 

357 ) 

358 

359 if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files: 

360 return 

361 

362 # The dh_python3 helper also supports -V and -X. We do not use them. They can be 

363 # replaced by bcep support instead, which is how we will be supporting this kind 

364 # of configuration down the line. 

365 ctrl.maintscript.unconditionally_in_script( 

366 "prerm", 

367 textwrap.dedent( 

368 f"""\ 

369 if command -v py3clean >/dev/null 2>&1; then 

370 py3clean -p {package} 

371 else 

372 dpkg -L {package} | sed -En -e '/^(.*)\\/(.+)\\.py$/s,,rm "\\1/__pycache__/\\2".*,e' 

373 find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir 

374 fi 

375 """ 

376 ), 

377 ) 

378 if public_search_dirs_has_py_files: 

379 ctrl.maintscript.on_configure( 

380 textwrap.dedent( 

381 f"""\ 

382 if command -v py3compile >/dev/null 2>&1; then 

383 py3compile -p {package} 

384 fi 

385 if command -v pypy3compile >/dev/null 2>&1; then 

386 pypy3compile -p {package} || true 

387 fi 

388 """ 

389 ) 

390 ) 

391 for private_dir in private_search_dirs_with_py_files: 

392 escaped_dir = ctrl.maintscript.escape_shell_words(private_dir.absolute) 

393 ctrl.maintscript.on_configure( 

394 textwrap.dedent( 

395 f"""\ 

396 if command -v py3compile >/dev/null 2>&1; then 

397 py3compile -p {package} {escaped_dir} 

398 fi 

399 if command -v pypy3compile >/dev/null 2>&1; then 

400 pypy3compile -p {package} {escaped_dir} || true 

401 fi 

402 """ 

403 ) 

404 ) 

405 

406 

407def translate_capabilities( 

408 fs_root: VirtualPath, 

409 ctrl: BinaryCtrlAccessor, 

410 _context: PackageProcessingContext, 

411) -> None: 

412 caps = [] 

413 maintscript = ctrl.maintscript 

414 for p in fs_root.all_paths(): 

415 if not p.is_file: 

416 continue 

417 metadata_ref = p.metadata(DebputyCapability) 

418 capability = metadata_ref.value 

419 if capability is None: 

420 continue 

421 

422 abs_path = maintscript.escape_shell_words(p.absolute) 

423 

424 cap_script = "".join( 

425 [ 

426 " # Triggered by: {DEFINITION_SOURCE}\n" 

427 " _TPATH=$(dpkg-divert --truename {ABS_PATH})\n", 

428 ' if setcap {CAP} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"; then\n', 

429 ' chmod {MODE} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"\n', 

430 ' echo "Successfully applied capabilities {CAP} on ${{_TPATH}}"\n', 

431 " else\n", 

432 # We do not reset the mode here; generally a re-install or upgrade would re-store both mode, 

433 # and remove the capabilities. 

434 ' echo "The setcap failed to processes {CAP} on ${{_TPATH}}; falling back to no capability support" >&2\n', 

435 " fi\n", 

436 ] 

437 ).format( 

438 CAP=maintscript.escape_shell_words(capability.capabilities).replace( 

439 "\\+", "+" 

440 ), 

441 DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED, 

442 ABS_PATH=abs_path, 

443 MODE=maintscript.escape_shell_words(str(capability.capability_mode)), 

444 DEFINITION_SOURCE=capability.definition_source.replace("\n", "\\n"), 

445 ) 

446 assert cap_script.endswith("\n") 

447 caps.append(cap_script) 

448 

449 if not caps: 

450 return 

451 

452 maintscript.on_configure( 

453 textwrap.dedent( 

454 """\ 

455 if command -v setcap > /dev/null; then 

456 {SET_CAP_COMMANDS} 

457 unset _TPATH 

458 else 

459 echo "The setcap utility is not installed available; falling back to no capability support" >&2 

460 fi 

461 """ 

462 ).format( 

463 SET_CAP_COMMANDS="".join(caps).rstrip("\n"), 

464 ) 

465 ) 

466 

467 

468def pam_auth_update( 

469 fs_root: VirtualPath, 

470 ctrl: BinaryCtrlAccessor, 

471 _context: PackageProcessingContext, 

472) -> None: 

473 pam_configs = fs_root.lookup("/usr/share/pam-configs") 

474 if not pam_configs: 

475 return 

476 maintscript = ctrl.maintscript 

477 for pam_config in pam_configs.iterdir: 

478 if not pam_config.is_file: 478 ↛ 479line 478 didn't jump to line 479, because the condition on line 478 was never true

479 continue 

480 maintscript.on_configure("pam-auth-update --package\n") 

481 maintscript.on_before_removal( 

482 textwrap.dedent( 

483 f"""\ 

484 if [ "${{DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT:-1}}" = 1 ]; then 

485 pam-auth-update --package --remove {maintscript.escape_shell_words(pam_config.name)} 

486 fi 

487 """ 

488 ) 

489 ) 

490 

491 

492def auto_depends_arch_any_solink( 

493 fs_foot: VirtualPath, 

494 ctrl: BinaryCtrlAccessor, 

495 context: PackageProcessingContext, 

496) -> None: 

497 package = context.binary_package 

498 if package.is_arch_all: 

499 return 

500 libbasedir = fs_foot.lookup("usr/lib") 

501 if not libbasedir: 

502 return 

503 libmadir = libbasedir.get(package.deb_multiarch) 

504 if libmadir: 504 ↛ 507line 504 didn't jump to line 507, because the condition on line 504 was never false

505 libdirs = [libmadir, libbasedir] 

506 else: 

507 libdirs = [libbasedir] 

508 targets = [] 

509 for libdir in libdirs: 

510 for path in libdir.iterdir: 

511 if not path.is_symlink or not path.name.endswith(".so"): 

512 continue 

513 target = path.readlink() 

514 resolved = assume_not_none(path.parent_dir).lookup(target) 

515 if resolved is not None: 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true

516 continue 

517 targets.append((libdir.path, target)) 

518 

519 roots = list(context.accessible_package_roots()) 

520 if not roots: 

521 return 

522 

523 for libdir, target in targets: 

524 final_path = os.path.join(libdir, target) 

525 matches = [] 

526 for opkg, ofs_root in roots: 

527 m = ofs_root.lookup(final_path) 

528 if not m: 528 ↛ 529line 528 didn't jump to line 529, because the condition on line 528 was never true

529 continue 

530 matches.append(opkg) 

531 if not matches or len(matches) > 1: 

532 if matches: 532 ↛ 539line 532 didn't jump to line 539, because the condition on line 532 was never false

533 all_matches = ", ".join(p.name for p in matches) 

534 _warn( 

535 f"auto-depends-solink: The {final_path} was found in multiple packages ({all_matches}):" 

536 f" Not generating a dependency." 

537 ) 

538 else: 

539 _warn( 

540 f"auto-depends-solink: The {final_path} was NOT found in any accessible package:" 

541 " Not generating a dependency. This detection only works when both packages are arch:any" 

542 " and they have the same build-profiles." 

543 ) 

544 continue 

545 pkg_dep = matches[0] 

546 # The debputy API should not allow this constraint to fail 

547 assert pkg_dep.is_arch_all == package.is_arch_all 

548 # If both packages are arch:all or both are arch:any, we can generate a tight dependency 

549 relation = f"{pkg_dep.name} (= ${{binary:Version}})" 

550 ctrl.substvars.add_dependency("misc:Depends", relation)