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

79 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-22 10:58 +0000

1import os 

2import textwrap 

3from collections.abc import Mapping 

4 

5from debian.deb822 import Deb822 

6from debian.substvars import Substvars 

7 

8from debputy.maintscript_snippet import ( 

9 UnboundMaintscriptSnippet, 

10 SnippetResolver, 

11 PackageMaintscriptSnippetContainer, 

12) 

13from debputy.packager_provided_files import PackagerProvidedFile 

14from debputy.packages import BinaryPackage 

15from debputy.packaging.makeshlibs import resolve_reserved_provided_file 

16from debputy.plugin.api import VirtualPath 

17from debputy.util import _error, escape_shell, POSTINST_DEFAULT_CONDITION 

18 

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

20# via join). 

21LINE_PREFIX = "\\\n " 

22 

23SYSTEM_DEFAULT_PATH_DIRS = frozenset( 

24 { 

25 "/usr/bin", 

26 "/bin", 

27 "/usr/sbin", 

28 "/sbin", 

29 "/usr/games", 

30 } 

31) 

32 

33 

34def process_alternatives( 

35 binary_package: BinaryPackage, 

36 fs_root: VirtualPath, 

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

38 maintscript_snippets: PackageMaintscriptSnippetContainer, 

39 substvars: Substvars, 

40) -> None: 

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

42 return 

43 

44 provided_alternatives_file = resolve_reserved_provided_file( 

45 "alternatives", 

46 reserved_packager_provided_files, 

47 ) 

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

49 return 

50 

51 with provided_alternatives_file.open() as fd: 

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

53 

54 for no, alternative in enumerate(alternatives): 

55 process_alternative( 

56 provided_alternatives_file.fs_path, 

57 fs_root, 

58 alternative, 

59 no, 

60 maintscript_snippets, 

61 substvars, 

62 ) 

63 

64 

65def process_alternative( 

66 provided_alternatives_fs_path: str, 

67 fs_root: VirtualPath, 

68 alternative_deb822: Deb822, 

69 no: int, 

70 maintscript_snippets: PackageMaintscriptSnippetContainer, 

71 substvars: Substvars, 

72) -> None: 

73 name = _mandatory_key( 

74 "Name", 

75 alternative_deb822, 

76 provided_alternatives_fs_path, 

77 f"Stanza number {no}", 

78 ) 

79 error_context = f"Alternative named {name}" 

80 link_path = _mandatory_key( 

81 "Link", 

82 alternative_deb822, 

83 provided_alternatives_fs_path, 

84 error_context, 

85 ) 

86 impl_path = _mandatory_key( 

87 "Alternative", 

88 alternative_deb822, 

89 provided_alternatives_fs_path, 

90 error_context, 

91 ) 

92 priority = _mandatory_key( 

93 "Priority", 

94 alternative_deb822, 

95 provided_alternatives_fs_path, 

96 error_context, 

97 ) 

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

99 _error( 

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

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

102 ) 

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

104 _error( 

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

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

107 ) 

108 impl = fs_root.lookup(impl_path) 

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

110 _error( 

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

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

113 ) 

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

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

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

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

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

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

120 _error( 

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

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

123 ) 

124 dependents = alternative_deb822.get("Dependents") 

125 install_command = [ 

126 escape_shell( 

127 "update-alternatives", 

128 "--install", 

129 link_path, 

130 name, 

131 impl_path, 

132 priority, 

133 ) 

134 ] 

135 remove_command = [ 

136 escape_shell( 

137 "update-alternatives", 

138 "--remove", 

139 name, 

140 impl_path, 

141 ) 

142 ] 

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

144 seen_link_path = set() 

145 for line in dependents.splitlines(): 

146 line = line.strip() 

147 if not line: # First line is usually empty 

148 continue 

149 dlink_path, dlink_name, dimpl_path = parse_dependent_link( 

150 line, 

151 error_context, 

152 provided_alternatives_fs_path, 

153 ) 

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

155 _error( 

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

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

158 ) 

159 dimpl = fs_root.lookup(dimpl_path) 

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

161 _error( 

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

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

164 ) 

165 seen_link_path.add(dlink_path) 

166 install_command.append(LINE_PREFIX) 

167 install_command.append( 

168 escape_shell( 

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

170 "--slave", 

171 dlink_path, 

172 dlink_name, 

173 dimpl_path, 

174 ) 

175 ) 

176 postinst = textwrap.dedent("""\ 

177 if {CONDITION}; then 

178 {COMMAND} 

179 fi 

180 """).format( 

181 CONDITION=POSTINST_DEFAULT_CONDITION, 

182 COMMAND=" ".join(install_command), 

183 ) 

184 

185 prerm = textwrap.dedent("""\ 

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

187 {COMMAND} 

188 fi 

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

190 maintscript_snippets["postinst"].append( 

191 UnboundMaintscriptSnippet( 

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

193 snippet=SnippetResolver.snippet(postinst), 

194 ) 

195 ) 

196 maintscript_snippets["prerm"].append( 

197 UnboundMaintscriptSnippet( 

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

199 snippet=SnippetResolver.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 )