Coverage for daklib/cruft.py: 87%

86 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-08-03 16:46 +0000

1""" 

2helper functions for cruft-report 

3 

4@contact: Debian FTPMaster <ftpmaster@debian.org> 

5@copyright 2011 Torsten Werner <twerner@debian.org> 

6""" 

7 

8# This program is free software; you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation; either version 2 of the License, or 

11# (at your option) any later version. 

12 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17 

18# You should have received a copy of the GNU General Public License 

19# along with this program; if not, write to the Free Software 

20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

21 

22################################################################################ 

23 

24from typing import TYPE_CHECKING, cast, override 

25 

26from sqlalchemy import func, select, sql 

27from sqlalchemy.engine import CursorResult 

28from sqlalchemy.orm import aliased, object_session, with_parent 

29 

30from daklib.dbconn import Architecture, DBBinary, DBSource, Suite, get_suite 

31 

32if TYPE_CHECKING: 

33 from sqlalchemy.orm import Session 

34 from sqlalchemy.sql import FromClause, Select 

35 

36 

37def newer_version( 

38 lowersuite_name: str, highersuite_name: str, session: "Session", include_equal=False 

39) -> list[tuple[str, str, str]]: 

40 """ 

41 Finds newer versions in lowersuite_name than in highersuite_name. Returns a 

42 list of tuples (source, higherversion, lowerversion) where higherversion is 

43 the newest version from highersuite_name and lowerversion is the newest 

44 version from lowersuite_name. 

45 """ 

46 

47 lowersuite = get_suite(lowersuite_name, session) 

48 assert lowersuite is not None 

49 highersuite = get_suite(highersuite_name, session) 

50 assert highersuite is not None 

51 

52 def get_suite_sources(suite: Suite) -> "FromClause": 

53 q1 = ( 

54 select(DBSource.source, func.max(DBSource.version).label("version")) 

55 .where(with_parent(suite, Suite.sources)) 

56 .group_by(DBSource.source) 

57 .subquery() 

58 ) 

59 return aliased(q1) 

60 

61 def get_suite_binaries(suite: Suite) -> "FromClause": 

62 q1 = ( 

63 select( 

64 DBBinary.package, 

65 DBSource.source, 

66 func.max(DBSource.version).label("version"), 

67 Architecture.arch_string, 

68 func.max(DBBinary.version).label("binversion"), 

69 ) 

70 .where(DBBinary.suites.contains(suite)) 

71 .join(DBBinary.source) 

72 .join(DBBinary.architecture) 

73 .group_by( 

74 DBBinary.package, 

75 DBSource.source, 

76 Architecture.arch_string, 

77 ) 

78 .subquery() 

79 ) 

80 return aliased(q1) 

81 

82 highq = get_suite_sources(highersuite) 

83 lowq = get_suite_sources(lowersuite) 

84 

85 query = select( 

86 highq.c.source, 

87 highq.c.version.label("higherversion"), 

88 lowq.c.version.label("lowerversion"), 

89 ).join(lowq, highq.c.source == lowq.c.source) 

90 

91 if include_equal: 

92 query = query.where(highq.c.version <= lowq.c.version) 

93 else: 

94 query = query.where(highq.c.version < lowq.c.version) 

95 

96 list = [] 

97 # get all sources that have a higher version in lowersuite than in 

98 # highersuite 

99 for source, higherversion, lowerversion in session.execute(query): 

100 q1 = ( 

101 select( 

102 DBBinary.package, 

103 DBSource.source, 

104 DBSource.version, 

105 Architecture.arch_string, 

106 ) 

107 .where(DBBinary.suites.contains(highersuite)) 

108 .join(DBBinary.source) 

109 .join(DBBinary.architecture) 

110 .where(DBSource.source == source) 

111 .subquery() 

112 ) 

113 q2 = select(q1.c.arch_string).group_by(q1.c.arch_string) 

114 # all architectures for which source has binaries in highersuite 

115 archs_high = {x[0] for x in session.execute(q2)} 

116 

117 highq = get_suite_binaries(highersuite) 

118 lowq = get_suite_binaries(lowersuite) 

119 

120 arch_query = ( 

121 select(highq.c.arch_string) 

122 .join(lowq, highq.c.source == lowq.c.source) 

123 .where(highq.c.arch_string == lowq.c.arch_string) 

124 .where(highq.c.package == lowq.c.package) 

125 .where(highq.c.source == source) 

126 ) 

127 

128 if include_equal: 

129 arch_query = arch_query.where( 

130 highq.c.binversion <= lowq.c.binversion 

131 ).where(highq.c.version <= lowq.c.version) 

