Coverage for src/debputy/packaging/alternatives.py: 77%
80 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-06-06 10:55 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-06-06 10:55 +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 (
10 MaintscriptSnippetContainer,
11 MaintscriptSnippet,
12 SnippetResolver,
13)
14from debputy.packager_provided_files import PackagerProvidedFile
15from debputy.packages import BinaryPackage
16from debputy.packaging.makeshlibs import resolve_reserved_provided_file
17from debputy.plugin.api import VirtualPath
18from debputy.util import _error, escape_shell, POSTINST_DEFAULT_CONDITION
20# Match debhelper (minus one space in each end, which comes
21# via join).
22LINE_PREFIX = "\\\n "
24SYSTEM_DEFAULT_PATH_DIRS = frozenset(
25 {
26 "/usr/bin",
27 "/bin",
28 "/usr/sbin",
29 "/sbin",
30 "/usr/games",
31 }
32)
35def process_alternatives(
36 binary_package: BinaryPackage,
37 fs_root: VirtualPath,
38 reserved_packager_provided_files: dict[str, list[PackagerProvidedFile]],
39 maintscript_snippets: dict[str, MaintscriptSnippetContainer],
40 substvars: Substvars,
41) -> None:
42 if binary_package.is_udeb: 42 ↛ 43line 42 didn't jump to line 43 because the condition on line 42 was never true
43 return
45 provided_alternatives_file = resolve_reserved_provided_file(
46 "alternatives",
47 reserved_packager_provided_files,
48 )
49 if provided_alternatives_file is None: 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true
50 return
52 with provided_alternatives_file.open() as fd:
53 alternatives = list(Deb822.iter_paragraphs(fd))
55 for no, alternative in enumerate(alternatives):
56 process_alternative(
57 provided_alternatives_file.fs_path,
58 fs_root,
59 alternative,
60 no,
61 maintscript_snippets,
62 substvars,
63 )
66def process_alternative(
67 provided_alternatives_fs_path: str,
68 fs_root: VirtualPath,
69 alternative_deb822: Deb822,
70 no: int,
71 maintscript_snippets: dict[str, MaintscriptSnippetContainer],
72 substvars: Substvars,
73) -> None:
74 name = _mandatory_key(
75 "Name",
76 alternative_deb822,
77 provided_alternatives_fs_path,
78 f"Stanza number {no}",
79 )
80 error_context = f"Alternative named {name}"
81 link_path = _mandatory_key(
82 "Link",
83 alternative_deb822,
84 provided_alternatives_fs_path,
85 error_context,
86 )
87 impl_path = _mandatory_key(
88 "Alternative",
89 alternative_deb822,
90 provided_alternatives_fs_path,
91 error_context,
92 )
93 priority = _mandatory_key(
94 "Priority",
95 alternative_deb822,
96 provided_alternatives_fs_path,
97 error_context,
98 )
99 if "/" in name: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 _error(
101 f'The "Name" ({link_path}) key must be a basename and cannot contain slashes'
102 f" ({error_context} in {provided_alternatives_fs_path})"
103 )
104 if link_path == impl_path: 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true
105 _error(
106 f'The "Link" key and the "Alternative" key must not have the same value'
107 f" ({error_context} in {provided_alternatives_fs_path})"
108 )
109 impl = fs_root.lookup(impl_path)
110 if impl is None or impl.is_dir: 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 _error(
112 f'The path listed in "Alternative" ("{impl_path}") does not exist'
113 f" in the package. ({error_context} in {provided_alternatives_fs_path})"
114 )
115 link_parent, link_basename = os.path.split(link_path)
116 if link_parent in SYSTEM_DEFAULT_PATH_DIRS: 116 ↛ 119line 116 didn't jump to line 119 because the condition on line 116 was always true
117 # Not really a dependency, but uses the same rules (comma-separated and only one instance).
118 substvars.add_dependency("misc:Command", link_basename)
119 for key in ["Slave", "Slaves", "Slave-Links"]:
120 if key in alternative_deb822: 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 _error(
122 f'Please use "Dependents" instead of "{key}".'
123 f" ({error_context} in {provided_alternatives_fs_path})"
124 )
125 dependents = alternative_deb822.get("Dependents")
126 install_command = [
127 escape_shell(
128 "update-alternatives",
129 "--install",
130 link_path,
131 name,
132 impl_path,
133 priority,
134 )
135 ]
136 remove_command = [
137 escape_shell(
138 "update-alternatives",
139 "--remove",
140 name,
141 impl_path,
142 )
143 ]
144 if dependents: 144 ↛ 177line 144 didn't jump to line 177 because the condition on line 144 was always true
145 seen_link_path = set()
146 for line in dependents.splitlines():
147 line = line.strip()
148 if not line: # First line is usually empty
149 continue
150 dlink_path, dlink_name, dimpl_path = parse_dependent_link(
151 line,
152 error_context,
153 provided_alternatives_fs_path,
154 )
155 if dlink_path in seen_link_path: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true
156 _error(
157 f'The Dependent link path "{dlink_path}" was used twice.'
158 f" ({error_context} in {provided_alternatives_fs_path})"
159 )
160 dimpl = fs_root.lookup(dimpl_path)
161 if dimpl is None or dimpl.is_dir: 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true
162 _error(
163 f'The path listed in "Dependents" ("{dimpl_path}") does not exist'
164 f" in the package. ({error_context} in {provided_alternatives_fs_path})"
165 )
166 seen_link_path.add(dlink_path)
167 install_command.append(LINE_PREFIX)
168 install_command.append(
169 escape_shell(
170 # update-alternatives still uses this old option name :-/
171 "--slave",
172 dlink_path,
173 dlink_name,
174 dimpl_path,
175 )
176 )
177 postinst = textwrap.dedent("""\
178 if {CONDITION}; then
179 {COMMAND}
180 fi
181 """).format(
182 CONDITION=POSTINST_DEFAULT_CONDITION,
183 COMMAND=" ".join(install_command),
184 )
186 prerm = textwrap.dedent("""\
187 if [ "$1" = "remove" ]; then
188 {COMMAND}
189 fi
190 """).format(COMMAND=" ".join(remove_command))
191 maintscript_snippets["postinst"].append(
192 MaintscriptSnippet(
193 f"debputy (via {provided_alternatives_fs_path})",
194 snippet=SnippetResolver.snippet_template(postinst),
195 )
196 )
197 maintscript_snippets["prerm"].append(
198 MaintscriptSnippet(
199 f"debputy (via {provided_alternatives_fs_path})",
200 snippet=SnippetResolver.snippet_template(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 )