PatchMaker

This commit is contained in:
David Rose 2009-08-26 06:23:13 +00:00
parent 1fc186bda4
commit f1588e4e9f
2 changed files with 444 additions and 21 deletions

View File

@ -519,27 +519,6 @@ class Packager:
self.cleanup()
## def buildPatch(self, origFilename, newFilename):
## """ Creates a patch file from origFilename to newFilename,
## in a temporary filename. Returns the temporary filename
## on success, or None on failure. """
## if not origFilename.exists():
## # No original version to patch from.
## return None
## print "Building patch from %s" % (origFilename)
## patchFilename = Filename.temporary('', self.packageName + '.', '.patch')
## p = Patchfile()
## if p.build(origFilename, newFilename, patchFilename):
## return patchFilename
## # Unable to build a patch for some reason.
## patchFilename.unlink()
## return None
def installSolo(self):
""" Installs the package as a "solo", which means we
simply copy the one file into the install directory. This

View File

@ -0,0 +1,444 @@
from direct.p3d.FileSpec import FileSpec
from pandac.PandaModules import *
class PatchMaker:
""" This class will operate on an existing package install
directory, as generated by the Packager, and create patchfiles
between versions as needed. """
class PackageVersion:
""" A specific patch version of a package. This is not just
the package's "version" number; it also corresponds to the
particular patch version, which increments independently of
the "version". """
def __init__(self, packageName, platform, version, host, hash):
self.packageName = packageName
self.platform = platform
self.version = version
self.host = host
self.hash = hash
# The Package object that produces this version, in the
# current form or the base form, respectively.
self.packageCurrent = None
self.packageBase = None
# A list of patchfiles that can produce this version.
self.fromPatches = []
# A list of patchfiles that can start from this version.
self.toPatches = []
# A temporary file for re-creating the archive file for
# this version.
self.tempFile = None
def cleanup(self):
if self.tempFile:
self.tempFile.unlink()
def getFile(self):
""" Returns the Filename of the archive file associated
with this version. If the file doesn't actually exist on
disk, a temporary file will be created. Returns None if
the file can't be recreated. """
if self.tempFile:
return self.tempFile
if self.packageCurrent:
package = self.packageCurrent
return Filename(package.packageDir, package.currentFile.filename)
if self.packageBase:
package = self.packageBase
return Filename(package.packageDir, package.baseFile.filename)
# We'll need to re-create the file.
for patchfile in self.fromPatches:
fromPv = patchfile.fromPv
prevFile = fromPv.getFile()
if prevFile:
patchFilename = Filename(patchfile.package.packageDir, patchfile.file.filename)
result = self.applyPatch(prevFile, patchFilename, patchfile.toHash)
if result:
self.tempFile = result
return result
# Couldn't re-create the file.
return None
def applyPatch(self, origFile, patchFilename, targetHash):
""" Applies the named patch to the indicated original
file, storing the results in a temporary file, and returns
that temporary Filename. Returns None on failure. """
result = Filename.temporary('', 'patch_')
print "patching %s + %s -> %s" % (
origFile, patchFilename, result)
p = Patchfile()
if not p.apply(patchFilename, origFile, result):
print "patching failed."
return None
hv = HashVal()
hv.hashFile(result)
if hv.asHex() != targetHash:
print "patching produced incorrect results."
result.unlink()
return None
return result
def getNext(self, package):
""" Gets the next patch in the chain towards this
package. """
for patch in self.toPatches:
if patch.packageName == package.packageName and \
patch.platform == package.platform and \
patch.version == package.version and \
patch.host == package.host:
return patch.toPv
return None
class Patchfile:
""" A single patchfile for a package. """
def __init__(self, package):
self.package = package
self.packageName = package.packageName
self.platform = package.platform
self.version = package.version
self.host = None
def getFromKey(self):
return (self.packageName, self.platform, self.version, self.host, self.fromHash)
def getToKey(self):
return (self.packageName, self.platform, self.version, self.host, self.toHash)
def fromFile(self, packageDir, patchFilename,
fromHash, toHash):
self.file = FileSpec()
self.file.fromFile(packageDir, patchFilename)
self.fromHash = fromHash
self.toHash = toHash
def loadXml(self, xpatch):
self.packageName = xpatch.Attribute('name') or self.packageName
self.platform = xpatch.Attribute('platform') or self.platform
self.version = xpatch.Attribute('version') or self.version
self.host = xpatch.Attribute('host') or self.host
self.file = FileSpec()
self.file.loadXml(xpatch)
self.fromHash = xpatch.Attribute('from_hash')
self.toHash = xpatch.Attribute('to_hash')
def makeXml(self, package):
xpatch = TiXmlElement('patch')
if self.packageName != package.packageName:
xpatch.SetAttribute('name', self.packageName)
if self.platform != package.platform:
xpatch.SetAttribute('platform', self.platform)
if self.version != package.version:
xpatch.SetAttribute('version', self.version)
if self.host != package.host:
xpatch.SetAttribute('host', self.host)
self.file.storeXml(xpatch)
xpatch.SetAttribute('from_hash', self.fromHash)
xpatch.SetAttribute('to_hash', self.toHash)
return xpatch
class Package:
""" This is a particular package. This contains all of the
information needed to reconstruct the package's desc file. """
def __init__(self, packageDesc, patchMaker):
self.packageDir = Filename(patchMaker.installDir, packageDesc.getDirname())
self.packageDesc = packageDesc
self.patchMaker = patchMaker
self.patchVersion = 1
self.doc = None
self.anyChanges = False
self.patches = []
def getCurrentKey(self):
return (self.packageName, self.platform, self.version, self.host, self.currentFile.hash)
def getBaseKey(self):
return (self.packageName, self.platform, self.version, self.host, self.baseFile.hash)
def readDescFile(self):
""" Reads the existing package.xml file and stores
it in this class for later rewriting. """
packageDescFullpath = Filename(self.patchMaker.installDir, self.packageDesc)
self.doc = TiXmlDocument(packageDescFullpath.toOsSpecific())
if not self.doc.LoadFile():
return
xpackage = self.doc.FirstChildElement('package')
if not xpackage:
return
self.packageName = xpackage.Attribute('name')
self.platform = xpackage.Attribute('platform')
self.version = xpackage.Attribute('version')
# All packages we defined in-line are assigned to the
# "none" host. TODO: support patching from packages on
# other hosts, which means we'll need to fill in a value
# here for those hosts.
self.host = None
# Get the current patch version. If we have a
# patch_version attribute, it refers to this particular
# instance of the file, and that is the current patch
# version number. If we only have a last_patch_version
# attribute, it means a patch has not yet been built for
# this particular instance, and that number is the
# previous version's patch version number.
patchVersion = xpackage.Attribute('patch_version')
if patchVersion:
self.patchVersion = int(patchVersion)
else:
patchVersion = xpackage.Attribute('last_patch_version')
if patchVersion:
self.patchVersion = int(patchVersion)
self.patchVersion += 1
self.currentFile = None
self.baseFile = None
xarchive = xpackage.FirstChildElement('uncompressed_archive')
if xarchive:
self.currentFile = FileSpec()
self.currentFile.loadXml(xarchive)
xarchive = xpackage.FirstChildElement('base_version')
if xarchive:
self.baseFile = FileSpec()
self.baseFile.loadXml(xarchive)
self.patches = []
xpatch = xpackage.FirstChildElement('patch')
while xpatch:
patchfile = PatchMaker.Patchfile(self)
patchfile.loadXml(xpatch)
self.patches.append(patchfile)
xpatch = xpatch.NextSiblingElement('patch')
self.anyChanges = False
def writeDescFile(self):
""" Rewrites the desc file with the new patch
information. """
if not self.anyChanges:
# No need to rewrite.
return
xpackage = self.doc.FirstChildElement('package')
if not xpackage:
return
# Remove all of the old patch entries from the desc file
# we read earlier.
xremove = []
for value in ['base_version', 'patch']:
xpatch = xpackage.FirstChildElement(value)
while xpatch:
xremove.append(xpatch)
xpatch = xpatch.NextSiblingElement(value)
for xelement in xremove:
xpackage.RemoveChild(xelement)
xpackage.RemoveAttribute('last_patch_version')
# Now replace them with the current patch information.
xpackage.SetAttribute('patch_version', str(self.patchVersion))
xarchive = TiXmlElement('base_version')
self.baseFile.storeXml(xarchive)
xpackage.InsertEndChild(xarchive)
for patchfile in self.patches:
xpatch = patchfile.makeXml(self)
xpackage.InsertEndChild(xpatch)
self.doc.SaveFile()
def __init__(self, installDir):
self.installDir = installDir
self.packageVersions = {}
def run(self):
if not self.readContentsFile():
return False
self.buildPatchChains()
self.processPackages()
self.cleanup()
return True
def cleanup(self):
for pv in self.packageVersions.values():
pv.cleanup()
def readContentsFile(self):
""" Reads the contents.xml file at the beginning of
processing. """
contentsFilename = Filename(self.installDir, 'contents.xml')
doc = TiXmlDocument(contentsFilename.toOsSpecific())
if not doc.LoadFile():
# Couldn't read file.
print "couldn't read %s" % (contentsFilename)
return False
self.packages = []
xcontents = doc.FirstChildElement('contents')
if xcontents:
xpackage = xcontents.FirstChildElement('package')
while xpackage:
solo = xpackage.Attribute('solo')
solo = int(solo or '0')
filename = xpackage.Attribute('filename')
if filename and not solo:
filename = Filename(filename)
package = self.Package(filename, self)
package.readDescFile()
self.packages.append(package)
xpackage = xpackage.NextSiblingElement('package')
return True
def getPackageVersion(self, key):
""" Returns a shared PackageVersion object for the indicated
key. """
pv = self.packageVersions.get(key, None)
if not pv:
pv = self.PackageVersion(*key)
self.packageVersions[key] = pv
return pv
def buildPatchChains(self):
""" Builds up the chains of PackageVersions and the patchfiles
that connect them. """
self.patchFilenames = {}
for package in self.packages:
currentPv = self.getPackageVersion(package.getCurrentKey())
package.currentPv = currentPv
currentPv.packageCurrent = package
basePv = self.getPackageVersion(package.getBaseKey())
package.basePv = basePv
basePv.packageBase = package
for patchfile in package.patches:
self.recordPatchfile(patchfile)
def recordPatchfile(self, patchfile):
""" Adds the indicated patchfile to the patch chains. """
self.patchFilenames[patchfile.file.filename] = patchfile
fromPv = self.getPackageVersion(patchfile.getFromKey())
patchfile.fromPv = fromPv
fromPv.toPatches.append(patchfile)
toPv = self.getPackageVersion(patchfile.getToKey())
patchfile.toPv = toPv
toPv.fromPatches.append(patchfile)
def processPackages(self):
""" Walks through the list of packages, and builds missing
patches for each one. """
for package in self.packages:
self.processPackage(package)
def processPackage(self, package):
""" Builds missing patches for the indicated package. """
# Starting with the package base, how far forward can we go?
currentPv = package.currentPv
basePv = package.basePv
pv = basePv
nextPv = pv.getNext(package)
while nextPv:
pv = nextPv
nextPv = pv.getNext(package)
if pv.packageCurrent != package:
# It doesn't reach all the way to the latest version, so
# build a new patch.
filename = Filename(package.currentFile.filename + '.%s.patch' % (package.patchVersion))
assert filename not in self.patchFilenames
if not self.buildPatch(pv, currentPv, package, filename):
raise StandardError, "Couldn't build patch."
package.writeDescFile()
def buildPatch(self, v1, v2, package, patchFilename):
""" Builds a patch from PackageVersion v1 to PackageVersion
v2, and stores it in patchFilename. Returns true on success,
false on failure."""
f1 = v1.getFile()
f2 = v2.getFile()
pathname = Filename(package.packageDir, patchFilename)
if not self.buildPatchFile(f1, f2, pathname):
return False
patchfile = self.Patchfile(package)
patchfile.fromFile(package.packageDir, patchFilename,
v1.hash, v2.hash)
package.patches.append(patchfile)
package.anyChanges = True
self.recordPatchfile(patchfile)
return True
def buildPatchFile(self, origFilename, newFilename, patchFilename):
""" Creates a patch file from origFilename to newFilename,
storing the result in patchFilename. Returns true on success,
false on failure. """
if not origFilename.exists():
# No original version to patch from.
return False
print "Building patch from %s to %s" % (origFilename, newFilename)
patchFilename.unlink()
p = Patchfile()
if p.build(origFilename, newFilename, patchFilename):
return True
# Unable to build a patch for some reason.
patchFilename.unlink()
return False
if __name__ == '__main__':
pm = PatchMaker(Filename('install'))
result = pm.run()
print "run returned %s" % (result)