summaryrefslogtreecommitdiffstats
path: root/src/debputy/plugin/debputy/metadata_detectors.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/plugin/debputy/metadata_detectors.py')
-rw-r--r--src/debputy/plugin/debputy/metadata_detectors.py550
1 files changed, 550 insertions, 0 deletions
diff --git a/src/debputy/plugin/debputy/metadata_detectors.py b/src/debputy/plugin/debputy/metadata_detectors.py
new file mode 100644
index 0000000..4338087
--- /dev/null
+++ b/src/debputy/plugin/debputy/metadata_detectors.py
@@ -0,0 +1,550 @@
+import itertools
+import os
+import re
+import textwrap
+from typing import Iterable, Iterator
+
+from debputy.plugin.api import (
+ VirtualPath,
+ BinaryCtrlAccessor,
+ PackageProcessingContext,
+)
+from debputy.plugin.debputy.paths import (
+ INITRAMFS_HOOK_DIR,
+ SYSTEMD_TMPFILES_DIR,
+ GSETTINGS_SCHEMA_DIR,
+ SYSTEMD_SYSUSERS_DIR,
+)
+from debputy.plugin.debputy.types import DebputyCapability
+from debputy.util import assume_not_none, _warn
+
+DPKG_ROOT = '"${DPKG_ROOT}"'
+DPKG_ROOT_UNQUOTED = "${DPKG_ROOT}"
+
+KERNEL_MODULE_EXTENSIONS = tuple(
+ f"{ext}{comp_ext}"
+ for ext, comp_ext in itertools.product(
+ (".o", ".ko"),
+ ("", ".gz", ".bz2", ".xz"),
+ )
+)
+
+
+def detect_initramfs_hooks(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _unused: PackageProcessingContext,
+) -> None:
+ hook_dir = fs_root.lookup(INITRAMFS_HOOK_DIR)
+ if not hook_dir:
+ return
+ for _ in hook_dir.iterdir:
+ # Only add the trigger if the directory is non-empty. It is unlikely to matter a lot,
+ # but we do this to match debhelper.
+ break
+ else:
+ return
+
+ ctrl.dpkg_trigger("activate-noawait", "update-initramfs")
+
+
+def _all_tmpfiles_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
+ seen_tmpfiles = set()
+ tmpfiles_dirs = [
+ SYSTEMD_TMPFILES_DIR,
+ "./etc/tmpfiles.d",
+ ]
+ for tmpfiles_dir_path in tmpfiles_dirs:
+ tmpfiles_dir = fs_root.lookup(tmpfiles_dir_path)
+ if not tmpfiles_dir:
+ continue
+ for path in tmpfiles_dir.iterdir:
+ if (
+ not path.is_file
+ or not path.name.endswith(".conf")
+ or path.name in seen_tmpfiles
+ ):
+ continue
+ seen_tmpfiles.add(path.name)
+ yield path
+
+
+def detect_systemd_tmpfiles(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _unused: PackageProcessingContext,
+) -> None:
+ tmpfiles_confs = [
+ x.name for x in sorted(_all_tmpfiles_conf(fs_root), key=lambda x: x.name)
+ ]
+ if not tmpfiles_confs:
+ return
+
+ tmpfiles_escaped = ctrl.maintscript.escape_shell_words(*tmpfiles_confs)
+
+ snippet = textwrap.dedent(
+ f"""\
+ if [ -x "$(command -v systemd-tmpfiles)" ]; then
+ systemd-tmpfiles ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {tmpfiles_escaped} || true
+ fi
+ """
+ )
+
+ ctrl.maintscript.on_configure(snippet)
+
+
+def _all_sysusers_conf(fs_root: VirtualPath) -> Iterable[VirtualPath]:
+ sysusers_dir = fs_root.lookup(SYSTEMD_SYSUSERS_DIR)
+ if not sysusers_dir:
+ return
+ for child in sysusers_dir.iterdir:
+ if not child.name.endswith(".conf"):
+ continue
+ yield child
+
+
+def detect_systemd_sysusers(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _unused: PackageProcessingContext,
+) -> None:
+ sysusers_confs = [p.name for p in _all_sysusers_conf(fs_root)]
+ if not sysusers_confs:
+ return
+
+ sysusers_escaped = ctrl.maintscript.escape_shell_words(*sysusers_confs)
+
+ snippet = textwrap.dedent(
+ f"""\
+ systemd-sysusers ${{DPKG_ROOT:+--root="$DPKG_ROOT"}} --create {sysusers_escaped} || true
+ """
+ )
+
+ ctrl.substvars.add_dependency(
+ "misc:Depends", "systemd | systemd-standalone-sysusers | systemd-sysusers"
+ )
+ ctrl.maintscript.on_configure(snippet)
+
+
+def detect_icons(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _unused: PackageProcessingContext,
+) -> None:
+ icons_root_dir = fs_root.lookup("./usr/share/icons")
+ if not icons_root_dir:
+ return
+ icon_dirs = []
+ for subdir in icons_root_dir.iterdir:
+ if subdir.name in ("gnome", "hicolor"):
+ # dh_icons skips this for some reason.
+ continue
+ for p in subdir.all_paths():
+ if p.is_file and p.name.endswith((".png", ".svg", ".xpm", ".icon")):
+ icon_dirs.append(subdir.absolute)
+ break
+ if not icon_dirs:
+ return
+
+ icon_dir_list_escaped = ctrl.maintscript.escape_shell_words(*icon_dirs)
+
+ postinst_snippet = textwrap.dedent(
+ f"""\
+ if command -v update-icon-caches >/dev/null; then
+ update-icon-caches {icon_dir_list_escaped}
+ fi
+ """
+ )
+
+ postrm_snippet = textwrap.dedent(
+ f"""\
+ if command -v update-icon-caches >/dev/null; then
+ update-icon-caches {icon_dir_list_escaped}
+ fi
+ """
+ )
+
+ ctrl.maintscript.on_configure(postinst_snippet)
+ ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
+
+
+def detect_gsettings_dependencies(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _unused: PackageProcessingContext,
+) -> None:
+ gsettings_schema_dir = fs_root.lookup(GSETTINGS_SCHEMA_DIR)
+ if not gsettings_schema_dir:
+ return
+
+ for path in gsettings_schema_dir.all_paths():
+ if path.is_file and path.name.endswith((".xml", ".override")):
+ ctrl.substvars.add_dependency(
+ "misc:Depends", "dconf-gsettings-backend | gsettings-backend"
+ )
+ break
+
+
+def detect_kernel_modules(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _unused: PackageProcessingContext,
+) -> None:
+ for prefix in [".", "./usr"]:
+ module_root_dir = fs_root.lookup(f"{prefix}/lib/modules")
+
+ if not module_root_dir:
+ continue
+
+ module_version_dirs = []
+
+ for module_version_dir in module_root_dir.iterdir:
+ if not module_version_dir.is_dir:
+ continue
+
+ for fs_path in module_version_dir.all_paths():
+ if fs_path.name.endswith(KERNEL_MODULE_EXTENSIONS):
+ module_version_dirs.append(module_version_dir.name)
+ break
+
+ for module_version in module_version_dirs:
+ module_version_escaped = ctrl.maintscript.escape_shell_words(module_version)
+ postinst_snippet = textwrap.dedent(
+ f"""\
+ if [ -e /boot/System.map-{module_version_escaped} ]; then
+ depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
+ fi
+ """
+ )
+
+ postrm_snippet = textwrap.dedent(
+ f"""\
+ if [ -e /boot/System.map-{module_version_escaped} ]; then
+ depmod -a -F /boot/System.map-{module_version_escaped} {module_version_escaped} || true
+ fi
+ """
+ )
+
+ ctrl.maintscript.on_configure(postinst_snippet)
+ # TODO: This should probably be on removal. However, this is what debhelper did and we should
+ # do the same until we are sure (not that it matters a lot).
+ ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
+
+
+def detect_xfonts(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ context: PackageProcessingContext,
+) -> None:
+ xfonts_root_dir = fs_root.lookup("./usr/share/fonts/X11/")
+ if not xfonts_root_dir:
+ return
+
+ cmds = []
+ cmds_postinst = []
+ cmds_postrm = []
+ escape_shell_words = ctrl.maintscript.escape_shell_words
+ package_name = context.binary_package.name
+
+ for xfonts_dir in xfonts_root_dir.iterdir:
+ xfonts_dirname = xfonts_dir.name
+ if not xfonts_dir.is_dir or xfonts_dirname.startswith("."):
+ continue
+ if fs_root.lookup(f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.scale"):
+ cmds.append(escape_shell_words("update-fonts-scale", xfonts_dirname))
+ cmds.append(
+ escape_shell_words("update-fonts-dir", "--x11r7-layout", xfonts_dirname)
+ )
+ alias_file = fs_root.lookup(
+ f"./etc/X11/xfonts/{xfonts_dirname}/{package_name}.alias"
+ )
+ if alias_file:
+ cmds_postinst.append(
+ escape_shell_words(
+ "update-fonts-alias",
+ "--include",
+ alias_file.absolute,
+ xfonts_dirname,
+ )
+ )
+ cmds_postrm.append(
+ escape_shell_words(
+ "update-fonts-alias",
+ "--exclude",
+ alias_file.absolute,
+ xfonts_dirname,
+ )
+ )
+
+ if not cmds:
+ return
+
+ postinst_snippet = textwrap.dedent(
+ f"""\
+ if command -v update-fonts-dir >/dev/null; then
+ {';'.join(itertools.chain(cmds, cmds_postinst))}
+ fi
+ """
+ )
+
+ postrm_snippet = textwrap.dedent(
+ f"""\
+ if [ -x "`command -v update-fonts-dir`" ]; then
+ {';'.join(itertools.chain(cmds, cmds_postrm))}
+ fi
+ """
+ )
+
+ ctrl.maintscript.unconditionally_in_script("postinst", postinst_snippet)
+ ctrl.maintscript.unconditionally_in_script("postrm", postrm_snippet)
+ ctrl.substvars.add_dependency("misc:Depends", "xfonts-utils")
+
+
+# debputy does not support python2, so we do not list python / python2.
+_PYTHON_PUBLIC_DIST_DIR_NAMES = re.compile(r"(?:pypy|python)3(?:[.]\d+)?")
+
+
+def _public_python_dist_dirs(fs_root: VirtualPath) -> Iterator[VirtualPath]:
+ usr_lib = fs_root.lookup("./usr/lib")
+ root_dirs = []
+ if usr_lib:
+ root_dirs.append(usr_lib)
+
+ dbg_root = fs_root.lookup("./usr/lib/debug/usr/lib")
+ if dbg_root:
+ root_dirs.append(dbg_root)
+
+ for root_dir in root_dirs:
+ python_dirs = (
+ path
+ for path in root_dir.iterdir
+ if path.is_dir and _PYTHON_PUBLIC_DIST_DIR_NAMES.match(path.name)
+ )
+ for python_dir in python_dirs:
+ dist_packages = python_dir.get("dist-packages")
+ if not dist_packages:
+ continue
+ yield dist_packages
+
+
+def _has_py_file_in_dir(d: VirtualPath) -> bool:
+ return any(f.is_file and f.name.endswith(".py") for f in d.all_paths())
+
+
+def detect_pycompile_files(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ context: PackageProcessingContext,
+) -> None:
+ package = context.binary_package.name
+ # TODO: Support configurable list of private dirs
+ private_search_dirs = [
+ fs_root.lookup(os.path.join(d, package))
+ for d in [
+ "./usr/share",
+ "./usr/share/games",
+ "./usr/lib",
+ f"./usr/lib/{context.binary_package.deb_multiarch}",
+ "./usr/lib/games",
+ ]
+ ]
+ private_search_dirs_with_py_files = [
+ p for p in private_search_dirs if p is not None and _has_py_file_in_dir(p)
+ ]
+ public_search_dirs_has_py_files = any(
+ p is not None and _has_py_file_in_dir(p)
+ for p in _public_python_dist_dirs(fs_root)
+ )
+
+ if not public_search_dirs_has_py_files and not private_search_dirs_with_py_files:
+ return
+
+ # The dh_python3 helper also supports -V and -X. We do not use them. They can be
+ # replaced by bcep support instead, which is how we will be supporting this kind
+ # of configuration down the line.
+ ctrl.maintscript.unconditionally_in_script(
+ "prerm",
+ textwrap.dedent(
+ f"""\
+ if command -v py3clean >/dev/null 2>&1; then
+ py3clean -p {package}
+ else
+ dpkg -L {package} | sed -En -e '/^(.*)\\/(.+)\\.py$/s,,rm "\\1/__pycache__/\\2".*,e'
+ find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
+ fi
+ """
+ ),
+ )
+ if public_search_dirs_has_py_files:
+ ctrl.maintscript.on_configure(
+ textwrap.dedent(
+ f"""\
+ if command -v py3compile >/dev/null 2>&1; then
+ py3compile -p {package}
+ fi
+ if command -v pypy3compile >/dev/null 2>&1; then
+ pypy3compile -p {package} || true
+ fi
+ """
+ )
+ )
+ for private_dir in private_search_dirs_with_py_files:
+ escaped_dir = ctrl.maintscript.escape_shell_words(private_dir.absolute)
+ ctrl.maintscript.on_configure(
+ textwrap.dedent(
+ f"""\
+ if command -v py3compile >/dev/null 2>&1; then
+ py3compile -p {package} {escaped_dir}
+ fi
+ if command -v pypy3compile >/dev/null 2>&1; then
+ pypy3compile -p {package} {escaped_dir} || true
+ fi
+ """
+ )
+ )
+
+
+def translate_capabilities(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _context: PackageProcessingContext,
+) -> None:
+ caps = []
+ maintscript = ctrl.maintscript
+ for p in fs_root.all_paths():
+ if not p.is_file:
+ continue
+ metadata_ref = p.metadata(DebputyCapability)
+ capability = metadata_ref.value
+ if capability is None:
+ continue
+
+ abs_path = maintscript.escape_shell_words(p.absolute)
+
+ cap_script = "".join(
+ [
+ " # Triggered by: {DEFINITION_SOURCE}\n"
+ " _TPATH=$(dpkg-divert --truename {ABS_PATH})\n",
+ ' if setcap {CAP} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"; then\n',
+ ' chmod {MODE} "{DPKG_ROOT_UNQUOTED}${{_TPATH}}"\n',
+ ' echo "Successfully applied capabilities {CAP} on ${{_TPATH}}"\n',
+ " else\n",
+ # We do not reset the mode here; generally a re-install or upgrade would re-store both mode,
+ # and remove the capabilities.
+ ' echo "The setcap failed to processes {CAP} on ${{_TPATH}}; falling back to no capability support" >&2\n',
+ " fi\n",
+ ]
+ ).format(
+ CAP=maintscript.escape_shell_words(capability.capabilities).replace(
+ "\\+", "+"
+ ),
+ DPKG_ROOT_UNQUOTED=DPKG_ROOT_UNQUOTED,
+ ABS_PATH=abs_path,
+ MODE=maintscript.escape_shell_words(str(capability.capability_mode)),
+ DEFINITION_SOURCE=capability.definition_source.replace("\n", "\\n"),
+ )
+ assert cap_script.endswith("\n")
+ caps.append(cap_script)
+
+ if not caps:
+ return
+
+ maintscript.on_configure(
+ textwrap.dedent(
+ """\
+ if command -v setcap > /dev/null; then
+ {SET_CAP_COMMANDS}
+ unset _TPATH
+ else
+ echo "The setcap utility is not installed available; falling back to no capability support" >&2
+ fi
+ """
+ ).format(
+ SET_CAP_COMMANDS="".join(caps).rstrip("\n"),
+ )
+ )
+
+
+def pam_auth_update(
+ fs_root: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ _context: PackageProcessingContext,
+) -> None:
+ pam_configs = fs_root.lookup("/usr/share/pam-configs")
+ if not pam_configs:
+ return
+ maintscript = ctrl.maintscript
+ for pam_config in pam_configs.iterdir:
+ if not pam_config.is_file:
+ continue
+ maintscript.on_configure("pam-auth-update --package\n")
+ maintscript.on_before_removal(
+ textwrap.dedent(
+ f"""\
+ if [ "${{DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT:-1}}" = 1 ]; then
+ pam-auth-update --package --remove {maintscript.escape_shell_words(pam_config.name)}
+ fi
+ """
+ )
+ )
+
+
+def auto_depends_arch_any_solink(
+ fs_foot: VirtualPath,
+ ctrl: BinaryCtrlAccessor,
+ context: PackageProcessingContext,
+) -> None:
+ package = context.binary_package
+ if package.is_arch_all:
+ return
+ libbasedir = fs_foot.lookup("usr/lib")
+ if not libbasedir:
+ return
+ libmadir = libbasedir.get(package.deb_multiarch)
+ if libmadir:
+ libdirs = [libmadir, libbasedir]
+ else:
+ libdirs = [libbasedir]
+ targets = []
+ for libdir in libdirs:
+ for path in libdir.iterdir:
+ if not path.is_symlink or not path.name.endswith(".so"):
+ continue
+ target = path.readlink()
+ resolved = assume_not_none(path.parent_dir).lookup(target)
+ if resolved is not None:
+ continue
+ targets.append((libdir.path, target))
+
+ roots = list(context.accessible_package_roots())
+ if not roots:
+ return
+
+ for libdir, target in targets:
+ final_path = os.path.join(libdir, target)
+ matches = []
+ for opkg, ofs_root in roots:
+ m = ofs_root.lookup(final_path)
+ if not m:
+ continue
+ matches.append(opkg)
+ if not matches or len(matches) > 1:
+ if matches:
+ all_matches = ", ".join(p.name for p in matches)
+ _warn(
+ f"auto-depends-solink: The {final_path} was found in multiple packages ({all_matches}):"
+ f" Not generating a dependency."
+ )
+ else:
+ _warn(
+ f"auto-depends-solink: The {final_path} was NOT found in any accessible package:"
+ " Not generating a dependency. This detection only works when both packages are arch:any"
+ " and they have the same build-profiles."
+ )
+ continue
+ pkg_dep = matches[0]
+ # The debputy API should not allow this constraint to fail
+ assert pkg_dep.is_arch_all == package.is_arch_all
+ # If both packages are arch:all or both are arch:any, we can generate a tight dependency
+ relation = f"{pkg_dep.name} (= ${{binary:Version}})"
+ ctrl.substvars.add_dependency("misc:Depends", relation)