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

228 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

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 \ 

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

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

90 fi 

91 """ 

92 ) 

93 

94 ctrl.maintscript.on_configure(snippet) 

95 

96 

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

98 sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR) 

99 if not sysusers_dir: 

100 return 

101 for child in sysusers_dir.iterdir: 

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

103 continue 

104 yield child 

105 

106 

107def detect_systemd_sysusers( 

108 fs_root: VirtualPath, 

109 ctrl: BinaryCtrlAccessor, 

110 _unused: PackageProcessingContext, 

111) -> None: 

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

113 if not sysusers_confs: 

114 return 

115 

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

117 

118 snippet = textwrap.dedent( 

119 f"""\ 

120 \ 

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

122 """ 

123 ) 

124 

125 ctrl.substvars.add_dependency( 

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

127 ) 

128 ctrl.maintscript.on_configure(snippet) 

129 

130 

131def detect_icons( 

132 fs_root: VirtualPath, 

133 ctrl: BinaryCtrlAccessor, 

134 _unused: PackageProcessingContext, 

135) -> None: 

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

137 if not icons_root_dir: 

138 return 

139 icon_dirs = [] 

140 for subdir in icons_root_dir.iterdir: 

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

142 # dh_icons skips this for some reason. 

143 continue 

144 for p in subdir.all_paths(): 

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

146 icon_dirs.append(subdir.absolute) 

147 break 

148 if not icon_dirs: 

149 return 

150 

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

152 

153 postinst_snippet = textwrap.dedent( 

154 f"""\ 

155 \ 

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

157 update-icon-caches {icon_dir_list_escaped} 

158 fi 

159 """ 

160 ) 

161 

162 postrm_snippet = textwrap.dedent( 

163 f"""\ 

164 \ 

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

166 update-icon-caches {icon_dir_list_escaped} 

167 fi 

168 """ 

169 ) 

170 

171 ctrl.maintscript.on_configure(postinst_snippet) 

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

173 

174 

175def detect_gsettings_dependencies( 

176 fs_root: VirtualPath, 

177 ctrl: BinaryCtrlAccessor, 

178 _unused: PackageProcessingContext, 

179) -> None: 

180 gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR) 

181 if not gsettings_schema_dir: 

182 return 

183 

184 for path in gsettings_schema_dir.all_paths(): 

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

186 ctrl.substvars.add_dependency( 

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

188 ) 

189 break 

190 

191 

192def detect_kernel_modules( 

193 fs_root: VirtualPath, 

194 ctrl: BinaryCtrlAccessor, 

195 _unused: PackageProcessingContext, 

196) -> None: 

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

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

199 

200 if not module_root_dir: 

201 continue 

202 

203 module_version_dirs = [] 

204 

205 for module_version_dir in module_root_dir.iterdir: 

206 if not module_version_dir.is_dir: 

207 continue 

208 

209 for fs_path in module_version_dir.all_paths(): 

210 if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS): 

211 module_version_dirs.append(module_version_dir.name) 

212 break 

213 

214 for module_version in module_version_dirs: 

215 module_version_escaped = ctrl.maintscript.escape_shell_words(module_version) 

216 postinst_snippet = textwrap.dedent( 

217 f"""\ 

218 \ 

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

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

221 fi 

222 """ 

223 ) 

224 

225 postrm_snippet = textwrap.dedent( 

226 f"""\ 

227 \ 

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

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

230 fi 

231 """ 

232 ) 

233 

234 ctrl.maintscript.on_configure(postinst_snippet) 

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

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

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

238 

239 

240def detect_xfonts( 

241 fs_root: VirtualPath, 

242 ctrl: BinaryCtrlAccessor, 

243 context: PackageProcessingContext, 

244) -> None: 

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

246 if not xfonts_root_dir: 

247 return 

248 

249 cmds = [] 

250 cmds_postinst = [] 

251 cmds_postrm = [] 

252 escape_shell_words = ctrl.maintscript.escape_shell_words 

253 package_name = context.binary_package.name 

254 

255 for xfonts_dir in xfonts_root_dir.iterdir: 

256 xfonts_dirname = xfonts_dir.name 

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

258 continue 

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

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

261 cmds.append( 

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

263 ) 

264 alias_file = fs_root.lookup( 

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

266 ) 

267 if alias_file: 

268 cmds_postinst.append( 

269 escape_shell_words( 

270 "update-fonts-alias", 

271 "--include", 

272 alias_file.absolute, 

273 xfonts_dirname, 

274 ) 

275 ) 

276 cmds_postrm.append( 

277 escape_shell_words( 

278 "update-fonts-alias", 

279 "--exclude", 

280 alias_file.absolute, 

281 xfonts_dirname, 

282 ) 

283 ) 

284 

285 if not cmds: 

286 return 

287 

288 postinst_snippet = textwrap.dedent( 

289 f"""\ 

290 \ 

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

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

293 fi 

294 """ 

295 ) 

296 

297 postrm_snippet = textwrap.dedent( 

298 f"""\ 

299 \ 

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

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

302 fi 

