aboutsummaryrefslogtreecommitdiff
blob: 43989bea0d71e12ca0bb4b40844824c6be860798 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
# R overlay -- roverlay package, packageinfo
# -*- coding: utf-8 -*-
# Copyright (C) 2012 André Erdmann <dywi@mailerd.de>
# Distributed under the terms of the GNU General Public License;
# either version 2 of the License, or (at your option) any later version.

"""package info data structure

This module provides one class, PackageInfo, which is a data structure that
offers access to package and ebuild related data. It's also able to calculate
some data if required, like getting description data from an R package.
"""

__all__ = [ 'PackageInfo', ]

import re
import os.path
import logging


import roverlay.digest
import roverlay.versiontuple
import roverlay.db.distmap
import roverlay.util.objects
import roverlay.util.ebuildparser
import roverlay.util.portage_regex.default

from roverlay          import config, strutil
from roverlay.rpackage import descriptionreader
from roverlay.util.portage_regex.default import RE_PVR


# PackageInfo info keys know to be used in roverlay's modules:
# *** some keys are not listed here (FIXME) ***
#
# * desc_data          -- dict containing DESCRIPTION data (created by
#                          rpackage.descriptionreader.DescriptionReader)
# * distdir            -- fs path to the directory containing the pkg (file)
# * ebuild             -- object representing the ebuild (printable via str())
# * ebuild_file        -- fs path to the ebuild file (str)
# * ebuild_verstr      -- version string as it's used in the ebuild
# * has_suggests       -- bool that indicates whether a package has optional
#                          dependencies
# * name               -- (ebuild) name of a package (no "special" chars etc.)
# * orig_name          -- original (ebuild) name (before "name" has been
#                          modified by package rules)
# * origin             -- a package's origin (repository object)
# * package_file       -- full fs path to the package file
# * package_filename   -- file name (including file extension)
# * package_filename_x -- file name without file extension
# * package_name       -- package name (file name without version, f-ext)
# * physical_only      -- bool that indicates whether a package exists as
#                          ebuild file only (True) or has additional
#                          runtime data (False)
# * src_uri            -- SRC_URI for a package
# * version            -- tuple containing a package's version
#
#
# Info (keys) that are created before applying package rules:
#
# * distdir
# * origin
# * package_{file{,name},name}
# * name (package_name)
# * src_uri (src_uri_base)
#
# "Foreign" info keys (never set or modified here):
#
# * category
# * src_uri_dest
#

LOGGER = logging.getLogger ( 'PackageInfo' )




def split_pvr_str ( pvr_str ):
   """Helper functions that splits a $PVR string into a version tuple ($PV)
   and an int ($PR).

   arguments:
   * pvr_str --
   """
   match = RE_PVR.match ( pvr_str )

   if not match:
      raise ValueError ( "invalid pvr: {!s}".format ( pvr_str ) )

   match_vars = match.groupdict()
   pv_list    = [ int(z) for z in match_vars['version'].split('.') ]

   if match_vars ['version_suffix']:
      # not supported
      version_tuple = (
         roverlay.versiontuple.SuffixedIntVersionTuple (
            pv_list, match_vars ['version_suffix']
         )
      )
   else:
      version_tuple = (
         roverlay.versiontuple.IntVersionTuple ( pv_list )
      )

   return ( version_tuple, int ( match_vars['revision'] or 0 ) )
# --- end of split_pvr_str (...) ---



class PackageInfoStatus ( roverlay.util.objects.ObjectView ):

   def __init__ ( self, package_info ):
      super ( PackageInfoStatus, self ).__init__ ( package_info )
      self.has_ebuild_file = False
   # --- end of __init__ (...) ---

   def get_name ( self ):
      return self.deref_safe().get ( 'name' )
   # --- end of get_name (...) ---

   def update ( self ):
      self.has_ebuild_file = bool (
         self.deref_safe().get ( 'ebuild_file', do_fallback=True )
      )
   # --- end of update (...) ---

# --- end of PackageInfoStatus ---


