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