Browse Source

wip: host support

David Rose 16 years ago
parent
commit
ae8c460b1b

+ 2 - 0
direct/src/plugin/Sources.pp

@@ -32,6 +32,7 @@
     p3dFileDownload.h p3dFileDownload.I \
     p3dFileParams.h p3dFileParams.I \
     p3dFloatObject.h \
+    p3dHost.h p3dHost.I \
     p3dInstance.h p3dInstance.I \
     p3dInstanceManager.h p3dInstanceManager.I \
     p3dIntObject.h \
@@ -62,6 +63,7 @@
     p3dFileDownload.cxx \
     p3dFileParams.cxx \
     p3dFloatObject.cxx \
+    p3dHost.cxx \
     p3dInstance.cxx \
     p3dInstanceManager.cxx \
     p3dIntObject.cxx \

+ 11 - 0
direct/src/plugin/fileSpec.I

@@ -24,6 +24,17 @@ get_filename() const {
   return _filename;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: FileSpec::set_filename
+//       Access: Private
+//  Description: Changes the relative path to this file on disk,
+//               within the package root directory.
+////////////////////////////////////////////////////////////////////
+inline void FileSpec::
+set_filename(const string &filename) {
+  _filename = filename;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: FileSpec::get_pathname
 //       Access: Private

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

@@ -32,6 +32,7 @@ public:
   void load_xml(TiXmlElement *element);
 
   inline const string &get_filename() const;
+  inline void set_filename(const string &filename);
   inline string get_pathname(const string &package_dir) const;
   inline size_t get_size() const;
   

+ 73 - 0
direct/src/plugin/p3dHost.I

@@ -0,0 +1,73 @@
+// Filename: p3dHost.I
+// Created by:  drose (21Aug09)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::get_host_dir
+//       Access: Public
+//  Description: Returns the local directory into which files
+//               downloaded from this host will be installed.
+////////////////////////////////////////////////////////////////////
+inline const string &P3DHost::
+get_host_dir() const {
+  return _host_dir;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::get_host_url
+//       Access: Public
+//  Description: Returns the root URL of this particular host, as
+//               passed from the package file.  This is a unique
+//               string that identifies each host.
+////////////////////////////////////////////////////////////////////
+inline const string &P3DHost::
+get_host_url() const {
+  return _host_url;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::get_host_url_prefix
+//       Access: Public
+//  Description: Returns the root URL of this host, for constructing
+//               full URL sequences.  This is the same as
+//               get_host_url(), except it is guaranteed to end in a
+//               slash character.
+////////////////////////////////////////////////////////////////////
+inline const string &P3DHost::
+get_host_url_prefix() const {
+  return _host_url_prefix;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::get_descriptive_name
+//       Access: Public
+//  Description: Returns the descriptive name provided for this host,
+//               if any.  This will be available after
+//               read_contents_file() has been called.
+////////////////////////////////////////////////////////////////////
+inline const string &P3DHost::
+get_descriptive_name() const {
+  return _descriptive_name;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::has_contents_file
+//       Access: Public
+//  Description: Returns true if a contents.xml file has been
+//               successfully read for this host, false otherwise.
+////////////////////////////////////////////////////////////////////
+inline bool P3DHost::
+has_contents_file() const {
+  return (_xcontents != NULL);
+}

+ 265 - 0
direct/src/plugin/p3dHost.cxx

@@ -0,0 +1,265 @@
+// Filename: p3dHost.cxx
+// Created by:  drose (21Aug09)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#include "p3dHost.h"
+#include "p3dInstanceManager.h"
+#include "p3dPackage.h"
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::Constructor
+//       Access: Private
+//  Description: Use P3DInstanceManager::get_host() to construct a new
+//               P3DHost.
+////////////////////////////////////////////////////////////////////
+P3DHost::
+P3DHost(P3DInstanceManager *inst_mgr, const string &host_url) :
+  _host_url(host_url) 
+{
+  _host_dir = inst_mgr->get_root_dir();
+  _host_dir += "/host";  // TODO.
+
+  // Ensure that the download URL ends with a slash.
+  _host_url_prefix = _host_url;
+  if (!_host_url_prefix.empty() && _host_url_prefix[_host_url_prefix.size() - 1] != '/') {
+    _host_url_prefix += "/";
+  }
+
+  _xcontents = NULL;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::Destructor
+//       Access: Private
+//  Description: 
+////////////////////////////////////////////////////////////////////
+P3DHost::
+~P3DHost() {
+  if (_xcontents != NULL) {
+    delete _xcontents;
+  }
+
+  Packages::iterator pi;
+  for (pi = _packages.begin(); pi != _packages.end(); ++pi) {
+    delete (*pi).second;
+  }
+  _packages.clear();
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::read_contents_file
+//       Access: Public
+//  Description: Reads the contents.xml file in the indicated
+//               filename.  On success, copies the contents.xml file
+//               into the standard location (if it's not there
+//               already).
+//
+//               Returns true on success, false on failure.
+////////////////////////////////////////////////////////////////////
+bool P3DHost::
+read_contents_file(const string &contents_filename) {
+  TiXmlDocument doc(contents_filename.c_str());
+  if (!doc.LoadFile()) {
+    return false;
+  }
+
+  TiXmlElement *xcontents = doc.FirstChildElement("contents");
+  if (xcontents == NULL) {
+    return false;
+  }
+
+  if (_xcontents != NULL) {
+    delete _xcontents;
+  }
+  _xcontents = (TiXmlElement *)xcontents->Clone();
+
+  string standard_filename = _host_dir + "/contents.xml";
+  if (standardize_filename(standard_filename) != 
+      standardize_filename(contents_filename)) {
+    copy_file(contents_filename, standard_filename);
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::get_package
+//       Access: Public
+//  Description: Returns a (possibly shared) pointer to the indicated
+//               package.
+////////////////////////////////////////////////////////////////////
+P3DPackage *P3DHost::
+get_package(const string &package_name, const string &package_version) {
+  string key = package_name + "_" + package_version;
+  Packages::iterator pi = _packages.find(key);
+  if (pi != _packages.end()) {
+    return (*pi).second;
+  }
+
+  P3DPackage *package = 
+    new P3DPackage(this, package_name, package_version);
+  bool inserted = _packages.insert(Packages::value_type(key, package)).second;
+  assert(inserted);
+
+  return package;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::get_package_desc_file
+//       Access: Public
+//  Description: Fills the indicated FileSpec with the hash
+//               information for the package's desc file, and also
+//               determines the package's platform.  Returns true if
+//               successful, false if the package is unknown.  This
+//               requires has_contents_file() to return true in order
+//               to be successful.
+////////////////////////////////////////////////////////////////////
+bool P3DHost::
+get_package_desc_file(FileSpec &desc_file,              // out
+                      string &package_platform,         // out
+                      const string &package_name,       // in
+                      const string &package_version) {  // in
+  if (_xcontents == NULL) {
+    return false;
+  }
+
+  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
+
+  // Scan the contents data for the indicated package.  First, we look
+  // for a platform-specific version.
+  TiXmlElement *xpackage = _xcontents->FirstChildElement("package");
+  while (xpackage != NULL) {
+    const char *name = xpackage->Attribute("name");
+    const char *platform = xpackage->Attribute("platform");
+    const char *version = xpackage->Attribute("version");
+    if (name != NULL && platform != NULL && version != NULL &&
+        package_name == name && 
+        inst_mgr->get_platform() == platform &&
+        package_version == version) {
+      // Here's the matching package definition.
+      desc_file.load_xml(xpackage);
+      package_platform = platform;
+      return true;
+    }
+
+    xpackage = xpackage->NextSiblingElement("package");
+  }
+
+  // Look again, this time looking for a non-platform-specific version.
+  xpackage = _xcontents->FirstChildElement("package");
+  while (xpackage != NULL) {
+    const char *name = xpackage->Attribute("name");
+    const char *platform = xpackage->Attribute("platform");
+    const char *version = xpackage->Attribute("version");
+    if (platform == NULL) {
+      platform = "";
+    }
+    if (name != NULL && version != NULL &&
+        package_name == name && 
+        *platform == '\0' &&
+        package_version == version) {
+      // Here's the matching package definition.
+      desc_file.load_xml(xpackage);
+      package_platform = platform;
+      return true;
+    }
+
+    xpackage = xpackage->NextSiblingElement("package");
+  }
+
+  // Couldn't find the named package.
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::standardize_filename
+//       Access: Private, Static
+//  Description: Attempts to change the filename into some standard
+//               form for comparison with other filenames.  On a
+//               case-insensitive filesystem, this converts the
+//               filename to lowercase.  On Windows, it further
+//               replaces forward slashes with backslashes.
+////////////////////////////////////////////////////////////////////
+string P3DHost::
+standardize_filename(const string &filename) {
+#if defined(_WIN32) || defined(__APPLE__)
+  string new_filename;
+  for (string::const_iterator si = filename.begin();
+       si != filename.end();
+       ++si) {
+    char ch = *si;
+#ifdef _WIN32
+    if (ch == '/') {
+      ch = '\\';
+    }
+#endif  // _WIN32
+    new_filename += tolower(ch);
+  }
+  return new_filename;
+#else  // _WIN32 || __APPLE__
+  return filename;
+#endif  // _WIN32 || __APPLE__
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DHost::copy_file
+//       Access: Private, Static
+//  Description: Copies the data in the file named by from_filename
+//               into the file named by to_filename.
+////////////////////////////////////////////////////////////////////
+bool P3DHost::
+copy_file(const string &from_filename, const string &to_filename) {
+  ifstream in(from_filename.c_str(), ios::in | ios::binary);
+
+  // Copy to a temporary file first, in case (a) the filenames
+  // actually refer to the same file, or (b) in case we have different
+  // processes writing to the same file, and (c) to prevent
+  // partially overwriting the file should something go wrong.
+  ostringstream strm;
+  strm << to_filename << ".t";
+#ifdef _WIN32
+  strm << GetCurrentProcessId() << "_" << GetCurrentThreadId();
+#else
+  strm << getpid();
+#endif
+  string temp_filename = strm.str();
+  ofstream out(temp_filename.c_str(), ios::out | ios::binary);
+        
+  static const size_t buffer_size = 4096;
+  char buffer[buffer_size];
+  
+  in.read(buffer, buffer_size);
+  size_t count = in.gcount();
+  while (count != 0) {
+    out.write(buffer, count);
+    if (out.fail()) {
+      unlink(temp_filename.c_str());
+      return false;
+    }
+    in.read(buffer, buffer_size);
+    count = in.gcount();
+  }
+  out.close();
+
+  if (!in.eof()) {
+    unlink(temp_filename.c_str());
+    return false;
+  }
+
+  if (rename(temp_filename.c_str(), to_filename.c_str()) == 0) {
+    return true;
+  }
+
+  unlink(temp_filename.c_str());
+  return false;
+}

+ 71 - 0
direct/src/plugin/p3dHost.h

@@ -0,0 +1,71 @@
+// Filename: p3dHost.h
+// Created by:  drose (21Aug09)
+//
+////////////////////////////////////////////////////////////////////
+//
+// PANDA 3D SOFTWARE
+// Copyright (c) Carnegie Mellon University.  All rights reserved.
+//
+// All use of this software is subject to the terms of the revised BSD
+// license.  You should have received a copy of this license along
+// with this source code in a file named "LICENSE."
+//
+////////////////////////////////////////////////////////////////////
+
+#ifndef P3DHOST_H
+#define P3DHOST_H
+
+#include "p3d_plugin_common.h"
+
+#include <map>
+
+class FileSpec;
+class P3DInstanceManager;
+class P3DPackage;
+
+////////////////////////////////////////////////////////////////////
+//       Class : P3DHost
+// Description : Represents a particular download host serving up
+//               Panda3D packages.
+////////////////////////////////////////////////////////////////////
+class P3DHost {
+private:
+  P3DHost(P3DInstanceManager *inst_mgr, const string &host_url);
+  ~P3DHost();
+
+public:
+  inline const string &get_host_dir() const;
+  inline const string &get_host_url() const;
+  inline const string &get_host_url_prefix() const;
+  inline const string &get_descriptive_name() const;
+
+  inline bool has_contents_file() const;
+  bool read_contents_file(const string &contents_filename);
+
+  P3DPackage *get_package(const string &package_name, 
+                          const string &package_version);
+  bool get_package_desc_file(FileSpec &desc_file, 
+                             string &package_platform,
+                             const string &package_name,
+                             const string &package_version);
+
+private:
+  static string standardize_filename(const string &filename);
+  static bool copy_file(const string &from_filename, const string &to_filename);
+
+private:
+  string _host_dir;
+  string _host_url;
+  string _host_url_prefix;
+  string _descriptive_name;
+  TiXmlElement *_xcontents;
+
+  typedef map<string, P3DPackage *> Packages;
+  Packages _packages;
+
+  friend class P3DInstanceManager;
+};
+
+#include "p3dHost.I"
+
+#endif

+ 8 - 5
direct/src/plugin/p3dInstance.cxx

@@ -884,12 +884,14 @@ scan_app_desc_file(TiXmlDocument *doc) {
   TiXmlElement *xrequires = xpackage->FirstChildElement("requires");
   while (xrequires != NULL) {
     const char *name = xrequires->Attribute("name");
-    if (name != NULL) {
+    const char *host_url = xrequires->Attribute("host");
+    if (name != NULL && host_url != NULL) {
       const char *version = xrequires->Attribute("version");
       if (version == NULL) {
         version = "";
       }
-      P3DPackage *package = inst_mgr->get_package(name, version);
+      P3DHost *host = inst_mgr->get_host(host_url);
+      P3DPackage *package = host->get_package(name, version);
       add_package(package);
     }
 
@@ -1177,9 +1179,10 @@ make_splash_window() {
   if (!_fparams.has_token("splash_img")) {
     // No specific splash image is specified; get the default splash
     // image.
-    P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-    splash_image_url = inst_mgr->get_download_url();
-    splash_image_url += "coreapi/splash.jpg";
+    if (_panda3d != NULL) {
+      splash_image_url = _panda3d->get_host()->get_host_url_prefix();
+      splash_image_url += "coreapi/splash.jpg";
+    }
   }
 
   if (splash_image_url.empty()) {

+ 0 - 23
direct/src/plugin/p3dInstanceManager.I

@@ -36,18 +36,6 @@ get_root_dir() const {
   return _root_dir;
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::get_download_url
-//       Access: Public
-//  Description: Returns the URL of the download server.  All
-//               downloadable files will be retrieved from various
-//               subdirectories of this URL root.
-////////////////////////////////////////////////////////////////////
-inline const string &P3DInstanceManager::
-get_download_url() const {
-  return _download_url;
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstanceManager::get_platform
 //       Access: Public
@@ -74,17 +62,6 @@ get_log_directory() const {
   return _log_directory;
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::has_contents_file
-//       Access: Public
-//  Description: Returns true if a contents.xml file has been
-//               successfully read, false otherwise.
-////////////////////////////////////////////////////////////////////
-inline bool P3DInstanceManager::
-has_contents_file() const {
-  return (_xcontents != NULL);
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstanceManager::get_num_instances
 //       Access: Public

+ 31 - 203
direct/src/plugin/p3dInstanceManager.cxx

@@ -15,7 +15,7 @@
 #include "p3dInstanceManager.h"
 #include "p3dInstance.h"
 #include "p3dSession.h"
-#include "p3dPackage.h"
+#include "p3dHost.h"
 #include "p3d_plugin_config.h"
 #include "p3dWinSplashWindow.h"
 #include "p3dUndefinedObject.h"
@@ -51,7 +51,6 @@ P3DInstanceManager() {
   _is_initialized = false;
   _next_temp_filename_counter = 0;
   _unique_id = 0;
-  _xcontents = NULL;
 
   _notify_thread_continue = false;
   _started_notify_thread = false;
@@ -103,13 +102,15 @@ P3DInstanceManager::
   sigaction(SIGPIPE, &_old_sigpipe, NULL);
 #endif  // _WIN32
 
-  if (_xcontents != NULL) {
-    delete _xcontents;
-  }
-
   assert(_instances.empty());
   assert(_sessions.empty());
 
+  Hosts::iterator hi;
+  for (hi = _hosts.begin(); hi != _hosts.end(); ++hi) {
+    delete (*hi).second;
+  }
+  _hosts.clear();
+
   // Delete any remaining temporary files.
   TempFilenames::iterator ti;
   for (ti = _temp_filenames.begin(); ti != _temp_filenames.end(); ++ti) {
@@ -145,7 +146,7 @@ P3DInstanceManager::
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstanceManager::initialize
 //       Access: Public
-//  Description: Called by the host at application startup.  It
+//  Description: Called by the plugin host at application startup.  It
 //               returns true if the DLL is successfully initialized,
 //               false if it should be immediately shut down and
 //               redownloaded.
@@ -156,12 +157,7 @@ initialize(const string &contents_filename, const string &download_url,
            const string &log_basename) {
 
   _root_dir = find_root_dir();
-  _download_url = download_url;
-#ifdef P3D_PLUGIN_DOWNLOAD
-  if (_download_url.empty()) {
-    _download_url = P3D_PLUGIN_DOWNLOAD;
-  }
-#endif
+
   _platform = platform;
   if (_platform.empty()) {
     _platform = DTOOL_PLATFORM;
@@ -197,7 +193,7 @@ initialize(const string &contents_filename, const string &download_url,
   }
   delete[] buffer_2;
 
-  // Also make sure the directory actually exists.
+  // And make sure the directory actually exists.
   mkdir_complete(_temp_directory, nout);
 
 #else
@@ -216,11 +212,6 @@ initialize(const string &contents_filename, const string &download_url,
   }
 #endif
 
-  // Ensure that the download URL ends with a slash.
-  if (!_download_url.empty() && _download_url[_download_url.size() - 1] != '/') {
-    _download_url += "/";
-  }
-
   // Ensure that the temp directory ends with a slash.
   if (!_temp_directory.empty() && _temp_directory[_temp_directory.size() - 1] != '/') {
 #ifdef _WIN32
@@ -251,8 +242,6 @@ initialize(const string &contents_filename, const string &download_url,
   }
 
   nout << "_root_dir = " << _root_dir
-       << ", contents = " << contents_filename
-       << ", download = " << _download_url
        << ", platform = " << _platform
        << "\n";
 
@@ -263,9 +252,11 @@ initialize(const string &contents_filename, const string &download_url,
 
   _is_initialized = true;
 
-  // Attempt to read the supplied contents.xml file.
-  if (!contents_filename.empty()) {
-    if (!read_contents_file(contents_filename)) {
+  if (!download_url.empty() && !contents_filename.empty()) {
+    // Attempt to pre-read the supplied contents.xml file, to avoid an
+    // unnecessary download later.
+    P3DHost *host = get_host(download_url);
+    if (!host->read_contents_file(contents_filename)) {
       nout << "Couldn't read " << contents_filename << "\n";
     }
   }
@@ -273,42 +264,6 @@ initialize(const string &contents_filename, const string &download_url,
   return true;
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::read_contents_file
-//       Access: Public
-//  Description: Reads the contents.xml file in the indicated
-//               filename.  On success, copies the contents.xml file
-//               into the standard location (if it's not there
-//               already).
-//
-//               Returns true on success, false on failure.
-////////////////////////////////////////////////////////////////////
-bool P3DInstanceManager::
-read_contents_file(const string &contents_filename) {
-  TiXmlDocument doc(contents_filename.c_str());
-  if (!doc.LoadFile()) {
-    return false;
-  }
-
-  TiXmlElement *xcontents = doc.FirstChildElement("contents");
-  if (xcontents == NULL) {
-    return false;
-  }
-
-  if (_xcontents != NULL) {
-    delete _xcontents;
-  }
-  _xcontents = (TiXmlElement *)xcontents->Clone();
-
-  string standard_filename = _root_dir + "/contents.xml";
-  if (standardize_filename(standard_filename) != 
-      standardize_filename(contents_filename)) {
-    copy_file(contents_filename, standard_filename);
-  }
-
-  return true;
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstanceManager::create_instance
 //       Access: Public
@@ -473,67 +428,23 @@ wait_request() {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::get_package
+//     Function: P3DInstanceManager::get_host
 //       Access: Public
 //  Description: Returns a (possibly shared) pointer to the indicated
-//               package.
-////////////////////////////////////////////////////////////////////
-P3DPackage *P3DInstanceManager::
-get_package(const string &package_name, const string &package_version) {
-  string package_platform = get_platform();
-  string key = package_name + "_" + package_platform + "_" + package_version;
-  Packages::iterator pi = _packages.find(key);
-  if (pi != _packages.end()) {
+//               download host.
+////////////////////////////////////////////////////////////////////
+P3DHost *P3DInstanceManager::
+get_host(const string &host_url) {
+  Hosts::iterator pi = _hosts.find(host_url);
+  if (pi != _hosts.end()) {
     return (*pi).second;
   }
 
-  P3DPackage *package = 
-    new P3DPackage(package_name, package_platform, package_version);
-  bool inserted = _packages.insert(Packages::value_type(key, package)).second;
+  P3DHost *host = new P3DHost(this, host_url);
+  bool inserted = _hosts.insert(Hosts::value_type(host_url, host)).second;
   assert(inserted);
 
-  return package;
-}
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::get_package_desc_file
-//       Access: Public
-//  Description: Fills the indicated FileSpec with the hash
-//               information for the package's desc file.  Returns
-//               true if successful, false if the package is unknown.
-//               This requires has_contents_file() to return true in
-//               order to be successful.
-////////////////////////////////////////////////////////////////////
-bool P3DInstanceManager::
-get_package_desc_file(FileSpec &desc_file,
-                      const string &package_name,
-                      const string &package_version) {
-  if (_xcontents == NULL) {
-    return false;
-  }
-
-  string package_platform = get_platform();
-
-  // Scan the contents data for the indicated package.
-  TiXmlElement *xpackage = _xcontents->FirstChildElement("package");
-  while (xpackage != NULL) {
-    const char *name = xpackage->Attribute("name");
-    const char *platform = xpackage->Attribute("platform");
-    const char *version = xpackage->Attribute("version");
-    if (name != NULL && platform != NULL && version != NULL &&
-        package_name == name && 
-        package_platform == platform &&
-        package_version == version) {
-      // Here's the matching package definition.
-      desc_file.load_xml(xpackage);
-      return true;
-    }
-
-    xpackage = xpackage->NextSiblingElement("package");
-  }
-
-  // Couldn't find the named package.
-  return false;
+  return host;
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -698,89 +609,6 @@ delete_global_ptr() {
   }
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::standardize_filename
-//       Access: Private, Static
-//  Description: Attempts to change the filename into some standard
-//               form for comparison with other filenames.  On a
-//               case-insensitive filesystem, this converts the
-//               filename to lowercase.  On Windows, it further
-//               replaces forward slashes with backslashes.
-////////////////////////////////////////////////////////////////////
-string P3DInstanceManager::
-standardize_filename(const string &filename) {
-#if defined(_WIN32) || defined(__APPLE__)
-  string new_filename;
-  for (string::const_iterator si = filename.begin();
-       si != filename.end();
-       ++si) {
-    char ch = *si;
-#ifdef _WIN32
-    if (ch == '/') {
-      ch = '\\';
-    }
-#endif  // _WIN32
-    new_filename += tolower(ch);
-  }
-  return new_filename;
-#else  // _WIN32 || __APPLE__
-  return filename;
-#endif  // _WIN32 || __APPLE__
-}
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DInstanceManager::copy_file
-//       Access: Private, Static
-//  Description: Copies the data in the file named by from_filename
-//               into the file named by to_filename.
-////////////////////////////////////////////////////////////////////
-bool P3DInstanceManager::
-copy_file(const string &from_filename, const string &to_filename) {
-  ifstream in(from_filename.c_str(), ios::in | ios::binary);
-
-  // Copy to a temporary file first, in case (a) the filenames
-  // actually refer to the same file, or (b) in case we have different
-  // processes writing to the same file, and (c) to prevent
-  // partially overwriting the file should something go wrong.
-  ostringstream strm;
-  strm << to_filename << ".t";
-#ifdef _WIN32
-  strm << GetCurrentProcessId() << "_" << GetCurrentThreadId();
-#else
-  strm << getpid();
-#endif
-  string temp_filename = strm.str();
-  ofstream out(temp_filename.c_str(), ios::out | ios::binary);
-        
-  static const size_t buffer_size = 4096;
-  char buffer[buffer_size];
-  
-  in.read(buffer, buffer_size);
-  size_t count = in.gcount();
-  while (count != 0) {
-    out.write(buffer, count);
-    if (out.fail()) {
-      unlink(temp_filename.c_str());
-      return false;
-    }
-    in.read(buffer, buffer_size);
-    count = in.gcount();
-  }
-  out.close();
-
-  if (!in.eof()) {
-    unlink(temp_filename.c_str());
-    return false;
-  }
-
-  if (rename(temp_filename.c_str(), to_filename.c_str()) == 0) {
-    return true;
-  }
-
-  unlink(temp_filename.c_str());
-  return false;
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstanceManager::nt_thread_run
 //       Access: Private
@@ -791,12 +619,12 @@ nt_thread_run() {
   // The notify thread exists because we need to be able to send
   // asynchronous notifications of request events.  These request
   // events were detected in the various read threads associated with
-  // each session, but we can't call back into the host space from the
-  // read thread, since if the host immediately response to a callback
-  // by calling back into the p3d_plugin space, now we have our read
-  // thread doing stuff in here that's not related to the read thread.
-  // Even worse, some of the things it might need to do might require
-  // a separate read thread to be running!
+  // each session, but we can't call back into the plugin host space
+  // from the read thread, since if the host immediately response to a
+  // callback by calling back into the p3d_plugin space, now we have
+  // our read thread doing stuff in here that's not related to the
+  // read thread.  Even worse, some of the things it might need to do
+  // might require a separate read thread to be running!
 
   _notify_ready.acquire();
   while (_notify_thread_continue) {

+ 4 - 19
direct/src/plugin/p3dInstanceManager.h

@@ -28,7 +28,7 @@
 
 class P3DInstance;
 class P3DSession;
-class P3DPackage;
+class P3DHost;
 class FileSpec;
 class TiXmlElement;
 
@@ -52,13 +52,9 @@ public:
   inline bool is_initialized() const;
 
   inline const string &get_root_dir() const;
-  inline const string &get_download_url() const;
   inline const string &get_platform() const;
   inline const string &get_log_directory() const;
 
-  inline bool has_contents_file() const;
-  bool read_contents_file(const string &contents_filename);
-
   P3DInstance *
   create_instance(P3D_request_ready_func *func, 
                   const P3D_token tokens[], size_t num_tokens, 
@@ -74,11 +70,7 @@ public:
   P3DInstance *check_request();
   void wait_request();
 
-  P3DPackage *get_package(const string &package_name, 
-                          const string &package_version);
-  bool get_package_desc_file(FileSpec &desc_file, 
-                             const string &package_name,
-                             const string &package_version);
+  P3DHost *get_host(const string &host_url);
 
   inline int get_num_instances() const;
 
@@ -97,10 +89,6 @@ public:
   static P3DInstanceManager *get_global_ptr();
   static void delete_global_ptr();
 
-private:
-  static string standardize_filename(const string &filename);
-  static bool copy_file(const string &from_filename, const string &to_filename);
-
 private:
   // The notify thread.  This thread runs only for the purpose of
   // generating asynchronous notifications of requests, to callers who
@@ -111,15 +99,12 @@ private:
 private:
   bool _is_initialized;
   string _root_dir;
-  string _download_url;
   string _platform;
   string _log_directory;
   string _log_basename;
   string _log_pathname;
   string _temp_directory;
 
-  TiXmlElement *_xcontents;
-
   P3D_object *_undefined_object;
   P3D_object *_none_object;
   P3D_object *_true_object;
@@ -131,8 +116,8 @@ private:
   typedef map<string, P3DSession *> Sessions;
   Sessions _sessions;
 
-  typedef map<string, P3DPackage *> Packages;
-  Packages _packages;
+  typedef map<string, P3DHost *> Hosts;
+  Hosts _hosts;
 
   typedef set<string> TempFilenames;
   TempFilenames _temp_filenames;

+ 11 - 0
direct/src/plugin/p3dPackage.I

@@ -81,6 +81,17 @@ get_failed() const {
   return _failed;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DPackage::get_host
+//       Access: Public
+//  Description: Returns the host server which offers this package for
+//               download.
+////////////////////////////////////////////////////////////////////
+inline P3DHost *P3DPackage::
+get_host() const {
+  return _host;
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DPackage::get_package_dir
 //       Access: Public

+ 26 - 39
direct/src/plugin/p3dPackage.cxx

@@ -36,21 +36,14 @@ static const double extract_portion = 0.05;
 //  Description: 
 ////////////////////////////////////////////////////////////////////
 P3DPackage::
-P3DPackage(const string &package_name,
-           const string &package_platform,
+P3DPackage(P3DHost *host, const string &package_name,
            const string &package_version) :
+  _host(host),
   _package_name(package_name),
-  _package_platform(package_platform),
   _package_version(package_version)
 {
-  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-
   _package_fullname = _package_name;
-  _package_dir = inst_mgr->get_root_dir() + string("/packages/") + _package_name;
-  if (!_package_platform.empty()) {
-    _package_fullname += string("_") + _package_platform;
-    _package_dir += string("/") + _package_platform;
-  }
+  _package_dir = _host->get_host_dir() + string("/packages/") + _package_name;
   _package_fullname += string("_") + _package_version;
   _package_dir += string("/") + _package_version;
 
@@ -169,16 +162,14 @@ begin_info_download() {
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 download_contents_file() {
-  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-
-  if (inst_mgr->has_contents_file()) {
+  if (_host->has_contents_file()) {
     // We've already got a contents.xml file; go straight to the
     // package desc file.
     download_desc_file();
     return;
   }
 
-  string url = inst_mgr->get_download_url();
+  string url = _host->get_host_url_prefix();
   url += "contents.xml";
 
   // Download contents.xml to a temporary filename first, in case
@@ -196,15 +187,13 @@ download_contents_file() {
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 contents_file_download_finished(bool success) {
-  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-
-  if (!inst_mgr->has_contents_file()) {
-    if (!success || !inst_mgr->read_contents_file(_temp_contents_file->get_filename())) {
+  if (!_host->has_contents_file()) {
+    if (!success || !_host->read_contents_file(_temp_contents_file->get_filename())) {
       nout << "Couldn't read " << *_temp_contents_file << "\n";
 
       // Maybe we can read an already-downloaded contents.xml file.
-      string standard_filename = inst_mgr->get_root_dir() + "/contents.xml";
-      if (!inst_mgr->read_contents_file(standard_filename)) {
+      string standard_filename = _host->get_host_dir() + "/contents.xml";
+      if (!_host->read_contents_file(standard_filename)) {
         // Couldn't even read that.  Fail.
         report_done(false);
         delete _temp_contents_file;
@@ -225,27 +214,31 @@ contents_file_download_finished(bool success) {
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DPackage::download_desc_file
 //       Access: Private
-//  Description: Starts downloading the desc file for the package.
+//  Description: Starts downloading the desc file for the package, if
+//               it's needed; or read to local version if it's fresh
+//               enough.
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 download_desc_file() {
-  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-
   // Attempt to check the desc file for freshness.  If it already
   // exists, and is consistent with the server contents file, we don't
   // need to re-download it.
-  string root_dir = inst_mgr->get_root_dir() + "/packages";
   FileSpec desc_file;
-  if (!inst_mgr->get_package_desc_file(desc_file, _package_name, _package_version)) {
+  if (!_host->get_package_desc_file(desc_file, _package_platform,
+                                    _package_name, _package_version)) {
     nout << "Couldn't find package " << _package_fullname
          << " in contents file.\n";
+    return;
+  }
 
-  } else if (desc_file.get_pathname(root_dir) != _desc_file_pathname) {
-    nout << "Wrong pathname for desc file: " 
-         << desc_file.get_pathname(root_dir) 
-         << " instead of " << _desc_file_pathname << "\n";
+  // The desc file might have a different path on the host server than
+  // it has locally, because we strip out the platform locally.
+  // Adjust desc_file to point to the local file.
+  string url_filename = desc_file.get_filename();
+  desc_file.set_filename(_desc_file_basename);
+  assert (desc_file.get_pathname(_package_dir) == _desc_file_pathname);
 
-  } else if (!desc_file.full_verify(root_dir)) {
+  if (!desc_file.full_verify(_package_dir)) {
     nout << _desc_file_pathname << " is stale.\n";
 
   } else {
@@ -258,13 +251,8 @@ download_desc_file() {
   }
 
   // The desc file is not current.  Go download it.
-  string url = inst_mgr->get_download_url();
-  url += _package_name;
-  if (!_package_platform.empty()) {
-    url += "/" + _package_platform;
-  }
-  url += "/" + _package_version;
-  url += "/" + _desc_file_basename;
+  string url = _host->get_host_url_prefix();
+  url += url_filename;
 
   start_download(DT_desc_file, url, _desc_file_pathname, false);
 }
@@ -415,8 +403,7 @@ begin_data_download() {
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 download_compressed_archive(bool allow_partial) {
-  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
-  string url = inst_mgr->get_download_url();
+  string url = _host->get_host_url_prefix();
   url += _package_name;
   if (!_package_platform.empty()) {
     url += "/" + _package_platform;

+ 14 - 6
direct/src/plugin/p3dPackage.h

@@ -20,6 +20,7 @@
 #include "fileSpec.h"
 #include "get_tinyxml.h"
 
+class P3DHost;
 class P3DInstance;
 class P3DTemporaryFile;
 
@@ -31,23 +32,26 @@ class P3DTemporaryFile;
 //               runtime, which consists of a bunch of dll's
 //               downloaded in a single tar file, is a package.
 //
-//               The plugin is responsible for managing these packages
-//               on disk, downloading new versions when needed, and
-//               removing stale versions to limit disk space waste.
+//               The core API is responsible for managing these
+//               packages on disk, downloading new versions when
+//               needed, and removing stale versions to limit disk
+//               space waste.
 ////////////////////////////////////////////////////////////////////
 class P3DPackage {
-public:
-  P3DPackage(const string &package_name, 
-             const string &package_platform,
+private:
+  P3DPackage(P3DHost *host,
+             const string &package_name, 
              const string &package_version);
   ~P3DPackage();
 
+public:
   inline bool get_info_ready() const;
   inline size_t get_download_size() const;
 
   inline void activate_download();
   inline bool get_ready() const;
   inline bool get_failed() const;
+  inline P3DHost *get_host() const;
   inline const string &get_package_dir() const;
   inline const string &get_package_name() const;
   inline const string &get_package_version() const;
@@ -101,6 +105,8 @@ private:
   bool is_extractable(const string &filename) const;
 
 private:
+  P3DHost *_host;
+
   string _package_name;
   string _package_version;
   string _package_platform;
@@ -132,6 +138,8 @@ private:
 
   friend class Download;
   friend class P3DMultifileReader;
+
+  friend class P3DHost;
 };
 
 #include "p3dPackage.I"

+ 1 - 1
direct/src/plugin/p3dPythonRun.cxx

@@ -18,7 +18,7 @@
 
 // There is only one P3DPythonRun object in any given process space.
 // Makes the statics easier to deal with, and we don't need multiple
-// instances of this think.
+// instances of this thing.
 P3DPythonRun *P3DPythonRun::_global_ptr = NULL;
 
 ////////////////////////////////////////////////////////////////////

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

@@ -7,6 +7,7 @@
 #include "p3dFileDownload.cxx"
 #include "p3dFileParams.cxx"
 #include "p3dFloatObject.cxx"
+#include "p3dHost.cxx"
 #include "p3dInstance.cxx"
 #include "p3dInstanceManager.cxx"
 #include "p3dIntObject.cxx"

+ 3 - 4
direct/src/plugin/p3d_plugin_config.h.pp

@@ -6,10 +6,9 @@
 /********************************** DO NOT EDIT ****************************/
 
 /* The URL that is the root of the download server that this plugin
-   should contact.  The nppanda3d.dll file should be found at this
-   location; as well as the contents.xml file that defines where the
-   various Panda3D packages will be found. */
-#$[]define P3D_PLUGIN_DOWNLOAD "$[P3D_PLUGIN_DOWNLOAD]$[if $[notdir $[P3D_PLUGIN_DOWNLOAD]],/]"
+   should contact.  The contents.xml file that defines this particular
+   "coreapi" package should be found at this location. */
+#$[]define PANDA_PACKAGE_HOST_URL "$[PANDA_PACKAGE_HOST_URL]"
 
 /* The filename(s) to generate output to when the plugin is running.
    For debugging purposes only. */

+ 2 - 2
direct/src/plugin_npapi/ppInstance.cxx

@@ -108,7 +108,7 @@ void PPInstance::
 begin() {
   if (!is_plugin_loaded()) {
     // Go download the contents file, so we can download the core DLL.
-    string url = P3D_PLUGIN_DOWNLOAD;
+    string url = PANDA_PACKAGE_HOST_URL;
     if (!url.empty() && url[url.length() - 1] != '/') {
       url += '/';
     }
@@ -837,7 +837,7 @@ get_core_api(TiXmlElement *xplugin) {
 
   } else {
     // The DLL file needs to be downloaded.  Go get it.
-    string url = P3D_PLUGIN_DOWNLOAD;
+    string url = PANDA_PACKAGE_HOST_URL;
     if (!url.empty() && url[url.length() - 1] != '/') {
       url += '/';
     }

+ 2 - 2
direct/src/plugin_standalone/panda3d.cxx

@@ -64,7 +64,7 @@ run(int argc, char *argv[]) {
   const char *optstr = "+mu:p:ft:s:o:l:h";
 
   bool allow_multiple = false;
-  string download_url = P3D_PLUGIN_DOWNLOAD;
+  string download_url = PANDA_PACKAGE_HOST_URL;
   string this_platform = DTOOL_PLATFORM;
   bool force_download = false;
 
@@ -752,7 +752,7 @@ usage() {
 
     << "  -u url\n"
     << "    Specify the URL of the Panda3D download server.  The default is\n"
-    << "    \"" << P3D_PLUGIN_DOWNLOAD << "\" .\n\n"
+    << "    \"" << PANDA_PACKAGE_HOST_URL << "\" .\n\n"
 
     << "  -p platform\n"
     << "    Specify the platform to masquerade as.  The default is \""

+ 120 - 60
direct/src/showutil/Packager.py

@@ -133,8 +133,9 @@ class Packager:
         def __init__(self, packageName, packager):
             self.packageName = packageName
             self.packager = packager
-            self.version = None
             self.platform = None
+            self.version = None
+            self.host = None
             self.p3dApplication = False
             self.compressionLevel = 0
             self.importedMapsDir = 'imported_maps'
@@ -672,6 +673,7 @@ class Packager:
                 xrequires.SetAttribute('name', package.packageName)
                 if package.version:
                     xrequires.SetAttribute('version', package.version)
+                xrequires.SetAttribute('host', package.host)
                 xpackage.InsertEndChild(xrequires)
 
             doc.InsertEndChild(xpackage)
@@ -699,6 +701,9 @@ class Packager:
                 raise PackagerError, message
 
         def writeDescFile(self):
+            """ Makes the package.xml file that describes the package
+            and its contents, for download. """
+            
             packageDescFullpath = Filename(self.packager.installDir, self.packageDesc)
             doc = TiXmlDocument(packageDescFullpath.toOsSpecific())
             decl = TiXmlDeclaration("1.0", "utf-8", "")
@@ -724,6 +729,7 @@ class Packager:
                     xrequires.SetAttribute('platform', package.platform)
                 if package.version:
                     xrequires.SetAttribute('version', package.version)
+                xrequires.SetAttribute('host', package.host)
                 xpackage.InsertEndChild(xrequires)
 
             xuncompressedArchive = self.getFileSpec(
@@ -743,6 +749,10 @@ class Packager:
             doc.SaveFile()
 
         def writeImportDescFile(self):
+            """ Makes the package_import.xml file that describes the
+            package and its contents, for other packages and
+            applications that may wish to "require" this one. """
+        
             packageImportDescFullpath = Filename(self.packager.installDir, self.packageImportDesc)
             doc = TiXmlDocument(packageImportDescFullpath.toOsSpecific())
             decl = TiXmlDeclaration("1.0", "utf-8", "")
@@ -754,6 +764,7 @@ class Packager:
                 xpackage.SetAttribute('platform', self.platform)
             if self.version:
                 xpackage.SetAttribute('version', self.version)
+            xpackage.SetAttribute('host', self.host)
 
             for package in self.requires:
                 xrequires = TiXmlElement('requires')
@@ -762,6 +773,7 @@ class Packager:
                     xrequires.SetAttribute('platform', package.platform)
                 if package.version:
                     xrequires.SetAttribute('version', package.version)
+                xrequires.SetAttribute('host', package.host)
                 xpackage.InsertEndChild(xrequires)
 
             self.components.sort()
@@ -785,6 +797,7 @@ class Packager:
             self.packageName = xpackage.Attribute('name')
             self.platform = xpackage.Attribute('platform')
             self.version = xpackage.Attribute('version')
+            self.host = xpackage.Attribute('host')
 
             self.requires = []
             xrequires = xpackage.FirstChildElement('requires')
@@ -792,8 +805,11 @@ class Packager:
                 packageName = xrequires.Attribute('name')
                 platform = xrequires.Attribute('platform')
                 version = xrequires.Attribute('version')
+                host = xrequires.Attribute('host')
                 if packageName:
-                    package = self.packager.findPackage(packageName, platform = platform, version = version, requires = self.requires)
+                    package = self.packager.findPackage(
+                        packageName, platform = platform, version = version,
+                        host = host, requires = self.requires)
                     if package:
                         self.requires.append(package)
                 xrequires = xrequires.NextSiblingElement('requires')
@@ -1012,9 +1028,12 @@ class Packager:
         self.installDir = None
         self.persistDir = None
 
-        # A search list of directories and/or URL's to search for
-        # installed packages.  We query it from a config variable
-        # initially, but we may also be extending it at runtime.
+        # The download URL at which these packages will eventually be
+        # hosted.  This may also be changed with the "host" command.
+        self.host = PandaSystem.getPackageHostUrl()
+        self.hostDescriptiveName = None
+
+        # A search list for previously-built local packages.
         self.installSearch = ConfigVariableSearchPath('pdef-path')
 
         # The system PATH, for searching dll's and exe's.
@@ -1195,23 +1214,8 @@ class Packager:
         # We must have an actual install directory.
         assert(self.installDir)
 
-##         # If the persist dir names an empty or nonexistent directory,
-##         # we will be generating a brand new publish with no previous
-##         # patches.
-##         self.persistDir.makeDir()
-
-##         # Within the persist dir, we make a temporary holding dir for
-##         # generating multifiles.
-##         self.mfTempDir = Filename(self.persistDir, Filename('mftemp/'))
-##         self.mfTempDir.makeDir()
-
-##         # We also need a temporary holding dir for squeezing py files.
-##         self.pyzTempDir = Filename(self.persistDir, Filename('pyz/'))
-##         self.pyzTempDir.makeDir()
-
-##         # Change to the persist directory so the temp files will be
-##         # created there
-##         os.chdir(self.persistDir.toOsSpecific())
+        if not PandaSystem.getPackageVersionString() or not PandaSystem.getPackageHostUrl():
+            raise PackagerError, 'This script must be run using a version of Panda3D that has been built\nfor distribution.  Try using ppackage.p3d or packp3d.p3d instead.'
 
     def __expandVariable(self, line, p):
         """ Given that line[p] is a dollar sign beginning a variable
@@ -1441,6 +1445,32 @@ class Packager:
         value = ExecutionEnvironment.expandString(value.strip())
         ExecutionEnvironment.setEnvironmentVariable(variable, value)
 
+    def parse_host(self, words):
+        """
+        host "url" ["descriptive name"]
+        """
+
+        hostDescriptiveName = None
+        try:
+            if len(words) == 2:
+                command, host = words
+            else:
+                command, host, hostDescriptiveName = words
+        except ValueError:
+            raise ArgumentNumber
+
+        if self.currentPackage:
+            self.currentPackage.host = host
+        else:
+            # Outside of a package, the "host" command specifies the
+            # host for all future packages.
+            self.host = host
+
+        # The descriptive name, if specified, is kept until the end,
+        # where it may be passed to make_contents by ppackage.py.
+        if hostDescriptiveName:
+            self.hostDescriptiveName = hostDescriptiveName
+
     def parse_model_path(self, words):
         """
         model_path directory
@@ -1469,10 +1499,10 @@ class Packager:
 
     def parse_begin_package(self, words):
         """
-        begin_package packageName [version=v]
+        begin_package packageName [version=v] [host=host]
         """
 
-        args = self.__parseArgs(words, ['version'])
+        args = self.__parseArgs(words, ['version', 'host'])
 
         try:
             command, packageName = words
@@ -1480,8 +1510,10 @@ class Packager:
             raise ArgumentNumber
 
         version = args.get('version', None)
+        host = args.get('host', None)
 
-        self.beginPackage(packageName, version = version, p3dApplication = False)
+        self.beginPackage(packageName, version = version, host = host,
+                          p3dApplication = False)
 
     def parse_end_package(self, words):
         """
@@ -1539,10 +1571,10 @@ class Packager:
 
     def parse_require(self, words):
         """
-        require packageName [version=v]
+        require packageName [version=v] [host=url]
         """
 
-        args = self.__parseArgs(words, ['version'])
+        args = self.__parseArgs(words, ['version', 'host'])
 
         try:
             command, packageName = words
@@ -1550,7 +1582,8 @@ class Packager:
             raise ArgumentError
 
         version = args.get('version', None)
-        self.require(packageName, version = version)
+        host = args.get('host', None)
+        self.require(packageName, version = version, host = host)
 
     def parse_module(self, words):
         """
@@ -1798,7 +1831,8 @@ class Packager:
             del words[-1]
                 
     
-    def beginPackage(self, packageName, version = None, p3dApplication = False):
+    def beginPackage(self, packageName, version = None, host = None,
+                     p3dApplication = False):
         """ Begins a new package specification.  packageName is the
         basename of the package.  Follow this with a number of calls
         to file() etc., and close the package with endPackage(). """
@@ -1806,20 +1840,33 @@ class Packager:
         if self.currentPackage:
             raise PackagerError, 'unmatched end_package %s' % (self.currentPackage.packageName)
 
-        # A special case for the Panda3D package.  We enforce that the
-        # version number matches what we've been compiled with.
+        if host is None and not p3dApplication:
+            # Every package that doesn't specify otherwise uses the
+            # current download host.
+            host = self.host
+
+        # A special case when building the "panda3d" package.  We
+        # enforce that the version number matches what we've been
+        # compiled with.
         if packageName == 'panda3d':
             if version is None:
                 version = PandaSystem.getPackageVersionString()
-            else:
-                if version != PandaSystem.getPackageVersionString():
-                    message = 'mismatched Panda3D version: requested %s, but Panda3D is built as %s' % (version, PandaSystem.getPackageVersionString())
-                    raise PackageError, message
+            if host is None:
+                host = PandaSystem.getPackageHostUrl()
+
+            if version != PandaSystem.getPackageVersionString():
+                message = 'mismatched Panda3D version: requested %s, but Panda3D is built as %s' % (version, PandaSystem.getPackageVersionString())
+                raise PackageError, message
+
+            if host != PandaSystem.getPackageHostUrl():
+                message = 'mismatched Panda3D host: requested %s, but Panda3D is built as %s' % (host, PandaSystem.getPackageHostUrl())
+                raise PackageError, message
 
         package = self.Package(packageName, self)
         self.currentPackage = package
 
         package.version = version
+        package.host = host
         package.p3dApplication = p3dApplication
 
         if package.p3dApplication:
@@ -1855,7 +1902,7 @@ class Packager:
         self.currentPackage = None
 
     def findPackage(self, packageName, platform = None, version = None,
-                    requires = None):
+                    host = None, requires = None):
         """ Searches for the named package from a previous publish
         operation along the install search path.
 
@@ -1872,25 +1919,32 @@ class Packager:
             platform = self.platform
 
         # Is it a package we already have resident?
-        package = self.packages.get((packageName, platform, version), None)
+        package = self.packages.get((packageName, platform, version, host), None)
         if package:
             return package
 
         # Look on the searchlist.
         for dirname in self.installSearch.getDirectories():
-            package = self.__scanPackageDir(dirname, packageName, platform, version, requires = requires)
+            package = self.__scanPackageDir(dirname, packageName, platform, version, host, requires = requires)
             if not package:
-                package = self.__scanPackageDir(dirname, packageName, None, version, requires = requires)
+                package = self.__scanPackageDir(dirname, packageName, None, version, host, requires = requires)
 
             if package:
-                package = self.packages.setdefault((package.packageName, package.platform, package.version), package)
-                self.packages[(packageName, platform, version)] = package
-                return package
+                break
+
+        if not package:
+            # Query the indicated host.
+            package = self.__findPackageOnHost(packageName, platform, version, host, requires = requires)
+
+        if package:
+            package = self.packages.setdefault((package.packageName, package.platform, package.version, package.host), package)
+            self.packages[(packageName, platform, version, host)] = package
+            return package
                 
         return None
 
     def __scanPackageDir(self, rootDir, packageName, platform, version,
-                         requires = None):
+                         host, requires = None):
         """ Scans a directory on disk, looking for *_import.xml files
         that match the indicated packageName and optional version.  If a
         suitable xml file is found, reads it and returns the assocated
@@ -1917,6 +1971,9 @@ class Packager:
             packageDir = Filename(packageDir, '*')
             basename += '_%s' % ('*')
 
+        # Actually, the host means little for this search, since we're
+        # only looking in a local directory at this point.
+
         basename += '_import.xml'
         filename = Filename(packageDir, basename)
         filelist = glob.glob(filename.toOsSpecific())
@@ -1934,6 +1991,10 @@ class Packager:
 
         return None
 
+    def __findPackageOnHost(self, packageName, platform, version, host, requires = None):
+        # TODO.
+        return None
+
     def __sortPackageImportFilelist(self, filelist):
         """ Given a list of *_import.xml filenames, sorts them in
         reverse order by version, so that the highest-numbered
@@ -1968,7 +2029,8 @@ class Packager:
             while p < len(version) and version[p] in string.digits:
                 w += version[p]
                 p += 1
-            words.append(int(w))
+            if w:
+                words.append(int(w))
 
         return tuple(words)
 
@@ -2024,7 +2086,7 @@ class Packager:
 
         self.currentPackage.configs[variable] = value
 
-    def require(self, packageName, version = None):
+    def require(self, packageName, version = None, host = None):
         """ Indicates a dependency on the named package, supplied as
         a name.
 
@@ -2035,13 +2097,17 @@ class Packager:
         if not self.currentPackage:
             raise OutsideOfPackageError
 
-        # A special case for the Panda3D package.  We enforce that the
-        # version number matches what we've been compiled with.
+        # A special case when requiring the "panda3d" package.  We
+        # supply the version number what we've been compiled with as a
+        # default.
         if packageName == 'panda3d':
             if version is None:
                 version = PandaSystem.getPackageVersionString()
+            if host is None:
+                host = PandaSystem.getPackageHostUrl()
         
-        package = self.findPackage(packageName, version = version, requires = self.currentPackage.requires)
+        package = self.findPackage(packageName, version = version, host = host,
+                                   requires = self.currentPackage.requires)
         if not package:
             message = 'Unknown package %s, version "%s"' % (packageName, version)
             raise PackagerError, message
@@ -2059,20 +2125,14 @@ class Packager:
         if not self.currentPackage:
             raise OutsideOfPackageError
 
-        # A special case for the Panda3D package.  We enforce that the
-        # version number matches what we've been compiled with.
+        # A special case when requiring the "panda3d" package.  We
+        # complain if the version number doesn't match what we've been
+        # compiled with.
         if package.packageName == 'panda3d':
             if package.version != PandaSystem.getPackageVersionString():
-                if not PandaSystem.getPackageVersionString():
-                    # We haven't been compiled with any particular
-                    # version of Panda.  This is a warning, not an
-                    # error.
-                    print "Warning: requiring panda3d version %s, which may or may not match the current build of Panda.  Recommend that you use only the official Panda3D build for making distributable applications to ensure compatibility." % (package.version)
-                else:
-                    # This particular version of Panda doesn't match
-                    # the requested version.  Again, a warning, not an
-                    # error.
-                    print "Warning: requiring panda3d version %s, which does not match the current build of Panda, which is version %s." % (package, PandaSystem.getPackageVersionString())
+                print "Warning: requiring panda3d version %s, which does not match the current build of Panda, which is version %s." % (package, PandaSystem.getPackageVersionString())
+            elif package.host != PandaSystem.getPackageHostUrl():
+                print "Warning: requiring panda3d host %s, which does not match the current build of Panda, which is host %s." % (package, PandaSystem.getPackageHostUrl())
 
         self.currentPackage.requirePackage(package)
 

+ 57 - 10
direct/src/showutil/make_contents.py

@@ -12,17 +12,24 @@ make_contents.py [opts]
 
 Options:
 
-  -d stage_dir
-
-     Specify the staging directory.  This is a temporary directory on
-     the local machine that contains a copy of the web server
-     contents.  The default is the current directory.
+  -i install_dir
+     The full path to a local directory that contains the
+     ready-to-be-published files, as populated by one or more
+     iterations of the ppackage script.  It is the user's
+     responsibility to copy this directory structure to a server.
+
+  -n "host descriptive name"
+     Specifies a descriptive name of the download server that will
+     host these contents.  This name may be presented to the user when
+     managing installed packages.  If this option is omitted, the name
+     is unchanged from the previous pass.
 
 """
 
 import sys
 import getopt
 import os
+import types
 
 try:
     import hashlib
@@ -56,6 +63,7 @@ class FileSpec:
 class ContentsMaker:
     def __init__(self):
         self.installDir = None
+        self.hostDescriptiveName = None
 
     def build(self):
         if not self.installDir:
@@ -67,20 +75,56 @@ class ContentsMaker:
         if not self.packages:
             raise ArgumentError, "No packages found."
 
-        # Now write the contents.xml file.
         contentsFileBasename = 'contents.xml'
         contentsFilePathname = os.path.join(self.installDir, contentsFileBasename)
+        contentsLine = None
+        if self.hostDescriptiveName is not None:
+            if self.hostDescriptiveName:
+                contentsLine = '<contents descriptive_name="%s">' % (
+                    self.quoteString(self.hostDescriptiveName))
+        else:
+            contentsLine = self.readContentsLine(contentsFilePathname)
+        if not contentsLine:
+            contentsLine = '<contents>'
 
+        # Now write the contents.xml file.
         f = open(contentsFilePathname, 'w')
-        print >> f, '<?xml version="1.0" ?>'
+        print >> f, '<?xml version="1.0" encoding="utf-8" ?>'
         print >> f, ''
-        print >> f, '<contents>'
+        print >> f, contentsLine
         for type, packageName, packagePlatform, packageVersion, file in self.packages:
             print >> f, '  <%s name="%s" platform="%s" version="%s" %s />' % (
                 type, packageName, packagePlatform or '', packageVersion, file.getParams())
         print >> f, '</contents>'
         f.close()
 
+    def readContentsLine(self, contentsFilePathname):
+        """ Reads the previous iteration of contents.xml to get the
+        previous top-level contents line, which contains the
+        hostDescriptiveName. """
+
+        try:
+            f = open(contentsFilePathname, 'r')
+        except OSError:
+            return None
+
+        for line in f.readlines():
+            if line.startswith('<contents'):
+                return line.rstrip()
+
+        return None
+
+    def quoteString(self, str):
+        """ Correctly quotes a string for embedding in the xml file. """
+        if isinstance(str, types.UnicodeType):
+            str = str.encode('utf-8')
+        str = str.replace('&', '&amp;')
+        str = str.replace('"', '&quot;')
+        str = str.replace('\'', '&apos;')
+        str = str.replace('<', '&lt;')
+        str = str.replace('>', '&gt;')
+        return str
+    
     def scanDirectory(self):
         """ Walks through all the files in the stage directory and
         looks for the package directory xml files. """
@@ -139,13 +183,16 @@ class ContentsMaker:
         
                 
 def makeContents(args):
-    opts, args = getopt.getopt(args, 'd:h')
+    opts, args = getopt.getopt(args, 'i:n:h')
 
     cm = ContentsMaker()
     cm.installDir = '.'
     for option, value in opts:
-        if option == '-d':
+        if option == '-i':
             cm.installDir = value
+
+        elif option == '-n':
+            cm.hostDescriptiveName = value
             
         elif option == '-h':
             print __doc__

+ 15 - 9
direct/src/showutil/packp3d.py

@@ -119,15 +119,21 @@ def makePackedApp(args):
     packager.installDir = appDir
     getModelPath().appendDirectory(root)
 
-    packager.setup()
-    packager.beginPackage(appBase, p3dApplication = True)
-    for requireName in requires:
-        packager.require(requireName)
-        
-    packager.dir(root)
-    packager.mainModule(mainModule)
-        
-    packager.endPackage(appBase, p3dApplication = True)
+    try:
+        packager.setup()
+        packager.beginPackage(appBase, p3dApplication = True)
+        for requireName in requires:
+            packager.require(requireName)
+
+        packager.dir(root)
+        packager.mainModule(mainModule)
+
+        packager.endPackage(appBase, p3dApplication = True)
+    except Packager.PackagerError:
+        # Just print the error message and exit gracefully.
+        inst = sys.exc_info()[1]
+        print inst.args[0]
+        sys.exit(1)
 
 def main(appRunner):
     """ This function is called when this module is invoked as

+ 33 - 6
direct/src/showutil/ppackage.py

@@ -38,12 +38,13 @@ Options:
      ready-to-be-published files into.  This directory structure may
      contain multiple different packages from multiple different
      invocations of this script.  It is the user's responsibility to
-     copy this directory structure to a web host where it may be
-     downloaded by the client.
+     copy this directory structure to a server, which will have the
+     URL specified by -u, below.
 
   -s search_dir
      Additional directories to search for previously-built packages.
-     This option may be repeated as necessary.
+     This option may be repeated as necessary.  These directories may
+     also be specified with the pdef-path Config.prc variable.
 
   -d persist_dir
      The full path to a local directory that retains persistant state
@@ -54,6 +55,21 @@ Options:
      empty, patches will not be created for this publish; but the
      directory structure will be populated for the next publish.
 
+  -u host_url
+     Specifies the URL to the download server that will eventually
+     host these packages (that is, the public URL of the install
+     directory).  This may also be overridden with a "host" command
+     appearing within the pdef file.  This is used for packages only;
+     it is ignored for p3d applications, which are not specific to a
+     particular host.
+
+  -n "host descriptive name"
+     Specifies a descriptive name of the download server named by -u.
+     This name may be presented to the user when managing installed
+     packages.  This may also be overridden with a "host" command
+     appearing within the pdef file.  This information is written to
+     the contents.xml file at the top of the install directory.
+
   -p platform
      Specify the platform to masquerade as.  The default is whatever
      platform Panda has been built for.  It is probably unwise to set
@@ -82,7 +98,7 @@ def usage(code, msg = ''):
 packager = Packager.Packager()
 
 try:
-    opts, args = getopt.getopt(sys.argv[1:], 'i:s:d:p:Hh')
+    opts, args = getopt.getopt(sys.argv[1:], 'i:s:d:p:u:n:Hh')
 except getopt.error, msg:
     usage(1, msg)
 
@@ -95,6 +111,10 @@ for opt, arg in opts:
         packager.persistDir = Filename.fromOsSpecific(arg)
     elif opt == '-p':
         packager.platform = arg
+    elif opt == '-u':
+        package.host = arg
+    elif opt == '-n':
+        package.hostDescriptiveName = arg
         
     elif opt == '-h':
         usage(0)
@@ -117,8 +137,14 @@ if not packager.installDir:
     packager.installDir = Filename('install')
 packager.installSearch.prependDirectory(packager.installDir)
 
-packager.setup()
-packages = packager.readPackageDef(packageDef)
+try:
+    packager.setup()
+    packages = packager.readPackageDef(packageDef)
+except Packager.PackagerError:
+    # Just print the error message and exit gracefully.
+    inst = sys.exc_info()[1]
+    print inst.args[0]
+    sys.exit(1)
 
 # Look to see if we built any true packages, or if all of them were
 # p3d files.
@@ -133,5 +159,6 @@ if anyPackages:
     # the root of the install directory.
     cm = make_contents.ContentsMaker()
     cm.installDir = packager.installDir.toOsSpecific()
+    cm.hostDescriptiveName = packager.hostDescriptiveName
     cm.build()