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

80 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-04-19 20:37 +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 if {CONDITION}; then 

175 {COMMAND} 

176 fi 

177 """).format( 

178 CONDITION=POSTINST_DEFAULT_CONDITION, 

179 COMMAND=" ".join(install_command), 

180 ) 

181 

182 prerm = textwrap.dedent("""\ 

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

184 {COMMAND} 

185 fi 

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

187 maintscript_snippets["postinst"].append( 

188 MaintscriptSnippet( 

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

190 snippet=postinst, 

191 ) 

192 ) 

193 maintscript_snippets["prerm"].append( 

194 MaintscriptSnippet( 

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

196 snippet=prerm, 

197 ) 

198 ) 

199 

200 

201def parse_dependent_link( 

202 line: str, 

203 error_context: str, 

204 provided_alternatives_file: str, 

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

206 parts = line.split() 

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

208 _error( 

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

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

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

212 ) 

213 

214 dlink_path, dlink_name, dimpl_path = parts 

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

216 _error( 

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

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

219 ) 

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

221 _error( 

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

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

224 ) 

225 return dlink_path, dlink_name, dimpl_path 

226 

227 

228def _mandatory_key( 

229 key: str, 

230 alternative_deb822: Mapping[str, str], 

231 provided_alternatives_file: str, 

232 error_context: str, 

233) -> str: 

234 try: 

235 return alternative_deb822[key] 

236 except KeyError: 

237 _error( 

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

239 )