~sbeattie/conflictchecker/conflictchecker-fixups

« back to all changes in this revision

Viewing changes to findconflicts/__init__.py

  • Committer: Robert Collins
  • Date: 2007-01-22 15:22:31 UTC
  • Revision ID: robertc@robertcollins.net-20070122152231-cbb405ccc4028d65
db version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
 
6
6
"""A conflict finder for packages."""
7
7
 
8
 
import apt_inst
9
 
import apt_pkg
 
8
import gc
 
9
import re
 
10
import shlex
 
11
import sys
10
12
import tempfile
 
13
import time
11
14
from unittest import TestSuite
12
15
import urllib
13
16
 
 
17
import apt_inst
 
18
import apt_pkg
 
19
from storm import properties, references
 
20
 
14
21
from findconflicts.tuned_gzip import *
15
22
 
16
23
def test_suite():
28
35
        self.set_mirror('http://archive.ubuntu.com/ubuntu/')
29
36
        # the default set of distributions to look at.
30
37
        # we should get this from launchpad
31
 
        edgy_stock_components = {
 
38
        feisty_stock_components = {
32
39
            'main':['amd64', 'i386', 'powerpc', 'sparc'],
33
40
            'multiverse':['amd64', 'i386', 'powerpc', 'sparc'],
34
41
            'restricted':['amd64', 'i386', 'powerpc', 'sparc'],
35
42
            'universe':['amd64', 'i386', 'powerpc', 'sparc'],
36
43
            }
 
44
        # edgy is currently identical to feisty
 
45
        edgy_stock_components = feisty_stock_components
37
46
        # dapper is currently identical to edgy
38
47
        dapper_stock_components = edgy_stock_components
39
48
        # as it happens, breezy is identical
51
60
            'edgy-backports': edgy_stock_components,
52
61
            'edgy-security': edgy_stock_components,
53
62
            'edgy-updates': edgy_stock_components,
 
63
            'feisty': feisty_stock_components,
 
64
            'feisty-backports': feisty_stock_components,
 
65
            'feisty-security': feisty_stock_components,
 
66
            'feisty-updates': feisty_stock_components,
54
67
            })
55
68
 
56
69
    def _extract_package_identity(self, sections):
65
78
        return info
66
79
 
67
80
    def infoFromPackageUrl(self, url):
68
 
        """Return standard package information from a package at a url."""
 
81
        """Return standard package information from a package at a url.
 
82
        
 
83
        The package is downloaded and analysed locally. If possible
 
84
        preinst diversions are detected, if that fails, the diversion list is
 
85
        set to ['UNKNOWN'] which cannot occur in regular packages.
 
86
        """
69
87
        package_file = urllib.urlopen(url)
70
88
        local_file = tempfile.TemporaryFile()
71
89
        try:
79
97
            control = apt_inst.debExtractControl(local_file)
80
98
            sections = apt_pkg.ParseSection(control)
81
99
            info = self._extract_package_identity(sections)
 
100
            info['controltext'] = control
82
101
            info['replaces'] = sections.get('replaces', '')
83
102
            info['conflicts'] = sections.get('conflicts', '')
84
103
            info['provides'] = sections.get('provides', '')
85
104
            info['files'] = []
 
105
            # find preinst diversions
 
106
            local_file.seek(0)
 
107
            preinst = apt_inst.debExtractControl(local_file, 'preinst')
 
108
            if preinst is not None:
 
109
                try:
 
110
                    info['diversions'] = PreinstParser().extract_diversions(preinst)
 
111
                except ParseError:
 
112
                    info['diversions'] = ['UNKNOWN']
86
113
            # extract the archive and throw it away again.
87
114
            local_file.seek(0)
