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
« 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
5from debian.deb822 import Deb822
6from debian.substvars import Substvars
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
15# Match debhelper (minus one space in each end, which comes
16# via join).
17LINE_PREFIX = "\\\n "
19SYSTEM_DEFAULT_PATH_DIRS = frozenset(
20 {
21 "/usr/bin",
22 "/bin",
23 "/usr/sbin",
24 "/sbin",
25 "/usr/games",
26 }
27)
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
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
47 with provided_alternatives_file.open() as fd:
48 alternatives = list(Deb822.iter_paragraphs(fd))
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 )
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 )
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 )
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 )
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
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 )