|
@@ -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)
|
|
|
|
|
+
|