Browse Source

auto-download _import files

David Rose 16 years ago
parent
commit
d5528ab21f

+ 146 - 82
direct/src/p3d/AppRunner.py

@@ -16,15 +16,15 @@ import __builtin__
 
 from direct.showbase import VFSImporter
 from direct.showbase.DirectObject import DirectObject
-from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties, readXmlStream, ExecutionEnvironment, HashVal
+from pandac.PandaModules import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, HTTPClient, Thread, WindowProperties, readXmlStream, ExecutionEnvironment, PandaSystem, URLSpec
 from direct.stdpy import file
 from direct.task.TaskManagerGlobal import taskMgr
 from direct.showbase.MessengerGlobal import messenger
 from direct.showbase import AppRunnerGlobal
-from PackageInfo import PackageInfo
+from direct.p3d.HostInfo import HostInfo
 
 # These imports are read by the C++ wrapper in p3dPythonRun.cxx.
-from JavaScript import UndefinedObject, Undefined, ConcreteStruct, BrowserObject
+from direct.p3d.JavaScript import UndefinedObject, Undefined, ConcreteStruct, BrowserObject
 
 class ArgumentError(AttributeError):
     pass
@@ -52,6 +52,7 @@ class AppRunner(DirectObject):
         self.started = False
         self.windowOpened = False
         self.windowPrc = None
+        self.http = HTTPClient.getGlobalPtr()
 
         self.fullDiskAccess = False
 
@@ -69,7 +70,11 @@ class AppRunner(DirectObject):
         self.rootDir = None
 
         # A list of the Panda3D packages that have been loaded.
-        self.packages = []
+        self.installedPackages = []
+
+        # A dictionary of HostInfo objects for the various download
+        # hosts we have imported packages from.
+        self.hosts = {}
 
         # The mount point for the multifile.  For now, this is always
         # the same, but when we move to multiple-instance sessions, it
@@ -82,7 +87,8 @@ class AppRunner(DirectObject):
         self.main = ScriptAttributes()
 
         # By default, we publish a stop() method so the browser can
-        # easy stop the plugin.
+        # easy stop the plugin.  A particular application can remove
+        # this if it chooses.
         self.main.stop = self.stop
 
         # This will be the browser's toplevel window DOM object;
@@ -106,9 +112,57 @@ class AppRunner(DirectObject):
         if AppRunnerGlobal.appRunner is None:
             AppRunnerGlobal.appRunner = self
 
-        # We use this messenger hook to dispatch this startIfReady()
+        # We use this messenger hook to dispatch this __startIfReady()
         # call back to the main thread.
