Coverage for src/debputy/dh/debhelper_emulation.py: 78%
114 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-02-14 10:41 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-02-14 10:41 +0000
1import dataclasses
2import os.path
3import re
4import shutil
5from re import Match
6from typing import (
7 Optional,
8 Union,
9 Tuple,
10 cast,
11 Any,
12 List,
13)
14from collections.abc import Callable, Iterable, Sequence, Mapping
16from debputy.packages import BinaryPackage
17from debputy.plugin.api import VirtualPath
18from debputy.substitution import Substitution
19from debputy.util import ensure_dir, print_command, _error
21SnippetReplacement = Union[str, Callable[[str], str]]
22MAINTSCRIPT_TOKEN_NAME_PATTERN = r"[A-Za-z0-9_.+]+"
23MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN)
24MAINTSCRIPT_TOKEN_REGEX = re.compile(f"#({MAINTSCRIPT_TOKEN_NAME_PATTERN})#")
25_ARCH_FILTER_START = re.compile(r"^\s*(\[([^]]*)])[ \t]+")
26_ARCH_FILTER_END = re.compile(r"\s+(\[([^]]*)])\s*$")
27_BUILD_PROFILE_FILTER = re.compile(r"(<([^>]*)>(?:\s+<([^>]*)>)*)")
30class CannotEmulateExecutableDHConfigFile(Exception):
32 def __init__(self, message: str, virtual_path: VirtualPath) -> None:
33 super().__init__(message, virtual_path)
35 def message(self) -> str:
36 return cast("str", self.args[0])
38 def config_file(self) -> VirtualPath:
39 return cast("VirtualPath", self.args[1])
42@dataclasses.dataclass(slots=True, frozen=True)
43class DHConfigFileLine:
44 config_file: VirtualPath
45 line_no: int
46 executable_config: bool
47 original_line: str
48 tokens: Sequence[str]
49 arch_filter: str | None
50 build_profile_filter: str | None
52 def conditional_key(self) -> tuple[str, ...]:
53 k = []
54 if self.arch_filter is not None:
55 k.append("arch")
56 k.append(self.arch_filter)
57 if self.build_profile_filter is not None:
58 k.append("build-profiles")
59 k.append(self.build_profile_filter)
60 return tuple(k)
62 def conditional(self) -> Mapping[str, Any] | None:
63 filters = []
64 if self.arch_filter is not None:
65 filters.append({"arch-matches": self.arch_filter})
66 if self.build_profile_filter is not None:
67 filters.append({"build-profiles-matches": self.build_profile_filter})
68 if not filters:
69 return None
70 if len(filters) == 1:
71 return filters[0]
72 return {"all-of": filters}
75def dhe_dbgsym_root_dir(binary_package: BinaryPackage) -> str:
76 return os.path.join("debian", ".debhelper", binary_package.name, "dbgsym-root")
79def read_dbgsym_file(binary_package: BinaryPackage) -> list[str]:
80 dbgsym_id_file = os.path.join(
81 "debian", ".debhelper", binary_package.name, "dbgsym-build-ids"
82 )
83 try:
84 with open(dbgsym_id_file, encoding="utf-8") as fd:
85 return fd.read().split()
86 except FileNotFoundError:
87 return []
90def assert_no_dbgsym_migration(binary_package: BinaryPackage) -> None:
91 dbgsym_migration_file = os.path.join(
92 "debian", ".debhelper", binary_package.name, "dbgsym-migration"
93 )
94 if os.path.lexists(dbgsym_migration_file):
95 _error(
96 "Sorry, debputy does not support dh_strip --dbgsym-migration feature. Please either finish the"
97 " migration first or migrate to debputy later"
98 )
101def _prune_match(
102 line: str,
103 match: Match[str] | None,
104 match_mapper: Callable[[Match[str]], str] | None = None,
105) -> tuple[str, str | None]:
106 if match is None:
107 return line, None
108 s, e = match.span()
109 if match_mapper:
110 matched_part = match_mapper(match)
111 else:
112 matched_part = line[s:e]
113 # We prune exactly the matched part and assume the regexes leaves behind spaces if they were important.
114 line = line[:s] + line[e:]
115 # One special-case, if the match is at the beginning or end, then we can safely discard left
116 # over whitespace.
117 return line.strip(), matched_part
120def dhe_filedoublearray(
121 config_file: VirtualPath,
122 substitution: Substitution,
123 *,
124 allow_dh_exec_rename: bool = False,
125) -> Iterable[DHConfigFileLine]:
126 with config_file.open() as fd:
127 is_executable = config_file.is_executable
128 for line_no, orig_line in enumerate(fd, start=1):
129 arch_filter = None
130 build_profile_filter = None
131 if ( 131 ↛ 138line 131 didn't jump to line 138 because the condition on line 131 was never true
132 line_no == 1
133 and is_executable
134 and not orig_line.startswith(
135 ("#!/usr/bin/dh-exec", "#! /usr/bin/dh-exec")
136 )
137 ):
138 raise CannotEmulateExecutableDHConfigFile(
139 "Only #!/usr/bin/dh-exec based executables can be emulated",
140 config_file,
141 )
142 orig_line = orig_line.rstrip("\n")
143 line = orig_line.strip()
144 if not line or line.startswith("#"):
145 continue
146 if is_executable:
147 if "=>" in line and not allow_dh_exec_rename: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 raise CannotEmulateExecutableDHConfigFile(
149 'Cannot emulate dh-exec\'s "=>" feature to rename files for the concrete file',
150 config_file,
151 )
152 line, build_profile_filter = _prune_match(
153 line,
154 _BUILD_PROFILE_FILTER.search(line),
155 )
156 line, arch_filter = _prune_match(
157 line,
158 _ARCH_FILTER_START.search(line) or _ARCH_FILTER_END.search(line),
159 # Remove the enclosing []
160 lambda m: m.group(1)[1:-1].strip(),
161 )
163 parts = tuple(
164 substitution.substitute(
165 w, f'{config_file.path} line {line_no} token "{w}"'
166 )
167 for w in line.split()
168 )
169 yield DHConfigFileLine(
170 config_file,
171 line_no,
172 is_executable,
173 orig_line,
174 parts,
175 arch_filter,
176 build_profile_filter,
177 )
180def dhe_pkgfile(
181 debian_dir: VirtualPath,
182 binary_package: BinaryPackage,
183 basename: str,
184 always_fallback_to_packageless_variant: bool = False,
185 bug_950723_prefix_matching: bool = False,
186) -> VirtualPath | None:
187 # TODO: Architecture specific files
188 maybe_at_suffix = "@" if bug_950723_prefix_matching else ""
189 possible_names = [f"{binary_package.name}{maybe_at_suffix}.{basename}"]
190 if binary_package.is_main_package or always_fallback_to_packageless_variant: 190 ↛ 195line 190 didn't jump to line 195 because the condition on line 190 was always true
191 possible_names.append(
192 f"{basename}@" if bug_950723_prefix_matching else basename
193 )
195 for name in possible_names:
196 match = debian_dir.get(name)
197 if match is not None and not match.is_dir:
198 return match
199 return None
202def dhe_pkgdir(
203 debian_dir: VirtualPath,
204 binary_package: BinaryPackage,
205 basename: str,
206) -> VirtualPath | None:
207 possible_names = [f"{binary_package.name}.{basename}"]
208 if binary_package.is_main_package:
209 possible_names.append(basename)
211 for name in possible_names:
212 match = debian_dir.get(name)
213 if match is not None and match.is_dir:
214 return match
215 return None