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