Coverage for src/debputy/debhelper_emulation.py: 73%

143 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import dataclasses 

2import os.path 

3import re 

4import shutil 

5from re import Match 

6from typing import ( 

7 Optional, 

8 Callable, 

9 Union, 

10 Iterable, 

11 Tuple, 

12 Sequence, 

13 cast, 

14 Mapping, 

15 Any, 

16 Set, 

17 List, 

18) 

19 

20from debputy.packages import BinaryPackage 

21from debputy.plugin.api import VirtualPath 

22from debputy.substitution import Substitution 

23from debputy.util import ensure_dir, print_command, _error 

24 

25SnippetReplacement = Union[str, Callable[[str], str]] 

26MAINTSCRIPT_TOKEN_NAME_PATTERN = r"[A-Za-z0-9_.+]+" 

27MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN) 

28MAINTSCRIPT_TOKEN_REGEX = re.compile(f"#({MAINTSCRIPT_TOKEN_NAME_PATTERN})#") 

29_ARCH_FILTER_START = re.compile(r"^\s*(\[([^]]*)])[ \t]+") 

30_ARCH_FILTER_END = re.compile(r"\s+(\[([^]]*)])\s*$") 

31_BUILD_PROFILE_FILTER = re.compile(r"(<([^>]*)>(?:\s+<([^>]*)>)*)") 

32 

33 

34class CannotEmulateExecutableDHConfigFile(Exception): 

35 def message(self) -> str: 

36 return cast("str", self.args[0]) 

37 

38 def config_file(self) -> VirtualPath: 

39 return cast("VirtualPath", self.args[1]) 

40 

41 

42@dataclasses.dataclass(slots=True, frozen=True) 

43class DHConfigFileLine: 

44 config_file: VirtualPath 

45 line_no: int 

46 executable_config: bool 

47 original_line: str 

48 tokens: Sequence[str] 

49 arch_filter: Optional[str] 

50 build_profile_filter: Optional[str] 

51 

52 def conditional_key(self) -> Tuple[str, ...]: 

53 k = [] 

54 if self.arch_filter is not None: 

55 k.append("arch") 

56 k.append(self.arch_filter) 

57 if self.build_profile_filter is not None: 

58 k.append("build-profiles") 

59 k.append(self.build_profile_filter) 

60 return tuple(k) 

61 

62 def conditional(self) -> Optional[Mapping[str, Any]]: 

63 filters = [] 

64 if self.arch_filter is not None: 

65 filters.append({"arch-matches": self.arch_filter}) 

66 if self.build_profile_filter is not None: 

67 filters.append({"build-profiles-matches": self.build_profile_filter}) 

68 if not filters: 

69 return None 

70 if len(filters) == 1: 

71 return filters[0] 

72 return {"all-of": filters} 

73 

74 

75def dhe_dbgsym_root_dir(binary_package: BinaryPackage) -> str: 

76 return os.path.join("debian", ".debhelper", binary_package.name, "dbgsym-root") 

77 

78 

79def read_dbgsym_file(binary_package: BinaryPackage) -> List[str]: 

80 dbgsym_id_file = os.path.join( 

81 "debian", ".debhelper", binary_package.name, "dbgsym-build-ids" 

82 ) 

83 try: 

84 with open(dbgsym_id_file, "rt", encoding="utf-8") as fd: 

85 return fd.read().split() 

86 except FileNotFoundError: 

87 return [] 

88 

89 

90def assert_no_dbgsym_migration(binary_package: BinaryPackage) -> None: 

91 dbgsym_migration_file = os.path.join( 

92 "debian", ".debhelper", binary_package.name, "dbgsym-migration" 

93 ) 

94 if os.path.lexists(dbgsym_migration_file): 

95 _error( 

96 "Sorry, debputy does not support dh_strip --dbgsym-migration feature. Please either finish the" 

97 " migration first or migrate to debputy later" 

98 ) 

99 

100 

101def _prune_match( 

102 line: str, 

103 match: Optional[Match[str]], 

104 match_mapper: Optional[Callable[[Match[str]], str]] = None, 

105) -> Tuple[str, Optional[str]]: 

106 if match is None: 

107 return line, None 

108 s, e = match.span() 

109 if match_mapper: 

110 matched_part = match_mapper(match) 

111 else: 

112 matched_part = line[s:e] 

113 # We prune exactly the matched part and assume the regexes leaves behind spaces if they were important. 

114 line = line[:s] + line[e:] 

115 # One special-case, if the match is at the beginning or end, then we can safely discard left 

116 # over whitespace. 

117 return line.strip(), matched_part 

118 

119 

120def dhe_filedoublearray( 

121 config_file: VirtualPath, 

122 substitution: Substitution, 

123 *, 

124 allow_dh_exec_rename: bool = False, 

125) -> Iterable[DHConfigFileLine]: 

126 with config_file.open() as fd: 

127 is_executable = config_file.is_executable 

128 for line_no, orig_line in enumerate(fd, start=1): 

129 arch_filter = None 

130 build_profile_filter = None 

