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

80 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-06-06 10:55 +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 ( 

10 MaintscriptSnippetContainer, 

11 MaintscriptSnippet, 

12 SnippetResolver, 

13) 

14from debputy.packager_provided_files import PackagerProvidedFile 

15from debputy.packages import BinaryPackage 

16from debputy.packaging.makeshlibs import resolve_reserved_provided_file 

17from debputy.plugin.api import VirtualPath 

18from debputy.util import _error, escape_shell, POSTINST_DEFAULT_CONDITION 

19 

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

21# via join). 

22LINE_PREFIX = "\\\n " 

23 

24SYSTEM_DEFAULT_PATH_DIRS = frozenset( 

25 { 

26 "/usr/bin", 

27 "/bin", 

28 "/usr/sbin", 

29 "/sbin", 

30 "/usr/games", 

31 } 

32) 

33 

34 

35def process_alternatives( 

36 binary_package: BinaryPackage, 

37 fs_root: VirtualPath, 

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

39 maintscript_snippets: dict[str, MaintscriptSnippetContainer], 

40 substvars: Substvars, 

41) -> None: 

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

43 return 

44 

45 provided_alternatives_file = resolve_reserved_provided_file( 

46 "alternatives", 

47 reserved_packager_provided_files, 

48 ) 

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

50 return 

51 

52 with provided_alternatives_file.open() as fd: 

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

54 

55 for no, alternative in enumerate(alternatives): 

56 process_alternative( 

57 provided_alternatives_file.fs_path, 

58 fs_root, 

59 alternative, 

60 no, 

61 maintscript_snippets, 

62 substvars, 

63 ) 

64 

65 

66def process_alternative( 

67 provided_alternatives_fs_path: str, 

68 fs_root: VirtualPath, 

69 alternative_deb822: Deb822, 

70 no: int, 

71 maintscript_snippets: dict[str, MaintscriptSnippetContainer], 

72 substvars: Substvars, 

73) -> None: 

74 name = _mandatory_key( 

75 "Name", 

76 alternative_deb822, 

77 provided_alternatives_fs_path, 

78 f"Stanza number {no}", 

79 ) 

80 error_context = f"Alternative named {name}" 

81 link_path = _mandatory_key( 

82 "Link", 

83 alternative_deb822, 

84 provided_alternatives_fs_path, 

85 error_context, 

86 ) 

87 impl_path = _mandatory_key( 

88 "Alternative", 

89 alternative_deb822, 

90 provided_alternatives_fs_path, 

91 error_context, 

92 ) 

93 priority = _mandatory_key( 

94 "Priority", 

95 alternative_deb822, 

96 provided_alternatives_fs_path, 

97 error_context, 

98 ) 

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

100 _error( 

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

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

103 ) 

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

105 _error( 

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

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

108 ) 

109 impl = fs_root.lookup(impl_path) 

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

111 _error( 

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

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

114 ) 

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

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

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

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

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

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

121 _error( 

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

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

124 ) 

125 dependents = alternative_deb822.get("Dependents") 

126 install_command = [ 

127 escape_shell( 

128 "update-alternatives", 

129 "--install", 

130 link_path, 

131 name, 

132 impl_path, 

133 priority, 

134 ) 

135 ] 

136 remove_command = [ 

137 escape_shell( 

138 "update-alternatives", 

139 "--remove", 

140 name, 

141 impl_path, 

142 ) 

143 ] 

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

145 seen_link_path = set() 

146 for line in dependents.splitlines(): 

147 line = line.strip() 

148 if not line: # First line is usually empty 

149 continue 

150 dlink_path, dlink_name, dimpl_path = parse_dependent_link( 

151 line, 

152 error_context, 

153 provided_alternatives_fs_path, 

154 ) 

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

156 _error( 

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

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

159 ) 

160 dimpl = fs_root.lookup(dimpl_path) 

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

162 _error( 

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

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

165 ) 

166 seen_link_path.add(dlink_path) 

167 install_command.append(LINE_PREFIX) 

168 install_command.append( 

169 escape_shell( 

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

171 "--slave", 

172 dlink_path, 

173 dlink_name, 

174 dimpl_path, 

175 ) 

176 ) 

177 postinst = textwrap.dedent("""\ 

178 if {CONDITION}; then 

179 {COMMAND} 

180 fi 

181 """).format( 

182 CONDITION=POSTINST_DEFAULT_CONDITION, 

183 COMMAND=" ".join(install_command), 

184 ) 

185 

186 prerm = textwrap.dedent("""\ 

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

188 {COMMAND} 

189 fi 

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

191 maintscript_snippets["postinst"].append( 

192 MaintscriptSnippet( 

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

194 snippet=SnippetResolver.snippet_template(postinst), 

195 ) 

196 ) 

197 maintscript_snippets["prerm"].append( 

198 MaintscriptSnippet( 

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

200 snippet=SnippetResolver.snippet_template(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 )