-        self.accept('startIfReady', self.startIfReady)
+        self.accept('AppRunner_startIfReady', self.__startIfReady)
+
+    def getHost(self, hostUrl):
+        """ Returns a new HostInfo object corresponding to the
+        indicated host URL.  If we have already seen this URL
+        previously, returns the same object. """
+
+        host = self.hosts.get(hostUrl, None)
+        if not host:
+            host = HostInfo(hostUrl, self)
+            self.hosts[hostUrl] = host
+        return host
+
+    def freshenFile(self, host, fileSpec, localPathname):
+        """ Ensures that the localPathname is the most current version
+        of the file defined by fileSpec, as offered by host.  If not,
+        it downloads a new version on-the-spot.  Returns true on
+        success, false on failure. """
+
+        if fileSpec.quickVerify(pathname = localPathname):
+            # It's good, keep it.
+            return True
+
+        # It's stale, get a new one.
+        url = URLSpec(host.hostUrlPrefix + fileSpec.filename)
+        print "Downloading %s" % (url)
+        doc = self.http.getDocument(url)
+        if not doc.isValid():
+            return False
+        
+        file = Filename.temporary('', 'p3d_')
+        if not doc.downloadToFile(file):
+            # Failed to download.
+            file.unlink()
+            return False
+
+        # Successfully downloaded!
+        localPathname.makeDir()
+        if not file.renameTo(localPathname):
+            # Couldn't move it into place.
+            file.unlink()
+            return False
+
+        if not fileSpec.fullVerify(pathname = localPathname):
+            # No good after download.
+            print "%s is still no good after downloading." % (url)
+            return False
+
+        return True
             
     def stop(self):
         """ This method can be called by JavaScript to stop the
@@ -191,7 +245,8 @@ class AppRunner(DirectObject):
             # we plan to mount there.
             vfs.chdir(self.multifileRoot)
 
-    def startIfReady(self):
+    def __startIfReady(self):
+        """ Called internally to start the application. """
         if self.started:
             return
 
@@ -199,10 +254,10 @@ class AppRunner(DirectObject):
             self.started = True
 
             # Now we can ignore future calls to startIfReady().
-            self.ignore('startIfReady')
+            self.ignore('AppRunner_startIfReady')
 
             # Hang a hook so we know when the window is actually opened.
-            self.acceptOnce('window-event', self.windowEvent)
+            self.acceptOnce('window-event', self.__windowEvent)
 
             # Look for the startup Python file.  This may be a magic
             # filename (like "__main__", or any filename that contains
@@ -259,13 +314,19 @@ class AppRunner(DirectObject):
         
         self.rootDir = Filename.fromOsSpecific(rootDir)
 
-    def addPackageInfo(self, name, platform, version, host, installDir):
+    def addPackageInfo(self, name, platform, version, hostUrl):
         """ Called by the browser to list all of the "required"
         packages that were preloaded before starting the
         application. """
 
-        installDir = Filename.fromOsSpecific(installDir)
-        self.packages.append(PackageInfo(name, platform, version, host, installDir))
+        host = self.getHost(hostUrl)
+        host.readContentsFile()
+
+        if not platform:
+            platform = None
+        package = host.getPackage(name, version, platform = platform)
+        assert package
+        self.installedPackages.append(package)
 
     def setP3DFilename(self, p3dFilename, tokens = [], argv = [],
                        instanceId = None):
@@ -342,9 +403,9 @@ class AppRunner(DirectObject):
         self.gotP3DFilename = True
 
         # Send this call to the main thread; don't call it directly.
-        messenger.send('startIfReady', taskChain = 'default')
+        messenger.send('AppRunner_startIfReady', taskChain = 'default')
 
-    def clearWindowPrc(self):
+    def __clearWindowPrc(self):
         """ Clears the windowPrc file that was created in a previous
         call to setupWindow(), if any. """
         
@@ -356,7 +417,8 @@ class AppRunner(DirectObject):
                     parent, subprocessWindow):
         """ Applies the indicated window parameters to the prc
         settings, for future windows; or applies them directly to the
-        main window if the window has already been opened. """
+        main window if the window has already been opened.  This is
+        called by the browser. """
 
         if self.started and base.win:
             # If we've already got a window, this must be a
@@ -395,77 +457,19 @@ class AppRunner(DirectObject):
         if width or height:
             data += 'win-size %s %s\n' % (width, height)
 
-        self.clearWindowPrc()
+        self.__clearWindowPrc()
         self.windowPrc = loadPrcFileData("setupWindow", data)
 
         self.gotWindow = True
 
         # Send this call to the main thread; don't call it directly.
-        messenger.send('startIfReady', taskChain = 'default')
+        messenger.send('AppRunner_startIfReady', taskChain = 'default')
 
     def setRequestFunc(self, func):
-        """ This method is called by the plugin at startup to supply a
+        """ This method is called by the browser at startup to supply a
         function that can be used to deliver requests upstream, to the
-        plugin, and thereby to the browser. """
+        core API, and thereby to the browser. """
         self.requestFunc = func
