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

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 

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) 

26 

27if TYPE_CHECKING: 

28 from debputy.highlevel_manifest import HighLevelManifest 

29 

30 

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] 

37 

38 

39@dataclasses.dataclass 

40class SONAMEInfo: 

41 path: VirtualPath 

42 full_soname: str 

43 library: str 

44 major_version: Optional[str] 

45 

46 

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

52 

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) 

72 

73 def __bool__(self) -> bool: 

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

75 

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) 

94 

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

96 fd.writelines(self._deb_lines) 

97 fd.writelines(self._udeb_lines) 

98 

99 

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) 

116 

117 

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 

133 

134 

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 ) 

154 

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

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

157 udeb_dependency = None 

158 

159 if udeb_package_name is not None: 

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

161 

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 ) 

178 

179 return shlibs_file_contents, unversioned_so_seen 

180 

181 

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 

193 

194 

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 } 

209 

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 

238 

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

242 

243 

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 ) 

270 

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 ) 

285 

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) 

290 

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 ] 

300 

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 

312 

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 

319 

320 if need_ldconfig: 

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

322 return sonames