class PackageInfo ( roverlay.util.objects.Referenceable ):
   """PackageInfo offers easy, subscriptable access to package
   information, whether stored or calculated.

   class-wide variables:
   * EBUILDVER_REGEX -- a regex containing chars that will be replaced by
                        a dot '.'. (e.g. 2-3 becomes 2.3)
   * PKGSUFFIX_REGEX -- a regex that matches the suffix of an R package file
                        name. The regex str is retrieved from the config
                        module (which also means that the config has to be
                        loaded before import this module)
   * ALWAYS_FALLBACK -- a set of keys for which get() always returns a
                        fallback value (None)

   * _UPDATE_KEYS_SIMPLE         -- a set of keys that can be added
                                    without further checks
   * _UPDATE_KEYS_SIMPLE_INITIAL -- like _UPDATE_KEYS_SIMPLE, but only used
                                    on the first update() call (as long as
                                    no keys have been stored)
   * _UPDATE_KEYS_FILTER_NONE    -- like _UPDATE_KEYS_SIMPLE, but stores
                                    key's value only if it is not None
   * _REMOVE_KEYS_EBUILD         -- a set of keys that will be removed when
                                    _remove_auto ( 'ebuild_written' ) is
                                    called.
   """

   CACHE_REF = True

   EBUILDVER_REGEX = re.compile ( '[-]{1,}' )
   PKGSUFFIX_REGEX = re.compile (
      config.get_or_fail ( 'R_PACKAGE.suffix_regex' ) + '$'
   )
   EBUILDREV_REGEX = re.compile ( '[-]r[0-9]+$' )
   ALWAYS_FALLBACK = frozenset ( ( 'ebuild', 'ebuild_file' ) )

   _UPDATE_KEYS_SIMPLE         = frozenset ((
      'ebuild',
      'ebuild_file',
      'imported',
      'physical_only',
      'src_uri',
      'has_suggests',
      'src_uri_dest',
   ))
   _UPDATE_KEYS_SIMPLE_INITIAL = frozenset ((
      'package_filename', 'name',
   ))
   _UPDATE_KEYS_FILTER_NONE    = frozenset ((
      'src_uri_base',
      'distdir',
   ))

   _REMOVE_KEYS_EBUILD         = frozenset ((
      'ebuild'
   ))

   # bind DIGEST_TYPE to this class
   DISTMAP_DIGEST_TYPE = roverlay.db.distmap.DistMapInfo.DIGEST_TYPE

   def __init__ ( self, **initial_info ):
      """Initializes a PackageInfo.

      arguments:
      * **initial_info -- passed to update ( **kw )
      """
      super ( PackageInfo, self ).__init__()

      self._info    = dict()
      self.readonly = False
      self.logger   = LOGGER
      # self.selfdeps is a list of _mandatory_ selfdeps
      self.selfdeps = None

      self.hashdict = dict()
      self.depconf  = None

      # addition override, set by addition control or package rules
      #  defaults to None == "not set"/"use default"
      self.overlay_addition_override = None

      # FIXME: when is overlay_package_ref set? -> list conditions
      #         more importantly, when is it *not* set?
      #self.overlay_package_ref = None

      #self.selfdeps_valid      = UNDEF

      #self._evars              = dict()
      #self._lazy_actions       = list()
      #(or set(), but list preserves order for actions with the same condition)

      self.update ( **initial_info )
   # --- end of __init__ (...) ---

   def init_selfdep_validate ( self, selfdeps ):
      """Tells this packageinfo to initialize selfdep validation.
      Returns True on success (=has selfdeps), else False.
      """
      if selfdeps:
         self.selfdeps_valid = True
         self.selfdeps = selfdeps
         return True
      else:
         self.selfdeps = None
         return False
   # --- end of init_selfdep_validate (...) ---

   def is_valid ( self ):
      return bool ( getattr ( self, 'selfdeps_valid', True ) )
   # --- end of is_valid (...) ---

   def end_selfdep_validate ( self ):
      v = self.has_valid_selfdeps()
      self.selfdeps = None
      return v
   # --- end of end_selfdep_validate (...) ---

   def has_selfdeps ( self ):
      return bool ( self.selfdeps )
   # --- end of has_selfdeps (...) ---

   def has_valid_selfdeps ( self ):
      """Returns True if all selfdeps of this package are valid."""
      if self.selfdeps is None:
         return True
      else:
         v = self.selfdeps_valid
         if v is True and not all (
            d.deps_satisfiable() for d in self.selfdeps
         ):
            self.selfdeps_valid = False
            return False

         return v
   # --- end of has_valid_selfdeps (...) ---

   def get_status_view ( self ):
      obj = PackageInfoStatus ( self )
      obj.update()
      return obj
   # --- end of get_status_view (...) ---

   def attach_lazy_action ( self, lazy_action ):
      """Attaches a lazy action.
      Unsafe operation (no locks will be acquired etc.).

      arguments:
      * lazy_action --
      """
      raise NotImplementedError ( "lazy actions are disabled." )
      if hasattr ( self, '_lazy_actions' ):
         self._lazy_actions.append ( lazy_action )
      else:
         self._lazy_actions = [ lazy_action ]
   # --- end of attach_lazy_action (...) ---

   def apply_lazy_actions ( self ):
      """Tries to apply all attached (lazy) actions.
      Removes actions that have been applied."""
      raise NotImplementedError ( "lazy actions are disabled." )
      if hasattr ( self, '_lazy_actions' ):
         retry_later = list()
         for action in self._lazy_actions:
            if not action.try_apply_action ( self ):
               retry_later.append ( action )

         if retry_later:
            self._lazy_actions = retry_later
         else:
            del self._lazy_actions
      # -- end if;
   # --- end of apply_lazy_actions (...) ---

   def set_readonly ( self, immediate=False, final=False ):
      """Makes the package info readonly.

      arguments:
      * immediate -- do not acquire lock, set readonly directly,
                      defaults to False
      * final     -- if set and True: make this decision final
     """
      self._set_mode ( True, immediate, final )
   # --- end of set_readonly (...) ---

   def set_writeable ( self, immediate=False ):
      """Makes the package info writeable.

      arguments:
      * immediate -- do not acquire lock, set writeable directly,
                      defaults to False
      """
      self._set_mode ( False, immediate )
   # --- end of set_writeable (...) ---

   def _set_mode ( self, readonly_val, immediate=False, final=False ):
      """Sets readonly to True/False.

      arguments:
      * readonly_val -- new value for readonly
      * immediate    -- do not acquire lock
      * final        -- only if readonly_val is True: make this decision final

      raises: Exception if self.readonly is a constant (_readonly_final is set)
      """
      if hasattr ( self, '_readonly_final' ):
         raise Exception ( "cannot modify readonly - it's a constant." )
      else:
         self.readonly = readonly_val
         if final and readonly_val:
            self._readonly_final = True
   # --- end of _set_mode (...) ---

   def _check_readonly ( self ):
      """Checks whether this object is writable.
      Returns True on success, else raises an exception.
      """
      if self.readonly or hasattr ( self, '_readonly_final' ):
         raise Exception ( "package info is readonly!" )
      else:
         return True
   # --- end of _check_readonly (...) ---

   def _has_log_keyerror_unexpected ( self, key, error ):
      self.logger.error (
         'FIXME: PackageInfo.get( {!r}, do_fallback=True ) '
         'raised KeyError'.format ( key )
      )
      self.logger.exception ( error )
   # --- end of _has_log_keyerror_unexpected (...) ---

   def has_key ( self, *keys ):
      """Returns False if at least one key out of keys is not accessible,
      i.e. its data cannot be retrieved using get()/__getitem__().

      arguments:
      * *keys -- keys to check
      """
      for k in keys:
         if k not in self._info:
            # try harder - use get() with fallback value to see if value
            # can be calculated
            try:
               if self.get ( k, do_fallback=True ) is None:
                  return False
            except KeyError as kerr:
               self._has_log_keyerror_unexpected ( k, kerr )
               return False
      return True
   # --- end of has_key (...) ---

   has = has_key

   def has_key_or ( self, *keys ):
      """Returns True if at least one key out of keys is accessible.

      arguments:
      * *keys -- keys to check
      """
      for k in keys:
         try:
            if k in self._info:
               return True
            elif self.get ( k, do_fallback=True ) is not None:
               return True
         except KeyError as kerr:
            self._has_log_keyerror_unexpected ( k, kerr )
      return False
   # --- end of has_key_or (...) ---

   has_or = has_key_or

   def compare_version ( self, other_package ):
      """Compares the version of two PackageInfo objects.
      Returns 1 if self's version is higher, -1 if lower and 0 if equal.

      arguments:
      * other_package --
      """
      if other_package is None: return 1

      my_ver    = self.get ( 'version', fallback_value=0 )
      other_ver = other_package.get ( 'version', fallback_value=0 )

      if my_ver > other_ver:
         return 1
      elif my_ver == other_ver:
         return 0
      else:
         return -1
   # --- end of compare_version (...) ---

   def get ( self, key, fallback_value=None, do_fallback=False ):
      """Returns the value specified by key.
      The value is either calculated or taken from dict self._info.

      arguments:
      * key --
      * fallback_value -- fallback value if key not found / unknown
      * do_fallback    -- if True: return fallback_value, else raise KeyError

      raises: KeyError
      """
      # normal dict access shouldn't be slowed down here
      if key in self._info:
         return self._info [key]

      key_low = key.lower()

      if key_low in self._info:
         return self._info [key_low]

      try:
         val = self._get_virtual ( key_low )
      except KeyError:
         self.logger.critical (
            "FIXME: _get_virtual({!r}) raised KeyError".format ( key_low )
         )
         raise

      if val is not None:
         return val
      elif fallback_value is not None or do_fallback:
         return fallback_value
      elif key_low in self.__class__.ALWAYS_FALLBACK:
         #return fallback_value
         return None
      else:
         raise KeyError ( key )
   # --- end of get (...) ---

   def _get_virtual ( self, key ):
      # 'virtual' keys - calculate result
      if key == 'package_file':
         distdir = self.get ( 'distdir', do_fallback=True )
         if distdir:
            fname = self._info.get ( 'package_filename', None )
            if fname:
               return distdir + os.path.sep + fname

      elif key == 'distdir':
         if 'origin' in self._info:
            # this doesn't work if the package is in a sub directory
            # of the repo's distdir
            return self._info ['origin'].distdir
         elif 'package_file' in self._info:
            return os.path.dirname ( self._info ['package_file'] )
         # else fallback/KeyError

      elif key == 'has_suggests':
         # 'has_suggests' not in self._info -> assume False
         return False

      elif key == 'physical_only':
         # 'physical_only' not in self._info -> assume False
         return False

      elif key == 'imported':
         # 'imported' not in self._info -> assume False
         return False

      elif key == 'src_uri':
         if 'src_uri_base' in self._info:
            return \
               self._info ['src_uri_base'] + '/' + \
               self._info ['package_filename']

         elif 'origin' in self._info:
            return self._info ['origin'].get_src_uri (
               self._info ['package_filename']
            )
         else:
            return "http://localhost/R-packages/" + \
               self._info ['package_filename']

      elif key == 'ebuild_dir':
         ebuild_file = self._info ['ebuild_file']
         if ebuild_file is not None:
            return os.path.dirname ( ebuild_file )

      elif key == 'ebuild_filename':
         ebuild_file = self._info ['ebuild_file']
         if ebuild_file is not None:
            return os.path.basename ( ebuild_file )

      elif key == 'package_src_destpath':
         # src file path relative to distroot (mirror root dir)
         destpath = self._info.get ('src_uri_dest', None )
         return ( destpath or self._info ['package_filename'] )

      # end if <key matches ...>

      # explicit return
      return None
   # --- end of _get_virtual (...) ---

   def get_create (
      self, key, newtype, convert=False, check_type=True, create_kw=None
   ):
      """Tries to get a value from the info dict. Creates it as newtype if
      necessary.

      Note: This operation is "unsafe". No locks will be acquired etc.

      arguments:
      * key         -- info key
      * newtype     -- "expected type", also used for creating new values
      * convert     -- if True: convert existing value (defaults to False)
      * check_type  -- if True: check whether the type of existing value is
                       a (sub-)type of newtype (defaults to True)
                       This arg can also be a type.
                       Has no effect if convert is set to True
      * create_kw   -- either None or a dict that will used as keyword args
                       when creating newtype
      """
      v = self.get ( key, do_fallback=True )
      if v is None:
         newv = newtype ( **create_kw ) if create_kw else newtype()
         self._info [key] = newv
         return newv
      elif convert:
         return newtype ( v )
      elif check_type:
         want_type = (
            check_type if ( type ( check_type ) is type ) else newtype
         )

         #if type ( v ) is want_type:
         if isinstance ( v, want_type ):
            return v
         else:
            raise TypeError (
               "key {k} should have type {t0}, but is a {t1}!".format (
                  k=key, t0=want_type, t1=type(v)
               )
            )
      else:
         return v
   # --- end of get_create (...) ---

   def get_desc_data ( self ):
      """Returns the DESCRIPTION data for this PackageInfo (by reading the
      R package file if necessary).
      """
      if 'desc_data' not in self._info:
         self._check_readonly()
         if 'desc_data' not in self._info:
            self._info ['desc_data'] = descriptionreader.read ( self )
      # -- end if;

      return self._info ['desc_data']
   # --- end of get_desc_data (...) ---

   def get_distmap_item ( self, allow_digest_create=False, no_digest=False ):
      """Returns a 2-tuple ( key, info ) for the distmap."""
      if no_digest:
         digest = None
      elif allow_digest_create:
         digest = self.make_distmap_hash()
      else:
         digest = self.hashdict [self.DISTMAP_DIGEST_TYPE]

      distfile = self.get ( "package_src_destpath" )
      repo     = self.get ( "origin" )

      return (
         distfile, (
            distfile,
            repo.name,
            os.path.relpath ( self.get ( "package_file" ), repo.distdir ),
            digest
         )
      )
   # --- end of get_distmap_item (...) ---

   def get_src_uri_dest ( self ):
      """Returns a the package's filesystem path relative to the mirror
      directory."""
      return self.get ( "package_src_destpath" )
   # --- end of get_src_uri_dest (...) ---

   # which is also the key for distmap entries
   get_distmap_key = get_src_uri_dest

   def get_distmap_value ( self, allow_digest_create=False, no_digest=False ):
      """Returns a data tuple for creating DistMapInfo instances.

      arguments:
      * allow_digest_create --
      * no_digest           -- use None as digest
      """
      return self.get_distmap_item ( allow_digest_create, no_digest ) [1]
   # --- end of get_distmap_value (...) ---

   def make_distmap_hash ( self ):
      """Creates (and returns) the distmap package file hash."""
      return self.make_hashes (
         { self.DISTMAP_DIGEST_TYPE, } ) [self.DISTMAP_DIGEST_TYPE]
   # --- end of make_distmap_hash (...) ---

   def make_hashes ( self, hashlist ):
      """Creates zero or more hashes and returns the hashdict.

      Note: the hashdict can also be accessed directly via <this>.hashdict.

      arguments:
      * hashlist -- list of hash names, e.g. "sha256", "md5"
      """
      pkgfile = self.get ( "package_file" )

      if self.hashdict:
         new_hashes = (
            frozenset ( hashlist ) - frozenset ( self.hashdict.keys() )
         )
      else:
         new_hashes = hashlist


      if new_hashes:
         self.hashdict.update (
            roverlay.digest.multihash_file ( pkgfile, new_hashes )
         )

      return self.hashdict
   # --- end of make_hashes (...) ---

   def revbump ( self, newrev=None, ebuild_only=False ):
      """Do whatever necessary to revbump this pakages, that is set/update
      all data like src_uri_dest and ebuild_verstr.

      arguments:
      * newrev      -- new revision, (current rev + 1) is used if this is None
      * ebuild_only -- if set and True: revbump the package only,
                       keeping the distfile path as-is
      """
      rev     = self._info['rev'] + 1 if newrev is None else int ( newrev )
      rev_str = ( '-r' + str ( rev ) ) if rev > 0 else ''
      vstr    = (
         '.'.join ( str ( k ) for k in self._info['version'] ) + rev_str
      )

      if not ebuild_only:
         # preserve destpath directory
         #  (this allows to handle paths like "a/b.tar/pkg.tgz" properly)
         #
         old_destpath = self ['package_src_destpath'].rpartition ( os.path.sep )

         # os.path.splitext does not "recognize" .tar.gz
         fhead, ftar, fext = old_destpath[2].rpartition ( '.tar' )
         if not ftar:
            fhead, fext = os.path.splitext ( fext )

         # FIXME: any way to get this reliably (+faster) done without a regex?
         #  ( a,b,c=fhead.rpartition ( '-r' ); try int(c) ...; ?)
         distfile = (
            old_destpath[0] + old_destpath[1]
            + self.EBUILDREV_REGEX.sub ( '', fhead ) + rev_str + ftar + fext
         )

         self._info ['src_uri_dest']  = distfile
      # -- end if <ebuild_only>

      self._info ['rev']           = rev
      self._info ['ebuild_verstr'] = vstr

      return self
   # --- end of revbump (...) ---

   def __getitem__ ( self, key ):
      """Returns an item."""
      return self.get ( key, do_fallback=False )
   # --- end of __getitem__ (...) ---

   def __setitem__ ( self, key, value ):
      """Sets an item.

      arguments:
      * key --
      * value --

      raises: Exception when readonly
      """
      self._check_readonly()
      self._info [key] = value
   # --- end of __setitem__ (...) ---

   def set_direct_unsafe ( self, key, value ):
      """Sets an item. This operation is unsafe (write-accessibility won't
      be checked, data won't be validated).

      arguments:
      * key   --
      * value --
      """
      self._info [key] = value
   # --- end of set_direct_unsafe (...) ---

   def update_now ( self, **info ):
      """Updates the package info data with temporarily enabling write access.
      Data will be readonly after calling this method.

      arguments:
      * **info --
      """
      self.set_writeable()
      try:
         self.update ( **info )
      finally:
         self.set_readonly()
   # --- end of update_now (...) ---

   def update_unsafe ( self, **info ):
      """Updates the package info data without retrieving any locks or
      checking writability.
      Meant for usage with "package actions" (packagerules module).

      arguments:
      * **info --
      """
      # remove_auto has to be the last action (keyword order is not "stable")
      remove_auto = info.pop ( 'remove_auto', None )

      self._update ( info )
      if remove_auto:
         self._remove_auto ( remove_auto )
   # --- end of update_unsafe (...) ---

   def update ( self, **info ):
      """Uses **info to update the package info data.

      arguments:
      * **info --

      raises: Exception when readonly
      """
      # remove_auto has to be the last action (keyword order is not "stable")
      remove_auto = info.pop ( 'remove_auto', None )

      self._check_readonly()

      self._update ( info )
      if remove_auto:
         self._remove_auto ( remove_auto )
   # --- end of update (**kw) ---

   def add_evar ( self, evar ):
      """Adds an ebuild variable.

      arguments:
      * evar   --
      """
      #self._check_readonly()

      if not hasattr ( self, '_evars' ):
         self._evars = dict()
      # -- end if;
      self._evars [evar.get_pseudo_hash()] = evar
   # --- end of add_evar (...) ---

   def parse_ebuild_src_uri ( self, category_name ):
      return roverlay.util.ebuildparser.SrcUriParser.from_file (
         self._info ['ebuild_file'], self.create_vartable ( category_name )
      )
   # --- end of parse_ebuild_src_uri (...) ---

   def parse_ebuild_distfiles ( self, category_name, **kw ):
      parser = self.parse_ebuild_src_uri ( category_name )
      for distfile in parser.iter_local_files ( **kw ):
         yield distfile
   # --- end of parse_ebuild_distfiles (...) ---

   def get_evars ( self ):
      """Returns all ebuild variables."""
      if hasattr ( self, '_evars' ):
         return list ( self._evars.values() )
      else:
         return None
   # --- end of get_evars (...) ---

   def _update ( self, info ):
      """Updates self._info using the given info dict.

      arguments:
      * info --
      """
      initial = len ( self._info ) == 0

      for key, value in info.items():

         if key in self.__class__._UPDATE_KEYS_SIMPLE or (
            initial and key in self.__class__._UPDATE_KEYS_SIMPLE_INITIAL
         ):
            self._info [key] = value

         elif key in self.__class__._UPDATE_KEYS_FILTER_NONE:
            if value is not None:
               self._info [key] = value

         elif key == 'origin':
            self._info ['origin'] = value
            self._info ['repo_name'] = value.name

         elif key == 'filename':
            self._use_filename ( value )

         elif key == 'pvr':
            self._use_pvr ( value )

         elif key == 'suggests':
            # FIXME/TODO: remove
            self._info ['has_suggests'] = value

         elif key == 'filepath':
            self._use_filepath ( value )

         elif key == 'remove':
            for k in value:
               try:
                  if k in self._info: del self._info [k]
               except KeyError:
                  pass

         elif key == 'make_desc_data':
            if value:
               self.get_desc_data()

         else:
            self.logger.error (
               "in _update(): unknown info key {!r}!".format ( key )
            )
      # -- end for;

      # FIXME (if needed):
      #  the package rule parser doesn't create lazy actions, currently,
      #  so calling apply_lazy_actions() would do nothing
      ##self.apply_lazy_actions()
   # --- end of _update (...) ---

   def _use_filename ( self, _filename ):
      """auxiliary method for update(**kw)

      arguments:
      * _filename --
      """
      filename_with_ext = _filename

      # remove .tar.gz .tar.bz2 etc.
      filename = PackageInfo.PKGSUFFIX_REGEX.sub ( '', filename_with_ext )

      self.logger = logging.getLogger ( filename )

      package_name, sepa, package_version = filename.partition (
         config.get ( 'R_PACKAGE.name_ver_separator', '_' )
      )

      if not sepa:
         # file name unexpected, tarball extraction will (probably) fail
         self.logger.error ( "unexpected file name {!r}.".format ( filename ) )
         raise Exception   ( "cannot use file {!r}.".format ( filename ) )

      version_str = PackageInfo.EBUILDVER_REGEX.sub ( '.', package_version )

      try:
         version = tuple ( int ( z ) for z in version_str.split ( '.' ) )
         self._info ['version'] = (
            roverlay.versiontuple.IntVersionTuple ( version )
         )
      except ValueError as ve:
         # version string is malformed, cannot use it
         self.logger.error (
            "Cannot parse version string {!r} for {!r}".format (
               _filename, version_str
            )
         )
         raise

      # using package name as name (unless modified later),
      #  using pkg_version for the ebuild version

      # removing illegal chars from the package_name
      ebuild_name = strutil.fix_ebuild_name ( package_name )

      # for DescriptionReader
      self._info ['package_name']       = package_name
      self._info ['rev']                = 0
      self._info ['name']               = ebuild_name
      self._info ['ebuild_verstr']      = version_str
      self._info ['package_filename']   = filename_with_ext
      self._info ['package_filename_x'] = filename
   # --- end of _use_filename (...) ---

   def _use_pvr ( self, pvr ):
      version_tuple, revision = split_pvr_str ( pvr )

      self._info ['version']       = version_tuple
      self._info ['rev']           = revision
      self._info ['ebuild_verstr'] = pvr
   # --- end of _use_pvr (...) ---

   def _remove_auto ( self, ebuild_status ):
      """Removes all keys from this PackageInfo instance that are useless
      after entering status 'ebuild_status' (like ebuild in overlay and
      written -> don't need the ebuild string etc.)
      """
      if ebuild_status == 'ebuild_written':

         # selectively delete entries that are no longer required

         for key in self.__class__._REMOVE_KEYS_EBUILD:
            try:
               del self._info [key]
            except KeyError:
               pass
      # -- if
   # --- end of _remove_auto (...) ---

   def _use_filepath ( self, _filepath ):
      """auxiliary method for update(**kw)

      arguments:
      * _filepath --
      """
      self.logger.warn (
         'Please note that _use_filepath is only meant for testing.'
      )
      filepath = os.path.abspath ( _filepath )
      self ['package_file'] = filepath
      self._use_filename ( os.path.basename ( filepath ) )
   # --- end of _use_filepath (...) ---

   def create_vartable ( self, category ):
      PN  = self._info ['name']
      PV  = str ( self._info ['version'] )
      PR  = str ( self._info ['rev'] )
      PVR = PV if PR == '0' else ( PV + '-' + PR )

      return {
         'P'        : PN + '-' + PV,
         'PN'       : PN,
         'PV'       : PV,
         'PR'       : PR,
         'PVR'      : PVR,
         'PF'       : PN + PVR,
         'CATEGORY' : category,
      }
   # --- end of create_vartable (...) ---

   def __str__ ( self ):
      return "<PackageInfo for {pkg}>".format (
         pkg=self.get (
            'package_file', fallback_value='[unknown file]', do_fallback=True
      ) )
   # --- end of __str__ (...) ---