Coverage for src/debputy/dh/debhelper_emulation.py: 74%

121 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-24 16:38 +0000

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 List, 

17) 

18 

19from debputy.packages import BinaryPackage 

20from debputy.plugin.api import VirtualPath 

21from debputy.substitution import Substitution 

22from debputy.util import ensure_dir, print_command, _error 

23 

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

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

26MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN) 

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

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

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

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

31 

32 

33class CannotEmulateExecutableDHConfigFile(Exception): 

34 def message(self) -> str: 

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

36 

37 def config_file(self) -> VirtualPath: 

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

39 

40 

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

42class DHConfigFileLine: 

43 config_file: VirtualPath 

44 line_no: int 

45 executable_config: bool 

46 original_line: str 

47 tokens: Sequence[str] 

48 arch_filter: Optional[str] 

49 build_profile_filter: Optional[str] 

50 

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

52 k = [] 

53 if self.arch_filter is not None: 

54 k.append("arch") 

55 k.append(self.arch_filter) 

56 if self.build_profile_filter is not None: 

57 k.append("build-profiles") 

58 k.append(self.build_profile_filter) 

59 return tuple(k) 

60 

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

62 filters = [] 

63 if self.arch_filter is not None: 

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

65 if self.build_profile_filter is not None: 

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

67 if not filters: 

68 return None 

69 if len(filters) == 1: 

70 return filters[0] 

71 return {"all-of": filters} 

72 

73 

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

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

76 

77 

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

79 dbgsym_id_file = os.path.join( 

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

81 ) 

82 try: 

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

84 return fd.read().split() 

85 except FileNotFoundError: 

86 return [] 

87 

88 

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

90 dbgsym_migration_file = os.path.join( 

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

92 ) 

93 if os.path.lexists(dbgsym_migration_file): 

94 _error( 

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

96 " migration first or migrate to debputy later" 

97 ) 

98 

99 

100def _prune_match( 

101 line: str, 

102 match: Optional[Match[str]], 

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

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

105 if match is None: 

106 return line, None 

107 s, e = match.span() 

108 if match_mapper: 

109 matched_part = match_mapper(match) 

110 else: 

111 matched_part = line[s:e] 

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

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

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

115 # over whitespace. 

116 return line.strip(), matched_part 

117 

118 

119def dhe_filedoublearray( 

120 config_file: VirtualPath, 

121 substitution: Substitution, 

122 *, 

123 allow_dh_exec_rename: bool = False, 

124) -> Iterable[DHConfigFileLine]: 

125 with config_file.open() as fd: 

126 is_executable = config_file.is_executable 

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

128 arch_filter = None 

129 build_profile_filter = None 

130 if ( 130 ↛ 137line 130 didn't jump to line 137

131 line_no == 1 

132 and is_executable 

133 and not orig_line.startswith( 

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

135 ) 

136 ): 

137 raise CannotEmulateExecutableDHConfigFile( 

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

139 config_file, 

140 ) 

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

142 line = orig_line.strip() 

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

144 continue 

145 if is_executable: 

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

147 raise CannotEmulateExecutableDHConfigFile( 

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

149 config_file, 

150 ) 

151 line, build_profile_filter = _prune_match( 

152 line, 

153 _BUILD_PROFILE_FILTER.search(line), 

154 ) 

155 line, arch_filter = _prune_match( 

156 line, 

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

158 # Remove the enclosing [] 

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

160 ) 

161 

162 parts = tuple( 

163 substitution.substitute( 

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

165 ) 

166 for w in line.split() 

167 ) 

168 yield DHConfigFileLine( 

169 config_file, 

170 line_no, 

171 is_executable, 

172 orig_line, 

173 parts, 

174 arch_filter, 

175 build_profile_filter, 

176 ) 

177 

178 

179def dhe_pkgfile( 

180 debian_dir: VirtualPath, 

181 binary_package: BinaryPackage, 

182 basename: str, 

183 always_fallback_to_packageless_variant: bool = False, 

184 bug_950723_prefix_matching: bool = False, 

185) -> Optional[VirtualPath]: 

186 # TODO: Architecture specific files 

187 maybe_at_suffix = "@" if bug_950723_prefix_matching else "" 

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

189 if binary_package.is_main_package or always_fallback_to_packageless_variant: 189 ↛ 194line 189 didn't jump to line 194 because the condition on line 189 was always true

190 possible_names.append( 

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

192 ) 

193 

194 for name in possible_names: 

195 match = debian_dir.get(name) 

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

197 return match 

198 return None 

199 

200 

201def dhe_pkgdir( 

202 debian_dir: VirtualPath, 

203 binary_package: BinaryPackage, 

204 basename: str, 

205) -> Optional[VirtualPath]: 

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

207 if binary_package.is_main_package: 

208 possible_names.append(basename) 

209 

210 for name in possible_names: 

211 match = debian_dir.get(name) 

212 if match is not None and match.is_dir: 

213 return match 

214 return None 

215 

216 

217def dhe_install_pkg_file_as_ctrl_file_if_present( 

218 debian_dir: VirtualPath, 

219 binary_package: BinaryPackage, 

220 basename: str, 

221 control_output_dir: str, 

222 mode: int, 

223) -> None: 

224 source = dhe_pkgfile(debian_dir, binary_package, basename) 

225 if source is None: 

226 return 

227 ensure_dir(control_output_dir) 

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

229 

230 

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

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

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

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

235 shutil.copyfile(source, dest) 

236 os.chmod(dest, mode)