-
-    def determineHostDir(self, hostUrl):
-        """ Hashes the indicated host URL into a (mostly) unique
-        directory string, which will be the root of the host's install
-        tree.  Returns the resulting path, as a Filename.
-
-        This code is duplicated in C++, in
-        P3DHost::determine_host_dir(). """
-
-        hostDir = self.rootDir + '/'
-
-        # Look for a server name in the URL.  Including this string in the
-        # directory name makes it friendlier for people browsing the
-        # directory.
-
-        # We could use URLSpec, but we do it by hand instead, to make
-        # it more likely that our hash code will exactly match the
-        # similar logic in P3DHost.
-        p = hostUrl.find('://')
-        if p != -1:
-            start = p + 3
-            end = hostUrl.find('/', start)
-            # Now start .. end is something like "username@host:port".
-
-            at = hostUrl.find('@', start)
-            if at != -1 and at < end:
-                start = at + 1
-
-            colon = hostUrl.find(':', start)
-            if colon != -1 and colon < end:
-                end = colon
-
-            # Now start .. end is just the hostname.
-            hostname = hostUrl[start : end]
-
-        # Now build a hash string of the whole URL.  We'll use MD5 to
-        # get a pretty good hash, with a minimum chance of collision.
-        # Even if there is a hash collision, though, it's not the end
-        # of the world; it just means that both hosts will dump their
-        # packages into the same directory, and they'll fight over the
-        # toplevel contents.xml file.  Assuming they use different
-        # version numbers (which should be safe since they have the
-        # same hostname), there will be minimal redownloading.
-
-        hashSize = 16
-        keepHash = hashSize
-        if hostname:
-            hostDir += hostname + '_'
-
-            # If we successfully got a hostname, we don't really need the
-            # full hash.  We'll keep half of it.
-            keepHash = keepHash / 2;
-
-        md = HashVal()
-        md.hashString(hostUrl)
-        hostDir += md.asHex()[:keepHash]
-
-        return hostDir
         
     def sendRequest(self, request, *args):
         """ Delivers a request to the browser via self.requestFunc.
@@ -475,7 +479,7 @@ class AppRunner(DirectObject):
         assert self.requestFunc
         return self.requestFunc(self.instanceId, request, args)
 
-    def windowEvent(self, win):
+    def __windowEvent(self, win):
         """ This method is called when we get a window event.  We
         listen for this to detect when the window has been
         successfully opened. """
@@ -485,7 +489,7 @@ class AppRunner(DirectObject):
 
             # Now that the window is open, we don't need to keep those
             # prc settings around any more.
-            self.clearWindowPrc()
+            self.__clearWindowPrc()
 
             # Inform the plugin and browser.
             self.notifyRequest('onwindowopen')
@@ -519,7 +523,10 @@ class AppRunner(DirectObject):
     def scriptRequest(self, operation, object, propertyName = '',
                       value = None, needsResponse = True):
         """ Issues a new script request to the browser.  This queries
-        or modifies one of the browser's DOM properties.
+        or modifies one of the browser's DOM properties.  This is a
+        low-level method that user code should not call directly;
+        instead, just operate on the Python wrapper objects that
+        shadow the DOM objects, beginning with appRunner.dom.
         
         operation may be one of [ 'get_property', 'set_property',
         'call', 'evaluate' ].
