Coverage for src/debputy/packaging/makeshlibs.py: 18%
185 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-01-27 13:59 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-01-27 13:59 +0000
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 always true
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] = {}
203 warn_dirs = {
204 "/usr/lib",
205 "/lib",
206 f"/usr/lib/{pkg.deb_multiarch}",
207 f"/lib/{pkg.deb_multiarch}",
208 }
210 for soname_info in soname_info_list:
211 elf_binary = soname_info.path
212 p = assume_not_none(elf_binary.parent_dir)
213 abs_parent_path = p.absolute
214 matches = dir_scanned.get(abs_parent_path)
215 materialized_dir = dirs.get(abs_parent_path)
216 if matches is None:
217 matches = collections.defaultdict(set)
218 for child in p.iterdir:
219 if not child.is_symlink:
220 continue
221 target = _normalize_link_target(child.readlink())
222 if "/" in target:
223 # The shlib symlinks (we are interested in) are relative to the same folder
224 continue
225 matches[target].add(child.name)
226 dir_scanned[abs_parent_path] = matches
227 symlinks = matches.get(elf_binary.name)
228 if not symlinks:
229 if abs_parent_path in warn_dirs:
230 _warn(
231 f"Could not find any SO symlinks pointing to {elf_binary.absolute} in {pkg.name} !?"
232 )
233 continue
234 if materialized_dir is None:
235 materialized_dir = tempfile.mkdtemp(prefix=f"{pkg.name}_", dir=root_dir)
236 materialized_dirs.append(materialized_dir)
237 dirs[abs_parent_path] = materialized_dir
239 os.symlink(elf_binary.fs_path, os.path.join(materialized_dir, elf_binary.name))
240 for link in symlinks:
241 os.symlink(elf_binary.name, os.path.join(materialized_dir, link))
244def compute_shlibs(
245 binary_package: BinaryPackage,
246 control_output_dir: str,
247 fs_root: VirtualPath,
248 manifest: "HighLevelManifest",
249 udeb_package_name: Optional[str],
250 ctrl: BinaryCtrlAccessor,
251 reserved_packager_provided_files: Dict[str, List[PackagerProvidedFile]],
252 combined_shlibs: ShlibsContent,
253) -> List[SONAMEInfo]:
254 assert not binary_package.is_udeb
255 shlibs_file = os.path.join(control_output_dir, "shlibs")
256 need_ldconfig = False
257 so_files = elf_util.find_all_elf_files(
258 fs_root,
259 with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
260 )
261 sonames = extract_soname_info(binary_package, fs_root)
262 provided_shlibs_file = resolve_reserved_provided_file(
263 "shlibs",
264 reserved_packager_provided_files,
265 )
266 symbols_template_file = resolve_reserved_provided_file(
267 "symbols",
268 reserved_packager_provided_files,
269 )
271 if provided_shlibs_file:
272 need_ldconfig = True
273 unversioned_so_seen = False
274 shutil.copyfile(provided_shlibs_file.fs_path, shlibs_file)
275 with open(shlibs_file) as fd:
276 combined_shlibs.add_entries_from_shlibs_file(fd)
277 else:
278 shlibs_file_contents, unversioned_so_seen = _compute_shlibs_content(
279 binary_package,
280 manifest,
281 sonames,
282 udeb_package_name,
283 combined_shlibs,
284 )
286 if shlibs_file_contents:
287 need_ldconfig = True
288 with open(shlibs_file, "wt", encoding="utf-8") as fd:
289 shlibs_file_contents.write_to(fd)
291 if symbols_template_file:
292 symbols_file = os.path.join(control_output_dir, "symbols")
293 symbols_cmd = [
294 "dpkg-gensymbols",
295 f"-p{binary_package.name}",
296 f"-I{symbols_template_file.fs_path}",
297 f"-P{control_output_dir}",
298 f"-O{symbols_file}",
299 ]
301 if so_files:
302 symbols_cmd.extend(f"-e{x.fs_path}" for x in so_files)
303 print_command(*symbols_cmd)
304 try:
305 subprocess.check_call(symbols_cmd)
306 except subprocess.CalledProcessError as e:
307 # Wrap in a special error, so debputy can run the other packages.
308 # The kde symbols helper relies on this behavior
309 raise DebputyDpkgGensymbolsError(
310 f"Error while running command for {binary_package.name}: {escape_shell(*symbols_cmd)}"
311 ) from e
313 with suppress(FileNotFoundError):
314 st = os.stat(symbols_file)
315 if stat.S_ISREG(st.st_mode) and st.st_size == 0:
316 os.unlink(symbols_file)
317 elif unversioned_so_seen:
318 need_ldconfig = True
320 if need_ldconfig:
321 ctrl.dpkg_trigger("activate-noawait", "ldconfig")
322 return sonames