Coverage for src/debputy/packaging/makeshlibs.py: 18%
193 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-05-11 16:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-05-11 16:06 +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, substitution
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.plugins.debputy.binary_package_rules import DpkgGensymbolsOptions
19from debputy.util import (
20 print_command,
21 escape_shell,
22 assume_not_none,
23 _normalize_link_target,
24 _warn,
25 _error,
26)
28if TYPE_CHECKING:
29 from debputy.highlevel_manifest import HighLevelManifest
32HAS_SONAME = re.compile(r"\s+SONAME\s+(\S+)")
33SHLIBS_LINE_READER = re.compile(r"^(?:(\S*):)?\s*(\S+)\s*(\S+)\s*(\S.+)$")
34SONAME_FORMATS = [
35 re.compile(r"\s+SONAME\s+((.*)[.]so[.](.*))"),
36 re.compile(r"\s+SONAME\s+((.*)-(\d.*)[.]so)"),
37]
40@dataclasses.dataclass
41class SONAMEInfo:
42 path: VirtualPath
43 full_soname: str
44 library: str
45 major_version: str | None
48class ShlibsContent:
49 def __init__(self) -> None:
50 self._deb_lines: list[str] = []
51 self._udeb_lines: list[str] = []
52 self._seen: set[tuple[str, str, str]] = set()
54 def add_library(
55 self,
56 library: str,
57 major_version: str,
58 dependency: str,
59 *,
60 udeb_dependency: str | None = None,
61 ) -> None:
62 line = f"{library} {major_version} {dependency}\n"
63 seen_key = ("deb", library, major_version)
64 if seen_key not in self._seen:
65 self._deb_lines.append(line)
66 self._seen.add(seen_key)
67 if udeb_dependency is not None:
68 seen_key = ("udeb", library, major_version)
69 udeb_line = f"udeb: {library} {major_version} {udeb_dependency}\n"
70 if seen_key not in self._seen:
71 self._udeb_lines.append(udeb_line)
72 self._seen.add(seen_key)
74 def __bool__(self) -> bool:
75 return bool(self._deb_lines) or bool(self._udeb_lines)
77 def add_entries_from_shlibs_file(self, fd: IO[str]) -> None:
78 for line in fd:
79 if line.startswith("#") or line.isspace():
80 continue
81 m = SHLIBS_LINE_READER.match(line)
82 if not m:
83 continue
84 shtype, library, major_version, dependency = m.groups()
85 if shtype is None or shtype == "":
86 shtype = "deb"
87 seen_key = (shtype, library, major_version)
88 if seen_key in self._seen:
89 continue
90 self._seen.add(seen_key)
91 if shtype == "udeb":
92 self._udeb_lines.append(line)
93 else:
94 self._deb_lines.append(line)
96 def write_to(self, fd: IO[str]) -> None:
97 fd.writelines(self._deb_lines)
98 fd.writelines(self._udeb_lines)
101def extract_so_name(
102 binary_package: BinaryPackage,
103 path: VirtualPath,
104) -> SONAMEInfo | None:
105 objdump = binary_package.cross_command("objdump")
106 output = subprocess.check_output([objdump, "-p", path.fs_path], encoding="utf-8")
107 for r in SONAME_FORMATS:
108 m = r.search(output)
109 if m:
110 full_soname, library, major_version = m.groups()
111 return SONAMEInfo(path, full_soname, library, major_version)
112 m = HAS_SONAME.search(output)
113 if not m:
114 return None
115 full_soname = m.group(1)
116 return SONAMEInfo(path, full_soname, full_soname, None)
119def extract_soname_info(
120 binary_package: BinaryPackage,
121 fs_root: VirtualPath,
122) -> list[SONAMEInfo]:
123 so_files = elf_util.find_all_elf_files(
124 fs_root,
125 with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
126 )
127 result = []
128 for so_file in so_files:
129 soname_info = extract_so_name(binary_package, so_file)
130 if not soname_info:
131 continue
132 result.append(soname_info)
133 return result
136def _compute_shlibs_content(
137 binary_package: BinaryPackage,
138 manifest: "HighLevelManifest",
139 soname_info_list: list[SONAMEInfo],
140 udeb_package_name: str | None,
141 combined_shlibs: ShlibsContent,
142) -> tuple[ShlibsContent, bool]:
143 shlibs_file_contents = ShlibsContent()
144 unversioned_so_seen = False
145 package_state = manifest.package_state_for(binary_package.name)
146 strict_version = package_state.binary_version
147 substitution = package_state.substitution
148 if strict_version is not None:
149 upstream_version = re.sub(r"-[^-]+$", "", strict_version)
150 else:
151 strict_version = substitution.substitute("{{DEB_VERSION}}", "<internal-usage>")
152 upstream_version = substitution.substitute(
153 "{{DEB_VERSION_EPOCH_UPSTREAM}}", "<internal-usage>"
154 )
156 dependency = f"{binary_package.name} (>= {upstream_version})"
157 strict_dependency = f"{binary_package.name} (= {strict_version})"
158 udeb_dependency = None
160 if udeb_package_name is not None:
161 udeb_dependency = f"{udeb_package_name} (>= {upstream_version})"
163 for soname_info in soname_info_list:
164 if soname_info.major_version is None:
165 unversioned_so_seen = True
166 continue
167 shlibs_file_contents.add_library(
168 soname_info.library,
169 soname_info.major_version,
170 dependency,
171 udeb_dependency=udeb_dependency,
172 )
173 combined_shlibs.add_library(
174 soname_info.library,
175 soname_info.major_version,
176 strict_dependency,
177 udeb_dependency=udeb_dependency,
178 )
180 return shlibs_file_contents, unversioned_so_seen
183def resolve_reserved_provided_file(
184 basename: str,
185 reserved_packager_provided_files: dict[str, list[PackagerProvidedFile]],
186) -> VirtualPath | None:
187 matches = reserved_packager_provided_files.get(basename)
188 if matches is None:
189 return None
190 assert len(matches) < 2
191 if matches: 191 ↛ 193line 191 didn't jump to line 193 because the condition on line 191 was always true
192 return matches[0].path
193 return None
196def generate_shlib_dirs(
197 pkg: BinaryPackage,
198 root_dir: str,
199 soname_info_list: list[SONAMEInfo],
200 materialized_dirs: list[str],
201) -> None:
202 dir_scanned: dict[str, dict[str, set[str]]] = {}
203 dirs: dict[str, str] = {}
204 warn_dirs = {
205 "/usr/lib",
206 "/lib",
207 f"/usr/lib/{pkg.deb_multiarch}",
208 f"/lib/{pkg.deb_multiarch}",
209 }
211 for soname_info in soname_info_list:
212 elf_binary = soname_info.path
213 p = assume_not_none(elf_binary.parent_dir)
214 abs_parent_path = p.absolute
215 matches = dir_scanned.get(abs_parent_path)
216 materialized_dir = dirs.get(abs_parent_path)
217 if matches is None:
218 matches = collections.defaultdict(set)
219 for child in p.iterdir():
220 if not child.is_symlink:
221 continue
222 target = _normalize_link_target(child.readlink())
223 if "/" in target:
224 # The shlib symlinks (we are interested in) are relative to the same folder
225 continue
226 matches[target].add(child.name)
227 dir_scanned[abs_parent_path] = matches
228 symlinks = matches.get(elf_binary.name, set())
229 full_soname = soname_info.full_soname
230 if full_soname != elf_binary.name and full_soname not in symlinks:
231 if abs_parent_path in warn_dirs:
232 _warn(
233 f"Could not find a {full_soname} symlink pointing to {elf_binary.absolute} in {pkg.name} !?"
234 )
235 continue
236 if materialized_dir is None:
237 materialized_dir = tempfile.mkdtemp(prefix=f"{pkg.name}_", dir=root_dir)
238 materialized_dirs.append(materialized_dir)
239 dirs[abs_parent_path] = materialized_dir
241 # Ideally, we would symlink here, but unfortunately, we might have materialized and stolen the
242 # file by the time it is processed. So we copy to be safe.
243 subprocess.check_call(
244 [
245 "cp",
246 "--reflink=auto",
247 elf_binary.fs_path,
248 os.path.join(materialized_dir, elf_binary.name),
249 ]
250 )
251 for link in symlinks:
252 os.symlink(elf_binary.name, os.path.join(materialized_dir, link))
255def compute_shlibs(
256 package_metadata_context: PackageProcessingContext,
257 control_output_dir: str,
258 fs_root: VirtualPath,
259 manifest: "HighLevelManifest",
260 udeb_package_name: str | None,
261 ctrl: BinaryCtrlAccessor,
262 reserved_packager_provided_files: dict[str, list[PackagerProvidedFile]],
263 combined_shlibs: ShlibsContent,
264) -> list[SONAMEInfo]:
265 binary_package = package_metadata_context.binary_package
266 assert not binary_package.is_udeb
267 shlibs_file = os.path.join(control_output_dir, "shlibs")
268 need_ldconfig = False
269 so_files = elf_util.find_all_elf_files(
270 fs_root,
271 with_linking_type=ELF_LINKING_TYPE_DYNAMIC,
272 )
273 sonames = extract_soname_info(binary_package, fs_root)
274 provided_shlibs_file = resolve_reserved_provided_file(
275 "shlibs",
276 reserved_packager_provided_files,
277 )
278 symbols_template_file = resolve_reserved_provided_file(
279 "symbols",
280 reserved_packager_provided_files,
281 )
283 if provided_shlibs_file:
284 need_ldconfig = True
285 unversioned_so_seen = False
286 shutil.copyfile(provided_shlibs_file.fs_path, shlibs_file)
287 with open(shlibs_file) as fd:
288 combined_shlibs.add_entries_from_shlibs_file(fd)
289 else:
290 shlibs_file_contents, unversioned_so_seen = _compute_shlibs_content(
291 binary_package,
292 manifest,
293 sonames,
294 udeb_package_name,
295 combined_shlibs,
296 )
298 if shlibs_file_contents:
299 need_ldconfig = True
300 with open(shlibs_file, "w", encoding="utf-8") as fd:
301 shlibs_file_contents.write_to(fd)
303 if symbols_template_file:
304 symbols_file = os.path.join(control_output_dir, "symbols")
305 symbols_cmd = [
306 "dpkg-gensymbols",
307 f"-p{binary_package.name}",
308 f"-I{symbols_template_file.fs_path}",
309 f"-P{control_output_dir}",
310 f"-O{symbols_file}",
311 ]
313 gensym_options = package_metadata_context.manifest_configuration(
314 binary_package, DpkgGensymbolsOptions
315 )
316 if gensym_options and (check_level := gensym_options.check_level) is not None:
317 symbols_cmd.append(f"-c{check_level}")
319 if so_files:
320 symbols_cmd.extend(f"-e{x.fs_path}" for x in so_files)
321 print_command(*symbols_cmd)
322 try:
323 subprocess.check_call(symbols_cmd)
324 except subprocess.CalledProcessError as e:
325 # Wrap in a special error, so debputy can run the other packages.
326 # The kde symbols helper relies on this behavior
327 raise DebputyDpkgGensymbolsError(
328 f"Error while running command for {binary_package.name}: {escape_shell(*symbols_cmd)}"
329 ) from e
331 with suppress(FileNotFoundError):
332 st = os.stat(symbols_file)
333 if stat.S_ISREG(st.st_mode) and st.st_size == 0:
334 os.unlink(symbols_file)
335 elif unversioned_so_seen:
336 need_ldconfig = True
338 if need_ldconfig:
339 ctrl.dpkg_trigger("activate-noawait", "ldconfig")
340 return sonames