@@ -552,6 +559,63 @@ class AppRunner(DirectObject):
     def dropObject(self, objectId):
         """ Inform the parent process that we no longer have an
         interest in the P3D_object corresponding to the indicated
-        objectId. """
+        objectId.  Not intended to be called by user code. """
 
         self.sendRequest('drop_p3dobj', objectId)
+
+def dummyAppRunner(tokens = [], argv = None, fullDiskAccess = False):
+    """ This function creates a dummy global AppRunner object, which
+    is useful for testing running in a packaged environment without
+    actually bothering to package up the application.  Call this at
+    the start of your application to enable it.
+
+    It places the current working directory under /mf, as if it were
+    mounted from a packed multifile.  It doesn't convert egg files to
+    bam files, of course; and there are other minor differences from
+    running in an actual packaged environment.  But it can be a useful
+    first-look sanity check. """
+
+    if AppRunnerGlobal.appRunner:
+        print "Already have AppRunner, not creating a new one."
+        return
+
+    appRunner = AppRunner()
+    AppRunnerGlobal.appRunner = appRunner
+
+    platform = PandaSystem.getPlatform()
+    version = PandaSystem.getPackageVersionString()
+    hostUrl = PandaSystem.getPackageHostUrl()
+    
+    if platform.startswith('win'):
+        rootDir = Filename(Filename.getUserAppDataDirectory(), 'Panda3D')
+    else:
+        rootDir = Filename(Filename.getHomeDirectory(), '.panda3d')
+
+    appRunner.rootDir = rootDir
+
+    # Of course we will have the panda3d application loaded.
+    appRunner.addPackageInfo('panda3d', platform, version, hostUrl)
+        
+    appRunner.tokens = tokens
+    appRunner.tokenDict = dict(tokens)
+    if argv is None:
+        argv = sys.argv
+    appRunner.argv = argv
+
+    appRunner.p3dInfo = None
+    appRunner.p3dPackage = None
+    appRunner.fullDiskAccess = fullDiskAccess
+
+    # Mount the current directory under the multifileRoot, as if it
+    # were coming from a multifile.
+    cwd = ExecutionEnvironment.getCwd()
+    vfs = VirtualFileSystem.getGlobalPtr()
+    vfs.mount(cwd, appRunner.multifileRoot, vfs.MFReadOnly)
+
+    appRunner.initPackedAppEnvironment()
+
+    __builtin__.file = file.file
+    __builtin__.open = file.open
+    os.listdir = file.listdir
+    os.walk = file.walk
+

+ 118 - 0
direct/src/p3d/FileSpec.py

@@ -0,0 +1,118 @@
+import os
+from pandac.PandaModules import Filename, HashVal
+
+class FileSpec:
+    """ This class represents a disk file whose hash and size
+    etc. were read from an xml file.  This class provides methods to
+    verify whether the file on disk matches the version demanded by
+    the xml. """
+
+    def __init__(self, xelement):
+        self.filename = xelement.Attribute('filename')
+        self.basename = Filename(self.filename).getBasename()
+        size = xelement.Attribute('size')
+        try:
+            self.size = int(size)
+        except ValueError:
+            self.size = 0
+
+        timestamp = xelement.Attribute('timestamp')
+        try:
+            self.timestamp = int(timestamp)
+        except ValueError:
+            self.timestamp = 0
+
+        self.hash = xelement.Attribute('hash')
+            
+    def quickVerify(self, packageDir = None, pathname = None):
+        """ Performs a quick test to ensure the file has not been
+        modified.  This test is vulnerable to people maliciously
+        attempting to fool the program (by setting datestamps etc.).
+
+        Returns true if it is intact, false if it needs to be
+        redownloaded. """
+
+        if not pathname:
+            pathname = Filename(packageDir, self.filename)
+        try:
+            st = os.stat(pathname.toOsSpecific())
+        except OSError:
+            # If the file is missing, the file fails.
+            #print "file not found: %s" % (pathname)
+            return False
+
+        if st.st_size != self.size:
+            # If the size is wrong, the file fails.
+            #print "size wrong: %s" % (pathname)
+            return False
+
+        if st.st_mtime == self.timestamp:
+            # If the size is right and the timestamp is right, the
+            # file passes.
+            #print "file ok: %s" % (pathname)
+            return True
+
+        #print "modification time wrong: %s" % (pathname)
+
+        # If the size is right but the timestamp is wrong, the file
+        # soft-fails.  We follow this up with a hash check.
+        if not self.checkHash(pathname):
+            # Hard fail, the hash is wrong.
+            #print "hash check wrong: %s" % (pathname)
+            return False
+
+        #print "hash check ok: %s" % (pathname)
+
+        # The hash is OK after all.  Change the file's timestamp back
+        # to what we expect it to be, so we can quick-verify it
+        # successfully next time.
+        os.utime(pathname.toOsSpecific(), (st.st_atime, self.timestamp))
+
+        return True
+        
+            
+    def fullVerify(self, packageDir = None, pathname = None):
+        """ Performs a more thorough test to ensure the file has not
+        been modified.  This test is less vulnerable to malicious
+        attacks, since it reads and verifies the entire file.
+
+        Returns true if it is intact, false if it needs to be
+        redownloaded. """
+
+        if not pathname:
+            pathname = Filename(packageDir, pathname)
+        try:
+            st = os.stat(pathname.toOsSpecific())
+        except OSError:
+            # If the file is missing, the file fails.
+            #print "file not found: %s" % (pathname)
+            return False
+
+        if st.st_size != self.size:
+            # If the size is wrong, the file fails;
+            #print "size wrong: %s" % (pathname)
+            return False
+
+        if not self.checkHash(pathname):
+            # Hard fail, the hash is wrong.
+            #print "hash check wrong: %s" % (pathname)
+            return False
+
+        #print "hash check ok: %s" % (pathname)
+
+        # The hash is OK.  If the timestamp is wrong, change it back
+        # to what we expect it to be, so we can quick-verify it
+        # successfully next time.
+        if st.st_mtime != self.timestamp:
+            os.utime(pathname.toOsSpecific(), (st.st_atime, self.timestamp))
+
+        return True
+
+    def checkHash(self, pathname):
+        """ Returns true if the file has the expected md5 hash, false
+        otherwise. """
+
+        hv = HashVal()
+        hv.hashFile(pathname)
+        return (hv.asHex() == self.hash)
+    

+ 175 - 0
direct/src/p3d/HostInfo.py

@@ -0,0 +1,175 @@
+from pandac.PandaModules import TiXmlDocument, HashVal, Filename, PandaSystem
+from direct.p3d.PackageInfo import PackageInfo
+from direct.p3d.FileSpec import FileSpec
+
+class HostInfo:
+    """ This class represents a particular download host serving up
+    Panda3D packages.  It is the Python equivalent of the P3DHost
+    class in the core API. """
+
+    def __init__(self, hostUrl, appRunner):
+        self.hostUrl = hostUrl
+
+        # hostUrlPrefix is the host URL, but it is guaranteed to end
+        # with a slash.
+        self.hostUrlPrefix = hostUrl
+        if self.hostUrlPrefix[-1] != '/':
+            self.hostUrlPrefix += '/'
+
+        # Initially false, this is set true when the contents file is
+        # successfully read.
+        self.hasContentsFile = False
+
+        # descriptiveName will be filled in later, when the
+        # contents file is read.
+        self.descriptiveName = ''
+
+        # This is a dictionary of packages by (name, version).  It
+        # will be filled in when the contents file is read.
+        self.packages = {}
+
+        self.__determineHostDir(appRunner)
+        self.importsDir = Filename(self.hostDir, 'imports')
+
+    def readContentsFile(self):
+        """ Reads the contents.xml file for this particular host.
+        Presumably this has already been downloaded and installed. """
+
+        if self.hasContentsFile:
+            # No need to read it again.
+            return
+
+        filename = Filename(self.hostDir, 'contents.xml')
+
+        doc = TiXmlDocument(filename.toOsSpecific())
+        if not doc.LoadFile():
+            raise IOError
+
+        xcontents = doc.FirstChildElement('contents')
+        if not xcontents:
+            raise ValueError
+
+        self.descriptiveName = xcontents.Attribute('descriptive_name')
+
+        # Get the list of packages available for download.
+        xpackage = xcontents.FirstChildElement('package')
+        while xpackage:
+            name = xpackage.Attribute('name')
+            platform = xpackage.Attribute('platform')
+            version = xpackage.Attribute('version')
+            package = self.__makePackage(name, platform, version)
+            package.descFile = FileSpec(xpackage)
+
+            xpackage = xpackage.NextSiblingElement('package')
+
+        # Also get the list of packages available for import.
+        ximport = xcontents.FirstChildElement('import')
+        while ximport:
+            name = ximport.Attribute('name')
+            platform = ximport.Attribute('platform')
+            version = ximport.Attribute('version')
+            package = self.__makePackage(name, platform, version)
+            package.importDescFile = FileSpec(ximport)
+
+            ximport = ximport.NextSiblingElement('import')
+
+        self.hasContentsFile = True
+
+    def __makePackage(self, name, platform, version):
+        """ Creates a new PackageInfo entry for the given name,
+        version, and platform.  If there is already a matching
+        PackageInfo, returns it. """
+
+        if not platform:
+            # Ensure that we're on the same page with non-specified
+            # platforms.  We always use None, not empty string.
+            platform = None
+
+        platforms = self.packages.setdefault((name, version), {})
+        package = platforms.get(platform, None)
+        if not package:
+            package = PackageInfo(self, name, version, platform = platform)
+            platforms[platform] = package
+
+        return package
+
+    def getPackage(self, name, version, platform = None):
+        """ Returns a PackageInfo that matches the indicated name and
+        version and the indicated platform or the current runtime
+        platform, if one is provided by this host, or None if not. """
+
+        platforms = self.packages.get((name, version), {})
+
+        if platform is not None:
+            # In this case, we are looking for a specific platform
+            # only.
+            return platforms.get(platform or None, None)
+
+        # We are looking for one matching the current runtime
+        # platform.  First, look for a package matching the current
+        # platform exactly.
+        package = platforms.get(PandaSystem.getPlatform(), None)
+
+        # If not found, look for one matching no particular platform.
+        if not package:
+            package = platforms.get(None, None)
+            
+        return package
+
+    def __determineHostDir(self, appRunner):
+        """ Hashes the host URL into a (mostly) unique directory
+        string, which will be the root of the host's install tree.
+        Stores the resulting path, as a Filename, in self.hostDir.
+
+        This code is duplicated in C++, in
+        P3DHost::determine_host_dir(). """
+
+        hostDir = ''
+
+        # Look for a server name in the URL.  Including this string in the
+        # directory name makes it friendlier for people browsing the
+        # directory.
+
+        # We could use URLSpec, but we do it by hand instead, to make
+        # it more likely that our hash code will exactly match the
+        # similar logic in P3DHost.
+        p = self.hostUrl.find('://')
+        if p != -1:
+            start = p + 3
+            end = self.hostUrl.find('/', start)
+            # Now start .. end is something like "username@host:port".
+
+            at = self.hostUrl.find('@', start)
+            if at != -1 and at < end:
+                start = at + 1
+
+            colon = self.hostUrl.find(':', start)
+            if colon != -1 and colon < end:
+                end = colon
+
+            # Now start .. end is just the hostname.
+            hostname = self.hostUrl[start : end]
+
+        # Now build a hash string of the whole URL.  We'll use MD5 to
+        # get a pretty good hash, with a minimum chance of collision.
+        # Even if there is a hash collision, though, it's not the end
+        # of the world; it just means that both hosts will dump their
+        # packages into the same directory, and they'll fight over the
+        # toplevel contents.xml file.  Assuming they use different
+        # version numbers (which should be safe since they have the
+        # same hostname), there will be minimal redownloading.
+
+        hashSize = 16
+        keepHash = hashSize
+        if hostname:
+            hostDir += hostname + '_'
+
+            # If we successfully got a hostname, we don't really need the
+            # full hash.  We'll keep half of it.
+            keepHash = keepHash / 2;
+
+        md = HashVal()
+        md.hashString(self.hostUrl)
+        hostDir += md.asHex()[:keepHash * 2]
+
+        self.hostDir = Filename(appRunner.rootDir, hostDir)

+ 16 - 6
direct/src/p3d/PackageInfo.py

@@ -1,12 +1,22 @@
+from pandac.PandaModules import Filename
 
 class PackageInfo:
 
     """ This class represents a downloadable Panda3D package file that
