Coverage for src/debputy/packaging/alternatives.py: 77%

80 statements  

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

1import os 

2import textwrap 

3from typing import List, Dict, Tuple 

4from collections.abc import Mapping 

5 

6from debian.deb822 import Deb822 

7from debian.substvars import Substvars 

8 

9from debputy.maintscript_snippet import MaintscriptSnippetContainer, MaintscriptSnippet 

10from debputy.packager_provided_files import PackagerProvidedFile 

11from debputy.packages import BinaryPackage 

12from debputy.packaging.makeshlibs import resolve_reserved_provided_file 

13from debputy.plugin.api import VirtualPath 

14from debputy.util import _error, escape_shell, POSTINST_DEFAULT_CONDITION 

15 

16# Match debhelper (minus one space in each end, which comes 

17# via join). 

18LINE_PREFIX = "\\\n " 

19 

20SYSTEM_DEFAULT_PATH_DIRS = frozenset( 

21 { 

22 "/usr/bin", 

23 "/bin", 

24 "/usr/sbin", 

25 "/sbin", 

26 "/usr/games", 

27 } 

28) 

29 

30 

31def process_alternatives( 

32 binary_package: BinaryPackage, 

33 fs_root: VirtualPath, 

34 reserved_packager_provided_files: dict[str, list[PackagerProvidedFile]], 

35 maintscript_snippets: dict[str, MaintscriptSnippetContainer], 

36 substvars: Substvars, 

37) -> None: 

38 if binary_package.is_udeb: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true

39 return 

40 

41 provided_alternatives_file = resolve_reserved_provided_file( 

42 "alternatives", 

43 reserved_packager_provided_files, 

44 ) 

45 if provided_alternatives_file is None: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 return 

47 

48 with provided_alternatives_file.open() as fd: 

49 alternatives = list(Deb822.iter_paragraphs(fd)) 

50 

51 for no, alternative in enumerate(alternatives): 

52 process_alternative( 

53 provided_alternatives_file.fs_path, 

54 fs_root, 

55 alternative, 

56 no, 

57 maintscript_snippets, 

58 substvars, 

59 ) 

60 

61 

62def process_alternative( 

63 provided_alternatives_fs_path: str, 

64 fs_root: VirtualPath, 

65 alternative_deb822: Deb822, 

66 no: int, 

67 maintscript_snippets: dict[str, MaintscriptSnippetContainer], 

68 substvars: Substvars, 

69) -> None: 

70 name = _mandatory_key( 

71 "Name", 

72 alternative_deb822, 

73 provided_alternatives_fs_path, 

74 f"Stanza number {no}", 

75 ) 

76 error_context = f"Alternative named {name}" 

77 link_path = _mandatory_key( 

78 "Link", 

79 alternative_deb822, 

80 provided_alternatives_fs_path, 

81 error_context, 

82 ) 

83 impl_path = _mandatory_key( 

84 "Alternative", 

85 alternative_deb822, 

86 provided_alternatives_fs_path, 

87 error_context, 

88 ) 

89 priority = _mandatory_key( 

90 "Priority", 

91 alternative_deb822, 

92 provided_alternatives_fs_path, 

93 error_context, 

94 ) 

95 if "/" in name: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true

96 _error( 

97 f'The "Name" ({link_path}) key must be a basename and cannot contain slashes' 

98 f" ({error_context} in {provided_alternatives_fs_path})" 

99 ) 

100 if link_path == impl_path: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 _error( 

102 f'The "Link" key and the "Alternative" key must not have the same value' 

103 f" ({error_context} in {provided_alternatives_fs_path})" 

104 ) 

105 impl = fs_root.lookup(impl_path) 

106 if impl is None or impl.is_dir: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true

107 _error( 

108 f'The path listed in "Alternative" ("{impl_path}") does not exist' 

109 f" in the package. ({error_context} in {provided_alternatives_fs_path})" 

110 ) 

111 link_parent, link_basename = os.path.split(link_path) 

112 if link_parent in SYSTEM_DEFAULT_PATH_DIRS: 112 ↛ 115line 112 didn't jump to line 115 because the condition on line 112 was always true

113 # Not really a dependency, but uses the same rules (comma-separated and only one instance). 

114 substvars.add_dependency("misc:Command", link_basename) 

115 for key in ["Slave", "Slaves", "Slave-Links"]: 

116 if key in alternative_deb822: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true

