From f1588e4e9f15009c0bf84e8f302b52db96f022ff Mon Sep 17 00:00:00 2001 From: David Rose Date: Wed, 26 Aug 2009 06:23:13 +0000 Subject: [PATCH] PatchMaker --- direct/src/p3d/Packager.py | 21 -- direct/src/p3d/PatchMaker.py | 444 +++++++++++++++++++++++++++++++++++ 2 files changed, 444 insertions(+), 21 deletions(-) create mode 100644 direct/src/p3d/PatchMaker.py diff --git a/direct/src/p3d/Packager.py b/direct/src/p3d/Packager.py index 35b6ac488b..40b618136e 100644 --- a/direct/src/p3d/Packager.py +++ b/direct/src/p3d/Packager.py @@ -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 diff --git a/direct/src/p3d/PatchMaker.py b/direct/src/p3d/PatchMaker.py new file mode 100644 index 0000000000..e62587e4ba --- /dev/null +++ b/direct/src/p3d/PatchMaker.py @@ -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) +