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