117 _error( 

118 f'Please use "Dependents" instead of "{key}".' 

119 f" ({error_context} in {provided_alternatives_fs_path})" 

120 ) 

121 dependents = alternative_deb822.get("Dependents") 

122 install_command = [ 

123 escape_shell( 

124 "update-alternatives", 

125 "--install", 

126 link_path, 

127 name, 

128 impl_path, 

129 priority, 

130 ) 

131 ] 

132 remove_command = [ 

133 escape_shell( 

134 "update-alternatives", 

135 "--remove", 

136 name, 

137 impl_path, 

138 ) 

139 ] 

140 if dependents: 140 ↛ 173line 140 didn't jump to line 173 because the condition on line 140 was always true

141 seen_link_path = set() 

142 for line in dependents.splitlines(): 

143 line = line.strip() 

144 if not line: # First line is usually empty 

145 continue 

146 dlink_path, dlink_name, dimpl_path = parse_dependent_link( 

147 line, 

148 error_context, 

149 provided_alternatives_fs_path, 

150 ) 

151 if dlink_path in seen_link_path: 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true

152 _error( 

153 f'The Dependent link path "{dlink_path}" was used twice.' 

154 f" ({error_context} in {provided_alternatives_fs_path})" 

155 ) 

156 dimpl = fs_root.lookup(dimpl_path) 

157 if dimpl is None or dimpl.is_dir: 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true

158 _error( 

159 f'The path listed in "Dependents" ("{dimpl_path}") does not exist' 

160 f" in the package. ({error_context} in {provided_alternatives_fs_path})" 

161 ) 

162 seen_link_path.add(dlink_path) 

163 install_command.append(LINE_PREFIX) 

164 install_command.append( 

165 escape_shell( 

166 # update-alternatives still uses this old option name :-/ 

167 "--slave", 

168 dlink_path, 

169 dlink_name, 

170 dimpl_path, 

171 ) 

172 ) 

173 postinst = textwrap.dedent( 

174 """\ 

175 if {CONDITION}; then 

176 {COMMAND} 

177 fi 

178 """ 

179 ).format( 

180 CONDITION=POSTINST_DEFAULT_CONDITION, 

181 COMMAND=" ".join(install_command), 

182 ) 

183 

184 prerm = textwrap.dedent( 

185 """\ 

186 if [ "$1" = "remove" ]; then 

187 {COMMAND} 

188 fi 

189 """ 

190 ).format(COMMAND=" ".join(remove_command)) 

191 maintscript_snippets["postinst"].append( 

192 MaintscriptSnippet( 

193 f"debputy (via {provided_alternatives_fs_path})", 

194 snippet=postinst, 

195 ) 

196 ) 

197 maintscript_snippets["prerm"].append( 

198 MaintscriptSnippet( 

199 f"debputy (via {provided_alternatives_fs_path})", 

200 snippet=prerm, 

201 ) 

202 ) 

203 

204 

205def parse_dependent_link( 

206 line: str, 

207 error_context: str, 

208 provided_alternatives_file: str, 

209) -> tuple[str, str, str]: 

210 parts = line.split() 

211 if len(parts) != 3: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true

212 _error( 

213 f"The each line in Dependents links must have exactly 3 space separated parts." 

214 f' The "{line}" split into {len(parts)} part(s).' 

215 f" ({error_context} in {provided_alternatives_file})" 

216 ) 

217 

218 dlink_path, dlink_name, dimpl_path = parts 

219 if "/" in dlink_name: 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true

220 _error( 

221 f'The Dependent link name "{dlink_path}" must be a basename and cannot contain slashes' 

222 f" ({error_context} in {provided_alternatives_file})" 

223 ) 

224 if dlink_path == dimpl_path: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 _error( 

226 f'The Dependent Link path and Alternative must not have the same value ["{dlink_path}"]' 

227 f" ({error_context} in {provided_alternatives_file})" 

228 ) 

229 return dlink_path, dlink_name, dimpl_path 

230 

231 

232def _mandatory_key( 

233 key: str, 

234 alternative_deb822: Mapping[str, str], 

235 provided_alternatives_file: str, 

236 error_context: str, 

237) -> str: 

238 try: 

239 return alternative_deb822[key] 

240 except KeyError: 

241 _error( 

242 f'Missing mandatory key "{key}" in {provided_alternatives_file} ({error_context})' 

243 )