]> git.ipfire.org Git - people/ms/pakfire.git/blob - src/pakfire/packages/base.py
Add scriptlets to the database and actually execute them.
[people/ms/pakfire.git] / src / pakfire / packages / base.py
1 #!/usr/bin/python
2 ###############################################################################
3 # #
4 # Pakfire - The IPFire package management system #
5 # Copyright (C) 2011 Pakfire development team #
6 # #
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 3 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, see <http://www.gnu.org/licenses/>. #
19 # #
20 ###############################################################################
21
22 import datetime
23 import os
24 import shutil
25 import xml.sax.saxutils
26
27 import logging
28 log = logging.getLogger("pakfire")
29
30 import pakfire.util as util
31
32 from pakfire.constants import *
33 from pakfire.i18n import _
34
35 class Package(object):
36 def __init__(self, pakfire, repo=None):
37 self.pakfire = pakfire
38 self._repo = repo
39
40 # Pointer to a package that is updated by this one.
41 self.old_package = None
42
43 def __repr__(self):
44 return "<%s %s>" % (self.__class__.__name__, self.friendly_name)
45
46 def __cmp__(self, other):
47 # if packages differ names return in alphabetical order
48 if not self.name == other.name:
49 return cmp(self.name, other.name)
50
51 # If UUIDs match, the packages are absolutely equal.
52 if self.uuid == other.uuid:
53 #log.debug("%s is equal to %s by UUID" % (self, other))
54 return 0
55
56 ret = util.version_compare(self.pakfire.pool,
57 self.friendly_version, other.friendly_version)
58
59 # XXX this is to move packages that have been built a while ago and
60 # do not have all the meta information that they won't be evaluated
61 # as the best match.
62 #if not ret:
63 # if "X"*3 in (self.build_id, other.build_id):
64 # if self.build_id == "X"*3 and not other.build_id == "X"*3:
65 # ret = -1
66 #
67 # elif not self.build_id == "X"*3 and other.build_id == "X"*3:
68 # ret = 1
69 # XXX hack end
70
71 # Compare the build times if we have a rebuilt package.
72 if not ret and self.build_time and other.build_time:
73 ret = cmp(self.build_time, other.build_time)
74
75 #if ret == 0:
76 # log.debug("%s is equal to %s" % (self, other))
77 #elif ret < 0:
78 # log.debug("%s is more recent than %s" % (other, self))
79 #elif ret > 0:
80 # log.debug("%s is more recent than %s" % (self, other))
81
82 # If no rank could be created, sort by repository priority
83 if not ret:
84 ret = cmp(self.repo, other.repo)
85
86 return ret
87
88 def __hash__(self):
89 hashstr = ["%s" % s for s in (self.name, self.epoch, self.version,
90 self.release, self.arch,)]
91
92 return hash("-".join(hashstr))
93
94 def dump(self, short=False, long=False, filelist=False):
95 if short:
96 return "%s.%s : %s" % (self.name, self.arch, self.summary)
97
98 items = [
99 (_("Name"), self.name),
100 ]
101
102 # Show supported arches if available.
103 if hasattr(self, "supported_arches") and not self.supported_arches == "all":
104 arch = "%s (%s)" % (self.arch, self.supported_arches)
105 else:
106 arch = self.arch
107 items.append((_("Arch"), arch))
108
109 items += [
110 (_("Version"), self.version),
111 (_("Release"), self.release),
112 ]
113
114 if self.size:
115 items.append((_("Size"), util.format_size(self.size)))
116
117 if self.inst_size:
118 items.append(
119 (_("Installed size"),
120 util.format_size(self.inst_size))
121 )
122
123 # filter dummy repository
124 if not self.repo == self.pakfire.repos.dummy:
125 items.append((_("Repo"), self.repo.name))
126
127 items += [
128 (_("Summary"), self.summary),
129 (_("Groups"), " ".join(self.groups)),
130 (_("URL"), self.url),
131 (_("License"), self.license),
132 ]
133
134 caption = _("Description")
135 for line in util.text_wrap(self.description):
136 items.append((caption, line))
137 caption = ""
138
139 if long:
140 if self.maintainer:
141 items.append((_("Maintainer"), self.maintainer))
142
143 items.append((_("Vendor"), self.vendor))
144
145 items.append((_("UUID"), self.uuid))
146 items.append((_("Build ID"), self.build_id))
147 items.append((_("Build date"), self.build_date))
148 items.append((_("Build host"), self.build_host))
149
150 caption = _("Signatures")
151 for sig in self.signatures:
152 items.append((caption, sig))
153 caption = ""
154
155 caption = _("Provides")
156 for prov in sorted(self.provides):
157 items.append((caption, prov))
158 caption = ""
159
160 caption = _("Pre-requires")
161 for req in sorted(self.prerequires):
162 items.append((caption, req))
163 caption = ""
164
165 caption = _("Requires")
166 for req in sorted(self.requires):
167 items.append((caption, req))
168 caption = ""
169
170 caption = _("Conflicts")
171 for req in sorted(self.conflicts):
172 items.append((caption, req))
173 caption = ""
174
175 caption = _("Obsoletes")
176 for req in sorted(self.obsoletes):
177 items.append((caption, req))
178 caption = ""
179
180 caption = _("Recommends")
181 for req in sorted(self.recommends):
182 items.append((caption, req))
183 caption = ""
184
185 caption = _("Suggests")
186 for req in sorted(self.suggests):
187 items.append((caption, req))
188 caption = ""
189
190 # Append filelist if requested.
191 if filelist:
192 for file in self.filelist:
193 items.append((_("File"), file))
194
195 format = "%%-%ds : %%s" % (max([len(k) for k, v in items]))
196
197 s = []
198 for caption, value in items:
199 s.append(format % (caption, value))
200
201 s.append("") # New line at the end
202
203 # XXX why do we need to decode this?
204 return "\n".join([str.decode("utf-8") for str in s])
205
206 @property
207 def info(self):
208 info = {
209 "name" : self.name,
210 "version" : self.version,
211 "release" : self.release,
212 "epoch" : self.epoch,
213 "arch" : self.arch,
214 "supported_arches" : self.supported_arches,
215 "groups" : self.groups,
216 "summary" : self.summary,
217 "description" : self.description,
218 "maintainer" : self.maintainer,
219 "url" : self.url,
220 "license" : self.license,
221 "hash1" : self.hash1,
222 "vendor" : self.vendor,
223 "build_date" : self.build_date,
224 "build_host" : self.build_host,
225 "build_id" : self.build_id,
226 "build_time" : self.build_time,
227 "size" : self.size,
228 "inst_size" : self.inst_size,
229 }
230
231 return info
232
233 @property
234 def hash1(self):
235 return "0"*40
236
237 @property
238 def size(self):
239 """
240 Return the size of the package file.
241
242 This should be overloaded by another class and returns 0 for
243 virtual packages.
244 """
245 return 0
246
247 @property
248 def inst_size(self):
249 """
250 The used disk space when the package is installed.
251
252 Returns None if inst_size is unknown.
253 """
254 return None
255
256 @property
257 def local(self):
258 """
259 Indicates whether a package is located "local" means on disk
260 and has not be downloaded.
261 """
262 return False
263
264 ### META INFORMATION ###
265
266 @property
267 def metadata(self):
268 raise NotImplementedError, self
269
270 @property
271 def friendly_name(self):
272 return "%s-%s.%s" % (self.name, self.friendly_version, self.arch)
273
274 @property
275 def friendly_version(self):
276 s = "%s-%s" % (self.version, self.release)
277
278 if self.epoch:
279 s = "%d:%s" % (self.epoch, s)
280
281 return s
282
283 @property
284 def repo(self):
285 if self._repo:
286 return self._repo
287
288 # By default, every package is connected to a dummy repository
289 return self.pakfire.repos.dummy
290
291 @property
292 def name(self):
293 return self.metadata.get("PKG_NAME")
294
295 @property
296 def version(self):
297 return self.metadata.get("PKG_VER")
298
299 @property
300 def release(self):
301 ret = None
302
303 for i in ("PKG_RELEASE", "PKG_REL"):
304 ret = self.metadata.get(i, None)
305 if ret:
306 break
307
308 return ret
309
310 @property
311 def epoch(self):
312 epoch = self.metadata.get("PKG_EPOCH", 0)
313
314 return int(epoch)
315
316 @property
317 def arch(self):
318 raise NotImplementedError
319
320 @property
321 def base(self):
322 """
323 Say if a package belongs to the basic set
324 that is installed by default.
325 """
326 return "Base" in self.groups
327
328 @property
329 def critical(self):
330 """
331 Return if a package is marked "critial".
332 """
333 return "Critical" in self.groups
334
335 def is_installed(self):
336 return self.repo.name == "@system"
337
338 @property
339 def type(self):
340 return self.metadata.get("TYPE", "unknown")
341
342 @property
343 def maintainer(self):
344 return self.metadata.get("PKG_MAINTAINER")
345
346 @property
347 def license(self):
348 return self.metadata.get("PKG_LICENSE")
349
350 @property
351 def summary(self):
352 return self.metadata.get("PKG_SUMMARY")
353
354 @property
355 def description(self):
356 return self.metadata.get("PKG_DESCRIPTION")
357
358 @property
359 def groups(self):
360 return self.metadata.get("PKG_GROUPS", "").split()
361
362 @property
363 def url(self):
364 return self.metadata.get("PKG_URL")
365
366 @property
367 def triggers(self):
368 triggers = self.metadata.get("PKG_TRIGGERS", "")
369
370 return triggers.split()
371
372 @property
373 def signatures(self):
374 return []
375
376 @property
377 def build_date(self):
378 """
379 Automatically convert the UNIX timestamp from self.build_time to
380 a humanly readable format.
381 """
382 if self.build_time is None:
383 return _("Not set")
384
385 return "%s UTC" % datetime.datetime.utcfromtimestamp(self.build_time)
386
387 @property
388 def build_host(self):
389 return self.metadata.get("BUILD_HOST")
390
391 @property
392 def build_id(self):
393 return self.metadata.get("BUILD_ID")
394
395 @property
396 def build_time(self):
397 build_time = self.metadata.get("BUILD_TIME", 0)
398
399 return int(build_time)
400
401 @property
402 def uuid(self):
403 return self.metadata.get("PKG_UUID", None)
404
405 @property
406 def supported_arches(self):
407 return self.metadata.get("PKG_SUPPORTED_ARCHES", "all")
408
409 @property
410 def vendor(self):
411 return self.metadata.get("PKG_VENDOR", "")
412
413 @staticmethod
414 def filter_deps(deps):
415 """
416 Filter out invalid dependencies.
417
418 This is just for reading packages and skipping comments
419 or empty lines.
420 """
421 ret = []
422
423 for dep in deps:
424 # Remove any leading or trailing spaces.
425 dep = dep.strip()
426
427 # Skip empty strings.
428 if not dep:
429 continue
430
431 # Skip comment lines.
432 if dep.startswith("#"):
433 continue
434
435 ret.append(dep)
436
437 return ret
438
439 @property
440 def prerequires(self):
441 requires = self.metadata.get("PKG_PREREQUIRES", "")
442
443 return requires.split()
444
445 @property
446 def requires(self):
447 ret = ""
448
449 # The default attributes, that are process for the requires.
450 attrs = ["PKG_REQUIRES", "PKG_DEPS",]
451
452 if self.arch == "src":
453 attrs += ["PKG_BUILD_DEPS",]
454
455 for i in attrs:
456 ret = self.metadata.get(i, ret)
457 if ret:
458 break
459
460 return ret.splitlines()
461
462 @property
463 def provides(self):
464 return self.metadata.get("PKG_PROVIDES", "").splitlines()
465
466 @property
467 def conflicts(self):
468 return self.metadata.get("PKG_CONFLICTS", "").splitlines()
469
470 @property
471 def obsoletes(self):
472 return self.metadata.get("PKG_OBSOLETES", "").splitlines()
473
474 @property
475 def recommends(self):
476 return []
477
478 @property
479 def suggests(self):
480 return []
481
482 @property
483 def scriptlets(self):
484 return self.metadata.get("PKG_SCRIPTLETS", "").splitlines()
485
486 def get_scriptlet(self, action):
487 raise NotImplementedError
488
489 @property
490 def filelist(self):
491 raise NotImplementedError
492
493 @property
494 def configfiles(self):
495 return []
496
497 @property
498 def datafiles(self):
499 return []
500
501 def extract(self, path, prefix=None):
502 raise NotImplementedError, "%s" % repr(self)
503
504 def remove(self, message=None, prefix=None):
505 # Make two filelists. One contains all binary files that need to be
506 # removed, the other one contains the configuration files which are
507 # kept. files and configfiles are disjunct.
508 files = []
509 configfiles = self.configfiles
510 datafiles = self.datafiles
511
512 for file in self.filelist:
513 if file in configfiles:
514 continue
515
516 if file in datafiles:
517 continue
518
519 assert file.startswith("/")
520 files.append(file)
521
522 self._remove_files(files, message, prefix)
523
524 def _remove_files(self, files, message, prefix):
525 if prefix in ("/", None):
526 prefix = ""
527
528 # Load progressbar.
529 pb = None
530 if message:
531 message = "%-10s : %s" % (message, self.friendly_name)
532 pb = util.make_progress(message, len(files), eta=False)
533
534 # Sort files by the length of their name to remove all files in
535 # a directory first and then check, if there are any files left.
536 files.sort(cmp=lambda x,y: cmp(len(x.name), len(y.name)), reverse=True)
537
538 # Messages to the user.
539 messages = []
540
541 i = 0
542 for _file in files:
543 # Update progress.
544 if pb:
545 i += 1
546 pb.update(i)
547
548 log.debug("Removing file: %s" % _file)
549
550 if prefix:
551 file = os.path.join(prefix, _file.name[1:])
552 assert file.startswith("%s/" % prefix)
553 else:
554 file = _file.name
555
556 # Rename configuration files.
557 if _file.is_config():
558 # Skip already removed config files.
559 try:
560 os.lstat(file)
561 except OSError:
562 continue
563
564 file_save = "%s%s" % (file, CONFIG_FILE_SUFFIX_SAVE)
565
566 try:
567 shutil.move(file, file_save)
568 except shutil.Error, e:
569 print e
570
571 if prefix:
572 file_save = os.path.relpath(file_save, prefix)
573 messages.append(_("Config file saved as %s.") % file_save)
574 continue
575
576 # Preserve datafiles.
577 if _file.is_datafile():
578 log.debug(_("Preserving datafile '/%s'") % _file)
579 continue
580
581 # Handle regular files and symlinks.
582 if os.path.isfile(file) or os.path.islink(file):
583 log.debug("Removing %s..." % _file)
584 try:
585 os.remove(file)
586 except OSError:
587 log.error("Cannot remove file: %s. Remove manually." % _file)
588
589 # Handle directories.
590 # Skip removal if the directory is a mountpoint.
591 elif os.path.isdir(file) and not os.path.ismount(file):
592 # Try to remove the directory. If it is not empty, OSError is raised,
593 # but we are okay with that.
594 try:
595 os.rmdir(file)
596 except OSError:
597 pass
598
599 # Handle files that have already been removed
600 # by somebody else.
601 elif not os.path.exists(file):
602 pass
603
604 # Log all unhandled types.
605 else:
606 log.warning("Cannot remove file: %s. Filetype is unhandled." % file)
607
608 if pb:
609 pb.finish()
610
611 for msg in messages:
612 log.warning(msg)