1 |
slords |
1.1 |
#! /usr/bin/env python |
2 |
|
|
|
3 |
|
|
# rpm-solver.py |
4 |
|
|
# Given a pile of RPMs will check dependency closure, will attempt to figure out |
5 |
|
|
# their installation order. |
6 |
|
|
# |
7 |
|
|
# Copyright 2005 Progeny Linux Systems, Inc. |
8 |
|
|
# |
9 |
|
|
# This program is free software; you can redistribute it and/or modify |
10 |
|
|
# it under the terms of the GNU General Public License as published by |
11 |
|
|
# the Free Software Foundation; either version 2 of the License, or |
12 |
|
|
# (at your option) any later version. |
13 |
|
|
# |
14 |
|
|
# This program is distributed in the hope that it will be useful, |
15 |
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 |
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 |
|
|
# GNU General Public License for more details. |
18 |
|
|
# |
19 |
|
|
# You should have received a copy of the GNU General Public License |
20 |
|
|
# along with this program; if not, write to the Free Software |
21 |
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
22 |
|
|
# |
23 |
|
|
# Author: Sam Hart |
24 |
|
|
|
25 |
|
|
import os |
26 |
|
|
import fnmatch |
27 |
|
|
import getopt |
28 |
|
|
import sys |
29 |
|
|
import getopt |
30 |
|
|
import rpm |
31 |
|
|
import traceback |
32 |
|
|
import commands |
33 |
|
|
import tempfile |
34 |
|
|
|
35 |
|
|
class progress_bar: |
36 |
|
|
def __init__(self, prefix="Progress :", prog_char="-", col=60, outnode=sys.stdout): |
37 |
|
|
self.f = outnode |
38 |
|
|
self.prog_char = prog_char |
39 |
|
|
self.col = col |
40 |
|
|
self.spinner = ["|", "/", "-", "\\"] |
41 |
|
|
self.spin_count = 0 |
42 |
|
|
self.prefix = prefix |
43 |
|
|
|
44 |
|
|
def set(self, prefix="Progress :"): |
45 |
|
|
self.prefix = prefix |
46 |
|
|
|
47 |
|
|
def clear(self): |
48 |
|
|
self.f.write("\r") |
49 |
|
|
for i in range(0, self.col): |
50 |
|
|
self.f.write(" ") |
51 |
|
|
|
52 |
|
|
self.f.write("\r") |
53 |
|
|
self.f.flush() |
54 |
|
|
|
55 |
|
|
def progress(self, percentage): |
56 |
|
|
"""Count must be out of 100%""" |
57 |
|
|
|
58 |
|
|
if percentage > 1.0: |
59 |
|
|
percentage = 1.0 |
60 |
|
|
|
61 |
|
|
self.f.write(("\r%s 0 |") % self.prefix) |
62 |
|
|
width = self.col - len(("\r%s 0 100 |") % self.prefix) + 1 |
63 |
|
|
count = width * percentage |
64 |
|
|
|
65 |
|
|
i = 1 |
66 |
|
|
while i < count: |
67 |
|
|
self.f.write(self.prog_char) |
68 |
|
|
i = i + 1 |
69 |
|
|
|
70 |
|
|
if count < width: |
71 |
|
|
self.f.write(">") |
72 |
|
|
while i < width: |
73 |
|
|
self.f.write(" ") |
74 |
|
|
i = i + 1 |
75 |
|
|
|
76 |
|
|
if self.spin_count >= len(self.spinner): |
77 |
|
|
self.spin_count = 0 |
78 |
|
|
|
79 |
|
|
self.f.write(self.spinner[self.spin_count]) |
80 |
|
|
self.spin_count = self.spin_count + 1 |
81 |
|
|
|
82 |
|
|
self.f.write(" 100 ") |
83 |
|
|
self.f.flush() |
84 |
|
|
|
85 |
|
|
class rpm_solver: |
86 |
|
|
def __init__(self, progress=0, verbose=0): |
87 |
|
|
self.progress = progress |
88 |
|
|
self.verbose = verbose |
89 |
|
|
self._initdb = 0 |
90 |
|
|
col = commands.getoutput("echo \"$COLUMNS\"") |
91 |
|
|
try: |
92 |
|
|
columns = int(col) |
93 |
|
|
except: |
94 |
|
|
columns = 60 |
95 |
|
|
self.pb = progress_bar("rpm_solver :", "-", columns, sys.stderr) |
96 |
|
|
|
97 |
|
|
|
98 |
|
|
def init_db(self, rpm_dir, avail_dir=None, recursive=0): |
99 |
|
|
""" Init the database """ |
100 |
|
|
|
101 |
|
|
self.solver_db = self.db(rpm_dir, recursive) |
102 |
|
|
self.solver_db.populate_db(self.verbose, self.pb, 1, self.progress) |
103 |
|
|
|
104 |
|
|
self.use_avail = 0 |
105 |
|
|
self._initdb = 1 |
106 |
|
|
|
107 |
|
|
if avail_dir: |
108 |
|
|
self.avail_db = self.db(avail_dir, recursive) |
109 |
|
|
self.avail_db.populate_db(self.verbose, self.pb, 0, self.progress) |
110 |
|
|
self.use_avail = 1 |
111 |
|
|
|
112 |
|
|
def what_provides(self, solver_db, name, version=None): |
113 |
|
|
""" Given a name and a version, see what provides it """ |
114 |
|
|
|
115 |
|
|
for hdr_key in solver_db.rpmdb.keys(): |
116 |
|
|
provides = solver_db.rpmdb[hdr_key][rpm.RPMTAG_PROVIDES] |
117 |
|
|
if name in provides: |
118 |
|
|
if version: |
119 |
|
|
version_check = solver_db.rpmdb[hdr_key][rpm.RPMTAG_VERSION] |
120 |
|
|
if version == version_check: |
121 |
|
|
return hdr_key |
122 |
|
|
else: |
123 |
|
|
return hdr_key |
124 |
|
|
file_list = solver_db.rpmdb[hdr_key][rpm.RPMTAG_FILENAMES] |
125 |
|
|
if name in file_list: |
126 |
|
|
return hdr_key |
127 |
|
|
|
128 |
|
|
return None |
129 |
|
|
|
130 |
|
|
def dep_closure(self): |
131 |
|
|
""" Determine if they have dependency closure """ |
132 |
|
|
|
133 |
|
|
needed = [] |
134 |
|
|
problems = [] |
135 |
|
|
|
136 |
|
|
if self._initdb: |
137 |
|
|
missing_deps = self.solver_db.ts.check() |
138 |
|
|
if self.verbose > 1: |
139 |
|
|
print "->Result of solver_db.ts.check():" |
140 |
|
|
print missing_deps |
141 |
|
|
if len(missing_deps): |
142 |
|
|
for dep in missing_deps: |
143 |
|
|
# XXX FIXME |
144 |
|
|
# Okay, we completely ignore the version here, which is |
145 |
|
|
# wrong wrong WRONG! We should be smacked. |
146 |
|
|
if self.use_avail: |
147 |
|
|
package = self.what_provides(self.avail_db, dep[1][0], dep[1][1]) |
148 |
|
|
if package: |
149 |
|
|
needed.append(package) |
150 |
|
|
else: |
151 |
|
|
problems.append("%s needs %s" % (dep[0][0], dep[1][0])) |
152 |
|
|
else: |
153 |
|
|
package = self.what_provides(self.solver_db, dep[1][0]) |
154 |
|
|
if package: |
155 |
|
|
needed.append(package) |
156 |
|
|
else: |
157 |
|
|
problems.append("%s needs %s" % (dep[0][0], dep[1][0])) |
158 |
|
|
else: |
159 |
|
|
problems.append("Database has not been populated") |
160 |
|
|
|
161 |
|
|
return needed, problems |
162 |
|
|
|
163 |
|
|
def _get_filename_from_hdr(self, pkg_te): |
164 |
|
|
""" Given a package name, find the filename for it """ |
165 |
|
|
|
166 |
|
|
pkg = pkg_te.N() |
167 |
|
|
|
168 |
|
|
for name in self.solver_db.rpmdb.keys(): |
169 |
|
|
if pkg == self.solver_db.rpmdb[name][rpm.RPMTAG_NAME]: |
170 |
|
|
return name |
171 |
|
|
|
172 |
|
|
return None |
173 |
|
|
|
174 |
|
|
def order_solver(self): |
175 |
|
|
""" Once the database has been populated, try to solve the order """ |
176 |
|
|
|
177 |
|
|
order_pkg = [] |
178 |
|
|
order_filename = [] |
179 |
|
|
|
180 |
|
|
self.solver_db.ts.order() |
181 |
|
|
while 1: |
182 |
|
|
try: |
183 |
|
|
order_pkg.append(self.solver_db.ts.next()) |
184 |
|
|
except: |
185 |
|
|
break |
186 |
|
|
|
187 |
|
|
#order_pkg = self.solver_db.ts() |
188 |
|
|
|
189 |
|
|
for pkg in order_pkg: |
190 |
|
|
order_filename.append(self._get_filename_from_hdr(pkg)) |
191 |
|
|
|
192 |
|
|
return order_filename |
193 |
|
|
|
194 |
|
|
class db: |
195 |
|
|
def __init__(self, rpm_dir=".", recurse=1, ext="*.rpm"): |
196 |
|
|
self.rpm_dir = rpm_dir |
197 |
|
|
self.recurse = recurse |
198 |
|
|
self.rpmdb = {} |
199 |
|
|
self.ext = ext |
200 |
|
|
self.tmp_dir = tempfile.mkdtemp() |
201 |
|
|
self.ts = rpm.TransactionSet(self.tmp_dir) |
202 |
|
|
self.ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) |
203 |
|
|
|
204 |
|
|
def close(self): |
205 |
|
|
self.ts.closeDB() |
206 |
|
|
for root, dirs, files in os.walk(self.tmp_dir, topdown=False): |
207 |
|
|
for name in files: |
208 |
|
|
os.remove(os.path.join(root, name)) |
209 |
|
|
for name in dirs: |
210 |
|
|
os.rmdir(os.path.join(root, name)) |
211 |
|
|
|
212 |
|
|
def get_rpmdb_size(self): |
213 |
|
|
return len(self.rpmdb) |
214 |
|
|
|
215 |
|
|
def populate_db(self, verbose, pb, pop_trans=1, progress=0): |
216 |
|
|
""" Populate our own DB :-)""" |
217 |
|
|
|
218 |
|
|
self.rpm_filenames = self._list_files() |
219 |
|
|
|
220 |
|
|
i = 0.0 |
221 |
|
|
if progress: |
222 |
|
|
pb.set("Populate RPMDB :") |
223 |
|
|
|
224 |
|
|
for filename in self.rpm_filenames: |
225 |
|
|
if verbose: print "rpm_solver.db.populate_db : Adding " + str(filename) |
226 |
|
|
if progress: |
227 |
|
|
i = i + 1.0 |
228 |
|
|
percent = i / len(self.rpm_filenames) |
229 |
|
|
pb.progress(percent) |
230 |
|
|
|
231 |
|
|
self.add(filename, pop_trans) |
232 |
|
|
|
233 |
|
|
if progress: |
234 |
|
|
pb.clear() |
235 |
|
|
|
236 |
|
|
if verbose > 1: |
237 |
|
|
print ("->The contents of this transaction set for %s:" % self.rpm_dir) |
238 |
|
|
for tx in self.ts: |
239 |
|
|
print tx |
240 |
|
|
|
241 |
|
|
def add(self, filename, pop_trans=1): |
242 |
|
|
if filename.startswith("http://"): |
243 |
|
|
# XXX FIXME: |
244 |
|
|
# Okay, I give up about doing this nicely. Screw it. |
245 |
|
|
tmpfile = tempfile.mktemp() |
246 |
|
|
problem = 0 |
247 |
|
|
while 1: |
248 |
|
|
output = commands.getoutput("wget -O %s %s" % (tmpfile, filename)) |
249 |
|
|
try: |
250 |
|
|
fdno = os.open(tmpfile, os.O_RDONLY) |
251 |
|
|
os.close(fdno) |
252 |
|
|
break |
253 |
|
|
except: |
254 |
|
|
if problem > 10: |
255 |
|
|
print "FATAL ERROR!" |
256 |
|
|
print "Could not download " + filename |
257 |
|
|
sys.exit(2) |
258 |
|
|
else: |
259 |
|
|
problem = problem + 1 |
260 |
|
|
localfile = tmpfile |
261 |
|
|
else: |
262 |
|
|
localfile = filename |
263 |
|
|
|
264 |
|
|
try: |
265 |
|
|
fname = filename.split("/")[-1] |
266 |
|
|
except: |
267 |
|
|
fname = filename |
268 |
|
|
|
269 |
|
|
fdno = os.open(localfile, os.O_RDONLY) |
270 |
|
|
hdr = self.ts.hdrFromFdno(fdno) |
271 |
|
|
os.close(fdno) |
272 |
|
|
|
273 |
|
|
self.rpmdb[fname] = hdr |
274 |
|
|
if pop_trans: |
275 |
|
|
self.ts.addInstall(hdr,None) |
276 |
|
|
|
277 |
|
|
if filename.startswith("http://"): |
278 |
|
|
os.unlink(tmpfile) |
279 |
|
|
|
280 |
|
|
def _list_files(self): |
281 |
|
|
"""List all the files in a directory""" |
282 |
|
|
|
283 |
|
|
root = self.rpm_dir |
284 |
|
|
patterns = self.ext |
285 |
|
|
recurse = self.recurse |
286 |
|
|
return_folders = 0 |
287 |
|
|
|
288 |
|
|
if root.startswith("http://"): |
289 |
|
|
output = commands.getoutput("links -dump %s | grep \"http://\" | grep \".rpm\" | awk '{print $2}'" % root) |
290 |
|
|
results = output.split("\n") |
291 |
|
|
return results |
292 |
|
|
else: |
293 |
|
|
# Expand patterns from semicolon-separated string to list |
294 |
|
|
pattern_list = patterns.split(';') |
295 |
|
|
|
296 |
|
|
class Bunch: |
297 |
|
|
def __init__(self, **kwds): self.__dict__.update(kwds) |
298 |
|
|
arg = Bunch(recurse=recurse, pattern_list=pattern_list, return_folders=return_folders, results=[]) |
299 |
|
|
|
300 |
|
|
def visit(arg, dirname, files): |
301 |
|
|
# Append to arg.results all relevant files |
302 |
|
|
for name in files: |
303 |
|
|
fullname = os.path.normpath(os.path.join(dirname, name)) |
304 |
|
|
fullname = fullname.rstrip() |
305 |
|
|
if arg.return_folders or os.path.isfile(fullname): |
306 |
|
|
for pattern in arg.pattern_list: |
307 |
|
|
if fnmatch.fnmatch(name, pattern): |
308 |
|
|
arg.results.append(fullname) |
309 |
|
|
break |
310 |
|
|
# Block recursion if disallowed |
311 |
|
|
if not arg.recurse: files[:]=[] |
312 |
|
|
|
313 |
|
|
os.path.walk(root, visit, arg) |
314 |
|
|
return arg.results |
315 |
|
|
|
316 |
|
|
def process(rpm_dir, solve_dir, check_only, recursive, progress, verbose): |
317 |
|
|
""" Main process if ran from command line """ |
318 |
|
|
|
319 |
|
|
solver = rpm_solver(progress, verbose) |
320 |
|
|
solver.init_db(rpm_dir, solve_dir, recursive) |
321 |
|
|
needed, problems = solver.dep_closure() |
322 |
|
|
|
323 |
|
|
if len(needed): |
324 |
|
|
print "Error! The following packages are needed for dependency closure:\n" |
325 |
|
|
for pkg in needed: |
326 |
|
|
print "\t" + str(pkg) |
327 |
|
|
if len(problems): |
328 |
|
|
print "Error! The following problems were encountered:\n" |
329 |
|
|
for pkg in problems: |
330 |
|
|
print "\t" + str(pkg) |
331 |
|
|
|
332 |
|
|
if len(problems) or len(needed): |
333 |
|
|
sys.exit(2) |
334 |
|
|
elif check_only: |
335 |
|
|
print ("The RPMs in %s have dependency closure" % rpm_dir) |
336 |
|
|
else: |
337 |
|
|
# Okay we do stuff |
338 |
|
|
ordered = solver.order_solver() |
339 |
|
|
i = 0 |
340 |
|
|
for name in ordered: |
341 |
|
|
print ("%d:%s" % (i, name)) |
342 |
|
|
i = i + 1 |
343 |
|
|
|
344 |
|
|
def usage(): |
345 |
|
|
print "rpm-solver.py -" |
346 |
|
|
print " Given a directory of RPMs, attempt to order their" |
347 |
|
|
print "installation or determine if they have dependency closure." |
348 |
|
|
print "\nUSAGE:" |
349 |
|
|
print " rpm-solver.py [options] <RPM_DIR>" |
350 |
|
|
print "\nWhere [options] may be one of the following:" |
351 |
|
|
print "\t-c | --check\tCheck for dependency closure only" |
352 |
|
|
print "\t-s | --solve\tUse the pool of rpms specified for solving" |
353 |
|
|
print "\t-v | --verbose\tBe verbose in processing" |
354 |
|
|
print "\t-p | --progress\tUse progress bar" |
355 |
|
|
print "\t-r | --recursive\tScan RPM_DIR recursively" |
356 |
|
|
print "\n\n" |
357 |
|
|
|
358 |
|
|
|
359 |
|
|
def main(): |
360 |
|
|
try: |
361 |
|
|
opts, args = getopt.getopt(sys.argv[1:], "vprs:c", ["verbose", "progress", "recursive", "solve=", "check"]) |
362 |
|
|
except getopt.GetoptError: |
363 |
|
|
# print help information and exit: |
364 |
|
|
usage() |
365 |
|
|
sys.exit(2) |
366 |
|
|
|
367 |
|
|
verbose = 0 |
368 |
|
|
progress = 0 |
369 |
|
|
recursive = 0 |
370 |
|
|
solve_dir = None |
371 |
|
|
check_only = 0 |
372 |
|
|
|
373 |
|
|
if len(sys.argv) < 2: |
374 |
|
|
usage() |
375 |
|
|
sys.exit(2) |
376 |
|
|
|
377 |
|
|
rpm_dir = sys.argv[-1] |
378 |
|
|
|
379 |
|
|
for o, a in opts: |
380 |
|
|
if o in ("-v", "--verbose"): |
381 |
|
|
verbose = verbose + 1 |
382 |
|
|
|
383 |
|
|
if o in ("-p", "--progress"): |
384 |
|
|
progress = 1 |
385 |
|
|
|
386 |
|
|
if o in ("-r", "--recursive"): |
387 |
|
|
recursive = 1 |
388 |
|
|
|
389 |
|
|
if o in ("-s", "--solve"): |
390 |
|
|
solve_dir = a |
391 |
|
|
|
392 |
|
|
if o in ("-c", "--check"): |
393 |
|
|
check_only = 1 |
394 |
|
|
|
395 |
|
|
if verbose > 1: print "WARNING: Excessive debugging" |
396 |
|
|
|
397 |
|
|
process(rpm_dir, solve_dir, check_only, recursive, progress, verbose) |
398 |
|
|
|
399 |
|
|
if __name__ == "__main__": |
400 |
|
|
main() |
401 |
|
|
|
402 |
|
|
# vim:set ai et sts=4 sw=4 tw=80: |