456
class ArchiveDatabase(object):
457
"""A place to put routines referring to the general idea of the Archive."""
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 ----
467
"CREATE TABLE architecture "
468
"(id INTEGER PRIMARY KEY,"
470
store.execute("CREATE UNIQUE INDEX _unique_arch ON architecture (arch)")
471
# ---- packagename ----
473
"CREATE TABLE packagename "
474
"(id INTEGER PRIMARY KEY,"
476
store.execute("CREATE UNIQUE INDEX _unique_packagename ON packagename (name)")
477
# ---- packageversion ----
479
"CREATE TABLE packageversion "
480
"(id INTEGER PRIMARY KEY,"
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 "
492
"CREATE TABLE filepath "
493
"(id INTEGER PRIMARY KEY,"
495
store.execute("CREATE UNIQUE INDEX _unique_filepath ON filepath "
497
# ---- binpackagefiles ----
499
"CREATE TABLE binpackagefiles "
500
"(package_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 ----
507
"CREATE TABLE diversion "
508
"(package_id INTEGER,"
510
store.execute("CREATE UNIQUE INDEX _unique_diversion ON diversion"
511
"(package_id, file_id)")
512
# --------------------
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)
525
list(store.find(PackageName))
527
except OperationalError, e:
528
if str(e) != 'no such table: packagename':
529
# unexpected error - bail
531
# expected error in a new db - make a new one
534
return ArchiveDatabase.initialize(uri)
537
class Filepath(object):
538
"""A filepath contained in a binary package."""
540
__table__ = "filepath", ("id", "path")
541
id = properties.Int()
542
path = properties.Str()
544
def __init__(self, path):
545
"""Create a Filepath.
547
:param path: The path to be recorded.
552
def Ensure(path, store):
553
"""Ensure that there is a path called path in store store and return it.
555
:param path: The string for the path.
556
:param store: The store to use.
560
return list(store.find(Filepath, path=path))[0]
562
result = Filepath(path)
566
def __eq__(self, other):
567
return other is not None and other.path == self.path
569
def __lt__(self, other):
570
return self.path < other.path
573
return "<findconflicts.Filepath at %d %s>" % (
580
class PackageArchitecture(object):
581
"""The architecture of a binary package."""
583
__table__ = "architecture", ("id", "arch")
584
id = properties.Int()
585
arch = properties.Str()
587
def __init__(self, arch):
588
"""Create a PackageArchitecture.
590
:param arch: The string representing the architecture. I.e. 'all'.
594
def __eq__(self, other):
595
return other is not None and other.arch == self.arch
598
def Ensure(arch, store):
599
"""Ensure that there is a arch 'name' in store store and return it.
601
:param arch: The string for the name.
602
:param store: The store to use.
603
:return: A PackageArchitecture.
606
return list(store.find(PackageArchitecture, arch=arch))[0]
608
result = PackageArchitecture(arch)
616
class PackageFile(object):
617
"""An occurance of a file in a specific package.
619
This class should not be used at all, it only exists to
620
facilitate describing the object links to STorm.
623
__table__ = "binpackagefiles", ("package_id", "file_id")
624
package_id = properties.Int()
625
file_id = properties.Int()
628
class PackageName(object):
629
"""The name of a binary package."""
631
__table__ = "packagename", ("id", "name")
632
id = properties.Int()
633
name = properties.Str()
635
def __init__(self, name):
636
"""Create a PackageName.
638
:param name: The string representing the name. I.e. 'atomix'.
642
def __eq__(self, other):
643
return other is not None and other.name == self.name
646
def Ensure(name, store):
647
"""Ensure that there is a name 'name' in store store and return it.
649
:param name: The string for the name.
650
:param store: The store to use.
651
:return: A PackageName.
654
return list(store.find(PackageName, name=name))[0]
656
result = PackageName(name)
664
class PackageVersion(object):
665
"""The name of a binary package."""
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)
679
def __init__(self, packagename, version, packagearch, controltext,
680
analyzed_level=0, diversions=None):
681
"""Create a PackageVersion.
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
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
694
self.controltext = controltext
695
self.name = packagename
696
self.version = version
697
self.arch = packagearch
698
self.analyzed_level = analyzed_level
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)
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)
718
def analyse(self, finder):
719
"""Analyse this package against the stores collection.
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.
725
:param finder: A MissingConflictFinder to use in the analysis.
726
:return: A list of conflicts.
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
738
def Analyse_ids(package_ids, store, finder, log):
739
"""Analyse package_ids in store, logging to log.
741
:param package_ids: The database ids of the package versions to analyse.
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.
749
total = len(package_ids)
750
for index, package_id in enumerate(package_ids):
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.
758
store._connection.commit()
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))
767
print "Finished %s in %.3f seconds [%d/%d]" % (package, time.time() - start, index, total)
769
# # stop storm going slower.
772
log.write("no errors (checked %d packages).\n" % total)
775
def conflicts_against_package(self, packageversion, finder):
776
"""Determine if this package and packageversion are safe together.
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).
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)):
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],
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()
811
raise KeyError(package_tuple)
814
def find_unsafely_colocated_packages(self):
815
"""Return a list of packages which install files this package does.
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.
822
Files that are diverted by either end of a pair of packages are
825
This function deliberately does not perform advanced filtering,
826
to allow callers to perform their own analysis.
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)
833
if self.arch.arch != 'all':
834
candidate_arches = [self.arch_id, all_id]
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
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))
852
def find_common_files(self, other):
853
"""Find the files in common with other - useful for debugging.
855
:param other: Another PackageVersion.
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)
866
return sorted(store.find(Filepath, Filepath.id.is_in(common_ids)))
869
def PackageTuplesToUnicode(package_tuples):
870
"""Convery package_tuples into a unicode string list for use in queries.
872
:param package_tuples: An iterable of (name, version, arch) tuples.
874
return [unicode("%s-%s-%s") % one_tuple for one_tuple in package_tuples]
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
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
893
def format_conflict(self, conflict_package):
894
"""Return a string describing the conflict with conflict_package.
896
:param conflict_package: A package that a conflict has been reported
898
:return: A string describing the conflict.
900
return "%s, %s %s" % (self, conflict_package,
901
[str(f) for f in self.find_common_files(conflict_package)])
904
"""Return a dict with key information about this package.
906
This is in the format expected by the MissingConflictFinder.
907
:return: A dict with the keys name, version, arch, conflicts, provides
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', '')
921
def InsertFromInfo(info, store):
922
"""Insert the package details from 'info' into the store 'store'.
924
:param info: A dictionary as returned by infoFromPackageUrl.
926
arch = PackageArchitecture.Ensure(info['arch'], store)
927
name = PackageName.Ensure(info['name'], store)
928
version = PackageVersion(
929
name, info['version'], arch, info['controltext'])
931
# we need version to have an id.
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
939
for path in new_files:
940
sqlresult = store.execute("INSERT INTO filepath (path) VALUES (?)",
942
assert sqlresult == []
943
# 3) insert all the paths into this table.
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 == []
951
if info.get('diversions', []):
952
diversions = list(set(info['diversions']))
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)
963
return "<findconflicts.PackageVersion at %d %s-%s-%s>" % (
964
id(self), self.name, self.version, self.arch)
967
return "%s-%s-%s" % (self.name, self.version, self.arch)
970
def To_analyze(store, published_packages=None):
971
"""Return the packageversion_ids that need further analysis.
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.
977
if not published_packages:
979
query = "select id from packageversion where analyzed_level = 0"
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)]
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)
995
class ParseError(Exception):
996
"""A parsing error occured."""
999
class PreinstParser(object):
1000
"""A parser for debian preinst scripts."""
1002
def extract_diversions(self, bytes):
1003
"""Extract diversion information from the preinst file 'bytes'.
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.
1009
lines = bytes.split('\n')
1010
if lines[0].startswith("#!/bin/sh"):
1012
elif lines[0].startswith("#! /bin/sh"):
1014
elif lines[0].startswith("#!/bin/bash"):
1016
elif lines[0].startswith("#! /bin/bash"):
1019
raise ParseError("unknown language")
1020
# assume shell for now.
1021
# combine \ extended lines.
1023
continuation = False
1024
for line in lines[1:]:
1026
normallines[-1] = normallines[-1][:-1] + line
1028
normallines.append(line)
1029
# TODO: ignore \ at the end of comments.
1030
continuation = line.endswith('\\')
1033
for line in normallines:
1034
# strip out comments.
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.
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)
1054
result.append(matchobj.groups()[0])
421
1059
apt_pkg.InitSystem()