131 if ( 131 ↛ 138line 131 didn't jump to line 138

132 line_no == 1 

133 and is_executable 

134 and not orig_line.startswith( 

135 ("#!/usr/bin/dh-exec", "#! /usr/bin/dh-exec") 

136 ) 

137 ): 

138 raise CannotEmulateExecutableDHConfigFile( 

139 "Only #!/usr/bin/dh-exec based executables can be emulated", 

140 config_file, 

141 ) 

142 orig_line = orig_line.rstrip("\n") 

143 line = orig_line.strip() 

144 if not line or line.startswith("#"): 

145 continue 

146 if is_executable: 

147 if "=>" in line and not allow_dh_exec_rename: 147 ↛ 148line 147 didn't jump to line 148, because the condition on line 147 was never true

148 raise CannotEmulateExecutableDHConfigFile( 

149 'Cannot emulate dh-exec\'s "=>" feature to rename files for the concrete file', 

150 config_file, 

151 ) 

152 line, build_profile_filter = _prune_match( 

153 line, 

154 _BUILD_PROFILE_FILTER.search(line), 

155 ) 

156 line, arch_filter = _prune_match( 

157 line, 

158 _ARCH_FILTER_START.search(line) or _ARCH_FILTER_END.search(line), 

159 # Remove the enclosing [] 

160 lambda m: m.group(1)[1:-1].strip(), 

161 ) 

162 

163 parts = tuple( 

164 substitution.substitute( 

165 w, f'{config_file.path} line {line_no} token "{w}"' 

166 ) 

167 for w in line.split() 

168 ) 

169 yield DHConfigFileLine( 

170 config_file, 

171 line_no, 

172 is_executable, 

173 orig_line, 

174 parts, 

175 arch_filter, 

176 build_profile_filter, 

177 ) 

178 

179 

180def dhe_pkgfile( 

181 debian_dir: VirtualPath, 

182 binary_package: BinaryPackage, 

183 basename: str, 

184 always_fallback_to_packageless_variant: bool = False, 

185 bug_950723_prefix_matching: bool = False, 

186) -> Optional[VirtualPath]: 

187 # TODO: Architecture specific files 

188 maybe_at_suffix = "@" if bug_950723_prefix_matching else "" 

189 possible_names = [f"{binary_package.name}{maybe_at_suffix}.{basename}"] 

190 if binary_package.is_main_package or always_fallback_to_packageless_variant: 190 ↛ 195line 190 didn't jump to line 195, because the condition on line 190 was never false

191 possible_names.append( 

192 f"{basename}@" if bug_950723_prefix_matching else basename 

193 ) 

194 

195 for name in possible_names: 

196 match = debian_dir.get(name) 

197 if match is not None and not match.is_dir: 

198 return match 

199 return None 

200 

201 

202def dhe_pkgdir( 

203 debian_dir: VirtualPath, 

204 binary_package: BinaryPackage, 

205 basename: str, 

206) -> Optional[VirtualPath]: 

207 possible_names = [f"{binary_package.name}.{basename}"] 

208 if binary_package.is_main_package: 

209 possible_names.append(basename) 

210 

211 for name in possible_names: 

212 match = debian_dir.get(name) 

213 if match is not None and match.is_dir: 

214 return match 

215 return None 

216 

217 

218def dhe_install_pkg_file_as_ctrl_file_if_present( 

219 debian_dir: VirtualPath, 

220 binary_package: BinaryPackage, 

221 basename: str, 

222 control_output_dir: str, 

223 mode: int, 

224) -> None: 

225 source = dhe_pkgfile(debian_dir, binary_package, basename) 

226 if source is None: 

227 return 

228 ensure_dir(control_output_dir) 

229 dhe_install_path(source.fs_path, os.path.join(control_output_dir, basename), mode) 

230 

231 

232def dhe_install_path(source: str, dest: str, mode: int) -> None: 

233 # TODO: "install -p -mXXXX foo bar" silently discards broken 

234 # symlinks to install the file in place. (#868204) 

235 print_command("install", "-p", f"-m{oct(mode)[2:]}", source, dest) 

236 shutil.copyfile(source, dest) 

237 os.chmod(dest, mode) 

238 

239 

240_FIND_DH_WITH = re.compile(r"--with(?:\s+|=)(\S+)") 

241_DEP_REGEX = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII) 

242 

243 

244def parse_drules_for_addons(lines: Iterable[str], sequences: Set[str]) -> None: 

245 for line in lines: 

246 if not line.startswith("\tdh "): 

247 continue 

248 for match in _FIND_DH_WITH.finditer(line): 

249 sequence_def = match.group(1) 

250 sequences.update(sequence_def.split(",")) 

251 

252 

253def extract_dh_addons_from_control( 

254 source_paragraph: Mapping[str, str], 

255 sequences: Set[str], 

256) -> None: 

257 for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"): 

258 field = source_paragraph.get(f) 

259 if not field: 

260 continue 

261 

262 for dep_clause in (d.strip() for d in field.split(",")): 

263 match = _DEP_REGEX.match(dep_clause.strip()) 

264 if not match: 

265 continue 

266 dep = match.group(1) 

267 if not dep.startswith("dh-sequence-"): 

268 continue 

269 sequences.add(dep[12:])