Coverage for src/debputy/debhelper_emulation.py: 73%
143 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
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 Set,
17 List,
18)
20from debputy.packages import BinaryPackage
21from debputy.plugin.api import VirtualPath
22from debputy.substitution import Substitution
23from debputy.util import ensure_dir, print_command, _error
25SnippetReplacement = Union[str, Callable[[str], str]]
26MAINTSCRIPT_TOKEN_NAME_PATTERN = r"[A-Za-z0-9_.+]+"
27MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN)
28MAINTSCRIPT_TOKEN_REGEX = re.compile(f"#({MAINTSCRIPT_TOKEN_NAME_PATTERN})#")
29_ARCH_FILTER_START = re.compile(r"^\s*(\[([^]]*)])[ \t]+")
30_ARCH_FILTER_END = re.compile(r"\s+(\[([^]]*)])\s*$")
31_BUILD_PROFILE_FILTER = re.compile(r"(<([^>]*)>(?:\s+<([^>]*)>)*)")
34class CannotEmulateExecutableDHConfigFile(Exception):
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: Optional[str]
50 build_profile_filter: Optional[str]
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) -> Optional[Mapping[str, Any]]:
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, "rt", 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: Optional[Match[str]],
104 match_mapper: Optional[Callable[[Match[str]], str]] = None,
105) -> Tuple[str, Optional[str]]:
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
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) -> Optional[VirtualPath]:
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 never false
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) -> Optional[VirtualPath]:
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
218def dhe_install_pkg_file_as_ctrl_file_if_present(
219 debian_dir: VirtualPath,
220 binary_package: BinaryPackage,
221 basename: str,
222 control_output_dir: str,
223 mode: int,
224) -> None:
225 source = dhe_pkgfile(debian_dir, binary_package, basename)
226 if source is None:
227 return
228 ensure_dir(control_output_dir)
229 dhe_install_path(source.fs_path, os.path.join(control_output_dir, basename), mode)
232def dhe_install_path(source: str, dest: str, mode: int) -> None:
233 # TODO: "install -p -mXXXX foo bar" silently discards broken
234 # symlinks to install the file in place. (#868204)
235 print_command("install", "-p", f"-m{oct(mode)[2:]}", source, dest)
236 shutil.copyfile(source, dest)
237 os.chmod(dest, mode)
240_FIND_DH_WITH = re.compile(r"--with(?:\s+|=)(\S+)")
241_DEP_REGEX = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII)
244def parse_drules_for_addons(lines: Iterable[str], sequences: Set[str]) -> None:
245 for line in lines:
246 if not line.startswith("\tdh "):
247 continue
248 for match in _FIND_DH_WITH.finditer(line):
249 sequence_def = match.group(1)
250 sequences.update(sequence_def.split(","))
253def extract_dh_addons_from_control(
254 source_paragraph: Mapping[str, str],
255 sequences: Set[str],
256) -> None:
257 for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"):
258 field = source_paragraph.get(f)
259 if not field:
260 continue
262 for dep_clause in (d.strip() for d in field.split(",")):
263 match = _DEP_REGEX.match(dep_clause.strip())
264 if not match:
265 continue
266 dep = match.group(1)
267 if not dep.startswith("dh-sequence-"):
268 continue
269 sequences.add(dep[12:])