Coverage for src/debputy/dh/debhelper_emulation.py: 78%
112 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +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):
31 def message(self) -> str:
32 return cast("str", self.args[0])
34 def config_file(self) -> VirtualPath:
35 return cast("VirtualPath", self.args[1])
38@dataclasses.dataclass(slots=True, frozen=True)
39class DHConfigFileLine:
40 config_file: VirtualPath
41 line_no: int
42 executable_config: bool
43 original_line: str
44 tokens: Sequence[str]
45 arch_filter: str | None
46 build_profile_filter: str | None
48 def conditional_key(self) -> tuple[str, ...]:
49 k = []
50 if self.arch_filter is not None:
51 k.append("arch")
52 k.append(self.arch_filter)
53 if self.build_profile_filter is not None:
54 k.append("build-profiles")
55 k.append(self.build_profile_filter)
56 return tuple(k)
58 def conditional(self) -> Mapping[str, Any] | None:
59 filters = []
60 if self.arch_filter is not None:
61 filters.append({"arch-matches": self.arch_filter})
62 if self.build_profile_filter is not None:
63 filters.append({"build-profiles-matches": self.build_profile_filter})
64 if not filters:
65 return None
66 if len(filters) == 1:
67 return filters[0]
68 return {"all-of": filters}
71def dhe_dbgsym_root_dir(binary_package: BinaryPackage) -> str:
72 return os.path.join("debian", ".debhelper", binary_package.name, "dbgsym-root")
75def read_dbgsym_file(binary_package: BinaryPackage) -> list[str]:
76 dbgsym_id_file = os.path.join(
77 "debian", ".debhelper", binary_package.name, "dbgsym-build-ids"
78 )
79 try:
80 with open(dbgsym_id_file, encoding="utf-8") as fd:
81 return fd.read().split()
82 except FileNotFoundError:
83 return []
86def assert_no_dbgsym_migration(binary_package: BinaryPackage) -> None:
87 dbgsym_migration_file = os.path.join(
88 "debian", ".debhelper", binary_package.name, "dbgsym-migration"
89 )
90 if os.path.lexists(dbgsym_migration_file):
91 _error(
92 "Sorry, debputy does not support dh_strip --dbgsym-migration feature. Please either finish the"
93 " migration first or migrate to debputy later"
94 )
97def _prune_match(
98 line: str,
99 match: Match[str] | None,
100 match_mapper: Callable[[Match[str]], str] | None = None,
101) -> tuple[str, str | None]:
102 if match is None:
103 return line, None
104 s, e = match.span()
105 if match_mapper:
106 matched_part = match_mapper(match)
107 else:
108 matched_part = line[s:e]
109 # We prune exactly the matched part and assume the regexes leaves behind spaces if they were important.
110 line = line[:s] + line[e:]
111 # One special-case, if the match is at the beginning or end, then we can safely discard left
112 # over whitespace.
113 return line.strip(), matched_part
116def dhe_filedoublearray(
117 config_file: VirtualPath,
118 substitution: Substitution,
119 *,
120 allow_dh_exec_rename: bool = False,
121) -> Iterable[DHConfigFileLine]:
122 with config_file.open() as fd:
123 is_executable = config_file.is_executable
124 for line_no, orig_line in enumerate(fd, start=1):
125 arch_filter = None
126 build_profile_filter = None
127 if ( 127 ↛ 134line 127 didn't jump to line 134 because the condition on line 127 was never true
128 line_no == 1
129 and is_executable
130 and not orig_line.startswith(
131 ("#!/usr/bin/dh-exec", "#! /usr/bin/dh-exec")
132 )
133 ):
134 raise CannotEmulateExecutableDHConfigFile(
135 "Only #!/usr/bin/dh-exec based executables can be emulated",
136 config_file,
137 )
138 orig_line = orig_line.rstrip("\n")
139 line = orig_line.strip()
140 if not line or line.startswith("#"):
141 continue
142 if is_executable:
143 if "=>" in line and not allow_dh_exec_rename: 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true
144 raise CannotEmulateExecutableDHConfigFile(
145 'Cannot emulate dh-exec\'s "=>" feature to rename files for the concrete file',
146 config_file,
147 )
148 line, build_profile_filter = _prune_match(
149 line,
150 _BUILD_PROFILE_FILTER.search(line),
151 )
152 line, arch_filter = _prune_match(
153 line,
154 _ARCH_FILTER_START.search(line) or _ARCH_FILTER_END.search(line),
155 # Remove the enclosing []
156 lambda m: m.group(1)[1:-1].strip(),
157 )
159 parts = tuple(
160 substitution.substitute(
161 w, f'{config_file.path} line {line_no} token "{w}"'
162 )
163 for w in line.split()
164 )
165 yield DHConfigFileLine(
166 config_file,
167 line_no,
168 is_executable,
169 orig_line,
170 parts,
171 arch_filter,
172 build_profile_filter,
173 )
176def dhe_pkgfile(
177 debian_dir: VirtualPath,
178 binary_package: BinaryPackage,
179 basename: str,
180 always_fallback_to_packageless_variant: bool = False,
181 bug_950723_prefix_matching: bool = False,
182) -> VirtualPath | None:
183 # TODO: Architecture specific files
184 maybe_at_suffix = "@" if bug_950723_prefix_matching else ""
185 possible_names = [f"{binary_package.name}{maybe_at_suffix}.{basename}"]
186 if binary_package.is_main_package or always_fallback_to_packageless_variant: 186 ↛ 191line 186 didn't jump to line 191 because the condition on line 186 was always true
187 possible_names.append(
188 f"{basename}@" if bug_950723_prefix_matching else basename
189 )
191 for name in possible_names:
192 match = debian_dir.get(name)
193 if match is not None and not match.is_dir:
194 return match
195 return None
198def dhe_pkgdir(
199 debian_dir: VirtualPath,
200 binary_package: BinaryPackage,
201 basename: str,
202) -> VirtualPath | None:
203 possible_names = [f"{binary_package.name}.{basename}"]
204 if binary_package.is_main_package:
205 possible_names.append(basename)
207 for name in possible_names:
208 match = debian_dir.get(name)
209 if match is not None and match.is_dir:
210 return match
211 return None