Coverage for daklib/packagelist.py: 94%
112 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-03-14 12:19 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-03-14 12:19 +0000
1"""parse Package-List field
3@copyright: 2014, Ansgar Burchardt <ansgar@debian.org>
4@license: GPL-2+
5"""
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21from typing import TYPE_CHECKING, Optional
23from daklib.architecture import match_architecture
24from daklib.utils import extract_component_from_section
26if TYPE_CHECKING:
27 from collections.abc import Mapping
29 from daklib.dbconn import MetadataProxy, Suite
32class InvalidSource(Exception):
33 pass
36class PackageListEntry:
37 def __init__(
38 self,
39 name: str,
40 package_type: str | None,
41 section: str | None,
42 component: str | None,
43 priority: str | None,
44 **other: str,
45 ):
46 self.name = name
47 self.type = package_type
48 self.section = section
49 self.component = component
50 self.priority = priority
51 self.other = other
53 self.architectures = self._architectures()
55 def _architectures(self) -> Optional[list[str]]:
56 archs = self.other.get("arch", None)
57 if archs is None:
58 return None
59 return archs.split(",")
61 def built_on_architecture(self, architecture: str) -> Optional[bool]:
62 archs = self.architectures
63 if archs is None:
64 return None
65 for arch in archs:
66 if match_architecture(architecture, arch):
67 return True
68 return False
70 def built_in_suite(self, suite: "Suite") -> Optional[bool]:
71 built: Optional[bool] = False
72 for arch in suite.architectures:
73 if arch.arch_string == "source":
74 continue
75 built_on_arch = self.built_on_architecture(arch.arch_string)
76 if built_on_arch:
77 return True
78 if built_on_arch is None:
79 built = None
80 return built
82 def built_in_default_profile_v0(self) -> bool:
83 # See man:dsc(5), this property has been deprecated by profile:v1
84 profiles_and = self.other.get("profile")
85 if profiles_and is None:
86 return True
87 return all(
88 any(profile.startswith("!") for profile in profiles_or.split("+"))
89 for profiles_or in profiles_and.split(",")
90 )
92 def built_in_default_profile(self) -> bool:
93 # See man:dsc(5) and https://bugs.debian.org/913965#77
94 profiles_and = self.other.get("profile:v1")
95 if profiles_and is None:
96 return self.built_in_default_profile_v0()
97 return all(
98 any(profile.startswith("!") for profile in profiles_or.split("|"))
99 for profiles_or in profiles_and.split("&")
100 )
103class PackageList:
104 def __init__(self, source: "Mapping[str, str] | MetadataProxy"):
105 if "Package-List" in source:
106 self.package_list = self._parse(source)
107 elif "Binary" in source: 107 ↛ 110line 107 didn't jump to line 110 because the condition on line 107 was always true
108 self.package_list = self._parse_fallback(source)
109 else:
110 raise InvalidSource(
111 "Source package has neither Package-List nor Binary field."
112 )
114 self.fallback = any(entry.architectures is None for entry in self.package_list)
116 def _binaries(self, source: "Mapping[str, str] | MetadataProxy") -> set[str]:
117 return set(name.strip() for name in source["Binary"].split(","))
119 def _parse(
120 self, source: "Mapping[str, str] | MetadataProxy"
121 ) -> list[PackageListEntry]:
122 package_list = []
124 binaries_binary = self._binaries(source)
125 binaries_package_list = set()
127 for line in source["Package-List"].split("\n"):
128 if not line:
129 continue
130 fields = line.split()
131 if len(fields) < 4: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true
132 raise InvalidSource("Package-List entry has less than four fields.")
134 # <name> <type> <component/section> <priority> [arch=<arch>[,<arch>]...]
135 name = fields[0]
136 package_type = fields[1]
137 section, component = extract_component_from_section(fields[2])
138 priority = fields[3]
139 other = dict(kv.split("=", 1) for kv in fields[4:])
141 if name in binaries_package_list: 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 raise InvalidSource(
143 "Package-List has two entries for '{0}'.".format(name)
144 )
145 if name not in binaries_binary: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 raise InvalidSource(
147 "Package-List lists {0} which is not listed in Binary.".format(name)
148 )
149 binaries_package_list.add(name)
151 entry = PackageListEntry(
152 name, package_type, section, component, priority, **other
153 )
154 package_list.append(entry)
156 if len(binaries_binary) != len(binaries_package_list): 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true
157 raise InvalidSource(
158 "Package-List and Binaries fields have a different number of entries."
159 )
161 return package_list
163 def _parse_fallback(
164 self, source: "Mapping[str, str] | MetadataProxy"
165 ) -> list[PackageListEntry]:
166 return [
167 PackageListEntry(binary, None, None, None, None)
168 for binary in self._binaries(source)
169 ]
171 def packages_for_suite(
172 self, suite: "Suite", only_default_profile=True
173 ) -> list[PackageListEntry]:
174 packages = []
175 for entry in self.package_list:
176 if only_default_profile and not entry.built_in_default_profile():
177 continue
178 built = entry.built_in_suite(suite)
179 if built or built is None:
180 packages.append(entry)
181 return packages
183 def has_arch_indep_packages(self) -> Optional[bool]:
184 has_arch_indep: Optional[bool] = False
185 for entry in self.package_list:
186 built = entry.built_on_architecture("all")
187 if built:
188 return True
189 if built is None:
190 has_arch_indep = None
191 return has_arch_indep
193 def has_arch_dep_packages(self) -> Optional[bool]:
194 has_arch_dep: Optional[bool] = False
195 for entry in self.package_list:
196 built_on_all = entry.built_on_architecture("all")
197 if built_on_all is False:
198 return True
199 if built_on_all is None:
200 has_arch_dep = None
201 return has_arch_dep