88
115
            def capture_filepaths(what, name, link, mode, uid, gid, size,
144
171
        :param distributions: a dictionary of distribution:components-dict,
145
172
        which then contains a list of architectures. e.g.
146
173
        {'dapper':{'main':['i386','sparc']}}
 
174
        :return: A dictionary of package tuples : package .deb urls.
147
175
        """
148
176
        result = {}
149
177
        if distributions is None:
344
372
        self._published_packages = set(packages)
345
373
 
346
374
 
347
 
class FileCollection(object):
348
 
    """A collection of files. Each file has a number of keys associated to it.
349
 
 
350
 
    Adding a new set of files will return all the keys across all the files
351
 
    that coexist with it.
 
375
class BinaryPackageFiles(object):
 
376
    """The files that are installed by a binary package. 
 
377
    
 
378
    Each file belongs to some number of packages, and likewise, each package
 
379
    has some number of files.
 
380
 
 
381
    Adding a new set of files will return all the packages across all the files
 
382
    that overlap.
 
383
 
 
384
    The relationship is bidirectional: its possible to ask for the files for a
 
385
    package, the packages that potentially collide on files for a given package,
 
386
    or the packages that provide a file.
352
387
    """
353
388
 
354
389
    def __init__(self):
418
453
        return False
419
454
 
420
455
 
 
456
class ArchiveDatabase(object):
 
457
    """A place to put routines referring to the general idea of the Archive."""
 
458
 
 
459
    @staticmethod
 
460
    def initialize(uri):
 
461
        """Initialise an Archive database on uri."""
 
462
        from storm.locals import create_database, Store
 
463
        database = create_database(uri)
 
464
        store = Store(database)
 
465
        # ---- architecture ----
 
466
        store.execute(
 
467
            "CREATE TABLE architecture "
 
468
            "(id INTEGER PRIMARY KEY,"
 
469
            " arch VARCHAR)")
 
470
        store.execute("CREATE UNIQUE INDEX _unique_arch ON architecture (arch)")
 
471
        # ---- packagename ----
 
472
        store.execute(
 
473
            "CREATE TABLE packagename "
 
474
            "(id INTEGER PRIMARY KEY,"
 
475
            " name VARCHAR)")
 
476
        store.execute("CREATE UNIQUE INDEX _unique_packagename ON packagename (name)")
 
477
        # ---- packageversion ----
 
478
        store.execute(
 
479
            "CREATE TABLE packageversion "
 
480
            "(id INTEGER PRIMARY KEY,"
 
481
            " name_id INTEGER,"
 
482
            " version VARCHAR,"
 
483
            " arch_id INTEGER,"
 
484
            " controltext VARCHAR,"
 
485
            " analyzed_level INTEGER)")
 
486
        store.execute("CREATE UNIQUE INDEX _unique_version ON packageversion "
 
487
            "(name_id, version, arch_id)")
 
488
        store.execute("CREATE INDEX _packageversion_analyzed on packageversion "
 
489
            "(analyzed_level)")
 
490
        # ---- filepath ----
 
491
        store.execute(
 
492
            "CREATE TABLE filepath "
 
493
            "(id INTEGER PRIMARY KEY,"
 
494
            " path VARCHAR)")
 
495
        store.execute("CREATE UNIQUE INDEX _unique_filepath ON filepath "
 
496
            "(path)")
 
497
        # ---- binpackagefiles ----
 
498
        store.execute(
 
499
            "CREATE TABLE binpackagefiles "
 
500
            "(package_id INTEGER,"
 
501
            " file_id INTEGER)")
 
502
        store.execute("CREATE UNIQUE INDEX _unique_packagefile ON binpackagefiles "
 
503
            "(package_id, file_id)")
 
504
        store.execute("CREATE INDEX _bpf_fileid on binpackagefiles (file_id)")
 
505
        # ---- diversion ----
 
506
        store.execute(
 
507
            "CREATE TABLE diversion "
 
508
            "(package_id INTEGER,"
 
509
            " file_id INTEGER)")
 
510
        store.execute("CREATE UNIQUE INDEX _unique_diversion ON diversion"
 
511
            "(package_id, file_id)")
 
512
        # --------------------
 
513
        store.commit()
 
514
        return store
 
515
 
 
516
    @staticmethod
 
517
    def open_or_create():
 
518
        """Open or create if needed the default store 'findconflicts.db'."""
 
519
        from storm.locals import create_database, Store
 
520
        from storm.exceptions import OperationalError
 
521
        uri = 'sqlite:findconflicts.db'
 
522
        database = create_database(uri)
 
523
        store = Store(database)
 
524
        try:
 
525
            list(store.find(PackageName))
 
526
            return store
 
527
        except OperationalError, e:
 
528
            if str(e) != 'no such table: packagename':
 
529
                # unexpected error - bail
 
530
                raise
 
531
            # expected error in a new db - make a new one
 
532
            del database
 
533
            del store
 
534
            return ArchiveDatabase.initialize(uri)
 
535
 
 
536
 
 
537
class Filepath(object):
 
538
    """A filepath contained in a binary package."""
 
539
 
 
540
    __table__ = "filepath", ("id", "path")
 
541
    id = properties.Int()
 
542
    path = properties.Str()
 
543
 
 
544
    def __init__(self, path):
 
545
        """Create a Filepath.
 
546
 
 
547
        :param path: The path to be recorded.
 
548
        """
 
549
        self.path = path
 
550
 
 
551
    @staticmethod
 
552
    def Ensure(path, store):
 
553
        """Ensure that there is a path called path in store store and return it.
 
554
 
 
555
        :param path: The string for the path.
 
556
        :param store: The store to use.
 
557
        :return: A Filepath.
 
558
        """
 
559
        try:
 
560
            return list(store.find(Filepath, path=path))[0]
 
561
        except IndexError:
 
562
            result = Filepath(path)
 
563
            store.add(result)
 
564
            return result
 
565
 
 
566
    def __eq__(self, other):
 
567
        return other is not None and other.path == self.path
 
568
 
 
569
    def __lt__(self, other):
 
570
        return self.path < other.path
 
571
 
 
572
    def __repr__(self):
 
573
        return "<findconflicts.Filepath at %d %s>" % (
 
574
            id(self), self.path)
 
575
 
 
576
    def __str__(self):
 
577
        return self.path
 
578
 
 
579
 
 
580
class PackageArchitecture(object):
 
581
    """The architecture of a binary package."""
 
582
 
 
583
    __table__ = "architecture", ("id", "arch")
 
584
    id = properties.Int()
 
585
    arch = properties.Str()
 
586
 
 
587
    def __init__(self, arch):
 
588
        """Create a PackageArchitecture.
 
589
 
 
590
        :param arch: The string representing the architecture. I.e. 'all'.
 
591
        """
 
592
        self.arch = arch
 
593
 
 
594
    def __eq__(self, other):
 
595
        return other is not None and other.arch == self.arch
 
596
 
 
597
    @staticmethod
 
598
    def Ensure(arch, store):
 
599
        """Ensure that there is a arch 'name' in store store and return it.
 
600
 
 
601
        :param arch: The string for the name.
 
602
        :param store: The store to use.
 
603
        :return: A PackageArchitecture.
 
604
        """
 
605
        try:
 
606
            return list(store.find(PackageArchitecture, arch=arch))[0]
 
607
        except IndexError:
 
608
            result = PackageArchitecture(arch)
 
609
            store.add(result)
 
610
            return result
 
611
 
 
612
    def __str__(self):
 
613
        return self.arch
 
614
 
 
615
 
 
616
class PackageFile(object):
 
617
    """An occurance of a file in a specific package.
 
618
    
 
619
    This class should not be used at all, it only exists to 
 
620
    facilitate describing the object links to STorm.
 
621
    """
 
622
 
 
623
    __table__ = "binpackagefiles", ("package_id", "file_id")
 
624
    package_id = properties.Int()
 
625
    file_id = properties.Int()
 
626
 
 
627
 
 
628
class PackageName(object):
 
629
    """The name of a binary package."""
 
630
 
 
631
    __table__ = "packagename", ("id", "name")
 
632
    id = properties.Int()
 
633
    name = properties.Str()
 
634
 
 
635
    def __init__(self, name):
 
636
        """Create a PackageName.
 
637
 
 
638
        :param name: The string representing the name. I.e. 'atomix'.
 
639
        """
 
640
        self.name = name
 
641
 
 
642
    def __eq__(self, other):
 
643
        return other is not None and other.name == self.name
 
644
 
 
645
    @staticmethod
 
646
    def Ensure(name, store):
 
647
        """Ensure that there is a name 'name' in store store and return it.
 
648
 
 
649
        :param name: The string for the name.
 
650
        :param store: The store to use.
 
651
        :return: A PackageName.
 
652
        """
 
653
        try:
 
654
            return list(store.find(PackageName, name=name))[0]
 
655
        except IndexError:
 
656
            result = PackageName(name)
 
657
            store.add(result)
 
658
            return result
 
659
 
 
660
    def __str__(self):
 
661
        return self.name
 
662
 
 
663
 
 
664
class PackageVersion(object):
 
665
    """The name of a binary package."""
 
666
 
 
667
    __table__ = "packageversion", "id"
 
668
    id = properties.Int()
 
669
    controltext = properties.Str()
 
670
    name_id = properties.Int()
 
671
    name = references.Reference(name_id, PackageName.id)
 
672
    version = properties.Str()
 
673
    arch_id = properties.Int()
 
674
    arch = references.Reference(arch_id, PackageArchitecture.id)
 
675
    analyzed_level = properties.Int()
 
676
    files = references.ReferenceSet(id, PackageFile.package_id,
 
677
        PackageFile.file_id, Filepath.id)
 
678
 
 
679
    def __init__(self, packagename, version, packagearch, controltext,
 
680
        analyzed_level=0, diversions=None):
 
681
        """Create a PackageVersion.
 
682
 
 
683
        :param packagename: The PackageName for the version.
 
684
        :param version: A string representing the version.
 
685
        :param packagearch: The PackageArchitecture for the version.
 
686
        :param controltext: The text of the control document for the version.
 
687
            This is used to generate the conflicts/replaces etc sections on
 
688
            demand.
 
689
        :param analyzed_level: The amount of analysis performed on this package.
 
690
            Current values: 0 - None, 1 - simple conflicts/replaces/provides.
 
691
        :param diversions: An iterable of the diversions introduced by this
 
692
            package.
 
693
        """
 
694
        self.controltext = controltext
 
695
        self.name = packagename
 
696
        self.version = version
 
697
        self.arch = packagearch
 
698
        self.analyzed_level = analyzed_level
 
699
 
 
700
    def __eq__(self, other):
 
701
        return (other is not None and 
 
702
            other.name == self.name and
 
703
            other.version == self.version and
 
704
            other.arch == self.arch and
 
705
            other.controltext == self.controltext and
 
706
            other.analyzed_level == self.analyzed_level)
 
707
 
 
708
    def __load__(self):
 
709
        """Set non-storm managed attributes on load."""
 
710
        store = getattr(self, '__object_info')['store']
 
711
        result = store.execute(
 
712
                    "SELECT path FROM packageversion, diversion, filepath "
 
713
                    "WHERE packageversion.id=? AND "
 
714
                    "packageversion.id=diversion.package_id AND "
 
715
                    "diversion.file_id=filepath.id", [self.id])
 
716
        self.diversions = set(str(row[0]) for row in result)
 
717
 
 
718
    def analyse(self, finder):
 
719
        """Analyse this package against the stores collection.
 
720
 
 
721
        If no conflicts are found, the packages analyzed_level is set to the
 
722
        current analysis level (1). This is set to mark the analysis as both
 
723
        complete and needing no further attention.
 
724
 
 
725
        :param finder: A MissingConflictFinder to use in the analysis.
 
726
        :return: A list of conflicts.
 
727
        """
 
728
        possible_conflicts = self.find_unsafely_colocated_packages()
 
729
        conflicting_packages = []
 
730
        for conflict_package in possible_conflicts:
 
731
            if self.conflicts_against_package(conflict_package, finder):
 
732
                conflicting_packages.append(conflict_package)
 
733
        if not conflicting_packages:
 
734
            self.analyzed_level = 1
 
735
        return conflicting_packages
 
736
 
 
737
    @staticmethod
 
738
    def Analyse_ids(package_ids, store, finder, log):
 
739
        """Analyse package_ids in store, logging to log.
 
740
 
 
741
        :param package_ids: The database ids of the package versions to analyse.
 
742
            Must be iterable.
 
743
        :param store: The store to perform database queries on.
 
744
        :param finder: a MissingConflictFinder to use.
 
745
        :param log: A stream to write results to.
 
746
        :return: Nothing.
 
747
        """
 
748
        errors_found = False
 
749
        total = len(package_ids)
 
750
        for index, package_id in enumerate(package_ids):
 
751
            start = time.time()
 
752
            package = store.find(PackageVersion, id=package_id).one()
 
753
            conflicting_packages = package.analyse(finder)
 
754
            if not conflicting_packages:
 
755
                # dont use the regular commit as it exhibits massive slowdowns
 
756
                # after thousands of commits.
 
757
                store.flush()
 
758
                store._connection.commit()
 
759
                #store.commit()
 
760
            else:
 
761
                errors_found = True
 
762
                for conflict_package in conflicting_packages:
 
763
                    #print "possible conflict: %s" % package.format_conflict(conflict_package)
 
764
                    log.write("possible conflict: %s\n" %
 
765
                        package.format_conflict(conflict_package))
 
766
                log.flush()
 
767
            print "Finished %s in %.3f seconds [%d/%d]" % (package, time.time() - start, index, total)
 
768
            #if not index % 500:
 
769
            #    # stop storm going slower.
 
770
            #    gc.collect()
 
771
        if not errors_found:
 
772
            log.write("no errors (checked %d packages).\n" % total)
 
773
            log.flush()
 
774
 
 
775
    def conflicts_against_package(self, packageversion, finder):
 
776
        """Determine if this package and packageversion are safe together.
 
777
 
 
778
        :param packageversion: A packageversion to check.
 
779
        :param finder: A MissingConflictFinder to perform the check with.
 
780
        :return: True if there is a conflict that the packaging system cannot
 
781
            predict (i.e. a missing replaces or conflicts clause).
 
782
        """
 
783
        current_info = self.get_info()
 
784
        conflict_info = packageversion.get_info()
 
785
        if (finder.concurrent_permitted(current_info, conflict_info) and not
 
786
            finder.concurrent_safe(current_info, conflict_info)):
 
787
            return True
 
788
        return False
 
789
 
 
790
    @staticmethod
 
791
    def Find(package_tuple, store):
 
792
        """Try to find package package_tuple in store."""
 
793
        results = store.find(
 
794
            (PackageName, PackageVersion, PackageArchitecture),
 
795
            PackageName.id == PackageVersion.name_id,
 
796
            PackageArchitecture.id == PackageVersion.arch_id,
 
797
            PackageName.name == package_tuple[0],
 
798
            PackageVersion.version == package_tuple[1],
 
799
            PackageArchitecture.arch == package_tuple[2],
 
800
            ).one()
 
801
        if not results:
 
802
            raise KeyError
 
803
        return results[1]
 
804
        
 
805
#        result = store.find(PackageVersion,
 
806
#            "packagename.name=%s and packageversion.version=%s and packagearch.arch=%s" % package_tuple).one()
 
807
#            name = PackageName(package_tuple[0]),
 
808
#            version = package_tuple[1],
 
809
#            arch = PackageArchitecture(package_tuple[2])).one()
 
810
        if result is None:
 
811
            raise KeyError(package_tuple)
 
812
        return result
 
813
 
 
814
    def find_unsafely_colocated_packages(self):
 
815
        """Return a list of packages which install files this package does.
 
816
 
 
817
        Packages that are returned are soley those that the package manager
 
818
        *might* allow to be coinstalled - packages with the same name, or on
 
819
        architectures that are mutually incompatible (i.e. ppc vs i386) are
 
820
        ignored - and which are not known to be ok to coinstall.
 
821
 
 
822
        Files that are diverted by either end of a pair of packages are
 
823
        considered safe.
 
824
 
 
825
        This function deliberately does not perform advanced filtering,
 
826
        to allow callers to perform their own analysis.
 
827
        """
 
828
        # peek under the hood to get the store instance to use.
 
829
        store = getattr(self, '__object_info')['store']
 
830
        all = PackageArchitecture.Ensure('all', store)
 
831
        store.flush()
 
832
        all_id = all.id
 
833
        if self.arch.arch != 'all':
 
834
            candidate_arches = [self.arch_id, all_id]
 
835
        else:
 
836
            candidate_arches = [arch.id for arch in iter(store.find(PackageArchitecture))]
 
837
        # search for package tuples: 
 
838
        query = ("SELECT DISTINCT packageversion.id FROM "
 
839
            "packageversion, binpackagefiles, binpackagefiles as bpf2 "
 
840
            "WHERE bpf2.package_id=%d AND " #the bpf for this package
 
841
            " bpf2.file_id=binpackagefiles.file_id AND " # and all bpf that intersect
 
842
            " bpf2.file_id NOT IN (SELECT file_id FROM diversion WHERE package_id in (%d,binpackagefiles.package_id)) AND" # filter out diversions - by knocking out files from this package that are diverted by it or the other package.
 
843
            " packageversion.arch_id IN "
 
844
            "(" % (self.id, self.id) + "?, " * len(candidate_arches) + ") AND "
 
845
            "packageversion.id = binpackagefiles.package_id AND " # limit to the matching packageversion
 
846
            "packageversion.name_id != %d" % self.name_id #exclude the same named packge
 
847
            )
 
848
        candidates = [result[0] for result in store.execute(query, candidate_arches).get_all()]
 
849
        return sorted(store.find(PackageVersion,
 
850
            PackageVersion.id.is_in(candidates)), key=lambda x:str(x))
 
851
 
 
852
    def find_common_files(self, other):
 
853
        """Find the files in common with other - useful for debugging.
 
854
 
 
855
        :param other: Another PackageVersion.
 
856
        """
 
857
        # peek under the hood to get the store instance to use.
 
858
        store = getattr(self, '__object_info')['store']
 
859
        common_ids = [result[0] for result in store.execute(
 
860
            "SELECT DISTINCT binpackagefiles.file_id FROM binpackagefiles, "
 
861
            "binpackagefiles as bpf2 "
 
862
            "WHERE binpackagefiles.package_id = %d AND "
 
863
            "bpf2.package_id = %d AND binpackagefiles.file_id = bpf2.file_id"
 
864
            % (self.id, other.id)
 
865
            ).get_all()]
 
866
        return sorted(store.find(Filepath, Filepath.id.is_in(common_ids)))
 
867
 
 
868
    @staticmethod
 
869
    def PackageTuplesToUnicode(package_tuples):
 
870
        """Convery package_tuples into a unicode string list for use in queries.
 
871
 
 
872
        :param package_tuples: An iterable of (name, version, arch) tuples.
 
873
        """
 
874
        return [unicode("%s-%s-%s") % one_tuple for one_tuple in package_tuples]
 
875
 
 
876
    @staticmethod
 
877
    def Find_new_versions(package_tuples, store):
 
878
        """Return the set subtraction of package_tuples and the store."""
 
879
        # We select all the packages that do exist from the db, and then do
 
880
        # a set subtraction within python. If the DB layer starts to error
 
881
        # due to the size of the IN list, we can just do it in chunks of X
 
882
        # thousand.
 
883
        package_strings = PackageVersion.PackageTuplesToUnicode(package_tuples) 
 
884
        existing_versions = set(store.execute("SELECT packagename.name, "
 
885
            " packageversion.version, architecture.arch FROM "
 
886
            "packageversion, packagename, architecture "
 
887
            "WHERE packagename.id=packageversion.name_id and "
 
888
            "packageversion.arch_id=architecture.id AND "
 
889
            "packagename.name||'-'||packageversion.version||'-'||architecture.arch IN (%s)" % ("?," * len(package_strings)),
 
890
            package_strings).get_all())
 
891
        return set(package_tuples) - existing_versions
 
892
 
 
893
    def format_conflict(self, conflict_package):
 
894
        """Return a string describing the conflict with conflict_package.
 
895
 
 
896
        :param conflict_package: A package that a conflict has been reported
 
897
            against.
 
898
        :return: A string describing the conflict.
 
899
        """
 
900
        return "%s, %s %s" % (self, conflict_package,
 
901
            [str(f) for f in self.find_common_files(conflict_package)])
 
902
 
 
903
    def get_info(self):
 
904
        """Return a dict with key information about this package.
 
905
 
 
906
        This is in the format expected by the MissingConflictFinder.
 
907
        :return: A dict with the keys name, version, arch, conflicts, provides 
 
908
            and replaces.
 
909
        """
 
910
        result = {}
 
911
        result['name'] = self.name.name
 
912
        result['version'] = self.version
 
913
        result['arch'] = self.arch.arch
 
914
        sections = apt_pkg.ParseSection(self.controltext)
 
915
        result['conflicts'] = sections.get('conflicts', '')
 
916
        result['provides'] = sections.get('provides', '')
 
917
        result['replaces'] = sections.get('replaces', '')
 
918
        return result
 
919
 
 
920
    @staticmethod
 
921
    def InsertFromInfo(info, store):
 
922
        """Insert the package details from 'info' into the store 'store'.
 
923
 
 
924
        :param info: A dictionary as returned by infoFromPackageUrl.
 
925
        """
 
926
        arch = PackageArchitecture.Ensure(info['arch'], store)
 
927
        name = PackageName.Ensure(info['name'], store)
 
928
        version = PackageVersion(
 
929
            name, info['version'], arch, info['controltext'])
 
930
        store.add(version)
 
931
        # we need version to have an id.
 
932
        store.flush()
 
933
        # 1) determine file paths to add - both files and diversions
 
934
        allfiles = set(info['files'])
 
935
        allfiles.update(set(info.get('diversions', [])))
 
936
        existing_files = set(map(str, iter(store.find(Filepath, Filepath.path.is_in(allfiles)))))
 
937
        new_files = allfiles - existing_files
 
938
        # 2) add them
 
939
        for path in new_files:
 
940
            sqlresult = store.execute("INSERT INTO filepath (path) VALUES (?)",
 
941
                (path,)).get_all()
 
942
            assert sqlresult == []
 
943
        # 3) insert all the paths into this table.
 
944
        query = (
 
945
            "INSERT INTO binpackagefiles (package_id, file_id) SELECT %d,"
 
946
            " filepath.id FROM filepath WHERE filepath.path in (" % version.id
 
947
            + "?, " * len(info['files']) + ")" )
 
948
        sqlresult = store.execute(query, info['files']).get_all()
 
949
        assert sqlresult == []
 
950
        # insert diversions
 
951
        if info.get('diversions', []):
 
952
            diversions = list(set(info['diversions']))
 
953
            query = (
 
954
            "INSERT INTO diversion (package_id, file_id) SELECT %d,"
 
955
            " filepath.id FROM filepath WHERE filepath.path in (" % version.id
 
956
            + "?, " * len(diversions) + ")" )
 
957
            sqlresult = store.execute(query, diversions).get_all()
 
958
            assert sqlresult == []
 
959
            version.diversions = set(diversions)
 
960
        return version
 
961
 
 
962
    def __repr__(self):
 
963
        return "<findconflicts.PackageVersion at %d %s-%s-%s>" % (
 
964
            id(self), self.name, self.version, self.arch)
 
965
 
 
966
    def __str__(self):
 
967
        return "%s-%s-%s" % (self.name, self.version, self.arch)
 
968
 
 
969
    @staticmethod
 
970
    def To_analyze(store, published_packages=None):
 
971
        """Return the packageversion_ids that need further analysis.
 
972
 
 
973
        :param store: A Storm store to use to retrieve the ids.
 
974
        :param published_packages: A subset of packages to examine.
 
975
        :return: An iterable of ids.
 
976
        """
 
977
        if not published_packages:
 
978
            params = None
 
979
            query = "select id from packageversion where analyzed_level = 0"
 
980
        else:
 
981
            params = PackageVersion.PackageTuplesToUnicode(published_packages)
 
982
            query = ("SELECT packageversion.id "
 
983
                "FROM packageversion, packagename, architecture "
 
984
                "WHERE packageversion.analyzed_level = 0 AND "
 
985
                "packagename.id=packageversion.name_id and "
 
986
                "packageversion.arch_id=architecture.id AND "
 
987
                "packagename.name||'-'||packageversion.version||'-'||architecture.arch IN (%s)" % ("?," * len(params)))
 
988
        return [result[0] for result in store.execute(query, params)]
 
989
 
 
990
Filepath.binpackages = references.ReferenceSet(Filepath.id, PackageFile.file_id,
 
991
        PackageFile.package_id, PackageVersion.id)
 
992
PackageName.versions = references.ReferenceSet(PackageName.id, PackageVersion.name_id)
 
993
 
 
994
 
 
995
class ParseError(Exception):
 
996
    """A parsing error occured."""
 
997
 
 
998
 
 
999
class PreinstParser(object):
 
1000
    """A parser for debian preinst scripts."""
 
1001
 
 
1002
    def extract_diversions(self, bytes):
 
1003
        """Extract diversion information from the preinst file 'bytes'.
 
1004
 
 
1005
        :param bytes: A debian preinst script.
 
1006
        :return a list of diversions made by the script.
 
1007
        :raise: When an unparseable script is given, ParseError is raised.
 
1008
        """
 
1009
        lines = bytes.split('\n')
 
1010
        if lines[0].startswith("#!/bin/sh"):
 
1011
            lang="shell"
 
1012
        elif lines[0].startswith("#! /bin/sh"):
 
1013
            lang="shell"
 
1014
        elif lines[0].startswith("#!/bin/bash"):
 
1015
            lang="shell"
 
1016
        elif lines[0].startswith("#! /bin/bash"):
 
1017
            lang="shell"
 
1018
        else:
 
1019
            raise ParseError("unknown language")
 
1020
        # assume shell for now.
 
1021
        # combine \ extended lines.
 
1022
        normallines = []
 
1023
        continuation = False
 
1024
        for line in lines[1:]:
 
1025
            if continuation:
 
1026
                normallines[-1] = normallines[-1][:-1] + line
 
1027
            else:
 
1028
                normallines.append(line)
 
1029
            # TODO: ignore \ at the end of comments.
 
1030
            continuation = line.endswith('\\')
 
1031
        result = []
 
1032
        in_string = False
 
1033
        for line in normallines:
 
1034
            # strip out comments.
 
1035
            try:
 
1036
                tokens = shlex.split(line, True)
 
1037
            except ValueError, e:
 
1038
                if e.args == ('No closing quotation', ):
 
1039
                    # tried to split a multiline string: ignore text until
 
1040
                    # the next one (which will be end of the multiline string
 
1041
                    in_string = not in_string
 
1042
                    # and skip this line regardless.
 
1043
                    continue
 
1044
                else:
 
1045
                    print line
 
1046
                    raise
 
1047
            if in_string:
 
1048
                continue
 
1049
            # rejoin the normalised line for regex processing.
 
1050
            line = ' '.join(tokens)
 
1051
            matchobj = re.search(
 
1052
                ".*dpkg-divert\s+--package\s+\S+\s+(?:(?:(?:--add)|(?:--rename)|(?:--divert\s+\S+))\s+){3}(\S+)$", line)
 
1053
            if matchobj:
 
1054
                result.append(matchobj.groups()[0])
 
1055
 
 
1056
        return result
 
1057
 
 
1058
 
421
1059
apt_pkg.InitSystem()