David Rose 16 роки тому
батько
коміт
687961b626

+ 118 - 17
direct/src/plugin/p3dInstance.cxx

@@ -24,6 +24,7 @@
 #include "p3dObject.h"
 #include "p3dToplevelObject.h"
 #include "p3dUndefinedObject.h"
+#include "p3dMultifileReader.h"
 
 #include <sstream>
 #include <algorithm>
@@ -68,6 +69,7 @@ P3DInstance(P3D_request_ready_func *func,
   INIT_LOCK(_request_lock);
 
   _session = NULL;
+  _panda3d = NULL;
   _splash_window = NULL;
   _instance_window_opened = false;
   _requested_stop = false;
@@ -157,15 +159,25 @@ set_p3d_filename(const string &p3d_filename) {
   // This also sets up some internal data based on the contents of the
   // above file and the associated tokens.
 
-  // For the moment, all sessions will be unique.
+  // Extract the application desc file from the p3d file.
+  P3DMultifileReader reader;
+  stringstream sstream;
+  if (!reader.extract_one(p3d_filename, sstream, "p3d_info.xml")) {
+    nout << "No p3d_info.xml file found in " << p3d_filename << "\n";
+  } else {
+    sstream.seekg(0);
+    TiXmlDocument doc;
+    sstream >> doc;
+    scan_app_desc_file(&doc);
+  }
+
+  // For the moment, all sessions will be unique.  TODO: support
+  // multiple instances per session.
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
   ostringstream strm;
   strm << inst_mgr->get_unique_id();
   _session_key = strm.str();
 
-  // TODO.
-  _python_version = "python24";
-
   // Generate a special notification: onpluginload, indicating the
   // plugin has read its parameters and is ready to be queried (even
   // if Python has not yet started).
@@ -605,15 +617,62 @@ handle_event(P3D_event_data event) {
 //       Access: Public
 //  Description: Adds the package to the list of packages used by this
 //               instance.  The instance will share responsibility for
-//               downloading the package will any of the other
+//               downloading the package with any of the other
 //               instances that use the same package.
 ////////////////////////////////////////////////////////////////////
 void P3DInstance::
 add_package(P3DPackage *package) {
-  assert(find(_packages.begin(), _packages.end(), package) == _packages.end());
+  if (find(_packages.begin(), _packages.end(), package) != _packages.end()) {
+    // Already have this package.
+    return;
+  }
 
   _packages.push_back(package);
   package->set_instance(this);
+
+  if (package->get_package_name() == "panda3d") {
+    _panda3d = package;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DInstance::get_packages_ready
+//       Access: Public
+//  Description: Returns true if all of the packages required by the
+//               instance (as specified in previous calls to
+//               add_package()) have been fully downloaded and are
+//               ready to run, or false if one or more of them still
+//               requires downloading.
+////////////////////////////////////////////////////////////////////
+bool P3DInstance::
+get_packages_ready() const {
+  Packages::const_iterator pi;
+  for (pi = _packages.begin(); pi != _packages.end(); ++pi) {
+    if (!(*pi)->get_ready()) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DInstance::get_packages_failed
+//       Access: Public
+//  Description: Returns true if any of the packages required by the
+//               instance have failed to download (and thus we will
+//               never be ready).
+////////////////////////////////////////////////////////////////////
+bool P3DInstance::
+get_packages_failed() const {
+  Packages::const_iterator pi;
+  for (pi = _packages.begin(); pi != _packages.end(); ++pi) {
+    if ((*pi)->get_failed()) {
+      return true;
+    }
+  }
+
+  return false;
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -700,6 +759,37 @@ make_xml() {
   return xinstance;
 }
 
+////////////////////////////////////////////////////////////////////
+//     Function: P3DInstance::scan_app_desc_file
+//       Access: Private
+//  Description: Reads the p3d_info.xml file at instance startup, to
+//               determine the set of required packages and so forth.
+////////////////////////////////////////////////////////////////////
+void P3DInstance::
+scan_app_desc_file(TiXmlDocument *doc) {
+  TiXmlElement *xpackage = doc->FirstChildElement("package");
+  if (xpackage == NULL) {
+    return;
+  }
+
+  P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
+
+  TiXmlElement *xrequires = xpackage->FirstChildElement("requires");
+  while (xrequires != NULL) {
+    const char *name = xrequires->Attribute("name");
+    if (name != NULL) {
+      const char *version = xrequires->Attribute("version");
+      if (version == NULL) {
+        version = "";
+      }
+      P3DPackage *package = inst_mgr->get_package(name, version);
+      add_package(package);
+    }
+
+    xrequires = xrequires->NextSiblingElement("requires");
+  }
+}
+
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DInstance::send_browser_script_object
 //       Access: Private
@@ -1010,31 +1100,42 @@ start_package_download(P3DPackage *package) {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DInstance::install_progress
+//     Function: P3DInstance::report_package_progress
 //       Access: Private
-//  Description: Notified as the _panda3d package is downloaded.
+//  Description: Notified as a required package is downloaded.
 ////////////////////////////////////////////////////////////////////
 void P3DInstance::
-install_progress(P3DPackage *package, double progress) {
+report_package_progress(P3DPackage *package, double progress) {
   if (_splash_window != NULL) {
-    _splash_window->set_install_label("Installing Panda3D");
+    if (!package->get_package_display_name().empty()) {
+      _splash_window->set_install_label("Installing " + package->get_package_display_name());
+    }
     _splash_window->set_install_progress(progress);
   }
   _panda_script_object->set_float_property("downloadProgress", progress);
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DInstance::package_ready
+//     Function: P3DInstance::report_package_done
 //       Access: Private
-//  Description: Notified when the package is fully downloaded.
+//  Description: Notified when a required package is fully downloaded,
+//               or failed.
 ////////////////////////////////////////////////////////////////////
 void P3DInstance::
-package_ready(P3DPackage *package, bool success) {
+report_package_done(P3DPackage *package, bool success) {
   if (success) {
-    install_progress(package, 1.0);
-    _panda_script_object->set_bool_property("downloadComplete", true);
-    _panda_script_object->set_string_property("status", "starting");
-    send_notify("ondownloadcomplete");
+    report_package_progress(package, 1.0);
+
+    if (get_packages_ready()) {
+      _panda_script_object->set_bool_property("downloadComplete", true);
+      _panda_script_object->set_string_property("status", "starting");
+      send_notify("ondownloadcomplete");
+
+      // Notify the session also.
+      if (_session != NULL) {
+        _session->report_packages_done(this, success);
+      }
+    }
   }
 }
 

+ 11 - 2
direct/src/plugin/p3dInstance.h

@@ -80,6 +80,8 @@ public:
   inline P3D_request_ready_func *get_request_ready_func() const;
 
   void add_package(P3DPackage *package);
+  bool get_packages_ready() const;
+  bool get_packages_failed() const;
   
   void start_download(P3DDownload *download);
   inline bool is_started() const;
@@ -100,6 +102,8 @@ private:
     P3DInstance *_inst;
   };
 
+  void scan_app_desc_file(TiXmlDocument *doc);
+
   void send_browser_script_object();
   P3D_request *make_p3d_request(TiXmlElement *xrequest);
   void handle_notify_request(const string &message);
@@ -108,8 +112,8 @@ private:
                              bool needs_response, int unique_id);
   void make_splash_window();
   void start_package_download(P3DPackage *package);
-  void install_progress(P3DPackage *package, double progress);
-  void package_ready(P3DPackage *package, bool success);
+  void report_package_progress(P3DPackage *package, double progress);
+  void report_package_done(P3DPackage *package, bool progress);
 
   void paint_window();
   void add_modifier_flags(unsigned int &swb_flags, int modifiers);
@@ -157,6 +161,10 @@ private:
   typedef vector<P3DPackage *> Packages;
   Packages _packages;
 
+  // We keep the _panda3d pointer separately because it's so
+  // important, but it's in the above vector also.
+  P3DPackage *_panda3d;  
+
   typedef map<int, P3DDownload *> Downloads;
   Downloads _downloads;
 
@@ -175,6 +183,7 @@ private:
   friend class P3DSession;
   friend class SplashDownload;
   friend class P3DWindowParams;
+  friend class P3DPackage;
 };
 
 #include "p3dInstance.I"

+ 3 - 4
direct/src/plugin/p3dInstanceManager.cxx

@@ -352,8 +352,7 @@ wait_request() {
 //               package.
 ////////////////////////////////////////////////////////////////////
 P3DPackage *P3DInstanceManager::
-get_package(const string &package_name, const string &package_version, 
-            const string &package_display_name) {
+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);
@@ -361,8 +360,8 @@ get_package(const string &package_name, const string &package_version,
     return (*pi).second;
   }
 
-  P3DPackage *package = new P3DPackage(package_name, package_platform, 
-                                       package_version, package_display_name);
+  P3DPackage *package = 
+    new P3DPackage(package_name, package_platform, package_version);
   bool inserted = _packages.insert(Packages::value_type(key, package)).second;
   assert(inserted);
 

+ 1 - 2
direct/src/plugin/p3dInstanceManager.h

@@ -66,8 +66,7 @@ public:
   void wait_request();
 
   P3DPackage *get_package(const string &package_name, 
-                          const string &package_version,
-                          const string &package_display_name);
+                          const string &package_version);
   bool get_package_desc_file(FileSpec &desc_file, 
                              const string &package_name,
                              const string &package_version);

+ 132 - 64
direct/src/plugin/p3dMultifileReader.cxx

@@ -48,11 +48,12 @@ P3DMultifileReader() {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DMultifileReader::extract
+//     Function: P3DMultifileReader::extract_all
 //       Access: Public
-//  Description: Reads the named multifile, and extracts all files
-//               within it to the indicated directory.  Returns true
-//               on success, false on failure.
+//  Description: Reads the named multifile, and extracts all the
+//               expected extractable components within it to the
+//               indicated directory.  Returns true on success, false
+//               on failure.
 //
 //               The parameters package, start_progress, and
 //               progress_size are provided to make the appropriate
@@ -60,50 +61,26 @@ P3DMultifileReader() {
 //               during this operation.
 ////////////////////////////////////////////////////////////////////
 bool P3DMultifileReader::
-extract(const string &pathname, const string &to_dir,
-        P3DPackage *package, double start_progress, double progress_size) {
-  _subfiles.clear();
-
-  _in.open(pathname.c_str(), ios::in | ios::binary);
-  if (!_in) {
-    nout << "Couldn't open " << pathname << "\n";
+extract_all(const string &pathname, const string &to_dir,
+            P3DPackage *package, double start_progress, double progress_size) {
+  if (!read_header(pathname)) {
     return false;
   }
 
-  for (size_t i = 0; i < _header_size; ++i) {
-    int ch = _in.get();
-    if (ch != _header[i]) {
-      nout << "Failed header check: " << pathname << "\n";
-      return false;
-    }
-  }
-
-  unsigned int major = read_uint16();
-  unsigned int minor = read_uint16();
-  if (major != _current_major_ver || minor != _current_minor_ver) {
-    nout << "Incompatible multifile version: " << pathname << "\n";
-    return false;
-  }
-
-  unsigned int scale = read_uint32();
-  if (scale != 1) {
-    nout << "Unsupported scale factor in " << pathname << "\n";
-    return false;
-  }
-
-  // We don't care about the overall timestamp.
-  read_uint32();
-
-  if (!read_index()) {
-    nout << "Error reading multifile index\n";
-    return false;
+  // Now walk through all of the files, and extract only the ones we
+  // expect to encounter.
+  size_t num_processed = 0;
+  size_t num_expected = _subfiles.size();
+  if (package != NULL) {
+    num_expected = package->_extracts.size();
   }
 
-  // Now walk through all of the files.
-  size_t num_processed = 0;
   Subfiles::iterator si;
   for (si = _subfiles.begin(); si != _subfiles.end(); ++si) {
     const Subfile &s = (*si);
+    if (package != NULL && !package->is_extractable(s._filename)) {
+      continue;
+    }
 
     string output_pathname = to_dir + "/" + s._filename;
     if (!mkfile_complete(output_pathname, nout)) {
@@ -116,23 +93,7 @@ extract(const string &pathname, const string &to_dir,
       return false;
     }
 
-    _in.seekg(s._start);
-
-    static const size_t buffer_size = 1024;
-    char buffer[buffer_size];
-
-    size_t remaining_data = s._length;
-    _in.read(buffer, min(buffer_size, remaining_data));
-    size_t count = _in.gcount();
-    while (count != 0) {
-      remaining_data -= count;
-      out.write(buffer, count);
-      _in.read(buffer, min(buffer_size, remaining_data));
-      count = _in.gcount();
-    }
-
-    if (remaining_data != 0) {
-      nout << "Unable to extract " << s._filename << "\n";
+    if (!extract_subfile(out, s)) {
       return false;
     }
 
@@ -146,7 +107,7 @@ extract(const string &pathname, const string &to_dir,
 
     ++num_processed;
     if (package != NULL) {
-      double progress = (double)num_processed / (double)_subfiles.size();
+      double progress = (double)num_processed / (double)num_expected;
       package->report_progress(start_progress + progress * progress_size);
     }
   }
@@ -155,8 +116,83 @@ extract(const string &pathname, const string &to_dir,
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DMultifileReader::read_index
+//     Function: P3DMultifileReader::extract_one
 //       Access: Public
+//  Description: Reads the named multifile, and extracts only the
+//               named component to the indicated stream.  Returns
+//               true on success, false on failure.
+////////////////////////////////////////////////////////////////////
+bool P3DMultifileReader::
+extract_one(const string &pathname, ostream &out, const string &filename) {
+  if (!read_header(pathname)) {
+    return false;
+  }
+
+  // Look for the named component.
+  Subfiles::iterator si;
+  for (si = _subfiles.begin(); si != _subfiles.end(); ++si) {
+    const Subfile &s = (*si);
+    if (s._filename == filename) {
+      return extract_subfile(out, s);
+    }
+  }
+
+  nout << "Could not extract " << filename << ": not found.\n";
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DMultifileReader::read_header
+//       Access: Private
+//  Description: Opens the named multifile and reads the header
+//               information and index, returning true on success,
+//               false on failure.
+////////////////////////////////////////////////////////////////////
+bool P3DMultifileReader::
+read_header(const string &pathname) {
+  _subfiles.clear();
+
+  _in.open(pathname.c_str(), ios::in | ios::binary);
+  if (!_in) {
+    nout << "Couldn't open " << pathname << "\n";
+    return false;
+  }
+
+  for (size_t i = 0; i < _header_size; ++i) {
+    int ch = _in.get();
+    if (ch != _header[i]) {
+      nout << "Failed header check: " << pathname << "\n";
+      return false;
+    }
+  }
+
+  unsigned int major = read_uint16();
+  unsigned int minor = read_uint16();
+  if (major != _current_major_ver || minor != _current_minor_ver) {
+    nout << "Incompatible multifile version: " << pathname << "\n";
+    return false;
+  }
+
+  unsigned int scale = read_uint32();
+  if (scale != 1) {
+    nout << "Unsupported scale factor in " << pathname << "\n";
+    return false;
+  }
+
+  // We don't care about the overall timestamp.
+  read_uint32();
+
+  if (!read_index()) {
+    nout << "Error reading multifile index\n";
+    return false;
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DMultifileReader::read_index
+//       Access: Private
 //  Description: Assuming the file stream is positioned at the first
 //               record, reads all of the records into the _subfiles
 //               list.  Returns true on success, false on failure.
@@ -172,10 +208,6 @@ read_index() {
     s._start = read_uint32();
     s._length = read_uint32();
     unsigned int flags = read_uint16();
-    if (flags != 0) {
-      nout << "Unsupported per-subfile options in multifile\n";
-      return false;
-    }
     s._timestamp = read_uint32();
     size_t name_length = read_uint16();
     char *buffer = new char[name_length];
@@ -189,7 +221,10 @@ read_index() {
     s._filename = string(buffer, name_length);
     delete[] buffer;
 
-    _subfiles.push_back(s);
+    if (flags == 0) {
+      // We can only support subfiles with no particular flags set.
+      _subfiles.push_back(s);
+    }
 
     _in.seekg(next_entry);
     next_entry = read_uint32();
@@ -200,3 +235,36 @@ read_index() {
 
   return true;
 }
+
+////////////////////////////////////////////////////////////////////
+//     Function: P3DMultifileReader::extract_subfile
+//       Access: Private
+//  Description: Extracts the indicated subfile and writes it to the
+//               indicated stream.  Returns true on success, false on
+//               failure.
+////////////////////////////////////////////////////////////////////
+bool P3DMultifileReader::
+extract_subfile(ostream &out, const Subfile &s) {
+  _in.seekg(s._start);
+
+  static const size_t buffer_size = 1024;
+  char buffer[buffer_size];
+  
+  size_t remaining_data = s._length;
+  _in.read(buffer, min(buffer_size, remaining_data));
+  size_t count = _in.gcount();
+  while (count != 0) {
+    remaining_data -= count;
+    out.write(buffer, count);
+    _in.read(buffer, min(buffer_size, remaining_data));
+    count = _in.gcount();
+  }
+  
+  if (remaining_data != 0) {
+    nout << "Unable to extract " << s._filename << "\n";
+    return false;
+  }
+
+  return true;
+}
+

+ 11 - 3
direct/src/plugin/p3dMultifileReader.h

@@ -31,12 +31,20 @@ class P3DMultifileReader {
 public:
   P3DMultifileReader();
 
-  bool extract(const string &pathname, const string &to_dir,
-               P3DPackage *package, double start_progress, 
-               double progress_size);
+  bool extract_all(const string &pathname, const string &to_dir,
+                   P3DPackage *package, double start_progress, 
+                   double progress_size);
+
+  bool extract_one(const string &pathname, ostream &out, 
+                   const string &filename);
 
 private:
+  class Subfile;
+
+  bool read_header(const string &pathname);
   bool read_index();
+  bool extract_subfile(ostream &out, const Subfile &s);
+
   inline unsigned int read_uint16();
   inline unsigned int read_uint32();
 

+ 45 - 94
direct/src/plugin/p3dPackage.cxx

@@ -37,12 +37,10 @@ static const double extract_portion = 0.05;
 P3DPackage::
 P3DPackage(const string &package_name,
            const string &package_platform,
-           const string &package_version,
-           const string &package_display_name) :
+           const string &package_version) :
   _package_name(package_name),
   _package_platform(package_platform),
-  _package_version(package_version),
-  _package_display_name(package_display_name)
+  _package_version(package_version)
 {
   P3DInstanceManager *inst_mgr = P3DInstanceManager::get_global_ptr();
 
@@ -87,43 +85,6 @@ P3DPackage::
   assert(_instances.empty());
 }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DPackage::set_callback
-//       Access: Public
-//  Description: Registers a callback on the package.  The callback
-//               object will be notified when the package is ready for
-//               use (or when it has failed to download properly).
-////////////////////////////////////////////////////////////////////
-void P3DPackage::
-set_callback(Callback *callback) {
-  if (_ready || _failed) {
-    // Actually, we're already done.  Signal the callback immediately.
-    callback->package_ready(this, _ready);
-    delete callback;
-  } else {
-    // Bootstrap still in progress.  Save the callback.
-    _callbacks.push_back(callback);
-  }
-}
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DPackage::cancel_callback
-//       Access: Public
-//  Description: Unregisters a particular callback object.  This
-//               object will no longer be notified when the package is
-//               ready.
-////////////////////////////////////////////////////////////////////
-void P3DPackage::
-cancel_callback(Callback *callback) {
-  Callbacks::iterator ci;
-  ci = find(_callbacks.begin(), _callbacks.end(), callback);
-  if (ci != _callbacks.end()) {
-    _callbacks.erase(ci);
-  } else {
-    nout << "Canceling unknown callback on " << _package_fullname << "\n";
-  }
-}
-
 ////////////////////////////////////////////////////////////////////
 //     Function: P3DPackage::set_instance
 //       Access: Public
@@ -334,6 +295,11 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
   TiXmlElement *compressed_archive = NULL;
   
   if (xpackage != NULL) {
+    const char *display_name_cstr = xpackage->Attribute("display_name");
+    if (display_name_cstr != NULL) {
+      _package_display_name = display_name_cstr;
+    }
+
     uncompressed_archive = xpackage->FirstChildElement("uncompressed_archive");
     compressed_archive = xpackage->FirstChildElement("compressed_archive");
   }
@@ -351,27 +317,27 @@ got_desc_file(TiXmlDocument *doc, bool freshly_downloaded) {
   _uncompressed_archive.load_xml(uncompressed_archive);
   _compressed_archive.load_xml(compressed_archive);
 
-  // Now get all the components.
-  _components.clear();
-  TiXmlElement *component = xpackage->FirstChildElement("component");
-  while (component != NULL) {
+  // Now get all the extractable components.
+  _extracts.clear();
+  TiXmlElement *extract = xpackage->FirstChildElement("extract");
+  while (extract != NULL) {
     FileSpec file;
-    file.load_xml(component);
-    _components.push_back(file);
-    component = component->NextSiblingElement("component");
+    file.load_xml(extract);
+    _extracts.push_back(file);
+    extract = extract->NextSiblingElement("extract");
   }
 
-  // Verify all of the components.
-  bool all_components_ok = true;
-  Components::iterator ci;
-  for (ci = _components.begin(); ci != _components.end(); ++ci) {
+  // Verify all of the extracts.
+  bool all_extracts_ok = true;
+  Extracts::iterator ci;
+  for (ci = _extracts.begin(); ci != _extracts.end(); ++ci) {
     if (!(*ci).quick_verify(_package_dir)) {
-      all_components_ok = false;
+      all_extracts_ok = false;
       break;
     }
   }
 
-  if (all_components_ok) {
+  if (all_extracts_ok) {
     // Great, we're ready to begin.
     report_done(true);
 
@@ -589,8 +555,8 @@ void P3DPackage::
 extract_archive() {
   string source_pathname = _package_dir + "/" + _uncompressed_archive.get_filename();
   P3DMultifileReader reader;
-  if (!reader.extract(source_pathname, _package_dir,
-                      this, download_portion + uncompress_portion, extract_portion)) {
+  if (!reader.extract_all(source_pathname, _package_dir,
+                          this, download_portion + uncompress_portion, extract_portion)) {
     nout << "Failure extracting " << _uncompressed_archive.get_filename()
          << "\n";
     report_done(false);
@@ -604,13 +570,13 @@ extract_archive() {
 //     Function: P3DPackage::report_progress
 //       Access: Private
 //  Description: Reports the indicated install progress to all
-//               listening callbacks.
+//               interested instances.
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 report_progress(double progress) {
-  Callbacks::iterator ci;
-  for (ci = _callbacks.begin(); ci != _callbacks.end(); ++ci) {
-    (*ci)->install_progress(this, progress);
+  Instances::iterator ii;
+  for (ii = _instances.begin(); ii != _instances.end(); ++ii) {
+    (*ii)->report_package_progress(this, progress);
   }
 }
 
@@ -618,8 +584,8 @@ report_progress(double progress) {
 //     Function: P3DPackage::report_done
 //       Access: Private
 //  Description: Transitions the package to "ready" or "failure"
-//               state, and reports this change to all the listening
-//               callbacks.
+//               state, and reports this change to all the interested
+//               instances.
 ////////////////////////////////////////////////////////////////////
 void P3DPackage::
 report_done(bool success) {
@@ -631,16 +597,10 @@ report_done(bool success) {
     _failed = true;
   }
 
-  Callbacks orig_callbacks;
-  orig_callbacks.swap(_callbacks);
-  Callbacks::iterator ci;
-  for (ci = orig_callbacks.begin(); ci != orig_callbacks.end(); ++ci) {
-    (*ci)->package_ready(this, _ready);
-    delete (*ci);
+  Instances::iterator ii;
+  for (ii = _instances.begin(); ii != _instances.end(); ++ii) {
+    (*ii)->report_package_done(this, success);
   }
-
-  // We shouldn't have added any more callbacks during the above loop.
-  assert(_callbacks.empty());
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -674,30 +634,21 @@ start_download(P3DPackage::DownloadType dtype, const string &url,
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DPackage::Callback::Destructor
-//       Access: Public, Virtual
-//  Description: 
-////////////////////////////////////////////////////////////////////
-P3DPackage::Callback::
-~Callback() {
-}
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DPackage::Callback::install_progress
-//       Access: Public, Virtual
-//  Description: 
-////////////////////////////////////////////////////////////////////
-void P3DPackage::Callback::
-install_progress(P3DPackage *package, double progress) {
-}
+//     Function: P3DPackage::is_extractable
+//       Access: Private
+//  Description: Returns true if the name file is on the extract list,
+//               false otherwise.
+////////////////////////////////////////////////////////////////////
+bool P3DPackage::
+is_extractable(const string &filename) const {
+  Extracts::const_iterator ci;
+  for (ci = _extracts.begin(); ci != _extracts.end(); ++ci) {
+    if ((*ci).get_filename() == filename) {
+      return true;
+    }
+  }
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DPackage::Callback::package_ready
-//       Access: Public, Virtual
-//  Description: 
-////////////////////////////////////////////////////////////////////
-void P3DPackage::Callback::
-package_ready(P3DPackage *package, bool success) {
+  return false;
 }
 
 ////////////////////////////////////////////////////////////////////

+ 5 - 17
direct/src/plugin/p3dPackage.h

@@ -38,17 +38,9 @@ class P3DPackage {
 public:
   P3DPackage(const string &package_name, 
              const string &package_platform,
-             const string &package_version,
-             const string &package_display_name);
+             const string &package_version);
   ~P3DPackage();
 
-  class Callback {
-  public:
-    virtual ~Callback();
-    virtual void install_progress(P3DPackage *package, double progress);
-    virtual void package_ready(P3DPackage *package, bool success);
-  };
-
   inline bool get_ready() const;
   inline bool get_failed() const;
   inline const string &get_package_dir() const;
@@ -56,9 +48,6 @@ public:
   inline const string &get_package_version() const;
   inline const string &get_package_display_name() const;
 
-  void set_callback(Callback *callback);
-  void cancel_callback(Callback *callback);
-
   void set_instance(P3DInstance *inst);
   void cancel_instance(P3DInstance *inst);
 
@@ -102,6 +91,8 @@ private:
   void start_download(DownloadType dtype, const string &url, 
                       const string &pathname, bool allow_partial);
 
+  bool is_extractable(const string &filename) const;
+
 private:
   string _package_name;
   string _package_version;
@@ -120,17 +111,14 @@ private:
   Download *_active_download;
   bool _partial_download;
 
-  typedef vector<Callback *> Callbacks;
-  Callbacks _callbacks;
-
   typedef vector<P3DInstance *> Instances;
   Instances _instances;
 
   FileSpec _compressed_archive;
   FileSpec _uncompressed_archive;
 
-  typedef vector<FileSpec> Components;
-  Components _components;
+  typedef vector<FileSpec> Extracts;
+  Extracts _extracts;
 
   friend class Download;
   friend class P3DMultifileReader;

+ 22 - 94
direct/src/plugin/p3dSession.cxx

@@ -65,14 +65,8 @@ P3DSession(P3DInstance *inst) {
   _output_filename = "/tmp/panda3d.3.log";
 #endif  // _WIN32
 
-  _panda3d_callback = NULL;
-
   INIT_LOCK(_instances_lock);
   INIT_THREAD(_read_thread);
-
-  _panda3d = inst_mgr->get_package("panda3d", "dev", "Panda3D");
-  _python_root_dir = _panda3d->get_package_dir();
-  inst->add_package(_panda3d);
 }
 
 
@@ -95,12 +89,6 @@ P3DSession::
 ////////////////////////////////////////////////////////////////////
 void P3DSession::
 shutdown() {
-  if (_panda3d_callback != NULL) {
-    _panda3d->cancel_callback(_panda3d_callback);
-    delete _panda3d_callback;
-    _panda3d_callback = NULL;
-  }
-
   if (_p3dpython_running) {
     // Tell the process we're going away.
     TiXmlDocument doc;
@@ -226,16 +214,14 @@ start_instance(P3DInstance *inst) {
   send_command(doc);
   inst->send_browser_script_object();
 
-  if (_panda3d->get_ready()) {
+  if (inst->get_packages_ready()) {
     // If it's ready immediately, go ahead and start.
-    start_p3dpython();
+    start_p3dpython(inst);
+
   } else {
-    // Otherwise, set a callback, so we'll know when it is ready.
-    if (_panda3d_callback == NULL) {
-      _panda3d_callback = new PackageCallback(this);
-      _panda3d->set_callback(_panda3d_callback);
-    }
-    inst->start_package_download(_panda3d);
+    // Otherwise, wait for the instance to download itself.  We'll
+    // automatically get a callback to report_packages_done() when
+    // it's done.
   }
 }
 
@@ -644,53 +630,36 @@ drop_p3dobj(int object_id) {
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::install_progress
+//     Function: P3DSession::report_packages_done
 //       Access: Private
-//  Description: Notified as the _panda3d package is downloaded.
+//  Description: Notified when a child instance is fully downloaded.
 ////////////////////////////////////////////////////////////////////
 void P3DSession::
-install_progress(P3DPackage *package, double progress) {
-  Instances::iterator ii;
-  for (ii = _instances.begin(); ii != _instances.end(); ++ii) {
-    P3DInstance *inst = (*ii).second;
-    inst->install_progress(package, progress);
+report_packages_done(P3DInstance *inst, bool success) {
+  if (success) {
+    start_p3dpython(inst);
   }
 }
 
 ////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::package_ready
+//     Function: P3DSession::start_p3dpython
 //       Access: Private
-//  Description: Notified when the package is fully downloaded.
+//  Description: Starts Python running in a child process.
 ////////////////////////////////////////////////////////////////////
 void P3DSession::
-package_ready(P3DPackage *package, bool success) {
-  _panda3d_callback = NULL;
-
-  Instances::iterator ii;
-  for (ii = _instances.begin(); ii != _instances.end(); ++ii) {
-    P3DInstance *inst = (*ii).second;
-    inst->package_ready(package, success);
+start_p3dpython(P3DInstance *inst) {
+  if (_p3dpython_running) {
+    // Already started.
+    return;
   }
 
-  if (package == _panda3d) {
-    if (success) {
-      start_p3dpython();
-    } else {
-      nout << "Failed to install " << package->get_package_name()
-           << "_" << package->get_package_version() << "\n";
-    }
-  } else {
-    nout << "Unexpected panda3d package: " << package << "\n";
+  if (inst->_panda3d == NULL) {
+    nout << "Couldn't start Python: no panda3d dependency.\n";
+    return;
   }
-}
 
-////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::start_p3dpython
-//       Access: Private
-//  Description: Starts Python running in a child process.
-////////////////////////////////////////////////////////////////////
-void P3DSession::
-start_p3dpython() {
+  _python_root_dir = inst->_panda3d->get_package_dir();
+
   string p3dpython = P3D_PLUGIN_P3DPYTHON;
   if (p3dpython.empty()) {
     p3dpython = _python_root_dir + "/p3dpython";
@@ -1116,44 +1085,3 @@ posix_create_process(const string &program, const string &start_dir,
   return child;
 }
 #endif  // _WIN32
-
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::PackageCallback::Constructor
-//       Access: Public
-//  Description: 
-////////////////////////////////////////////////////////////////////
-P3DSession::PackageCallback::
-PackageCallback(P3DSession *session) :
-  _session(session)
-{
-}
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::PackageCallback::install_progress
-//       Access: Public, Virtual
-//  Description: This callback is received during the download process
-//               to inform us how much has been installed so far.
-////////////////////////////////////////////////////////////////////
-void P3DSession::PackageCallback::
-install_progress(P3DPackage *package, double progress) {
-  if (this == _session->_panda3d_callback) {
-    _session->install_progress(package, progress);
-  } else {
-    nout << "Unexpected callback for P3DSession\n";
-  }
-}
-
-////////////////////////////////////////////////////////////////////
-//     Function: P3DSession::PackageCallback::package_ready
-//       Access: Public, Virtual
-//  Description: 
-////////////////////////////////////////////////////////////////////
-void P3DSession::PackageCallback::
-package_ready(P3DPackage *package, bool success) {
-  if (this == _session->_panda3d_callback) {
-    _session->package_ready(package, success);
-  } else {
-    nout << "Unexpected callback for P3DSession\n";
-  }
-}

+ 3 - 17
direct/src/plugin/p3dSession.h

@@ -61,9 +61,8 @@ public:
   void drop_p3dobj(int object_id);
 
 private:
-  void install_progress(P3DPackage *package, double progress);
-  void package_ready(P3DPackage *package, bool success);
-  void start_p3dpython();
+  void report_packages_done(P3DInstance *inst, bool success);
+  void start_p3dpython(P3DInstance *inst);
 
   void spawn_read_thread();
   void join_read_thread();
@@ -87,23 +86,11 @@ private:
                        HandleStream &pipe_read, HandleStream &pipe_write);
 #endif
 
-  class PackageCallback : public P3DPackage::Callback {
-  public:
-    PackageCallback(P3DSession *session);
-    
-    virtual void install_progress(P3DPackage *package, double progress);
-    virtual void package_ready(P3DPackage *package, bool success);
-    
-  protected:
-    P3DSession *_session;
-  };
-
 private:
   int _session_id;
   string _session_key;
   string _python_version;
   string _output_filename;
-
   string _python_root_dir;
 
   typedef map<int, P3DInstance *> Instances;
@@ -123,7 +110,6 @@ private:
   SentObjects _sent_objects;
 
   P3DPackage *_panda3d;
-  PackageCallback *_panda3d_callback;
 
   // Members for communicating with the p3dpython child process.
 #ifdef _WIN32
@@ -146,7 +132,7 @@ private:
   bool _read_thread_continue;
   THREAD _read_thread;
 
-  friend class PackageCallback;
+  friend class P3DInstance;
 };
 
 #include "p3dSession.I"

+ 222 - 95
direct/src/showutil/Packager.py

@@ -8,6 +8,7 @@ import os
 import glob
 import marshal
 import new
+import string
 from direct.showbase import Loader
 from direct.showutil import FreezeTool
 from direct.directnotify.DirectNotifyGlobal import *
@@ -29,7 +30,7 @@ class Packager:
 
     class PackFile:
         def __init__(self, filename, newName = None, deleteTemp = False,
-                     extract = False):
+                     extract = None):
             assert isinstance(filename, Filename)
             self.filename = filename
             self.newName = newName
@@ -43,6 +44,7 @@ class Packager:
             self.version = None
             self.platform = None
             self.p3dApplication = False
+            self.displayName = None
             self.files = []
             self.compressionLevel = 0
             self.importedMapsDir = 'imported_maps'
@@ -208,13 +210,14 @@ class Packager:
             if self.version:
                 xpackage.SetAttribute('version', self.version)
 
+            if self.displayName:
+                xpackage.SetAttribute('display_name', self.displayName)
+
             xpackage.SetAttribute('main_module', self.mainModule)
 
             for package in self.requires:
                 xrequires = TiXmlElement('requires')
                 xrequires.SetAttribute('name', package.packageName)
-                if package.platform:
-                    xrequires.SetAttribute('platform', package.platform)
                 if package.version:
                     xrequires.SetAttribute('version', package.version)
                 xpackage.InsertEndChild(xrequires)
@@ -225,7 +228,11 @@ class Packager:
             # can add it to the multifile.
             filename = Filename.temporary('', 'p3d_', '.xml')
             doc.SaveFile(filename.toOsSpecific())
-            self.multifile.addSubfile('p3d_info.xml', filename, self.compressionLevel)
+
+            # It's important not to compress this file: the core API
+            # runtime can't decode compressed subfiles.
+            self.multifile.addSubfile('p3d_info.xml', filename, 0)
+            
             self.multifile.flush()
             filename.unlink()
             
@@ -252,6 +259,9 @@ class Packager:
             if self.version:
                 xpackage.SetAttribute('version', self.version)
 
+            if self.displayName:
+                xpackage.SetAttribute('display_name', self.displayName)
+
             xuncompressedArchive = self.getFileSpec(
                 'uncompressed_archive', self.packageFullpath,
                 self.packageBasename)
@@ -485,13 +495,13 @@ class Packager:
             # not further compressible.
             self.addComponent(file, compressible = False)
 
-        def addComponent(self, file, compressible = True, extract = False):
+        def addComponent(self, file, compressible = True, extract = None):
             ext = Filename(file.newName).getExtension()
             if ext in self.packager.uncompressibleExtensions:
                 compressible = False
 
             extract = file.extract
-            if ext in self.packager.extractExtensions:
+            if extract is None and ext in self.packager.extractExtensions:
                 extract = True
 
             if ext in self.packager.platformSpecificExtensions:
@@ -599,7 +609,7 @@ class Packager:
         self.musicManager = None
 
         # This is filled in during readPackageDef().
-        self.packageList = None
+        self.packageList = []
 
         # A table of all known packages by name.
         self.packages = {}
@@ -636,54 +646,152 @@ class Packager:
 ##         # created there
 ##         os.chdir(self.persistDir.toOsSpecific())
 
+    def __expandVariable(self, line, p):
+        """ Given that line[p] is a dollar sign beginning a variable
+        reference, advances p to the first dollar sign following the
+        reference, and looks up the variable referenced.
+
+        Returns (value, p) where value is the value of the named
+        variable, and p is the first character following the variable
+        reference. """
+
+        p += 1
+        if p >= len(line):
+            return '', p
+        
+        var = ''
+        if line[p] == '{':
+            # Curly braces exactly delimit the variable name.
+            p += 1
+            while p < len(line) and line[p] != '}':
+                var += line[p]
+                p += 1
+        else:
+            # Otherwise, a string of alphanumeric characters,
+            # including underscore, delimits the variable name.
+            var += line[p]
+            p += 1
+            while p < len(line) and (line[p] in string.letters or line[p] in string.digits or line[p] == '_'):
+                var += line[p]
+                p += 1
+
+        return ExecutionEnvironment.getEnvironmentVariable(var), p
+        
+
+    def __splitLine(self, line):
+        """ Separates the indicated line into words at whitespace.
+        Quotation marks and escape characters protect spaces.  This is
+        designed to be similar to the algorithm employed by the Unix
+        shell. """
+
+        words = []
+
+        p = 0
+        while p < len(line):
+            if line[p] == '#':
+                # A word that begins with a hash mark indicates an
+                # inline comment, and the end of the parsing.
+                break
+                
+            # Scan to the end of the word.
+            word = ''
+            while p < len(line) and line[p] not in string.whitespace:
+                if line[p] == '\\':
+                    # Output an escaped character.
+                    p += 1
+                    if p < len(line):
+                        word += line[p]
+                        p += 1
+
+                elif line[p] == '$':
+                    # Output a variable reference.
+                    expand, p = self.__expandVariable(line, p)
+                    word += expand
+
+                elif line[p] == '"':
+                    # Output a double-quoted string.
+                    p += 1
+                    while p < len(line) and line[p] != '"':
+                        if line[p] == '\\':
+                            # Output an escaped character.
+                            p += 1
+                            if p < len(line):
+                                word += line[p]
+                                p += 1
+                        elif line[p] == '$':
+                            # Output a variable reference.
+                            expand, p = self.__expandVariable(line, p)
+                            word += expand
+                        else:
+                            word += line[p]
+                            p += 1
+
+                elif line[p] == "'":
+                    # Output a single-quoted string.  Escape
+                    # characters and dollar signs within single quotes
+                    # are not special.
+                    p += 1
+                    while p < len(line) and line[p] != "'":
+                        word += line[p]
+                        p += 1
+
+                else:
+                    # Output a single character.
+                    word += line[p]
+                    p += 1
+
+            words.append(word)
+
+            # Scan to the beginning of the next word.
+            while p < len(line) and line[p] in string.whitespace:
+                p += 1
+
+        return words
+
+    def __getNextLine(self, file):
+        """ Extracts the next line from the input file, and splits it
+        into words.  Returns the list of words, or None at end of
+        file. """
+
+        line = file.readline()
+        while line:
+            line = line.strip()
+            if not line:
+                # Skip the line, it was just a blank line
+                pass
+            
+            elif line[0] == '#':
+                # Eat python-style comments.
+                pass
+
+            else:
+                return self.__splitLine(line)
+
+            line = file.readline()
+
+        # End of file.
+        return None
+
     def readPackageDef(self, packageDef):
         """ Reads the lines in the .pdef file named by packageDef and
         dispatches to the appropriate handler method for each
         line.  Returns the list of package files."""
 
-        assert self.packageList is None
         self.packageList = []
 
         self.notify.info('Reading %s' % (packageDef))
         file = open(packageDef.toOsSpecific())
-        lines = file.readlines()
-        file.close()
-
-        lineNum = [0]
-        def getNextLine(lineNum = lineNum):
-            """
-            Read in the next line of the packageDef
-            """
-            while lineNum[0] < len(lines):
-                line = lines[lineNum[0]].strip()
-                lineNum[0] += 1
-                if not line:
-                    # Skip the line, it was just a blank line
-                    pass
-                elif line[0] == '#':
-                    # Eat python-style comments.
-                    pass
-                else:
-                    # Remove any trailing comment.
-                    hash = line.find(' #')
-                    if hash != -1:
-                        line = line[:hash].strip()
-                    # Return the line as an array split at whitespace.
-                    return line.split()
-
-            # EOF.
-            return None
 
         # Now start parsing the packageDef lines
         try:
-            lineList = getNextLine()
-            while lineList:
-                command = lineList[0]
+            words = self.__getNextLine(file)
+            while words:
+                command = words[0]
                 try:
                     methodName = 'parse_%s' % (command)
                     method = getattr(self, methodName, None)
                     if method:
-                        method(lineList)
+                        method(words)
 
                     else:
                         message = 'Unknown command %s' % (command)
@@ -695,7 +803,7 @@ class Packager:
                     message = '%s command encounted outside of package specification' %(command)
                     raise OutsideOfPackageError, message
 
-                lineList = getNextLine()
+                words = self.__getNextLine(file)
 
         except PackagerError:
             # Append the line number and file name to the exception
@@ -705,17 +813,17 @@ class Packager:
             raise
 
         packageList = self.packageList
-        self.packageList = None
+        self.packageList = []
 
         return packageList
 
-    def parse_set(self, lineList):
+    def parse_set(self, words):
         """
         set variable=value
         """
         
         try:
-            command, assign = lineList
+            command, assign = words
         except ValueError:
             raise ArgumentNumber
         
@@ -728,15 +836,15 @@ class Packager:
         value = ExecutionEnvironment.expandString(value.strip())
         ExecutionEnvironment.setEnvironmentVariable(variable, value)
 
-    def parse_begin_package(self, lineList):
+    def parse_begin_package(self, words):
         """
         begin_package packageName [version=v]
         """
 
-        args = self.parseArgs(lineList, ['version'])
+        args = self.__parseArgs(words, ['version'])
 
         try:
-            command, packageName = lineList
+            command, packageName = words
         except ValueError:
             raise ArgumentNumber
 
@@ -744,127 +852,149 @@ class Packager:
 
         self.beginPackage(packageName, version = version, p3dApplication = False)
 
-    def parse_end_package(self, lineList):
+    def parse_end_package(self, words):
         """
         end_package packageName
         """
 
         try:
-            command, packageName = lineList
+            command, packageName = words
         except ValueError:
             raise ArgumentError
 
         self.endPackage(packageName, p3dApplication = False)
 
-    def parse_begin_p3d(self, lineList):
+    def parse_begin_p3d(self, words):
         """
         begin_p3d appName
         """
 
         try:
-            command, packageName = lineList
+            command, packageName = words
         except ValueError:
             raise ArgumentNumber
 
         self.beginPackage(packageName, p3dApplication = True)
 
-    def parse_end_p3d(self, lineList):
+    def parse_end_p3d(self, words):
         """
         end_p3d appName
         """
 
         try:
-            command, packageName = lineList
+            command, packageName = words
         except ValueError:
             raise ArgumentError
 
         self.endPackage(packageName, p3dApplication = True)
 
-    def parse_require(self, lineList):
+    def parse_display_name(self, words):
+        """
+        display_name "name"
+        """
+
+        try:
+            command, displayName = words
+        except ValueError:
+            raise ArgumentError
+
+        if not self.currentPackage:
+            raise OutsideOfPackageError
+
+        self.currentPackage.displayName = displayName
+
+    def parse_require(self, words):
         """
         require packageName [version=v]
         """
 
-        args = self.parseArgs(lineList, ['version'])
+        args = self.__parseArgs(words, ['version'])
 
         try:
-            command, packageName = lineList
+            command, packageName = words
         except ValueError:
             raise ArgumentError
 
         version = args.get('version', None)
         self.require(packageName, version = version)
 
-    def parse_module(self, lineList):
+    def parse_module(self, words):
         """
         module moduleName [newName]
         """
         newName = None
 
         try:
-            if len(lineList) == 2:
-                command, moduleName = lineList
+            if len(words) == 2:
+                command, moduleName = words
             else:
-                command, moduleName, newName = lineList
+                command, moduleName, newName = words
         except ValueError:
             raise ArgumentError
 
         self.module(moduleName, newName = newName)
 
-    def parse_main_module(self, lineList):
+    def parse_main_module(self, words):
         """
         main_module moduleName
         """
 
         try:
-            command, moduleName = lineList
+            command, moduleName = words
         except ValueError:
             raise ArgumentError
 
         self.mainModule(moduleName)
 
-    def parse_freeze_exe(self, lineList):
+    def parse_freeze_exe(self, words):
         """
         freeze_exe path/to/basename
         """
 
         try:
-            command, filename = lineList
+            command, filename = words
         except ValueError:
             raise ArgumentError
 
         self.freeze(filename, compileToExe = True)
 
-    def parse_freeze_dll(self, lineList):
+    def parse_freeze_dll(self, words):
         """
         freeze_dll path/to/basename
         """
 
         try:
-            command, filename = lineList
+            command, filename = words
         except ValueError:
             raise ArgumentError
 
         self.freeze(filename, compileToExe = False)
 
-    def parse_file(self, lineList):
+    def parse_file(self, words):
         """
-        file filename [newNameOrDir]
+        file filename [newNameOrDir] [extract=1]
         """
 
+        args = self.__parseArgs(words, ['extract'])
+
         newNameOrDir = None
 
         try:
-            if len(lineList) == 2:
-                command, filename = lineList
+            if len(words) == 2:
+                command, filename = words
             else:
-                command, filename, newNameOrDir = lineList
+                command, filename, newNameOrDir = words
         except ValueError:
             raise ArgumentError
 
-        self.file(filename, newNameOrDir = newNameOrDir)
+        extract = args.get('extract', None)
+        if extract is not None:
+            extract = int(extract)
+
+        self.file(Filename.fromOsSpecific(filename),
+                  newNameOrDir = newNameOrDir, extract = extract)
 
-    def parse_dir(self, lineList):
+    def parse_dir(self, words):
         """
         dir dirname [newDir]
         """
@@ -872,20 +1002,20 @@ class Packager:
         newDir = None
 
         try:
-            if len(lineList) == 2:
-                command, dirname = lineList
+            if len(words) == 2:
+                command, dirname = words
             else:
-                command, dirname, newDir = lineList
+                command, dirname, newDir = words
         except ValueError:
             raise ArgumentError
 
-        self.dir(dirname, newDir = newDir)
+        self.dir(Filename.fromOsSpecific(dirname), newDir = newDir)
 
-    def parseArgs(self, lineList, argList):
+    def __parseArgs(self, words, argList):
         args = {}
         
-        while len(lineList) > 1:
-            arg = lineList[-1]
+        while len(words) > 1:
+            arg = words[-1]
             if '=' not in arg:
                 return args
 
@@ -901,7 +1031,7 @@ class Packager:
 
             args[parameter] = value
 
-            del lineList[-1]
+            del words[-1]
                 
     
     def beginPackage(self, packageName, version = None, p3dApplication = False):
@@ -1026,11 +1156,11 @@ class Packager:
 
     def require(self, packageName, version = None):
         """ Indicates a dependency on the named package, supplied as
-        name.
+        a name.
 
         Attempts to install this package will implicitly install the
         named package also.  Files already included in the named
-        package will be omitted from this one. """
+        package will be omitted from this one when building it. """
 
         # A special case for the Panda3D package.  We enforce that the
         # version number matches what we've been compiled with.
@@ -1136,14 +1266,13 @@ class Packager:
         package.mainModule = None
 
 
-    def file(self, filename, newNameOrDir = None):
+    def file(self, filename, newNameOrDir = None, extract = None):
         """ Adds the indicated arbitrary file to the current package.
 
         The file is placed in the named directory, or the toplevel
         directory if no directory is specified.
 
-        The filename may include environment variable references and
-        shell globbing characters.
+        The filename may include shell globbing characters.
 
         Certain special behavior is invoked based on the filename
         extension.  For instance, .py files may be automatically
@@ -1166,10 +1295,10 @@ class Packager:
         if not self.currentPackage:
             raise OutsideOfPackageError
 
-        expanded = Filename.expandFrom(filename)
-        files = glob.glob(expanded.toOsSpecific())
+        filename = Filename(filename)
+        files = glob.glob(filename.toOsSpecific())
         if not files:
-            self.notify.warning("No such file: %s" % (expanded))
+            self.notify.warning("No such file: %s" % (filename))
             return
 
         newName = None
@@ -1188,9 +1317,9 @@ class Packager:
             filename = Filename.fromOsSpecific(filename)
             basename = filename.getBasename()
             if newName:
-                self.addFile(filename, newName = newName)
+                self.addFile(filename, newName = newName, extract = extract)
             else:
-                self.addFile(filename, newName = prefix + basename)
+                self.addFile(filename, newName = prefix + basename, extract = extract)
 
     def dir(self, dirname, newDir = None):
 
@@ -1198,8 +1327,6 @@ class Packager:
         package.  The directory hierarchy is walked recursively, and
         all files that match a known extension are added to the package.
 
-        The dirname may include environment variable references.
-
         newDir specifies the directory name within the package which
         the contents of the named directory should be installed to.
         If it is omitted, the contents of the named directory are
@@ -1209,7 +1336,7 @@ class Packager:
         if not self.currentPackage:
             raise OutsideOfPackageError
 
-        dirname = Filename.expandFrom(dirname)
+        dirname = Filename(dirname)
         if not newDir:
             newDir = ''
 
@@ -1242,7 +1369,7 @@ class Packager:
             if ext in self.knownExtensions:
                 self.addFile(filename, newName = newName)
 
-    def addFile(self, filename, newName = None):
+    def addFile(self, filename, newName = None, extract = None):
         """ Adds the named file, giving it the indicated name within
         the package. """
 
@@ -1250,4 +1377,4 @@ class Packager:
             raise OutsideOfPackageError
 
         self.currentPackage.files.append(
-            self.PackFile(filename, newName = newName))
+            self.PackFile(filename, newName = newName, extract = extract))

+ 36 - 19
direct/src/plugin/make_contents.py → direct/src/showutil/make_contents.py

@@ -23,21 +23,37 @@ Options:
 import sys
 import getopt
 import os
-
-import direct
-from pandac.PandaModules import *
-
-from FileSpec import FileSpec
+import md5
 
 class ArgumentError(AttributeError):
     pass
 
+class FileSpec:
+    """ Represents a single file in the directory, and its associated
+    timestamp, size, and md5 hash. """
+    
+    def __init__(self, filename, pathname):
+        self.filename = filename
+        self.pathname = pathname
+
+        s = os.stat(pathname)
+        self.size = s.st_size
+        self.timestamp = s.st_mtime
+
+        m = md5.new()
+        m.update(open(pathname, 'rb').read())
+        self.hash = m.hexdigest()
+
+    def getParams(self):
+        return 'filename="%s" size="%s" timestamp="%s" hash="%s"' % (
+            self.filename, self.size, self.timestamp, self.hash)
+
 class ContentsMaker:
     def __init__(self):
-        self.stageDir = None
+        self.installDir = None
 
     def build(self):
-        if not self.stageDir:
+        if not self.installDir:
             raise ArgumentError, "Stage directory not specified."
 
         self.packages = []
@@ -48,9 +64,9 @@ class ContentsMaker:
 
         # Now write the contents.xml file.
         contentsFileBasename = 'contents.xml'
-        contentsFilePathname = Filename(self.stageDir, contentsFileBasename)
+        contentsFilePathname = os.path.join(self.installDir, contentsFileBasename)
 
-        f = open(contentsFilePathname.toOsSpecific(), 'w')
+        f = open(contentsFilePathname, 'w')
         print >> f, '<?xml version="1.0" ?>'
         print >> f, ''
         print >> f, '<contents>'
@@ -64,7 +80,7 @@ class ContentsMaker:
         """ Walks through all the files in the stage directory and
         looks for the package directory xml files. """
 
-        startDir = self.stageDir.toOsSpecific()
+        startDir = self.installDir
         if startDir.endswith(os.sep):
             startDir = startDir[:-1]
         prefix = startDir + os.sep
@@ -91,14 +107,14 @@ class ContentsMaker:
                 packageName, packageVersion, junk = localpath.split('/')
                 packagePlatform = None
                 file = FileSpec(localpath + xml,
-                                Filename(self.stageDir, localpath + xml))
+                                os.path.join(self.installDir, localpath + xml))
                 print file.filename
                 self.packages.append((packageName, packagePlatform, packageVersion, file))
 
             if localpath.count('/') == 3:
                 packageName, packagePlatform, packageVersion, junk = localpath.split('/')
                 file = FileSpec(localpath + xml,
-                                Filename(self.stageDir, localpath + xml))
+                                os.path.join(self.installDir, localpath + xml))
                 print file.filename
                 self.packages.append((packageName, packagePlatform, packageVersion, file))
         
@@ -107,10 +123,10 @@ def makeContents(args):
     opts, args = getopt.getopt(args, 'd:h')
 
     cm = ContentsMaker()
-    cm.stageDir = Filename('.')
+    cm.installDir = '.'
     for option, value in opts:
         if option == '-d':
-            cm.stageDir = Filename.fromOsSpecific(value)
+            cm.installDir = value
             
         elif option == '-h':
             print __doc__
@@ -119,8 +135,9 @@ def makeContents(args):
     cm.build()
         
 
-try:
-    makeContents(sys.argv[1:])
-except ArgumentError, e:
-    print e.args[0]
-    sys.exit(1)
+if __name__ == '__main__':
+    try:
+        makeContents(sys.argv[1:])
+    except ArgumentError, e:
+        print e.args[0]
+        sys.exit(1)

+ 61 - 295
direct/src/showutil/packp3d.py

@@ -2,11 +2,14 @@
 
 """
 
-This module will pack a Panda application, consisting of a directory
+This command will pack a Panda application, consisting of a directory
 tree of .py files and models, into a p3d file for convenient
 distribution.  The resulting p3d file can be run by the Panda3D
 runtime executable, or by the Panda3D web browser plugin.
 
+Also see ppackage.py, which can be used to build p3d files more
+generally, using a pdef description file.
+
 Usage:
 
   packp3d.py [opts] app.p3d
@@ -14,13 +17,11 @@ Usage:
 Options:
 
   -r application_root
-
      Specify the root directory of the application source; this is a
      directory tree that contains all of your .py files and models.
      If this is omitted, the default is the current directory.
 
   -m main.py
-  
      Names the Python file that begins the application.  This should
      be a file within the root directory. If this is omitted, the
      default is a file named "main.py", or if there is only one Python
@@ -29,313 +30,48 @@ Options:
      (this is preferable to having the module start itself immediately
      upon importing).
 
-  -c [py,pyc,pyo]
+  -s search_dir
+     Additional directories to search for previously-built packages.
+     This option may be repeated as necessary.
 
-     Specifies the compilation mode of python files.  'py' means to
-     leave them as source files, 'pyc' and 'pyo' are equivalent, and
-     mean to compile to byte code.  pyc files will be written if the
-     interpreter is running in normal debug mode, while pyo files will
-     be written if it is running in optimize mode (-O or -OO).
+  -x
+     If this is specified, a version-independent application is built.
+     This stores source py files instead of compiled pyc files, and
+     egg files instead of bam files.  This application can then be run
+     with any version of Panda (provided you are careful not to make
+     any Python or Panda calls that are version-specific).  This is
+     not recommended except for very small, simple applications.
 
 """
 
 import sys
+import os
 import getopt
-import imp
-import marshal
+import glob
 import direct
-from direct.stdpy.file import open
-from direct.showbase import Loader
+from direct.showutil import Packager 
 from pandac.PandaModules import *
 
-vfs = VirtualFileSystem.getGlobalPtr()
-
-class ArgumentError(AttributeError):
+class ArgumentError(StandardError):
     pass
 
-class AppPacker:
-
-    compression_level = 6
-    imported_maps = 'imported_maps'
-
-    # Text files that are copied (and compressed) to the multifile
-    # without processing.
-    text_extensions = [ 'prc' ]
-
-    # Binary files that are copied and compressed without processing.
-    binary_extensions = [ 'ttf', 'wav', 'mid' ]
-
-    # Binary files that are considered uncompressible, and are copied
-    # without compression.
-    uncompressible_extensions = [ 'mp3' ]
-
-    # Specifies how or if python files are compiled.
-    compilation_mode = 'pyc'
-
-    def __init__(self, multifile_name):
-        # Make sure any pre-existing file is removed.
-        Filename(multifile_name).unlink()
-            
-        self.multifile = Multifile()
-        if not self.multifile.openReadWrite(multifile_name):
-            raise ArgumentError, 'Could not open %s for writing' % (multifile_name)
-        self.imported_textures = {}
-
-        # Get the list of filename extensions that are recognized as
-        # image files.
-        self.image_extensions = []
-        for type in PNMFileTypeRegistry.getGlobalPtr().getTypes():
-            self.image_extensions += type.getExtensions()
-
-        self.loader = Loader.Loader(self)
-        
-    def scan(self, root, main):
-        if self.compilation_mode != 'py':
-            if __debug__:
-                self.compilation_mode = 'pyc'
-            else:
-                self.compilation_mode = 'pyo'
-
-        self.root = Filename(root)
-        self.root.makeAbsolute(vfs.getCwd())
-
-        # Check if there is just one .py file.
-        pyFiles = self.findPyFiles(self.root)
-        if main == None:
-            if len(pyFiles) == 1:
-                main = pyFiles[0]
-            else:
-                main = 'main.py'
-        if main not in pyFiles:
-            raise StandardError, 'No file %s in root directory.' % (main)
-        self.main = Filename(self.root, main)
-        
-        self._recurse(self.root)
-
-        self.multifile.repack()
-
-    def findPyFiles(self, dirname):
-        """ Returns a list of Python filenames at the root directory
-        level. """
-        
-        dirList = vfs.scanDirectory(dirname)
-        pyFiles = []
-        for file in dirList:
-            if file.getFilename().getExtension() == 'py':
-                pyFiles.append(file.getFilename().getBasename())
-
-        return pyFiles
-
-    def _recurse(self, filename):
-        dirList = vfs.scanDirectory(filename)
-        if dirList:
-            # It's a directory name.  Recurse.
-            for subfile in dirList:
-                self._recurse(subfile.getFilename())
-            return
-
-        # It's a real file.  Is it something we care about?
-        ext = filename.getExtension().lower()
-        outFilename = filename
-        if ext == 'pz':
-            # Strip off an implicit .pz extension.
-            outFilename = Filename(filename)
-            outFilename.setExtension('')
-            outFilename = Filename(outFilename.cStr())
-            ext = outFilename.getExtension().lower()
-            
-        if ext == 'py':
-            self.addPyFile(filename)
-        elif ext == 'egg':
-            self.addEggFile(filename, outFilename)
-        elif ext == 'bam':
-            self.addBamFile(filename, outFilename)
-        elif ext in self.image_extensions:
-            self.addTexture(filename)
-        elif ext in self.text_extensions:
-            self.addTextFile(filename)
-        elif ext in self.binary_extensions:
-            self.addBinaryFile(filename)
-        elif ext in self.uncompressible_extensions:
-            self.addUncompressibleFile(filename)
-
-    def addPyFile(self, filename):
-        targetFilename = self.makeRelFilename(filename)
-
-        if filename == self.main:
-            # This one is the "main.py"; the starter file.
-            targetFilename = Filename('main.py')
-
-        if self.compilation_mode == 'py':
-            # Add python files as source files.
-            self.multifile.addSubfile(targetFilename.cStr(), filename, self.compression_level)
-        elif self.compilation_mode == 'pyc' or self.compilation_mode == 'pyo':
-            # Compile it to bytecode.
-            targetFilename.setExtension(self.compilation_mode)
-            source = open(filename, 'r').read()
-            if source and source[-1] != '\n':
-                source = source + '\n'
-            code = compile(source, targetFilename.cStr(), 'exec')
-            data = imp.get_magic() + '\0\0\0\0' + marshal.dumps(code)
-
-            stream = StringStream(data)
-            self.multifile.addSubfile(targetFilename.cStr(), stream, self.compression_level)
-            self.multifile.flush()
-        else:
-            raise StandardError, 'Unsupported compilation mode %s' % (self.compilation_mode)
-            
-    def addEggFile(self, filename, outFilename):
-        # Precompile egg files to bam's.
-        np = self.loader.loadModel(filename, okMissing = True)
-        if not np:
-            raise StandardError, 'Could not read egg file %s' % (filename)
-
-        self.addNode(np.node(), outFilename)
-
-    def addBamFile(self, filename, outFilename):
-        # Load the bam file so we can massage its textures.
-        bamFile = BamFile()
-        if not bamFile.openRead(filename):
-            raise StandardError, 'Could not read bam file %s' % (filename)
-
-        if not bamFile.resolve():
-            raise StandardError, 'Could not resolve bam file %s' % (filename)
-
-        node = bamFile.readNode()
-        if not node:
-            raise StandardError, 'Not a model file: %s' % (filename)
-            
-        self.addNode(node, outFilename)
-
-    def addNode(self, node, filename):
-        """ Converts the indicated node to a bam stream, and adds the
-        bam file to the multifile under the indicated filename. """
-        
-        # Be sure to import all of the referenced textures, and tell
-        # them their new location within the multifile.
-        
-        for tex in NodePath(node).findAllTextures():
-            if not tex.hasFullpath() and tex.hasRamImage():
-                # We need to store this texture as a raw-data image.
-                # Clear the filename so this will happen
-                # automatically.
-                tex.clearFilename()
-                tex.clearAlphaFilename()
-
-            else:
-                # We can store this texture as a file reference to its
-                # image.  Copy the file into our multifile, and rename
-                # its reference in the texture.
-                if tex.hasFilename():
-                    tex.setFilename(self.addTexture(tex.getFullpath()))
-                if tex.hasAlphaFilename():
-                    tex.setAlphaFilename(self.addTexture(tex.getAlphaFullpath()))
-                
-        # Now generate an in-memory bam file.  Tell the bam writer to
-        # keep the textures referenced by their in-multifile path.
-        bamFile = BamFile()
-        stream = StringStream()
-        bamFile.openWrite(stream)
-        bamFile.getWriter().setFileTextureMode(bamFile.BTMUnchanged)
-        bamFile.writeObject(node)
-        bamFile.close()
-
-        # Clean the node out of memory.
-        node.removeAllChildren()
-
-        # Now we have an in-memory bam file.
-        rel = self.makeRelFilename(filename)
-        rel.setExtension('bam')
-        stream.seekg(0)
-        self.multifile.addSubfile(rel.cStr(), stream, self.compression_level)
-
-        # Flush it so the data gets written to disk immediately, so we
-        # don't have to keep it around in ram.
-        self.multifile.flush()
-    
-    def addTexture(self, filename):
-        """ Adds the texture to the multifile, if it has not already
-        been added.  If it is not within the root directory, copies it
-        in (virtually) into a directory within the multifile named
-        imported_maps.  Returns the new filename within the
-        multifile. """
-
-        assert not filename.isLocal()
-
-        filename = Filename(filename)
-        filename.makeAbsolute(vfs.getCwd())
-        filename.makeTrueCase()
-
-        rel = self.imported_textures.get(filename.cStr())
-        if not rel:
-            rel = Filename(filename)
-            if not rel.makeRelativeTo(self.root, False):
-                # Not within the multifile.
-                rel = Filename(self.imported_maps, filename.getBasename())
-
-            # Need to add it now.
-            self.imported_textures[filename.cStr()] = rel
-            filename = Filename(filename)
-            filename.setBinary()
-            self.multifile.addSubfile(rel.cStr(), filename, 0)
-
-        return rel
-
-    def mapTextureFilename(self, filename):
-        """ Returns the filename within the multifile of the
-        already-added texture. """
-
-        filename = Filename(filename)
-        filename.makeAbsolute(vfs.getCwd())
-        filename.makeTrueCase()
-
-        return self.imported_textures[filename.cStr()]
-
-    def addTextFile(self, filename):
-        """ Adds a generic text file to the multifile. """
-
-        rel = self.makeRelFilename(filename)
-        filename = Filename(filename)
-        filename.setText()
-        self.multifile.addSubfile(rel.cStr(), filename, self.compression_level)
-
-    def addBinaryFile(self, filename):
-        """ Adds a generic binary file to the multifile. """
-
-        rel = self.makeRelFilename(filename)
-        filename = Filename(filename)
-        filename.setBinary()
-        self.multifile.addSubfile(rel.cStr(), filename, self.compression_level)
-
-    def addUncompressibleFile(self, filename):
-        """ Adds a generic binary file to the multifile, without compression. """
-
-        rel = self.makeRelFilename(filename)
-        filename = Filename(filename)
-        filename.setBinary()
-        self.multifile.addSubfile(rel.cStr(), filename, 0)
-
-    def makeRelFilename(self, filename):
-        """ Returns the same filename, relative to self.root """
-        rel = Filename(filename)
-        result = rel.makeRelativeTo(self.root, False)
-        assert result
-        return rel
-
-
 def makePackedApp(args):
-    opts, args = getopt.getopt(args, 'r:m:c:h')
+    opts, args = getopt.getopt(args, 'r:m:s:xh')
+
+    packager = Packager.Packager()
 
     root = '.'
     main = None
-    compilation_mode = AppPacker.compilation_mode
+    versionIndependent = False
     for option, value in opts:
         if option == '-r':
-            root = value
+            root = Filename.fromOsSpecific(value)
         elif option == '-m':
             main = value
-        elif option == '-c':
-            compilation_mode = value
+        elif option == '-s':
+            packager.installSearch.append(Filename.fromOsSpecific(value))
+        elif option == '-x':
+            versionIndependent = True
         elif option == '-h':
             print __doc__
             sys.exit(1)
@@ -343,13 +79,43 @@ def makePackedApp(args):
     if not args:
         raise ArgumentError, "No destination app specified.  Use:\npackp3d.py app.p3d"
 
-    multifile_name = args[0]
     if len(args) > 1:
         raise ArgumentError, "Too many arguments."
 
-    p = AppPacker(multifile_name)
-    p.compilation_mode = compilation_mode
-    p.scan(root = root, main = main)
+    appFilename = Filename.fromOsSpecific(args[0])
+    if appFilename.getExtension() != 'p3d':
+        raise ArgumentError, 'Application filename must end in ".p3d".'
+
+    appDir = Filename(appFilename.getDirname())
+    appBase = appFilename.getBasenameWoExtension()
+
+    if not main:
+        main = Filename(root, 'main.py')
+        if main.exists():
+            main = 'main.py'
+        else:
+            main = glob.glob(os.path.join(root.toOsSpecific(), '*.py'))
+            if len(main) == 0:
+                raise ArgumentError, 'No Python files in root directory.'
+            elif len(main) > 1:
+                raise ArgumentError, 'Multiple Python files in root directory; specify the main application with -m "main".'
+            main = os.path.split(main[0])[1]
+
+    main = Filename.fromOsSpecific(main)
+    main.setExtension('')
+
+    mainModule = main.cStr().replace('/', '.')
+    
+    packager.installDir = appDir
+
+    packager.setup()
+    packager.beginPackage(appBase, p3dApplication = True)
+
+    packager.require('panda3d')
+    packager.dir(root)
+    packager.mainModule(mainModule)
+        
+    packager.endPackage(appBase, p3dApplication = True)
 
 if __name__ == '__main__':
     try:

+ 15 - 2
direct/src/showutil/ppackage.py

@@ -34,6 +34,10 @@ Options:
      copy this directory structure to a web host where it may be
      downloaded by the client.
 
+  -s search_dir
+     Additional directories to search for previously-built packages.
+     This option may be repeated as necessary.
+
   -d persist_dir
      The full path to a local directory that retains persistant state
      between publishes.  This directory structure keeps files that are
@@ -60,6 +64,7 @@ import getopt
 import os
 
 from direct.showutil import Packager
+from direct.showutil import make_contents
 from pandac.PandaModules import *
 
 def usage(code, msg = ''):
@@ -70,13 +75,15 @@ def usage(code, msg = ''):
 packager = Packager.Packager()
 
 try:
-    opts, args = getopt.getopt(sys.argv[1:], 'i:d:p:Hh')
+    opts, args = getopt.getopt(sys.argv[1:], 'i:s:d:p:Hh')
 except getopt.error, msg:
     usage(1, msg)
 
 for opt, arg in opts:
     if opt == '-i':
         packager.installDir = Filename.fromOsSpecific(arg)
+    elif opt == '-s':
+        packager.installSearch.append(Filename.fromOsSpecific(arg))
     elif opt == '-d':
         packager.persistDir = Filename.fromOsSpecific(arg)
     elif opt == '-p':
@@ -101,7 +108,13 @@ packageDef = Filename.fromOsSpecific(args[0])
 
 if not packager.installDir:
     packager.installDir = Filename('install')
-packager.installSearch = [packager.installDir]
+packager.installSearch = [packager.installDir] + packager.installSearch
 
 packager.setup()
 packager.readPackageDef(packageDef)
+
+# Update the contents.xml at the root of the install directory.
+cm = make_contents.ContentsMaker()
+cm.installDir = packager.installDir.toOsSpecific()
+cm.build()
+