132 else: 

133 arch_query = arch_query.where(highq.c.binversion < lowq.c.binversion).where( 

134 highq.c.version < lowq.c.version 

135 ) 

136 

137 arch_query = arch_query.group_by(highq.c.arch_string) 

138 

139 # all architectures for which source has a newer binary in lowersuite 

140 archs_newer = {x[0] for x in session.execute(arch_query)} 

141 

142 # if has at least one binary in lowersuite which is newer than the one 

143 # in highersuite on each architecture for which source has binaries in 

144 # highersuite, we know that the builds for all relevant architecture 

145 # are done, so we can remove the old source with it's binaries 

146 if archs_newer >= archs_high: 

147 list.append((source, higherversion, lowerversion)) 

148 

149 list.sort() 

150 return list 

151 

152 

153def get_package_names(suite: Suite) -> "Select[tuple[str]]": 

154 """ 

155 Returns a select() that selects all distinct package names from suite 

156 ordered by package name. 

157 """ 

158 

159 return ( 

160 select(DBBinary.package) 

161 .where(with_parent(suite, Suite.binaries)) 

162 .group_by(DBBinary.package) 

163 .order_by(DBBinary.package) 

164 ) 

165 

166 

167class NamedSource: 

168 """ 

169 A source package identified by its name with all of its versions in a 

170 suite. 

171 """ 

172 

173 def __init__(self, suite: Suite, source: str): 

174 self.source = source 

175 query = suite.sources.filter_by(source=source).order_by(DBSource.version) 

176 self.versions = [src.version for src in query] 

177 

178 @override 

179 def __str__(self): 

180 return "%s(%s)" % (self.source, ", ".join(self.versions)) 

181 

182 

183class DejavuBinary: 

184 """ 

185 A binary package identified by its name which gets built by multiple source 

186 packages in a suite. The architecture is ignored which leads to the 

187 following corner case, e.g.: 

188 

189 If a source package 'foo-mips' that builds a binary package 'foo' on mips 

190 and another source package 'foo-mipsel' builds a binary package with the 

191 same name 'foo' on mipsel then the binary package 'foo' will be reported as 

192 built from multiple source packages. 

193 """ 

194 

195 def __init__(self, suite: Suite, package: str): 

196 self.package = package 

197 session = object_session(suite) 

198 assert session is not None 

199 # We need a subquery to make sure that both binary and source packages 

200 # are in the right suite. 

201 bin_query = suite.binaries.filter_by(package=package).subquery() 

202 src_query = ( 

203 select(DBSource.source) 

204 .where(with_parent(suite, Suite.sources)) 

205 .join(bin_query) 

206 .order_by(DBSource.source) 

207 .group_by(DBSource.source) 

208 ) 

209 self.sources = [] 

210 rows = session.execute(src_query).all() 

211 if len(rows) > 1: 

212 for (source,) in rows: 

213 self.sources.append(str(NamedSource(suite, source))) 

214 

215 def has_multiple_sources(self) -> bool: 

216 "Has the package been built by multiple sources?" 

217 return len(self.sources) > 1 

218 

219 @override 

220 def __str__(self): 

221 return "%s built by: %s" % (self.package, ", ".join(self.sources)) 

222 

223 

224def report_multiple_source(suite: Suite) -> None: 

225 """ 

226 Reports binary packages built from multiple source package with different 

227 names. 

228 """ 

229 

230 session = object_session(suite) 

231 assert session is not None 

232 

233 print("Built from multiple source packages") 

234 print("-----------------------------------") 

235 print() 

236 for (package,) in session.execute(get_package_names(suite)): 

237 binary = DejavuBinary(suite, package) 

238 if binary.has_multiple_sources(): 

239 print(binary) 

240 print() 

241 

242 

243def query_without_source( 

244 suite_id: int, session: "Session" 

245) -> CursorResult[tuple[str, str]]: 

246 """searches for arch: all packages from suite that do no longer 

247 reference a source package in the same suite 

248 

249 subquery unique_binaries: selects all packages with only 1 version 

250 in suite since 'dak rm' does not allow to specify version numbers""" 

251 

252 query = """ 

253 with unique_binaries as 

254 (select package, max(version) as version, max(source) as source 

255 from bin_associations_binaries 

256 where architecture = 2 and suite = :suite_id 

257 group by package having count(*) = 1) 

258 select ub.package, ub.version 

259 from unique_binaries ub 

260 left join src_associations_src sas 

261 on ub.source = sas.src and sas.suite = :suite_id 

262 where sas.id is null 

263 order by ub.package""" 

