Coverage for src/debputy/packaging/alternatives.py: 77%
80 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +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 """\
175 if {CONDITION}; then
176 {COMMAND}
177 fi
178 """
179 ).format(
180 CONDITION=POSTINST_DEFAULT_CONDITION,
181 COMMAND=" ".join(install_command),
182 )
184 prerm = textwrap.dedent(
185 """\
186 if [ "$1" = "remove" ]; then
187 {COMMAND}
188 fi
189 """
190 ).format(COMMAND=" ".join(remove_command))
191 maintscript_snippets["postinst"].append(
192 MaintscriptSnippet(
193 f"debputy (via {provided_alternatives_fs_path})",
194 snippet=postinst,
195 )
196 )
197 maintscript_snippets["prerm"].append(
198 MaintscriptSnippet(
199 f"debputy (via {provided_alternatives_fs_path})",
200 snippet=prerm,
201 )
202 )
205def parse_dependent_link(
206 line: str,
207 error_context: str,
208 provided_alternatives_file: str,
209) -> tuple[str, str, str]:
210 parts = line.split()
211 if len(parts) != 3: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 _error(
213 f"The each line in Dependents links must have exactly 3 space separated parts."
214 f' The "{line}" split into {len(parts)} part(s).'
215 f" ({error_context} in {provided_alternatives_file})"
216 )
218 dlink_path, dlink_name, dimpl_path = parts
219 if "/" in dlink_name: 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 _error(
221 f'The Dependent link name "{dlink_path}" must be a basename and cannot contain slashes'
222 f" ({error_context} in {provided_alternatives_file})"
223 )
224 if dlink_path == dimpl_path: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true
225 _error(
226 f'The Dependent Link path and Alternative must not have the same value ["{dlink_path}"]'
227 f" ({error_context} in {provided_alternatives_file})"
228 )
229 return dlink_path, dlink_name, dimpl_path
232def _mandatory_key(
233 key: str,
234 alternative_deb822: Mapping[str, str],
235 provided_alternatives_file: str,
236 error_context: str,
237) -> str:
238 try:
239 return alternative_deb822[key]
240 except KeyError:
241 _error(
242 f'Missing mandatory key "{key}" in {provided_alternatives_file} ({error_context})'
243 )