open_toontown_panda3d/direct/src/p3d/PatchMaker.py

561 lines
21 KiB
Python

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. It is also used at runtime, to apply
the downloaded patches. """
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, file):
self.packageName = packageName
self.platform = platform
self.version = version
self.host = host
self.file = file
# The Package object that produces this version, if this
# is 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 getPatchChain(self, startPv):
""" Returns a list of patches that, when applied in
sequence to the indicated patchVersion object, will
produce this patchVersion object. Returns None if no
chain can be found. """
if self is startPv:
# We're already here. A zero-length patch chain is
# therefore the answer.
return []
bestPatchChain = None
for patchfile in self.fromPatches:
fromPv = patchfile.fromPv
patchChain = fromPv.getPatchChain(startPv)
if patchChain is not None:
# There's a path through this patchfile.
patchChain = patchChain + [patchfile]
if bestPatchChain is None or len(patchChain) < len(bestPatchChain):
bestPatchChain = patchChain
# Return the shortest path found, or None if there were no
# paths found.
return bestPatchChain
def getRecreateFilePlan(self):
""" Returns the tuple (startFile, plan), describing how to
recreate the archive file for this version. startFile is
the Filename of the file to start with, and plan is a list
of tuples (patchfile, pv), listing the patches to apply in
sequence, and the packageVersion object associated with
each patch. Returns (None, None) if there is no way to
recreate this archive file. """
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.
bestPlan = None
bestStartFile = None
for patchfile in self.fromPatches:
fromPv = patchfile.fromPv
startFile, plan = fromPv.getRecreateFilePlan()
if plan is not None:
# There's a path through this patchfile.
plan = plan + [(patchfile, self)]
if bestPlan is None or len(plan) < len(bestPlan):
bestPlan = plan
bestStartFile = startFile
# Return the shortest path found, or None if there were no
# paths found.
return (bestStartFile, bestPlan)
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. """
startFile, plan = self.getRecreateFilePlan()
if not plan:
# If plan is a zero-length list, we're already
# here--return startFile. If plan is None, there's no
# solution, and startFile is None. In either case, we
# can return startFile.
return startFile
# If plan is a non-empty list, we have to walk the list to
# apply the patch plan.
prevFile = startFile
for patchfile, pv in plan:
fromPv = patchfile.fromPv
patchFilename = Filename(patchfile.package.packageDir, patchfile.file.filename)
result = self.applyPatch(prevFile, patchFilename)
if not result:
# Failure trying to re-create the file.
return None
pv.tempFile = result
prevFile = result
# Successfully patched.
assert pv is self and prevFile is self.tempFile
return prevFile
def applyPatch(self, origFile, patchFilename):
""" 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_')
p = Patchfile()
if not p.apply(patchFilename, origFile, result):
print "Internal patching failed: %s" % (patchFilename)
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 getSourceKey(self):
return (self.packageName, self.platform, self.version, self.host, self.sourceFile)
def getTargetKey(self):
return (self.packageName, self.platform, self.version, self.host, self.targetFile)
def fromFile(self, packageDir, patchFilename, sourceFile, targetFile):
self.file = FileSpec()
self.file.fromFile(packageDir, patchFilename)
self.sourceFile = sourceFile
self.targetFile = targetFile
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)
xsource = xpatch.FirstChildElement('source')
if xsource:
self.sourceFile = FileSpec()
self.sourceFile.loadXml(xsource)
xtarget = xpatch.FirstChildElement('target')
if xtarget:
self.targetFile = FileSpec()
self.targetFile.loadXml(xtarget)
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)
xsource = TiXmlElement('source')
self.sourceFile.storeMiniXml(xsource)
xpatch.InsertEndChild(xsource)
xtarget = TiXmlElement('target')
self.targetFile.storeMiniXml(xtarget)
xpatch.InsertEndChild(xtarget)
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.currentPv = None
self.basePv = None
self.doc = None
self.anyChanges = False
self.patches = []
def getCurrentKey(self):
return (self.packageName, self.platform, self.version, self.host, self.currentFile)
def getBaseKey(self):
return (self.packageName, self.platform, self.version, self.host, self.baseFile)
def getGenericKey(self, fileSpec):
""" Returns the key that has the indicated FileSpec. """
return (self.packageName, self.platform, self.version, self.host, fileSpec)
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 = {}
self.packages = []
def buildPatches(self, packageNames = None):
""" Makes the patches required in a particular directory
structure on disk. If packageNames is None, this makes
patches for all packages; otherwise, it should be a list of
package name strings, limiting the set of packages that are
processed. """
if not self.readContentsFile():
return False
self.buildPatchChains()
if packageNames is None:
self.processAllPackages()
else:
self.processSomePackages(packageNames)
self.cleanup()
return True
def cleanup(self):
""" Should be called on exit to remove temporary files and
such created during processing. """
for pv in self.packageVersions.values():
pv.cleanup()
def readPackageDescFile(self, descFilename):
""" Reads a desc file associated with a particular package,
and adds the package to self.packageVersions. Returns the
Package object. """
package = self.Package(Filename(descFilename), self)
package.readDescFile()
self.packages.append(package)
return package
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
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. """
packageName, platform, version, host, file = key
# We actually key on the hash, not the FileSpec itself.
k = (packageName, platform, version, host, file.hash)
pv = self.packageVersions.get(k, None)
if not pv:
pv = self.PackageVersion(*key)
self.packageVersions[k] = 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:
if not package.baseFile:
# This package doesn't have any versions yet.
continue
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.getSourceKey())
patchfile.fromPv = fromPv
fromPv.toPatches.append(patchfile)
toPv = self.getPackageVersion(patchfile.getTargetKey())
patchfile.toPv = toPv
toPv.fromPatches.append(patchfile)
def processSomePackages(self, packageNames):
""" Builds missing patches only for the named packages. """
remainingNames = packageNames[:]
for package in self.packages:
if package.packageName in remainingNames:
self.processPackage(package)
remainingNames.remove(package.packageName)
if remainingNames:
print "Unknown packages: %s" % (remainingNames,)
def processAllPackages(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. """
if not package.baseFile:
# No versions.
return
# 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.pz. 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
compressedPathname = Filename(pathname + '.pz')
compressedPathname.unlink()
if not compressFile(pathname, compressedPathname, 9):
raise StandardError, "Couldn't compress patch."
pathname.unlink()
patchfile = self.Patchfile(package)
patchfile.fromFile(package.packageDir, patchFilename + '.pz',
v1.file, v2.file)
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 to %s" % (newFilename)
patchFilename.unlink()
p = Patchfile() # The C++ class
if p.build(origFilename, newFilename, patchFilename):
return True
# Unable to build a patch for some reason.
patchFilename.unlink()
return False