-    can be (or has been) installed into the current runtime. """
+    can be (or has been) installed into the current runtime.  It is
+    the Python equivalent of the P3DPackage class in the core API. """
 
-    def __init__(self, name, platform, version, host, installDir):
-        self.name = name
-        self.platform = platform
-        self.version = version
+    def __init__(self, host, packageName, packageVersion, platform = None):
         self.host = host
-        self.installDir = installDir
+        self.packageName = packageName
+        self.packageVersion = packageVersion
+        self.platform = platform
+
+        self.packageFullname = '%s_%s' % (self.packageName, self.packageVersion)
+        self.packageDir = Filename(host.hostDir, 'packages/%s/%s' % (self.packageName, self.packageVersion))
+        self.descFileBasename = self.packageFullname + '.xml'
+
+        # These will be filled in by HostInfo when the package is read
+        # from contents.xml.
+        self.descFile = None
+        self.importDescFile = None

+ 28 - 3
direct/src/p3d/Packager.py

@@ -11,6 +11,7 @@ import new
 import string
 import types
 from direct.showbase import Loader
+from direct.showbase import AppRunnerGlobal
 from direct.showutil import FreezeTool
 from direct.directnotify.DirectNotifyGlobal import *
 from pandac.PandaModules import *
@@ -1994,9 +1995,33 @@ class Packager:
 
         return None
 
