Browse Source

PatchMaker

David Rose 16 years ago
parent
commit
f1588e4e9f
2 changed files with 444 additions and 21 deletions
  1. 0 21
      direct/src/p3d/Packager.py
  2. 444 0
      direct/src/p3d/PatchMaker.py

+ 0 - 21
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

+ 444 - 0
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)
+