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

112 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +0000

1import dataclasses 

2import os.path 

3import re 

4import shutil 

5from re import Match 

6from typing import ( 

7 Optional, 

8 Union, 

9 Tuple, 

10 cast, 

11 Any, 

12 List, 

13) 

14from collections.abc import Callable, Iterable, Sequence, Mapping 

15 

16from debputy.packages import BinaryPackage 

17from debputy.plugin.api import VirtualPath 

18from debputy.substitution import Substitution 

19from debputy.util import ensure_dir, print_command, _error 

20 

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

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

23MAINTSCRIPT_TOKEN_NAME_REGEX = re.compile(MAINTSCRIPT_TOKEN_NAME_PATTERN) 

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

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

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

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

28 

29 

30class CannotEmulateExecutableDHConfigFile(Exception): 

31 def message(self) -> str: 

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

33 

34 def config_file(self) -> VirtualPath: 

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

36 

37 

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

39class DHConfigFileLine: 

40 config_file: VirtualPath 

41 line_no: int 

42 executable_config: bool 

43 original_line: str 

44 tokens: Sequence[str] 

45 arch_filter: str | None 

46 build_profile_filter: str | None 

47 

48 def conditional_key(self) -> tuple[str, ...]: 

49 k = [] 

50 if self.arch_filter is not None: 

51 k.append("arch") 

52 k.append(self.arch_filter) 

53 if self.build_profile_filter is not None: 

54 k.append("build-profiles") 

55 k.append(self.build_profile_filter) 

56 return tuple(k) 

57 

58 def conditional(self) -> Mapping[str, Any] | None: 

59 filters = [] 

60 if self.arch_filter is not None: 

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

62 if self.build_profile_filter is not None: 

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

64 if not filters: 

65 return None 

66 if len(filters) == 1: 

67 return filters[0] 

68 return {"all-of": filters} 

69 

70 

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

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

73 

74 

75def read_dbgsym_file(binary_package: BinaryPackage) -> list[str]: 

76 dbgsym_id_file = os.path.join( 

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

78 ) 

79 try: 

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

81 return fd.read().split() 

82 except FileNotFoundError: 

83 return [] 

84 

85 

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

87 dbgsym_migration_file = os.path.join( 

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

89 ) 

90 if os.path.lexists(dbgsym_migration_file): 

91 _error( 

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

93 " migration first or migrate to debputy later" 

94 ) 

95 

96 

97def _prune_match( 

98 line: str, 

99 match: Match[str] | None, 

100 match_mapper: Callable[[Match[str]], str] | None = None, 

101) -> tuple[str, str | None]: 

102 if match is None: 

103 return line, None 

104 s, e = match.span() 

105 if match_mapper: 

106 matched_part = match_mapper(match) 

107 else: 

108 matched_part = line[s:e] 

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

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

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

112 # over whitespace. 

113 return line.strip(), matched_part 

114 

115 

116def dhe_filedoublearray( 

117 config_file: VirtualPath, 

118 substitution: Substitution, 

119 *, 

120 allow_dh_exec_rename: bool = False, 

121) -> Iterable[DHConfigFileLine]: 

122 with config_file.open() as fd: 

123 is_executable = config_file.is_executable 

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

125 arch_filter = None 

126 build_profile_filter = None 

127 if ( 127 ↛ 134line 127 didn't jump to line 134 because the condition on line 127 was never true

128 line_no == 1 

129 and is_executable 

130 and not orig_line.startswith( 

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

132 ) 

133 ): 

134 raise CannotEmulateExecutableDHConfigFile( 

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

136 config_file, 

137 ) 

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

139 line = orig_line.strip() 

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

141 continue 

142 if is_executable: 

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

144 raise CannotEmulateExecutableDHConfigFile( 

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

146 config_file, 

147 ) 

148 line, build_profile_filter = _prune_match( 

149 line, 

150 _BUILD_PROFILE_FILTER.search(line), 

151 ) 

152 line, arch_filter = _prune_match( 

153 line, 

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

155 # Remove the enclosing [] 

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

157 ) 

158 

159 parts = tuple( 

160 substitution.substitute( 

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

162 ) 

163 for w in line.split() 

164 ) 

165 yield DHConfigFileLine( 

166 config_file, 

167 line_no, 

168 is_executable, 

169 orig_line, 

170 parts, 

171 arch_filter, 

172 build_profile_filter, 

173 ) 

174 

175 

176def dhe_pkgfile( 

177 debian_dir: VirtualPath, 

178 binary_package: BinaryPackage, 

179 basename: str, 

180 always_fallback_to_packageless_variant: bool = False, 

181 bug_950723_prefix_matching: bool = False, 

182) -> VirtualPath | None: 

183 # TODO: Architecture specific files 

184 maybe_at_suffix = "@" if bug_950723_prefix_matching else "" 

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

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

187 possible_names.append( 

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

189 ) 

190 

191 for name in possible_names: 

192 match = debian_dir.get(name) 

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

194 return match 

195 return None 

196 

197 

198def dhe_pkgdir( 

199 debian_dir: VirtualPath, 

200 binary_package: BinaryPackage, 

201 basename: str, 

202) -> VirtualPath | None: 

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

204 if binary_package.is_main_package: 

205 possible_names.append(basename) 

206 

207 for name in possible_names: 

208 match = debian_dir.get(name) 

209 if match is not None and match.is_dir: 

210 return match 

211 return None