-    def __findPackageOnHost(self, packageName, platform, version, host, requires = None):
-        # TODO.
-        return None
+    def __findPackageOnHost(self, packageName, platform, version, hostUrl, requires = None):
+        appRunner = AppRunnerGlobal.appRunner
+        if not appRunner:
+            # We don't download import files from a host unless we're
+            # running in a packaged environment ourselves.  It would
+            # be possible to do this, but a fair bit of work for not
+            # much gain--this is meant to be run in a packaged
+            # environment.
+            return None
+
+        host = appRunner.getHost(hostUrl)
+        package = host.getPackage(packageName, version, platform = platform)
+        if not package or not package.importDescFile:
+            return None
+
+        # Now we've retrieved a PackageInfo.  Get the import desc file
+        # from it.
+        filename = Filename(host.importsDir, package.importDescFile.basename)
+        if not appRunner.freshenFile(host, package.importDescFile, filename):
+            print "Couldn't download import file."
+            return None
+
+        # Now that we have the import desc file, use it to load one of
+        # our Package objects.
+        package = self.Package('', self)
+        if package.readImportDescFile(filename):
+            return package
 
     def __sortPackageImportFilelist(self, filelist):
         """ Given a list of *_import.xml filenames, sorts them in

+ 3 - 0
direct/src/p3d/packp3d.py

@@ -50,6 +50,9 @@ import direct
 from direct.p3d import Packager 
 from pandac.PandaModules import *
 
+# Temp hack for debugging.
+#from direct.p3d.AppRunner import dummyAppRunner; dummyAppRunner(fullDiskAccess = 1)
+
 class ArgumentError(StandardError):
     pass
 

+ 0 - 0
direct/src/configfiles/panda3d.pdef → direct/src/p3d/panda3d.pdef


+ 5 - 7
direct/src/plugin/fileSpec.cxx

@@ -52,26 +52,26 @@ FileSpec() {
 //  Description: Reads the data from the indicated XML file.
 ////////////////////////////////////////////////////////////////////
 void FileSpec::
-load_xml(TiXmlElement *element) {
-  const char *filename = element->Attribute("filename");
+load_xml(TiXmlElement *xelement) {
+  const char *filename = xelement->Attribute("filename");
   if (filename != NULL) {
     _filename = filename;
   }
 
-  const char *size = element->Attribute("size");
+  const char *size = xelement->Attribute("size");
   if (size != NULL) {
     char *endptr;
     _size = strtoul(size, &endptr, 10);
   }
 
-  const char *timestamp = element->Attribute("timestamp");
+  const char *timestamp = xelement->Attribute("timestamp");
   if (timestamp != NULL) {
     char *endptr;
     _timestamp = strtoul(timestamp, &endptr, 10);
   }
 
   _got_hash = false;
-  const char *hash = element->Attribute("hash");
+  const char *hash = xelement->Attribute("hash");
   if (hash != NULL && strlen(hash) == (hash_size * 2)) {
     // Decode the hex hash string.
     _got_hash = decode_hex(_hash, hash, hash_size);
@@ -159,8 +159,6 @@ full_verify(const string &package_dir) const {
     return false;
   }
 
-  // If the size is right but the timestamp is wrong, the file
-  // soft-fails.  We follow this up with a hash check.
   if (!check_hash(pathname)) {
     // Hard fail, the hash is wrong.
     //cerr << "hash check wrong: " << _filename << "\n";

+ 1 - 1
direct/src/plugin/fileSpec.h

@@ -29,7 +29,7 @@ using namespace std;
 class FileSpec {
 public:
   FileSpec();
-  void load_xml(TiXmlElement *element);
+  void load_xml(TiXmlElement *xelement);
 
   inline const string &get_filename() const;
   inline void set_filename(const string &filename);

+ 6 - 1
direct/src/plugin/p3dHost.cxx

@@ -97,6 +97,11 @@ read_contents_file(const string &contents_filename) {
   }
   _xcontents = (TiXmlElement *)xcontents->Clone();
 
+  const char *descriptive_name = _xcontents->Attribute("descriptive_name");
+  if (descriptive_name != NULL) {
+    _descriptive_name = descriptive_name;
+  }
+
   string standard_filename = _host_dir + "/contents.xml";
   if (standardize_filename(standard_filename) != 
       standardize_filename(contents_filename)) {
@@ -203,7 +208,7 @@ get_package_desc_file(FileSpec &desc_file,              // out
 //               tree.  Stores the result in _host_dir.
 //
 //               This code is duplicated in Python, in
-//               AppRunner.determineHostDir().
+//               HostInfo.determineHostDir().
 ////////////////////////////////////////////////////////////////////
 void P3DHost::
 determine_host_dir() {

+ 0 - 1
direct/src/plugin/p3dPackage.cxx

@@ -148,7 +148,6 @@ make_xml() {
     xpackage->SetAttribute("version", _package_version);
   }
   xpackage->SetAttribute("host", _host->get_host_url());
-  xpackage->SetAttribute("install_dir", _package_dir);
 
   return xpackage;
 }

+ 2 - 6
direct/src/plugin/p3dPythonRun.cxx

@@ -977,20 +977,16 @@ add_package_info(P3DCInstance *inst, TiXmlElement *xpackage) {
   const char *platform = xpackage->Attribute("platform");
   const char *version = xpackage->Attribute("version");
   const char *host = xpackage->Attribute("host");
-  const char *install_dir = xpackage->Attribute("install_dir");
   if (name == NULL || version == NULL || host == NULL) {
     return;
   }
   if (platform == NULL) {
     platform = "";
   }
-  if (install_dir == NULL) {
-    install_dir = "";
-  }
 
   PyObject *result = PyObject_CallMethod
-    (_runner, (char *)"addPackageInfo", (char *)"sssss",
-     name, platform, version, host, install_dir);
+    (_runner, (char *)"addPackageInfo", (char *)"ssss",
+     name, platform, version, host);
 
   if (result == NULL) {
     PyErr_Print();