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

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 

11 

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) 

27 

28if TYPE_CHECKING: 

29 from debputy.highlevel_manifest import HighLevelManifest 

30 

31 

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] 

38 

39 

40@dataclasses.dataclass 

41class SONAMEInfo: 

42 path: VirtualPath 

43 full_soname: str 

44 library: str 

45 major_version: str | None 

46 

47 

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() 

53 

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) 

73 

74 def __bool__(self) -> bool: 

75 return bool(self._deb_lines) or bool(self._udeb_lines) 

76 

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) 

95 

96 def write_to(self, fd: IO[str]) -> None: 

97 fd.writelines(self._deb_lines) 

98 fd.writelines(self._udeb_lines) 

99 

100 

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) 

117 

118 

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 

134 

135 

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 ) 

155 

156 dependency = f"{binary_package.name} (>= {upstream_version})" 

157 strict_dependency = f"{binary_package.name} (= {strict_version})" 

158 udeb_dependency = None 

159 

160 if udeb_package_name is not None: 

161 udeb_dependency = f"{udeb_package_name} (>= {upstream_version})" 

162 

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 ) 

179 

180 return shlibs_file_contents, unversioned_so_seen 

181 

182 

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 

194 

195 

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 } 

210 

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 

240 

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)) 

253 

254 

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 ) 

282 

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 ) 

297 

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) 

302 

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 ] 

312 

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}") 

318 

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 

330 

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 

337 

338 if need_ldconfig: 

339 ctrl.dpkg_trigger("activate-noawait", "ldconfig") 

340 return sonames