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