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

79 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +0000

1import os 

2import textwrap 

3from typing import List, Dict, Tuple, Mapping 

4 

5from debian.deb822 import Deb822 

6from debian.substvars import Substvars 

7 

8from debputy.maintscript_snippet import MaintscriptSnippetContainer, MaintscriptSnippet 

9from debputy.packager_provided_files import PackagerProvidedFile 

10from debputy.packages import BinaryPackage 

11from debputy.packaging.makeshlibs import resolve_reserved_provided_file 

12from debputy.plugin.api import VirtualPath 

13from debputy.util import _error, escape_shell, POSTINST_DEFAULT_CONDITION 

14 

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

16# via join). 

17LINE_PREFIX = "\\\n " 

18 

19SYSTEM_DEFAULT_PATH_DIRS = frozenset( 

20 { 

21 "/usr/bin", 

22 "/bin", 

23 "/usr/sbin", 

24 "/sbin", 

25 "/usr/games", 

26 } 

27) 

28 

29 

30def process_alternatives( 

31 binary_package: BinaryPackage, 

32 fs_root: VirtualPath, 

33 reserved_packager_provided_files: Dict[str, List[PackagerProvidedFile]], 

34 maintscript_snippets: Dict[str, MaintscriptSnippetContainer], 

35 substvars: Substvars, 

36) -> None: 

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

38 return 

39 

40 provided_alternatives_file = resolve_reserved_provided_file( 

41 "alternatives", 

42 reserved_packager_provided_files, 

43 ) 

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

45 return 

46 

47 with provided_alternatives_file.open() as fd: 

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

49 

50 for no, alternative in enumerate(alternatives): 

51 process_alternative( 

52 provided_alternatives_file.fs_path, 

53 fs_root, 

54 alternative, 

55 no, 

56 maintscript_snippets, 

57 substvars, 

58 ) 

59 

60 

61def process_alternative( 

62 provided_alternatives_fs_path: str, 

63 fs_root: VirtualPath, 

64 alternative_deb822: Deb822, 

65 no: int, 

66 maintscript_snippets: Dict[str, MaintscriptSnippetContainer], 

67 substvars: Substvars, 

68) -> None: 

69 name = _mandatory_key( 

70 "Name", 

71 alternative_deb822, 

72 provided_alternatives_fs_path, 

73 f"Stanza number {no}", 

74 ) 

75 error_context = f"Alternative named {name}" 

76 link_path = _mandatory_key( 

77 "Link", 

78 alternative_deb822, 

79 provided_alternatives_fs_path, 

80 error_context, 

81 ) 

82 impl_path = _mandatory_key( 

83 "Alternative", 

84 alternative_deb822, 

85 provided_alternatives_fs_path, 

86 error_context, 

87 ) 

88 priority = _mandatory_key( 

89 "Priority", 

90 alternative_deb822, 

91 provided_alternatives_fs_path, 

92 error_context, 

93 ) 

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

95 _error( 

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

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

98 ) 

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

100 _error( 

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

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

103 ) 

104 impl = fs_root.lookup(impl_path) 

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

106 _error( 

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

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

109 ) 

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

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

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

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

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

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

116 _error( 

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

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

119 ) 

120 dependents = alternative_deb822.get("Dependents") 

121 install_command = [ 

122 escape_shell( 

123 "update-alternatives", 

124 "--install", 

125 link_path, 

126 name, 

127 impl_path, 

128 priority, 

129 ) 

130 ] 

131 remove_command = [ 

132 escape_shell( 

133 "update-alternatives", 

134 "--remove", 

135 name, 

136 impl_path, 

137 ) 

138 ] 

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

140 seen_link_path = set() 

141 for line in dependents.splitlines(): 

142 line = line.strip() 

143 if not line: # First line is usually empty 

144 continue 

145 dlink_path, dlink_name, dimpl_path = parse_dependent_link( 

146 line, 

147 error_context, 

148 provided_alternatives_fs_path, 

149 ) 

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

151 _error( 

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

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

154 ) 

155 dimpl = fs_root.lookup(dimpl_path) 

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

157 _error( 

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

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

160 ) 

161 seen_link_path.add(dlink_path) 

162 install_command.append(LINE_PREFIX) 

163 install_command.append( 

164 escape_shell( 

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

166 "--slave", 

167 dlink_path, 

168 dlink_name, 

169 dimpl_path, 

170 ) 

171 ) 

172 postinst = textwrap.dedent( 

173 """\ 

174 if {CONDITION}; then 

175 {COMMAND} 

176 fi 

177 """ 

178 ).format( 

179 CONDITION=POSTINST_DEFAULT_CONDITION, 

180 COMMAND=" ".join(install_command), 

181 ) 

182 

183 prerm = textwrap.dedent( 

184 """\ 

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

186 {COMMAND} 

187 fi 

188 """ 

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

190 maintscript_snippets["postinst"].append( 

191 MaintscriptSnippet( 

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

193 snippet=postinst, 

194 ) 

195 ) 

196 maintscript_snippets["prerm"].append( 

197 MaintscriptSnippet( 

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

199 snippet=prerm, 

200 ) 

201 ) 

202 

203 

204def parse_dependent_link( 

205 line: str, 

206 error_context: str, 

207 provided_alternatives_file: str, 

208) -> Tuple[str, str, str]: 

209 parts = line.split() 

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

211 _error( 

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

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

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

215 ) 

216 

217 dlink_path, dlink_name, dimpl_path = parts 

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

219 _error( 

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

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

222 ) 

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

224 _error( 

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

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

227 ) 

228 return dlink_path, dlink_name, dimpl_path 

229 

230 

231def _mandatory_key( 

232 key: str, 

233 alternative_deb822: Mapping[str, str], 

234 provided_alternatives_file: str, 

235 error_context: str, 

236) -> str: 

237 try: 

238 return alternative_deb822[key] 

239 except KeyError: 

240 _error( 

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

242 )