Coverage for src/debputy/packaging/makeshlibs.py: 18%
182 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 collections
2import dataclasses
3import os
4import re
5import shutil
6import stat
7import subprocess
8import tempfile
9from contextlib import suppress
10from typing import Optional, Set, List, Tuple, TYPE_CHECKING, Dict, IO
12from debputy import elf_util
13from debputy.elf_util import ELF_LINKING_TYPE_DYNAMIC
14from debputy.exceptions import DebputyDpkgGensymbolsError
15from debputy.packager_provided_files import PackagerProvidedFile
16from debputy.packages import BinaryPackage
17from debputy.plugin.api import VirtualPath, PackageProcessingContext, BinaryCtrlAccessor
18from debputy.util import (
19 print_command,
20 escape_shell,
21 assume_not_none,
22 _normalize_link_target,
23 _warn,
24 _error,
25)
27if TYPE_CHECKING:
28 from debputy.highlevel_manifest import HighLevelManifest
31HAS_SONAME = re.compile(r"\s+SONAME\s+(\S+)")
32SHLIBS_LINE_READER = re.compile(r"^(?:(\S*):)?\s*(\S+)\s*(\S+)\s*(\S.+)$")
33SONAME_FORMATS = [
34 re.compile(r"\s+SONAME\s+((.*)[.]so[.](.*))"),
35 re.compile(r"\s+SONAME\s+((.*)-(\d.*)[.]so)"),
36]
39@dataclasses.dataclass
40class SONAMEInfo:
41 path: VirtualPath
42 full_soname: str
43 library: str
44 major_version: Optional[str]
47class ShlibsContent:
48 def __init__(self) -> None:
49 self._deb_lines: List[str] = []
50 self._udeb_lines: List[str] = []
51 self._seen: Set[Tuple[str, str, str]] = set()
53 def add_library(
54 self,
55 library: str,
56 major_version: str,
57 dependency: str,
58 *,
59 udeb_dependency: Optional[str] = None,
60 ) -> None:
61 line = f"{library} {major_version} {dependency}\n"
62 seen_key = ("deb", library, major_version)
63 if seen_key not in self._seen:
64 self._deb_lines.append(line)
65 self._seen.add(seen_key)
66 if udeb_dependency is not None:
67 seen_key = ("udeb", library, major_version)
68 udeb_line = f"udeb: {library} {major_version} {udeb_dependency}\n"
69 if seen_key not in self._seen:
70 self._udeb_lines.append(udeb_line)
71 self._seen.add(seen_key)
73 def __bool__(self) -> bool:
74 return bool(self._deb_lines) or bool(self._udeb_lines)
76 def add_entries_from_shlibs_file(self, fd: IO[str]) -> None:
77 for line in fd:
78 if line.startswith("#") or line.isspace():
79 continue
80 m = SHLIBS_LINE_READER.match(line)
81 if not m:
82 continue
83 shtype, library, major_version, dependency = m.groups()
84 if shtype is None or shtype == "":
85 shtype = "deb"
86 seen_key = (shtype, library, major_version)
87 if seen_key in self._seen:
88 continue
89 self._seen.add(seen_key)
90 if shtype == "udeb":
91 self._udeb_lines.append(line)
92 else:
93 self._deb_lines.append(line)
95 def write_to(self, fd: IO[str]) -> None:
96 fd.writelines(self._deb_lines)
97 fd.writelines(self._udeb_lines)
100def extract_so_name(
101 binary_package: BinaryPackage,
102 path: VirtualPath,
103) -> Optional[SONAMEInfo]:
104 objdump = binary_package.cross_command("objdump")
105 output = subprocess.check_output([objdump, "-p", path.fs_path], encoding="utf-8")
106 for r in SONAME_FORMATS:
107 m = r.search(output)
108 if m:
109 full_soname, library, major_version = m.groups()
110 return SONAMEInfo(path, full_soname, library, major_version)
111 m = HAS_SONAME.search(output)
112 if not m:
113 return None
114 full_soname = m.group(1)
115 return SONAMEInfo(path, full_soname, full_soname, None)
118def extract_soname_info(
119 binary_package: BinaryPackage,
120 fs_root: VirtualPath,
121) -> List[SONAMEInfo]:
122 so_files = elf_util.find_all_elf_files(
123 fs_root,
124 with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
125 )
126 result = []
127 for so_file in so_files:
128 soname_info = extract_so_name(binary_package, so_file)
129 if not soname_info:
130 continue
131 result.append(soname_info)
132 return result
135def _compute_shlibs_content(
136 binary_package: BinaryPackage,
137 manifest: "HighLevelManifest",
138 soname_info_list: List[SONAMEInfo],
139 udeb_package_name: Optional[str],
140 combined_shlibs: ShlibsContent,
141) -> Tuple[ShlibsContent, bool]:
142 shlibs_file_contents = ShlibsContent()
143 unversioned_so_seen = False
144 strict_version = manifest.package_state_for(binary_package.name).binary_version
145 if strict_version is not None:
146 upstream_version = re.sub(r"-[^-]+$", "", strict_version)
147 else:
148 strict_version = manifest.substitution.substitute(
149 "{{DEB_VERSION}}", "<internal-usage>"
150 )
151 upstream_version = manifest.substitution.substitute(
152 "{{DEB_VERSION_EPOCH_UPSTREAM}}", "<internal-usage>"
153 )
155 dependency = f"{binary_package.name} (>= {upstream_version})"
156 strict_dependency = f"{binary_package.name} (= {strict_version})"
157 udeb_dependency = None
159 if udeb_package_name is not None:
160 udeb_dependency = f"{udeb_package_name} (>= {upstream_version})"
162 for soname_info in soname_info_list:
163 if soname_info.major_version is None:
164 unversioned_so_seen = True
165 continue
166 shlibs_file_contents.add_library(
167 soname_info.library,
168 soname_info.major_version,
169 dependency,
170 udeb_dependency=udeb_dependency,
171 )
172 combined_shlibs.add_library(
173 soname_info.library,
174 soname_info.major_version,
175 strict_dependency,
176 udeb_dependency=udeb_dependency,
177 )
179 return shlibs_file_contents, unversioned_so_seen
182def resolve_reserved_provided_file(
183 basename: str,
184 reserved_packager_provided_files: Dict[str, List[PackagerProvidedFile]],
185) -> Optional[VirtualPath]:
186 matches = reserved_packager_provided_files.get(basename)
187 if matches is None: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true
188 return None
189 assert len(matches) < 2
190 if matches: 190 ↛ 192line 190 didn't jump to line 192, because the condition on line 190 was never false
191 return matches[0].path
192 return None
195def generate_shlib_dirs(
196 pkg: BinaryPackage,
197 root_dir: str,
198 soname_info_list: List[SONAMEInfo],
199 materialized_dirs: List[str],
200) -> None:
201 dir_scanned: Dict[str, Dict[str, Set[str]]] = {}
202 dirs: Dict[str, str] = {}
204 for soname_info in soname_info_list:
205 elf_binary = soname_info.path
206 p = assume_not_none(elf_binary.parent_dir)
207 matches = dir_scanned.get(p.absolute)
208 materialized_dir = dirs.get(p.absolute)
209 if matches is None:
210 matches = collections.defaultdict(set)
211 for child in p.iterdir:
212 if not child.is_symlink:
213 continue
214 target = _normalize_link_target(child.readlink())
215 if "/" in target:
216 # The shlib symlinks (we are interested in) are relative to the same folder
217 continue
218 matches[target].add(child.name)
219 dir_scanned[p.absolute] = matches
220 symlinks = matches.get(elf_binary.name)
221 if not symlinks:
222 _warn(
223 f"Could not find any SO symlinks pointing to {elf_binary.absolute} in {pkg.name} !?"
224 )
225 continue
226 if materialized_dir is None:
227 materialized_dir = tempfile.mkdtemp(prefix=f"{pkg.name}_", dir=root_dir)
228 materialized_dirs.append(materialized_dir)
229 dirs[p.absolute] = materialized_dir
231 os.symlink(elf_binary.fs_path, os.path.join(materialized_dir, elf_binary.name))
232 for link in symlinks:
233 os.symlink(elf_binary.name, os.path.join(materialized_dir, link))
236def compute_shlibs(
237 binary_package: BinaryPackage,
238 control_output_dir: str,
239 fs_root: VirtualPath,
240 manifest: "HighLevelManifest",
241 udeb_package_name: Optional[str],
242 ctrl: BinaryCtrlAccessor,
243 reserved_packager_provided_files: Dict[str, List[PackagerProvidedFile]],
244 combined_shlibs: ShlibsContent,
245) -> List[SONAMEInfo]:
246 assert not binary_package.is_udeb
247 shlibs_file = os.path.join(control_output_dir, "shlibs")
248 need_ldconfig = False
249 so_files = elf_util.find_all_elf_files(
250 fs_root,
251 with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
252 )
253 sonames = extract_soname_info(binary_package, fs_root)
254 provided_shlibs_file = resolve_reserved_provided_file(
255 "shlibs",
256 reserved_packager_provided_files,
257 )
258 symbols_template_file = resolve_reserved_provided_file(
259 "symbols",
260 reserved_packager_provided_files,
261 )
263 if provided_shlibs_file:
264 need_ldconfig = True
265 unversioned_so_seen = False
266 shutil.copyfile(provided_shlibs_file.fs_path, shlibs_file)
267 with open(shlibs_file) as fd:
268 combined_shlibs.add_entries_from_shlibs_file(fd)
269 else:
270 shlibs_file_contents, unversioned_so_seen = _compute_shlibs_content(
271 binary_package,
272 manifest,
273 sonames,
274 udeb_package_name,
275 combined_shlibs,
276 )
278 if shlibs_file_contents:
279 need_ldconfig = True
280 with open(shlibs_file, "wt", encoding="utf-8") as fd:
281 shlibs_file_contents.write_to(fd)
283 if symbols_template_file:
284 symbols_file = os.path.join(control_output_dir, "symbols")
285 symbols_cmd = [
286 "dpkg-gensymbols",
287 f"-p{binary_package.name}",
288 f"-I{symbols_template_file.fs_path}",
289 f"-P{control_output_dir}",
290 f"-O{symbols_file}",
291 ]
293 if so_files:
294 symbols_cmd.extend(f"-e{x.fs_path}" for x in so_files)
295 print_command(*symbols_cmd)
296 try:
297 subprocess.check_call(symbols_cmd)
298 except subprocess.CalledProcessError as e:
299 # Wrap in a special error, so debputy can run the other packages.
300 # The kde symbols helper relies on this behaviour
301 raise DebputyDpkgGensymbolsError(
302 f"Error while running command for {binary_package.name}: {escape_shell(*symbols_cmd)}"
303 ) from e
305 with suppress(FileNotFoundError):
306 st = os.stat(symbols_file)
307 if stat.S_ISREG(st.st_mode) and st.st_size == 0:
308 os.unlink(symbols_file)
309 elif unversioned_so_seen:
310 need_ldconfig = True
312 if need_ldconfig:
313 ctrl.dpkg_trigger("activate-noawait", "ldconfig")
314 return sonames