aurweb/tupkg/update/tupkgupdate

470 lines
14 KiB
Python
Executable file

#!/usr/bin/python -O
import re, os, sys, pacman, getopt
import MySQLdb, MySQLdb.connections
import ConfigParser
###########################################################
# Deal with configuration
###########################################################
conffile = '/home/aur/tupkgs.conf'
if not os.path.isfile(conffile):
print "Error: cannot access config file ("+conffile+")"
usage(argv[0])
sys.exit(1)
config = ConfigParser.ConfigParser()
config.read(conffile)
############################################################
# Define some classes we need
class Version:
def __init__(self):
self.version = None
self.file = None
class Package:
def __init__(self):
self.name = None
self.old = None
self.new = None
self.desc = None
self.url = None
self.depends = None
self.sources = None
class PackageDatabase:
def __init__(self, host, user, password, dbname):
self.host = host
self.user = user
self.password = password
self.dbname = dbname
self.connection = MySQLdb.connect(host=host, user=user, passwd=password, db=dbname)
def cursor(self):
return self.connection.cursor()
def lookup(self, packagename):
warning("DB: Looking up package: " + packagename)
q = self.cursor()
q.execute("SELECT ID FROM Packages WHERE Name = '" +
MySQLdb.escape_string(packagename) + "'")
if (q.rowcount != 0):
row = q.fetchone()
return row[0]
return None
def insert(self, package, locationId):
warning("DB: Inserting package: " + package.name)
global repo_dir
q = self.cursor()
q.execute("INSERT INTO Packages " +
"(Name, Version, FSPath, LocationID, Description, URL) VALUES ('" +
MySQLdb.escape_string(package.name) + "', '" +
MySQLdb.escape_string(package.new.version) + "', '" +
MySQLdb.escape_string(
os.path.join(repo_dir, os.path.basename(package.new.file))) + "', " +
str(locationId) + ", '" +
MySQLdb.escape_string(str(package.desc)) + "', '" +
MySQLdb.escape_string(str(package.url)) + "')")
id = self.lookup(package.name)
self.insertNewInfo(package, id, locationId)
def update(self, id, package, locationId):
warning("DB: Updating package: " + package.name + " with id " + str(id))
global repo_dir
q = self.cursor()
q.execute("UPDATE Packages SET " +
"Version = '" + MySQLdb.escape_string(package.new.version) + "', " +
"FSPath = '" + MySQLdb.escape_string(
os.path.join(repo_dir, os.path.basename(package.new.file))) + "', " +
"Description = '" + MySQLdb.escape_string(str(package.desc)) + "', " +
"URL = '" + MySQLdb.escape_string(str(package.url)) + "', " +
"LocationID = " + str(locationId) + " " +
"WHERE ID = " + str(id))
self.insertNewInfo(package, id, locationId)
def remove(self, id, locationId):
warning("DB: Removing package with id: " + str(id))
q = self.cursor()
q.execute("DELETE FROM Packages WHERE " +
"LocationID = " + str(locationId) + " AND ID = " + str(id))
def clearOldInfo(self, id):
warning("DB: Clearing old info for package with id : " + str(id))
q = self.cursor()
q.execute("DELETE FROM PackageContents WHERE PackageID = " + str(id))
q.execute("DELETE FROM PackageDepends WHERE PackageID = " + str(id))
q.execute("DELETE FROM PackageSources WHERE PackageID = " + str(id))
def lookupOrDummy(self, packagename):
retval = self.lookup(packagename)
if (retval != None):
return retval
return self.createDummy(packagename)
def createDummy(self, packagename):
warning("DB: Creating dummy package for: " + packagename)
q = self.cursor()
q.execute("INSERT INTO Packages " +
"(Name, Description, LocationID, DummyPkg) " +
"VALUES ('" +
MySQLdb.escape_string(packagename) + "', '" +
MySQLdb.escape_string("A dummy package") + "', 1, 1)")
return self.lookup(packagename)
def insertNewInfo(self, package, id, locationId):
q = self.cursor()
# first delete the old; this is never bad
self.clearOldInfo(id)
warning("DB: Inserting new package info for " + package.name +
" with id " + str(id))
# PackageSources
for source in package.sources:
q.execute("INSERT INTO PackageSources (PackageID, Source) " +
"VALUES (" + str(id) + ", '" + source + "')")
# PackageDepends
for dep in package.depends:
depid = self.lookupOrDummy(dep)
q.execute("INSERT INTO PackageDepends (PackageID, DepPkgID) " +
"VALUES (" + str(id) + ", " + str(depid) + ")")
############################################################
# Functions for walking the file trees
############################################################
def filesForRegexp(topdir, regexp):
retval = []
def matchfile(regexp, dirpath, namelist):
for name in namelist:
if (regexp.match(name)):
retval.append(os.path.join(dirpath, name))
os.path.walk(topdir, matchfile, regexp)
return retval
def packagesInTree(topdir):
return filesForRegexp(topdir, re.compile("^.*\.pkg\.tar\.gz$"))
def pkgbuildsInTree(topdir):
return filesForRegexp(topdir, re.compile("^PKGBUILD$"))
############################################################
# Function for testing if two files are identical
############################################################
def areFilesIdentical(file_a, file_b):
command = "cmp '" + file_a + "' '" + file_b + "' >/dev/null"
retval = os.system(command)
if (retval == 0):
return True
return False
############################################################
# Function for fetching info from PKGBUILDs and packages
############################################################
def infoFromPackageFile(filename):
pkg = pacman.load(filename)
return pkg.name, pkg.version + "-" + pkg.release
def infoFromPkgbuildFile(filename):
# open and source the file
pf_stdin, pf_stdout = os.popen2("/bin/bash", 't', 0)
print >>pf_stdin, ". " + filename
#print "PKGBUILD: " + filename
# get pkgname
print >>pf_stdin, 'echo $pkgname'
pkgname = pf_stdout.readline().strip()
#print "PKGBUILD: pkgname: " + pkgname
# get pkgver
print >>pf_stdin, 'echo $pkgver'
pkgver = pf_stdout.readline().strip()
#print "PKGBUILD: pkgver: " + pkgver
# get pkgrel
print >>pf_stdin, 'echo $pkgrel'
pkgrel = pf_stdout.readline().strip()
#print "PKGBUILD: pkgrel: " + pkgrel
# get url
print >>pf_stdin, 'echo $url'
url = pf_stdout.readline().strip()
#print "PKGBUILD: url: " + url
# get desc
print >>pf_stdin, 'echo $pkgdesc'
pkgdesc = pf_stdout.readline().strip()
#print "PKGBUILD: pkgdesc: " + pkgdesc
# get source array
print >>pf_stdin, 'echo ${source[*]}'
source = (pf_stdout.readline().strip()).split(" ")
# get depends array
print >>pf_stdin, 'echo ${depends[*]}'
depends = (pf_stdout.readline().strip()).split(" ")
# clean up
pf_stdin.close()
pf_stdout.close()
return pkgname, pkgver + "-" + pkgrel, pkgdesc, url, depends, source
def infoFromPkgbuildFileWorse(filename):
# load the file with pacman library
pkg = pacman.load(filename)
return (pkg.name, pkg.version + "-" + pkg.release, pkg.desc,
pkg.url, pkg.depends, pkg.source)
############################################################
# Functions for doing the final steps of execution
############################################################
def execute(command):
global switches
print(command)
if not (switches.get("-n") == True):
os.system(command)
def copyFileToRepo(filename, repodir):
destfile = os.path.join(repodir, os.path.basename(filename))
command = "cp -p '" + filename + "' '" + destfile + "'"
execute(command)
def deleteFile(filename):
command = "rm '" + filename + "'"
execute(command)
def runGensync(repo, pkgbuild):
target = os.path.join(repo, os.path.basename(repo) + ".db.tar.gz")
command = "gensync '" + pkgbuild_dir + "' '" + target + "'"
execute(command)
############################################################
# Functions for error handling
############################################################
def warning(string):
print >>sys.stderr, string
had_error = 0
def error(string):
global had_error
warning(string)
had_error = 1
############################################################
# MAIN
############################################################
# ARGUMENTS
#
# tupkgupdate [-n] [--delete] [--paranoid] <repo_dir> <pkgbuild_dir> <build_dir>
# First call getopt
switch_list,args_proper = getopt.getopt(sys.argv[1:], 'n',
[ "delete", "paranoid" ])
switches = {}
for switch in switch_list:
switches[switch[0]] = 1
# Then handle the remaining arguments
if (len(args_proper) < 3):
print >>sys.stderr, "syntax: tupkgupdate [-n] [--delete] [--paranoid] <repo_dir> <pkgbuild_tree> <build_tree>"
sys.exit(-1)
repo_dir, pkgbuild_dir, build_dir = args_proper
# Open the database so we find out now if we can't!
db = PackageDatabase(config.get('mysql', 'host'),
config.get('mysql', 'username'),
config.get('mysql', 'password'),
config.get('mysql', 'db'))
# Set up the lists and tables
packages = dict()
copy = list()
delete = list()
dbremove = list()
dbmodify = list()
# PASS 1: PARSING/LOCATING
#
# A) Go through the PKGBUILD tree
# For each PKGBUILD, create a Package with new Version containing
# parsed version and and None for file
a_files = pkgbuildsInTree(pkgbuild_dir)
for a_file in a_files:
pkgname, ver, desc, url, depends, sources = infoFromPkgbuildFile(a_file)
# Error (and skip) if we encounter any invalid PKGBUILD files
if (pkgname == None or ver == None):
error("Pkgbuild '" + a_file + "' is invalid!")
continue
# Error (and skip) if we encounter any duplicate package names
# in the PKGBUILDs
if (packages.get(pkgname)):
error("Pkgbuild '" + a_file + "' is a duplicate!")
continue
version = Version()
version.version = ver
version.file = None
package = Package()
package.name = pkgname
package.desc = desc
package.url = url
package.depends = depends
package.sources = sources
package.new = version
# print "Package: desc " + desc
packages[pkgname] = package
# B) Go through the old repo dir
# For each package file we encounter, create a Package with old
# Version containing parsed version and filepath
b_files = packagesInTree(repo_dir)
for b_file in b_files:
pkgname, ver = infoFromPackageFile(b_file)
version = Version()
version.version = ver
version.file = b_file
package = packages.get(pkgname)
if (package == None):
package = Package()
package.name = pkgname
packages[pkgname] = package
package.old = version
# C) Go through the build tree
# For each package file we encounter:
# 1 - look up the package name; if it fails, ignore the file (no error)
# 2 - if package.new == None, ignore the package (no error)
# 3 - if package.new.version doesn't match, then skip (no error)
# 4 - if package.new.file == None, point it to this file
# otherwise, log an error (and skip)
c_files = packagesInTree(build_dir)
for c_file in c_files:
pkgname, ver = infoFromPackageFile(c_file)
# 1
package = packages.get(pkgname)
if (package == None):
continue
# 2
if (package.new == None):
continue
# 3
if (package.new.version != ver):
continue
# 4
if (package.new.file == None):
package.new.file = c_file
continue
else:
error("Duplicate new file '" + c_file + "'")
continue
# PASS 2: CHECKING
#
# Go through the package collection
# 1 - if package has no new, place its old file on the "delete" list (and package on "dbremove")
# 2 - if package has a new but no new.file, and old file doesn't
# have the same version, then error (because gensync won't rebuild)
# 3 - if package has no old, add new file to "copy" list into repo dir (and package on "dbmodify")
# 4 - if new == old and paranoid is set, compare the files and error if not the same;
# otherwise just skip (no update)
# 5 - if we got here, it's a legit nontrivial new version which we allow
# add entry to "delete" list for old file and "copy" list for
# new file into repo dir (and package to "dbmodify")
for package in packages.values():
# 1
if (package.new == None):
delete.append(package.old.file)
dbremove.append(package)
continue
# 2
if (package.new.file == None):
if (package.old == None or package.old.file == None or
package.old.version != package.new.version):
errstr = "No new package supplied for " + package.name + " " + package.new.version + "!"
error(errstr)
continue
# 3
if (package.old == None):
copy.append(package.new.file)
dbmodify.append(package)
continue
# 4
if (package.old.version == package.new.version):
if (switches.get("--paranoid") == True and package.new.file != None):
if not (areFilesIdentical(package.old.file, package.new.file)):
warning("New package file with identical version '" +
package.new.file + "' is different than the old one:")
if (switches.get("--delete") == True):
warning(" Deleting the new file.")
delete.append(package.new.file)
else:
warning(" Ignoring the new file.")
continue
# 5
delete.append(package.old.file)
copy.append(package.new.file)
dbmodify.append(package)
continue
## IF WE HAVE HAD ANY ERRORS AT THIS POINT, ABORT! ##
if (had_error == 1):
error("Aborting due to errors.")
sys.exit(-1)
# PASS 3: EXECUTION
#
# First, do all the database updates
for package in dbremove:
id = db.lookup(package.name)
# Note: this could remove a package from unsupported; probably want to restrict to locationId and/or non-dummy
if (id != None):
db.clearOldInfo(id)
db.remove(id, 3)
for package in dbmodify:
warning("DB: Package in dbmodify: " + package.name)
id = db.lookup(package.name)
if (id == None):
db.insert(package, 3)
else:
db.update(id, package, 3)
# Copy
for file in copy:
copyFileToRepo(file, repo_dir)
# Delete (second, for safety's sake)
for file in delete:
deleteFile(file)
# Now that we've copied new files and deleted, we should delete the source
# files, if we're supposed to
if (switches.get("--delete") == True):
for file in copy:
deleteFile(file)
# Run gensync to build the repo index
if (len(copy) + len(delete) > 0):
runGensync(repo_dir, pkgbuild_dir)