diff options
Diffstat (limited to 'src/debputy/debhelper_emulation.py')
-rw-r--r-- | src/debputy/debhelper_emulation.py | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/src/debputy/debhelper_emulation.py b/src/debputy/debhelper_emulation.py new file mode 100644 index 0000000..88352bd --- /dev/null +++ b/src/debputy/debhelper_emulation.py @@ -0,0 +1,270 @@ +import dataclasses +import os.path +import re +import shutil +from re import Match +from typing import ( + Optional, + Callable, + Union, + Iterable, + Tuple, + Sequence, + cast, + Mapping, + Any, + Set, + List, +) + +from debputy.packages import BinaryPackage +from debputy.plugin.api import VirtualPath +from debputy.substitution import Substitution +from debputy.util import ensure_dir, print_command, _error + +SnippetReplacement = Union[str, Callable[[str], str]] +MAINTSCRIPT_TOKEN_NAME_PATTERN = r"[A-Za-z0-9_.+]+" +MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN) +MAINTSCRIPT_TOKEN_REGEX = re.compile(f"#({MAINTSCRIPT_TOKEN_NAME_PATTERN})#") +_ARCH_FILTER_START = re.compile(r"^\s*(\[([^]]*)])[ \t]+") +_ARCH_FILTER_END = re.compile(r"\s+(\[([^]]*)])\s*$") +_BUILD_PROFILE_FILTER = re.compile(r"(<([^>]*)>(?:\s+<([^>]*)>)*)") + + +class CannotEmulateExecutableDHConfigFile(Exception): + def message(self) -> str: + return cast("str", self.args[0]) + + def config_file(self) -> VirtualPath: + return cast("VirtualPath", self.args[1]) + + +@dataclasses.dataclass(slots=True, frozen=True) +class DHConfigFileLine: + config_file: VirtualPath + line_no: int + executable_config: bool + original_line: str + tokens: Sequence[str] + arch_filter: Optional[str] + build_profile_filter: Optional[str] + + def conditional_key(self) -> Tuple[str, ...]: + k = [] + if self.arch_filter is not None: + k.append("arch") + k.append(self.arch_filter) + if self.build_profile_filter is not None: + k.append("build-profiles") + k.append(self.build_profile_filter) + return tuple(k) + + def conditional(self) -> Optional[Mapping[str, Any]]: + filters = [] + if self.arch_filter is not None: + filters.append({"arch-matches": self.arch_filter}) + if self.build_profile_filter is not None: + filters.append({"build-profiles-matches": self.build_profile_filter}) + if not filters: + return None + if len(filters) == 1: + return filters[0] + return {"all-of": filters} + + +def dhe_dbgsym_root_dir(binary_package: BinaryPackage) -> str: + return os.path.join("debian", ".debhelper", binary_package.name, "dbgsym-root") + + +def read_dbgsym_file(binary_package: BinaryPackage) -> List[str]: + dbgsym_id_file = os.path.join( + "debian", ".debhelper", binary_package.name, "dbgsym-build-ids" + ) + try: + with open(dbgsym_id_file, "rt", encoding="utf-8") as fd: + return fd.read().split() + except FileNotFoundError: + return [] + + +def assert_no_dbgsym_migration(binary_package: BinaryPackage) -> None: + dbgsym_migration_file = os.path.join( + "debian", ".debhelper", binary_package.name, "dbgsym-migration" + ) + if os.path.lexists(dbgsym_migration_file): + _error( + "Sorry, debputy does not support dh_strip --dbgsym-migration feature. Please either finish the" + " migration first or migrate to debputy later" + ) + + +def _prune_match( + line: str, + match: Optional[Match[str]], + match_mapper: Optional[Callable[[Match[str]], str]] = None, +) -> Tuple[str, Optional[str]]: + if match is None: + return line, None + s, e = match.span() + if match_mapper: + matched_part = match_mapper(match) + else: + matched_part = line[s:e] + # We prune exactly the matched part and assume the regexes leaves behind spaces if they were important. + line = line[:s] + line[e:] + # One special-case, if the match is at the beginning or end, then we can safely discard left + # over whitespace. + return line.strip(), matched_part + + +def dhe_filedoublearray( + config_file: VirtualPath, + substitution: Substitution, + *, + allow_dh_exec_rename: bool = False, +) -> Iterable[DHConfigFileLine]: + with config_file.open() as fd: + is_executable = config_file.is_executable + for line_no, orig_line in enumerate(fd, start=1): + arch_filter = None + build_profile_filter = None + if ( + line_no == 1 + and is_executable + and not orig_line.startswith( + ("#!/usr/bin/dh-exec", "#! /usr/bin/dh-exec") + ) + ): + raise CannotEmulateExecutableDHConfigFile( + "Only #!/usr/bin/dh-exec based executables can be emulated", + config_file, + ) + orig_line = orig_line.rstrip("\n") + line = orig_line.strip() + if not line or line.startswith("#"): + continue + if is_executable: + if "=>" in line and not allow_dh_exec_rename: + raise CannotEmulateExecutableDHConfigFile( + 'Cannot emulate dh-exec\'s "=>" feature to rename files for the concrete file', + config_file, + ) + line, build_profile_filter = _prune_match( + line, + _BUILD_PROFILE_FILTER.search(line), + ) + line, arch_filter = _prune_match( + line, + _ARCH_FILTER_START.search(line) or _ARCH_FILTER_END.search(line), + # Remove the enclosing [] + lambda m: m.group(1)[1:-1].strip(), + ) + + parts = tuple( + substitution.substitute( + w, f'{config_file.path} line {line_no} token "{w}"' + ) + for w in line.split() + ) + yield DHConfigFileLine( + config_file, + line_no, + is_executable, + orig_line, + parts, + arch_filter, + build_profile_filter, + ) + + +def dhe_pkgfile( + debian_dir: VirtualPath, + binary_package: BinaryPackage, + basename: str, + always_fallback_to_packageless_variant: bool = False, + bug_950723_prefix_matching: bool = False, +) -> Optional[VirtualPath]: + # TODO: Architecture specific files + maybe_at_suffix = "@" if bug_950723_prefix_matching else "" + possible_names = [f"{binary_package.name}{maybe_at_suffix}.{basename}"] + if binary_package.is_main_package or always_fallback_to_packageless_variant: + possible_names.append( + f"{basename}@" if bug_950723_prefix_matching else basename + ) + + for name in possible_names: + match = debian_dir.get(name) + if match is not None and not match.is_dir: + return match + return None + + +def dhe_pkgdir( + debian_dir: VirtualPath, + binary_package: BinaryPackage, + basename: str, +) -> Optional[VirtualPath]: + possible_names = [f"{binary_package.name}.{basename}"] + if binary_package.is_main_package: + possible_names.append(basename) + + for name in possible_names: + match = debian_dir.get(name) + if match is not None and match.is_dir: + return match + return None + + +def dhe_install_pkg_file_as_ctrl_file_if_present( + debian_dir: VirtualPath, + binary_package: BinaryPackage, + basename: str, + control_output_dir: str, + mode: int, +) -> None: + source = dhe_pkgfile(debian_dir, binary_package, basename) + if source is None: + return + ensure_dir(control_output_dir) + dhe_install_path(source.fs_path, os.path.join(control_output_dir, basename), mode) + + +def dhe_install_path(source: str, dest: str, mode: int) -> None: + # TODO: "install -p -mXXXX foo bar" silently discards broken + # symlinks to install the file in place. (#868204) + print_command("install", "-p", f"-m{oct(mode)[2:]}", source, dest) + shutil.copyfile(source, dest) + os.chmod(dest, mode) + + +_FIND_DH_WITH = re.compile(r"--with(?:\s+|=)(\S+)") +_DEP_REGEX = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII) + + +def parse_drules_for_addons(debian_rules: VirtualPath, sequences: Set[str]) -> None: + with debian_rules.open() as fd: + for line in fd: + if not line.startswith("\tdh "): + continue + for match in _FIND_DH_WITH.finditer(line): + sequence_def = match.group(1) + sequences.update(sequence_def.split(",")) + + +def extract_dh_addons_from_control( + source_paragraph: Mapping[str, str], + sequences: Set[str], +) -> None: + for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"): + field = source_paragraph.get(f) + if not field: + continue + + for dep_clause in (d.strip() for d in field.split(",")): + match = _DEP_REGEX.match(dep_clause.strip()) + if not match: + continue + dep = match.group(1) + if not dep.startswith("dh-sequence-"): + continue + sequences.add(dep[12:]) |