#!/usr/bin/env python
'''Build debian/ubuntu package indexes'''
# Original code from Bcfg2 sources
import apt_pkg
import gzip
import os
import re
import sys
# Compatibility imports
from Bcfg2.Compat import StringIO
from Bcfg2.Compat import ConfigParser
from Bcfg2.Compat import urlopen
apt_pkg.init()
def debug(msg):
'''print debug messages'''
if '-v' in sys.argv:
sys.stdout.write(msg)
def get_as_list(somestring):
""" Input : a string like this : 'a, g, f,w'
Output : a list like this : ['a', 'g', 'f', 'w'] """
return somestring.replace(' ', '').split(',')
def list_contains_all_the_same_values(l):
if len(l) == 0:
return True
# The list contains all the same values if all elements in
# the list are equal to the first element.
first = l[0]
for elem in l:
if first != elem:
return False
return True
class SourceURL:
def __init__(self, deb_url, arch):
deb_url_tokens = deb_url.split()
# ex: deb http://somemirror.com/ubuntu dapper main restricted universe
self.url = deb_url_tokens[1]
self.distribution = deb_url_tokens[2]
self.sections = deb_url_tokens[3:]
self.arch = arch
def __str__(self):
return "deb %s %s %s" % (self.url, self.distribution, ' '.join(self.sections))
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, str(self))
class Source:
def __init__(self, confparser, section, bcfg2_repos_prefix):
self.filename = "%s/Pkgmgr/%s.xml" % (bcfg2_repos_prefix, section)
self.groups = get_as_list(confparser.get(section, "group_names"))
self.priority = confparser.getint(section, "priority")
try:
self.pattern = confparser.get(section, "pattern", raw=True)
except:
self.pattern = '.*'
self.architectures = get_as_list(confparser.get(section, "architectures"))
self.arch_specialurl = set()
self.source_urls = []
self.source_urls.append(SourceURL(confparser.get(section, "deb_url"),
"all"))
# Agregate urls in the form of deb_url0, deb_url1, ... to deb_url9
for i in range(10): # 0 to 9
option_name = "deb_url%s" % i
if confparser.has_option(section, option_name):
self.source_urls.append(SourceURL(confparser.get(section,
option_name),
"all"))
# Aggregate architecture specific urls (if present)
for arch in self.architectures:
if not confparser.has_option(section, "deb_" + arch + "_url"):
continue
self.source_urls.append(SourceURL(confparser.get(section,
"deb_" + arch + "_url"),
arch))
# Agregate urls in the form of deb_url0, deb_url1, ... to deb_url9
for i in range(10): # 0 to 9
option_name = "deb_" + arch + "_url%s" % i
if confparser.has_option(section, option_name):
self.source_urls.append(SourceURL(confparser.get(section,
option_name),
arch))
self.arch_specialurl.add(arch)
self.file = None
self.indent_level = 0
def __str__(self):
return """File: %s
Groups: %s
Priority: %s
Architectures: %s
Source URLS: %s""" % (self.filename, self.groups, self.priority, self.architectures, self.source_urls)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, str(self))
def _open_file(self):
self.file = open(self.filename + '~', 'w')
def _close_file(self):
self.file.close()
def _write_to_file(self, msg):
self.file.write("%s%s\n" % (self.indent_level * ' ', msg))
def _rename_file(self):
os.rename(self.filename + '~', self.filename)
def _pkg_version_is_older(self, version1, version2):
""" Use dpkg to compare the two version
Return true if version1 < version2 """
# Avoid forking a new process if the two strings are equals
if version1 == version2:
return False
status = apt_pkg.VersionCompare(version1, version2)
return status < 0
def _update_pkgdata(self, pkgdata, source_url):
for section in source_url.sections:
for arch in self.architectures:
if source_url.arch != arch and source_url.arch != "all":
continue
if source_url.arch == "all" and arch in self.arch_specialurl:
continue
url = "%s/dists/%s/%s/binary-%s/Packages.gz" % (source_url.url,
source_url.distribution,
section,
arch)
debug("Processing url %s\n" % (url))
try:
data = urlopen(url)
buf = StringIO(''.join(data.readlines()))
reader = gzip.GzipFile(fileobj=buf)
for line in reader.readlines():
if line[:8] == 'Package:':
pkgname = line.split(' ')[1].strip()
elif line[:8] == 'Version:':
version = line.split(' ')[1].strip()
if pkgname in pkgdata:
if arch in pkgdata[pkgname]:
# The package is listed twice for the same architecture
# We keep the most recent version
old_version = pkgdata[pkgname][arch]
if self._pkg_version_is_older(old_version, version):
pkgdata[pkgname][arch] = version
else:
# The package data exists for another architecture,
# but not for this one. Add it.
pkgdata[pkgname][arch] = version
else:
# First entry for this package
pkgdata[pkgname] = {arch: version}
else:
continue
except:
raise Exception("Could not process URL %s\n%s\nPlease "
"verify the URL." % (url, sys.exc_info()[1]))
return dict((k, v) for (k, v) in list(pkgdata.items()) \
if re.search(self.pattern, k))
def _get_sorted_pkg_keys(self, pkgdata):
pkgs = []
for k in list(pkgdata.keys()):
pkgs.append(k)
pkgs.sort()
return pkgs
def _write_common_entries(self, pkgdata):
# Write entries for packages that have the same version
# across all architectures
#coalesced = 0
for pkg in self._get_sorted_pkg_keys(pkgdata):
# Dictionary of archname: pkgversion
# (There is exactly one version per architecture)
archdata = pkgdata[pkg]
# List of versions for all architectures of this package
pkgversions = list(archdata.values())
# If the versions for all architectures are the same
if len(self.architectures) == len(pkgversions) and list_contains_all_the_same_values(pkgversions):
# Write the package data
ver = pkgversions[0]
self._write_to_file('' % (pkg, ver))
#coalesced += 1
# Remove this package entry
del pkgdata[pkg]
def _write_perarch_entries(self, pkgdata):
# Write entries that are left, i.e. packages that have different
# versions per architecture
#perarch = 0
if pkgdata:
for arch in self.architectures:
self._write_to_file('' % (arch))
self.indent_level = self.indent_level + 1
for pkg in self._get_sorted_pkg_keys(pkgdata):
if arch in pkgdata[pkg]:
self._write_to_file('' % (pkg, pkgdata[pkg][arch]))
#perarch += 1
self.indent_level = self.indent_level - 1
self._write_to_file('')
#debug("Got %s coalesced, %s per-arch\n" % (coalesced, perarch))
def process(self):
'''Build package indices for source'''
# First, build the pkgdata structure without touching the file,
# so the file does not contain incomplete informations if the
# network in not reachable.
pkgdata = {}
for source_url in self.source_urls:
pkgdata = self._update_pkgdata(pkgdata, source_url)
# Construct the file.
self._open_file()
for source_url in self.source_urls:
self._write_to_file('' % source_url)
self._write_to_file('' % self.priority)
self.indent_level = self.indent_level + 1
for group in self.groups:
self._write_to_file('' % group)
self.indent_level = self.indent_level + 1
self._write_common_entries(pkgdata)
self._write_perarch_entries(pkgdata)
for group in self.groups:
self.indent_level = self.indent_level - 1
self._write_to_file('')
self.indent_level = self.indent_level - 1
self._write_to_file('')
self._close_file()
self._rename_file()
if __name__ == '__main__':
# Prefix is relative to script path
complete_script_path = os.path.join(os.getcwd(), sys.argv[0])
prefix = complete_script_path[:-len('etc/create-debian-pkglist.py')]
confparser = ConfigParser.SafeConfigParser()
confparser.read(prefix + "etc/debian-pkglist.conf")
# We read the whole configuration file before processing each entries
# to avoid doing work if there is a problem in the file.
sources_list = []
for section in confparser.sections():
sources_list.append(Source(confparser, section, prefix))
for source in sources_list:
source.process()