264 return cast(CursorResult, session.execute(sql.text(query), {"suite_id": suite_id})) 

265 

266 

267def queryNBS( 

268 suite_id: int, session: "Session" 

269) -> CursorResult[tuple[list[str], list[str], str, str]]: 

270 """This one is really complex. It searches arch != all packages that 

271 are no longer built from current source packages in suite. 

272 

273 temp table unique_binaries: will be populated with packages that 

274 have only one version in suite because 'dak rm' does not allow 

275 specifying version numbers 

276 

277 temp table newest_binaries: will be populated with packages that are 

278 built from current sources 

279 

280 subquery uptodate_arch: returns all architectures built from current 

281 sources 

282 

283 subquery unique_binaries_uptodate_arch: returns all packages in 

284 architectures from uptodate_arch 

285 

286 subquery unique_binaries_uptodate_arch_agg: same as 

287 unique_binaries_uptodate_arch but with column architecture 

288 aggregated to array 

289 

290 subquery uptodate_packages: similar to uptodate_arch but returns all 

291 packages built from current sources 

292 

293 subquery outdated_packages: returns all packages with architectures 

294 no longer built from current source 

295 """ 

296 

297 query = """ 

298with 

299 unique_binaries as 

300 (select 

301 bab.package, 

302 bab.architecture, 

303 max(bab.source) as source 

304 from bin_associations_binaries bab 

305 where bab.suite = :suite_id and bab.architecture > 2 

306 group by package, architecture having count(*) = 1), 

307 newest_binaries as 

308 (select ub.package, ub.architecture, nsa.source, nsa.version 

309 from unique_binaries ub 

310 join newest_src_association nsa 

311 on ub.source = nsa.src and nsa.suite = :suite_id), 

312 uptodate_arch as 

313 (select architecture, source, version 

314 from newest_binaries 

315 group by architecture, source, version), 

316 unique_binaries_uptodate_arch as 

317 (select ub.package, (select a.arch_string from architecture a where a.id = ub.architecture) as arch_string, ua.source, ua.version 

318 from unique_binaries ub 

319 join source s 

320 on ub.source = s.id 

321 join uptodate_arch ua 

322 on ub.architecture = ua.architecture and s.source = ua.source), 

323 unique_binaries_uptodate_arch_agg as 

324 (select ubua.package, 

325 array_agg(ubua.arch_string order by ubua.arch_string) as arch_list, 

326 ubua.source, ubua.version 

327 from unique_binaries_uptodate_arch ubua 

328 group by ubua.source, ubua.version, ubua.package), 

329 uptodate_packages as 

330 (select package, source, version 

331 from newest_binaries 

332 group by package, source, version), 

333 outdated_packages as 

334 (select array_agg(package order by package) as pkg_list, 

335 arch_list, source, version 

336 from unique_binaries_uptodate_arch_agg 

337 where package not in 

338 (select package from uptodate_packages) 

339 group by arch_list, source, version) 

340 select * from outdated_packages order by source""" 

341 return cast(CursorResult, session.execute(sql.text(query), {"suite_id": suite_id})) 

342 

343 

344def queryNBS_metadata( 

345 suite_id: int, session: "Session" 

346) -> CursorResult[tuple[str, str]]: 

347 """searches for NBS packages based on metadata extraction of the 

348 newest source for a given suite""" 

349 

350 query = """ 

351 select string_agg(bin.package, ' ' order by bin.package), ( 

352 select arch_string 

353 from architecture 

354 where id = bin.architecture) as architecture, src.source, newsrc.version 

355 from bin_associations_binaries bin 

356 join src_associations_src src 

357 on src.src = bin.source 

358 and src.suite = bin.suite 

359 join newest_src_association newsrc 

360 on newsrc.source = src.source 

361 and newsrc.version != src.version 

362 and newsrc.suite = bin.suite 

363 where bin.suite = :suite_id 

364 and bin.package not in ( 

365 select trim(' \n' from unnest(string_to_array(meta.value, ','))) 

366 from source_metadata meta 

367 where meta.src_id = ( 

368 select newsrc.src 

369 from newest_src_association newsrc 

370 where newsrc.source = ( 

371 select s.source 

372 from source s 

373 where s.id = bin.source) 

374 and newsrc.suite = bin.suite) 

375 and key_id = ( 

376 select key_id 

377 from metadata_keys 

378 where key = 'Binary')) 

379 group by src.source, newsrc.version, architecture 

380 order by src.source, newsrc.version, bin.architecture""" 

381 return cast(CursorResult, session.execute(sql.text(query), {"suite_id": suite_id}))