303 """ 

304 ) 

305 

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

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

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

309 

310 

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

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

313 

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

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

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

317 root_dirs = [] 

318 if usr_lib: 

319 root_dirs.append(usr_lib) 

320 

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

322 if dbg_root: 

323 root_dirs.append(dbg_root) 

324 

325 for root_dir in root_dirs: 

326 python_dirs = ( 

327 path 

328 for path in root_dir.iterdir 

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

330 ) 

331 for python_dir in python_dirs: 331 ↛ exitline 331 didn't finish the generator expression on line 331

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

333 if not dist_packages: 

334 continue 

335 yield dist_packages 

336 

337 

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

339 return any(f.is_file and f.name.endswith(".py") for f in d.all_paths()) 

340 

341 

342def detect_pycompile_files( 

343 fs_root: VirtualPath, 

344 ctrl: BinaryCtrlAccessor, 

345 context: PackageProcessingContext, 

346) -> None: 

347 package = context.binary_package.name 

348 # TODO: Support configurable list of private dirs 

349 private_search_dirs = [ 

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

351 for d in [ 

352 "./usr/share", 

353 "./usr/share/games", 

354 "./usr/lib", 

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

356 "./usr/lib/games", 

357 ] 

358 ] 

359 private_search_dirs_with_py_files = [ 

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

361 ] 

362 public_search_dirs_has_py_files = any( 

363 p is not None and _has_py_file_in_dir(p) 

364 for p in _public_python_dist_dirs(fs_root) 

365 ) 

366 

367 if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files: 

368 return 

369 

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

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

372 # of configuration down the line. 

373 ctrl.maintscript.unconditionally_in_script( 

374 "prerm", 

375 textwrap.dedent( 

376 f"""\ 

377 \ 

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

379 py3clean -p {package} 

380 else 

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

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

383 fi 

384 """ 

385 ), 

386 ) 

387 if public_search_dirs_has_py_files: 

388 ctrl.maintscript.on_configure( 

389 textwrap.dedent( 

390 f"""\ 

391 \ 

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

393 py3compile -p {package} 

394 fi 

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

396 pypy3compile -p {package} || true 

397 fi 

398 """ 

399 ) 

400 ) 

401 for private_dir in private_search_dirs_with_py_files: 

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

403 ctrl.maintscript.on_configure( 

404 textwrap.dedent( 

405 f"""\ 

406 \ 

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

408 py3compile -p {package} {escaped_dir} 

409 fi 

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

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

412 fi 

413 """ 

414 ) 

415 ) 

416 

417 

418def translate_capabilities( 

419 fs_root: VirtualPath, 

420 ctrl: BinaryCtrlAccessor, 

421 _context: PackageProcessingContext, 

422) -> None: 

423 caps = [] 

424 maintscript = ctrl.maintscript 

425 for p in fs_root.all_paths(): 

426 if not p.is_file: 

427 continue 

428 metadata_ref = p.metadata(DebputyCapability) 

429 capability = metadata_ref.value 

430 if capability is None: 

431 continue 

432 

433 abs_path = maintscript.escape_shell_words(p.absolute) 

434 

435 cap_script = "".join( 

436 [ 

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

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

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

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

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

442 " else\n", 

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

444 # and remove the capabilities. 

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

446 " fi\n", 

447 ] 

448 ).format( 

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

450 "\\+", "+" 

451 ), 

452 DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED, 

453 ABS_PATH=abs_path, 

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

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

456 ) 

457 assert cap_script.endswith("\n") 

458 caps.append(cap_script) 

459 

460 if not caps: 

461 return 

462 

463 maintscript.on_configure( 

464 textwrap.dedent( 

465 """\ 

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

467 {SET_CAP_COMMANDS} 

468 unset _TPATH 

469 else 

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

471 fi 

472 """ 

473 ).format( 

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

475 ) 

476 ) 

477 

478 478 ↛ 479line 478 didn't jump to line 479 because the condition on line 478 was never true

479def pam_auth_update( 

480 fs_root: VirtualPath, 

481 ctrl: BinaryCtrlAccessor, 

482 _context: PackageProcessingContext, 

483) -> None: 

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

485 if not pam_configs: 

486 return 

487 maintscript = ctrl.maintscript 

488 for pam_config in pam_configs.iterdir: 

489 if not pam_config.is_file: 

490 continue 

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

492 maintscript.on_before_removal( 

493 textwrap.dedent( 

494 f"""\ 

495 \ 

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

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

498 fi 

499 """ 

500 ) 

501 ) 

502 

503 

504def auto_depends_arch_any_solink( 504 ↛ 507line 504 didn't jump to line 507 because the condition on line 504 was always true

505 fs_foot: VirtualPath, 

506 ctrl: BinaryCtrlAccessor, 

507 context: PackageProcessingContext, 

508) -> None: 

509 package = context.binary_package 

510 if package.is_arch_all: 

511 return 

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

513 if not libbasedir: 

514 return 

515 libmadir = libbasedir.get(package.deb_multiarch) 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true

516 if libmadir: 

517 libdirs = [libmadir, libbasedir] 

518 else: 

519 libdirs = [libbasedir] 

520 targets = [] 

521 for libdir in libdirs: 

522 for path in libdir.iterdir: 

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

524 continue 

525 target = path.readlink() 

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

527 if resolved is not None: 

528 continue 528 ↛ 529line 528 didn't jump to line 529 because the condition on line 528 was never true

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

530 

531 roots = list(context.accessible_package_roots()) 

532 if not roots: 532 ↛ 539line 532 didn't jump to line 539 because the condition on line 532 was always true

533 return 

534 

535 for libdir_path, target in targets: 

536 final_path = os.path.join(libdir_path, target) 

537 matches = [] 

538 for opkg, ofs_root in roots: 

539 m = ofs_root.lookup(final_path) 

540 if not m: 

541 continue 

542 matches.append(opkg) 

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

544 if matches: 

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

546 _warn( 

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

548 f" Not generating a dependency." 

549 ) 

550 else: 

551 _warn( 

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

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

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

555 ) 

556 continue 

557 pkg_dep = matches[0] 

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

559 assert pkg_dep.is_arch_all == package.is_arch_all 

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

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

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