Browse Source

pdeploy is working for standalone executables now

rdb 16 years ago
parent
commit
b9821f1df8
4 changed files with 582 additions and 290 deletions
  1. 399 0
      direct/src/p3d/DeploymentTools.py
  2. 0 189
      direct/src/p3d/InstallerMaker.py
  3. 12 0
      direct/src/p3d/panda3d.pdef
  4. 171 101
      direct/src/p3d/pdeploy.py

+ 399 - 0
direct/src/p3d/DeploymentTools.py

@@ -0,0 +1,399 @@
+""" This module is used to build a graphical installer
+or a standalone executable from a p3d file. It will try
+to build for as many platforms as possible. """
+
+__all__ = ["Standalone", "Installer"]
+
+import os, sys, subprocess, tarfile, shutil, time
+from direct.directnotify.DirectNotifyGlobal import *
+from pandac.PandaModules import PandaSystem, HTTPClient, Filename, VirtualFileSystem
+from direct.p3d.HostInfo import HostInfo
+from direct.showbase.AppRunnerGlobal import appRunner
+
+class CachedFile:
+    def __init__(self): self.str = ""
+    def write(self, data): self.str += data
+
+# Make sure this matches with the magic in p3dEmbed.cxx.
+P3DEMBED_MAGIC = "\xFF\x3D\x3D\x00"
+
+class Standalone:
+    """ This class creates a standalone executable from a given .p3d file. """
+    notify = directNotify.newCategory("Standalone")
+    
+    def __init__(self, p3dfile, tokens = {}):
+        if isinstance(p3dfile, Filename):
+            self.p3dfile = p3dfile
+        else:
+            self.p3dfile = Filename(p3dfile)
+        self.basename = self.p3dfile.getBasenameWoExtension()
+        self.tokens = tokens
+        
+        if appRunner:
+            self.host = appRunner.getHost("http://runtime.panda3d.org")
+        else:
+            hostDir = Filename(Filename.getTempDirectory(), 'pdeploy/')
+            hostDir.makeDir()
+            self.host = HostInfo("http://runtime.panda3d.org", hostDir = hostDir, asMirror = True)
+        
+        self.http = HTTPClient.getGlobalPtr()
+        if not self.host.downloadContentsFile(self.http):
+            Standalone.notify.error("couldn't read host")
+            return False
+    
+    def buildAll(self, outputDir = "."):
+        """ Builds standalone executables for every known platform,
+        into the specified output directory. """
+        
+        platforms = set()
+        for package in self.host.getPackages(name = "p3dembed"):
+            platforms.add(package.platform)
+        if len(platforms) == 0:
+            Standalone.notify.error("No platforms found to build for!")
+        
+        for platform in platforms:
+            if platform.startswith("win"):
+                self.build(Filename(outputDir, platform + "/" + self.basename + ".exe"), platform)
+            else:
+                self.build(Filename(outputDir, platform + "/" + self.basename), platform)
+    
+    def build(self, output, platform = None):
+        """ Builds a standalone executable and stores it into the path
+        indicated by the 'output' argument. You can specify to build for
+        other platforms by altering the 'platform' argument. """
+        
+        if platform == None:
+            platform = PandaSystem.getPlatform()
+        for package in self.host.getPackages(name = "p3dembed", platform = platform):
+            if not package.downloadDescFile(self.http):
+                Standalone.notify.error("  -> %s failed for platform %s" % (package.packageName, package.platform))
+                continue
+            if not package.downloadPackage(self.http):
+                Standalone.notify.error("  -> %s failed for platform %s" % (package.packageName, package.platform))
+                continue
+            
+            # Figure out where p3dembed might be now.
+            if package.platform.startswith("win"):
+                p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembed.exe" % package.platform)
+            else:
+                p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembed" % package.platform)
+            
+            # We allow p3dembed to be pzipped.
+            if Filename(p3dembed + ".pz").exists():
+                p3dembed = Filename(p3dembed + ".pz")
+            if not p3dembed.exists():
+                Standalone.notify.error("  -> %s failed for platform %s" % (package.packageName, package.platform))
+                continue
+            
+            self.embed(output, p3dembed)
+            return
+        
+        Standalone.notify.error("Failed to build standalone for platform %s" % platform)
+    
+    def embed(self, output, p3dembed):
+        """ Embeds the p3d file into the provided p3dembed executable.
+        This function is not really useful - use build() or buildAll() instead. """
+        
+        # Load the p3dembed data into memory
+        size = p3dembed.getFileSize()
+        p3dembed_data = VirtualFileSystem.getGlobalPtr().readFile(p3dembed, True)
+        assert len(p3dembed_data) == size
+        
+        # Find the magic size string and replace it with the real size,
+        # regardless of the endianness of the p3dembed executable.
+        hex_size = hex(size)[2:].rjust(8, "0")
+        enc_size = "".join([chr(int(hex_size[i] + hex_size[i + 1], 16)) for i in range(0, len(hex_size), 2)])
+        p3dembed_data = p3dembed_data.replace(P3DEMBED_MAGIC, enc_size)
+        p3dembed_data = p3dembed_data.replace(P3DEMBED_MAGIC[::-1], enc_size[::-1])
+        
+        # Write the output file
+        Standalone.notify.info("Creating %s..." % output)
+        output.makeDir()
+        ohandle = open(output.toOsSpecific(), "wb")
+        ohandle.write(p3dembed_data)
+        for token in self.tokens.items():
+            ohandle.write("\0%s=%s" % token)
+        ohandle.write("\0\0")
+        
+        # Buffer the p3d file to the output file. 1 MB buffer size.
+        phandle = open(self.p3dfile.toOsSpecific(), "rb")
+        buf = phandle.read(1024 * 1024)
+        while len(buf) != 0:
+            ohandle.write(buf)
+            buf = phandle.read(1024 * 1024)
+        ohandle.close()
+        phandle.close()
+        
+        os.chmod(output.toOsSpecific(), 0755)
+
+class Installer:
+    """ This class creates a (graphical) installer from a given .p3d file. """
+    notify = directNotify.newCategory("Installer")
+
+    def __init__(self, shortname, fullname, p3dfile, version):
+        self.shortname = shortname
+        self.fullname = fullname
+        self.version = str(version)
+        self.licensename = ""
+        self.authorid = "org.panda3d"
+        self.authorname = ""
+        self.licensefile = Filename()
+        self.builder = StandaloneBuilder(p3dfile)
+
+    def buildAll(self):
+        """ Creates a (graphical) installer for every known platform.
+        Call this after you have set the desired parameters. """
+        
+        # Download the 'p3dembed' package
+        tempdir = Filename.temporary("", self.shortname + "_p3d_", "") + "/"
+        tempdir.makeDir()
+        host = HostInfo("http://runtime.panda3d.org", hostDir = tempdir, asMirror = True)
+        tempdir = tempdir.toOsSpecific()
+        http = HTTPClient.getGlobalPtr()
+        if not host.downloadContentsFile(http):
+            Installer.notify.error("couldn't read host")
+            return False
+
+        for package in host.getPackages(name = "p3dembed"):
+            print package.packageName, package.packageVersion, package.platform
+            if not package.downloadDescFile(http):
+                Installer.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
+                continue
+            if not package.downloadPackage(http):
+                Installer.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
+                continue
+            
+            if package.platform.startswith("linux_"):
+                plugin_standalone = os.path.join(tempdir, "plugin_standalone", package.platform, "panda3d")
+                assert os.path.isfile(plugin_standalone)
+                self.__buildDEB(plugin_standalone, arch = package.platform.replace("linux_", ""))
+            elif package.platform.startswith("win"):
+                plugin_standalone = os.path.join(tempdir, "plugin_standalone", package.platform, "panda3d.exe")
+                assert os.path.isfile(plugin_standalone)
+                self.__buildNSIS(plugin_standalone)
+            elif package.platform == "darwin":
+                plugin_standalone = os.path.join(tempdir, "plugin_standalone", package.platform, "panda3d_mac")
+                assert os.path.isfile(plugin_standalone)
+                self.__buildAPP(plugin_standalone)
+            else:
+                Installer.notify.info("Ignoring unknown platform " + package.platform)
+        
+        #shutil.rmtree(tempdir)
+        return True
+
+    def __buildDEB(self, plugin_standalone, arch = "all"):
+        debfn = "%s_%s_all.deb" % (self.shortname.lower(), self.version)
+        Installer.notify.info("Creating %s..." % debfn)
+
+        # Create a temporary directory and write the control file + launcher to it
+        tempdir = Filename.temporary("", self.shortname.lower() + "_deb_", "") + "/"
+        tempdir.makeDir()
+        tempdir = tempdir.toOsSpecific()
+        controlfile = open(os.path.join(tempdir, "control"), "w")
+        controlfile.write("Package: %s\n" % self.shortname.lower())
+        controlfile.write("Version: %s\n" % self.version)
+        controlfile.write("Section: games\n")
+        controlfile.write("Priority: optional\n")
+        controlfile.write("Architecture: %s\n" % arch)
+        controlfile.write("Description: %s\n" % self.fullname)
+        controlfile.close()
+        os.makedirs(os.path.join(tempdir, "usr", "bin"))
+        os.makedirs(os.path.join(tempdir, "usr", "share", "games", self.shortname.lower()))
+        os.makedirs(os.path.join(tempdir, "usr", "libexec", self.shortname.lower()))
+        if not self.licensefile.empty():
+            os.makedirs(os.path.join(tempdir, "usr", "share", "doc", self.shortname.lower()))
+        launcherfile = open(os.path.join(tempdir, "usr", "bin", self.shortname.lower()), "w")
+        launcherfile.write("#!/bin/sh\n")
+        launcherfile.write("/usr/libexec/%s/panda3d /usr/share/games/%s/%s.p3d\n" % ((self.shortname.lower(),) * 3))
+        launcherfile.close()
+        os.chmod(os.path.join(tempdir, "usr", "bin", self.shortname.lower()), 0755)
+        shutil.copyfile(self.p3dfile.toOsSpecific(), os.path.join(tempdir, "usr", "share", "games", self.shortname.lower(), self.shortname.lower() + ".p3d"))
+        shutil.copyfile(plugin_standalone, os.path.join(tempdir, "usr", "libexec", self.shortname.lower(), "panda3d"))
+        if not self.licensefile.empty():
+            shutil.copyfile(self.licensefile.toOsSpecific(), os.path.join(tempdir, "usr", "share", "doc", self.shortname.lower(), "copyright"))
+
+        # Create a control.tar.gz file in memory
+        controltargz = CachedFile()
+        controltarfile = tarfile.TarFile.gzopen("control.tar.gz", "w", controltargz, 9)
+        controltarfile.add(os.path.join(tempdir, "control"), "control")
+        controltarfile.close()
+        os.remove(os.path.join(tempdir, "control"))
+
+        # Create the data.tar.gz file in the temporary directory
+        datatargz = CachedFile()
+        datatarfile = tarfile.TarFile.gzopen("data.tar.gz", "w", datatargz, 9)
+        datatarfile.add(tempdir + "/usr", "/usr")
+        datatarfile.close()
+
+        # Open the deb file and write to it. It's actually
+        # just an AR file, which is very easy to make.
+        modtime = int(time.time())
+        if os.path.isfile(debfn):
+            os.remove(debfn)
+        debfile = open(debfn, "wb")
+        debfile.write("!<arch>\x0A")
+        debfile.write("debian-binary   %-12lu0     0     100644  %-10ld\x60\x0A" % (modtime, 4))
+        debfile.write("2.0\x0A")
+        debfile.write("control.tar.gz  %-12lu0     0     100644  %-10ld\x60\x0A" % (modtime, len(controltargz.str)))
+        debfile.write(controltargz.str)
+        if (len(controltargz.str) & 1): debfile.write("\x0A")
+        debfile.write("data.tar.gz     %-12lu0     0     100644  %-10ld\x60\x0A" % (modtime, len(datatargz.str)))
+        debfile.write(datatargz.str)
+        if (len(datatargz.str) & 1): debfile.write("\x0A")
+        debfile.close()
+        shutil.rmtree(tempdir)
+
+    def __buildAPP(self, plugin_standalone):
+        pkgfn = "%s %s.pkg" % (self.shortname, self.version)
+        appname = "/Applications/%s.app" % self.longname
+        Installer.notify.info("Creating %s..." % pkgfn)
+        
+        # Create a temporary directory to hold the application in
+        tempdir = Filename.temporary("", self.shortname.lower() + "_app_", "") + "/"
+        tempdir = tempdir.toOsSpecific()
+        if os.path.exists(tempdir):
+            shutil.rmtree(tempdir)
+        os.makedirs(tempdir)
+        contents = os.path.join(tempdir, appname.lstrip("/"), "Contents")
+        os.makedirs(os.path.join(contents, "MacOS"))
+        os.makedirs(os.path.join(contents, "Resources"))
+        
+        # Create the "launch" script used to run the game.
+        launch = open(os.path.join(contents, "MacOS", "launch"), "w")
+        print >>launch, '#!/bin/sh'
+        print >>launch, 'panda3d_mac ../Resources/%s' % self.p3dfile.getBasename()
+        launch.close()
+        shutil.copyfile(self.p3dfile.toOsSpecific(), os.path.join(contents, "Resources", self.p3dfile.getBasename()))
+        shutil.copyfile(target, os.path.join(contents, "MacOS", "panda3d_mac"))
+        
+        # Create the application plist file.
+        # Although it might make more sense to use Python's plistlib module here,
+        # it is not available on non-OSX systems before Python 2.6.
+        plist = open(os.path.join(tempdir, appname.lstrip("/"), "Contents", "Info.plist"), "w")
+        print >>plist, '<?xml version="1.0" encoding="UTF-8"?>'
+        print >>plist, '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
+        print >>plist, '<plist version="1.0">'
+        print >>plist, '<dict>'
+        print >>plist, '\t<key>CFBundleDevelopmentRegion</key>'
+        print >>plist, '\t<string>English</string>'
+        print >>plist, '\t<key>CFBundleDisplayName</key>'
+        print >>plist, '\t<string>%s</string>' % self.fullname
+        print >>plist, '\t<key>CFBundleExecutable</key>'
+        print >>plist, '\t<string>launch</string>'
+        print >>plist, '\t<key>CFBundleIdentifier</key>'
+        print >>plist, '\t<string>%s.%s</string>' % (self.authorid, self.shortname)
+        print >>plist, '\t<key>CFBundleInfoDictionaryVersion</key>'
+        print >>plist, '\t<string>6.0</string>'
+        print >>plist, '\t<key>CFBundleName</key>'
+        print >>plist, '\t<string>%s</string>' % self.shortname
+        print >>plist, '\t<key>CFBundlePackageType</key>'
+        print >>plist, '\t<string>APPL</string>'
+        print >>plist, '\t<key>CFBundleShortVersionString</key>'
+        print >>plist, '\t<string>%s</string>' % self.version
+        print >>plist, '\t<key>CFBundleVersion</key>'
+        print >>plist, '\t<string>%s</string>' % self.version
+        print >>plist, '\t<key>LSHasLocalizedDisplayName</key>'
+        print >>plist, '\t<false/>'
+        print >>plist, '\t<key>NSAppleScriptEnabled</key>'
+        print >>plist, '\t<false/>'
+        print >>plist, '\t<key>NSPrincipalClass</key>'
+        print >>plist, '\t<string>NSApplication</string>'
+        print >>plist, '</dict>'
+        print >>plist, '</plist>'
+        plist.close()
+
+    def __buildNSIS(self):
+        # Check if we have makensis first
+        makensis = None
+        if (sys.platform.startswith("win")):
+            for p in os.defpath.split(";") + os.environ["PATH"].split(";"):
+                if os.path.isfile(os.path.join(p, "makensis.exe")):
+                    makensis = os.path.join(p, "makensis.exe")
+            if not makensis:
+                import pandac
+                makensis = os.path.dirname(os.path.dirname(pandac.__file__))
+                makensis = os.path.join(makensis, "nsis", "makensis.exe")
+                if not os.path.isfile(makensis): makensis = None
+        else:
+            for p in os.defpath.split(":") + os.environ["PATH"].split(":"):
+                if os.path.isfile(os.path.join(p, "makensis")):
+                    makensis = os.path.join(p, "makensis")
+        
+        if makensis == None:
+            Installer.notify.warning("Makensis utility not found, no Windows installer will be built!")
+            return
+        Installer.notify.info("Creating %s.exe..." % self.shortname)
+
+        tempfile = self.shortname + ".nsi"
+        nsi = open(tempfile, "w")
+
+        # Some global info
+        nsi.write('Name "%s"\n' % self.fullname)
+        nsi.write('OutFile "%s.exe"\n' % self.shortname)
+        nsi.write('InstallDir "$PROGRAMFILES\%s"\n' % self.fullname)
+        nsi.write('SetCompress auto\n')
+        nsi.write('SetCompressor lzma\n')
+        nsi.write('ShowInstDetails nevershow\n')
+        nsi.write('ShowUninstDetails nevershow\n')
+        nsi.write('InstType "Typical"\n')
+
+        # Tell Vista that we require admin rights
+        nsi.write('RequestExecutionLevel admin\n')
+        nsi.write('\n')
+        nsi.write('Function launch\n')
+        nsi.write('  ExecShell "open" "$INSTDIR\%s.bat"\n' % self.shortname)
+        nsi.write('FunctionEnd\n')
+        nsi.write('\n')
+        nsi.write('!include "MUI2.nsh"\n')
+        nsi.write('!define MUI_ABORTWARNING\n')
+        nsi.write('!define MUI_FINISHPAGE_RUN\n')
+        nsi.write('!define MUI_FINISHPAGE_RUN_FUNCTION launch\n')
+        nsi.write('!define MUI_FINISHPAGE_RUN_TEXT "Play %s"\n' % self.fullname)
+        nsi.write('\n')
+        nsi.write('Var StartMenuFolder\n')
+        nsi.write('!insertmacro MUI_PAGE_WELCOME\n')
+        if not self.licensefile.empty():
+            nsi.write('!insertmacro MUI_PAGE_LICENSE "%s"\n' % self.licensefile.toOsSpecific())
+        nsi.write('!insertmacro MUI_PAGE_DIRECTORY\n')
+        nsi.write('!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder\n')
+        nsi.write('!insertmacro MUI_PAGE_INSTFILES\n')
+        nsi.write('!insertmacro MUI_PAGE_FINISH\n')
+        nsi.write('!insertmacro MUI_UNPAGE_WELCOME\n')
+        nsi.write('!insertmacro MUI_UNPAGE_CONFIRM\n')
+        nsi.write('!insertmacro MUI_UNPAGE_INSTFILES\n')
+        nsi.write('!insertmacro MUI_UNPAGE_FINISH\n')
+        nsi.write('!insertmacro MUI_LANGUAGE "English"\n')
+
+        # This section defines the installer.
+        nsi.write('Section "Install"\n')
+        nsi.write('  SetOutPath "$INSTDIR"\n')
+        nsi.write('  WriteUninstaller "$INSTDIR\Uninstall.exe"\n')
+        nsi.write('  ; Start menu items\n')
+        nsi.write('  !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n')
+        nsi.write('    CreateDirectory "$SMPROGRAMS\$StartMenuFolder"\n')
+        nsi.write('    CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe"\n')
+        nsi.write('  !insertmacro MUI_STARTMENU_WRITE_END\n')
+        nsi.write('SectionEnd\n')
+
+        # This section defines the uninstaller.
+        nsi.write('Section "Uninstall"\n')
+        nsi.write('  Delete "$INSTDIR\Uninstall.exe"\n')
+        nsi.write('  RMDir "$INSTDIR"\n')
+        nsi.write('  ; Start menu items\n')
+        nsi.write('  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n')
+        nsi.write('  Delete "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk"\n')
+        nsi.write('  RMDir "$SMPROGRAMS\$StartMenuFolder"\n')
+        nsi.write('SectionEnd')
+        nsi.close()
+
+        options = ["V2"]
+        cmd = makensis
+        for o in options:
+            if sys.platform.startswith("win"):
+                cmd += " /" + o
+            else:
+                cmd += " -" + o
+        cmd += " " + tempfile
+        os.system(cmd)
+
+        os.remove(tempfile)

+ 0 - 189
direct/src/p3d/InstallerMaker.py

@@ -1,189 +0,0 @@
-""" This module is used to build a graphical installer
-from a p3d file. It will try to build installers for as
-many platforms as possible. """
-
-__all__ = ["InstallerMaker"]
-
-import os, sys, subprocess, tarfile, shutil, time
-from direct.directnotify.DirectNotifyGlobal import *
-from pandac.PandaModules import Filename
-
-class CachedFile:
-    def __init__(self): self.str = ""
-    def write(self, data): self.str += data
-
-class InstallerMaker:
-    notify = directNotify.newCategory("InstallerMaker")
-
-    def __init__(self, shortname, fullname, p3dfile, version):
-        self.shortname = shortname
-        self.fullname = fullname
-        self.version = str(version)
-        self.licensename = ""
-        # All paths given must be a Filename instance!
-        assert isinstance(p3dfile, Filename)
-        self.p3dfile = p3dfile
-        self.licensefile = Filename()
-
-    def build(self):
-        """ Creates the installer. Call this after you have set all the parameters. """
-        self.__buildDEB()
-        self.__buildNSIS()
-
-    def __buildDEB(self):
-        debfn = "%s_%s_all.deb" % (self.shortname, self.version)
-        InstallerMaker.notify.info("Creating %s..." % debfn)
-
-        # Create a temporary directory and write the control file + launcher to it
-        tempdir = Filename.temporary("", self.shortname + "_deb_", "") + "/"
-        tempdir.makeDir()
-        tempdir = tempdir.toOsSpecific()
-        controlfile = open(os.path.join(tempdir, "control"), "w")
-        controlfile.write("Package: %s\n" % self.shortname)
-        controlfile.write("Version: %s\n" % self.version)
-        controlfile.write("Section: games\n")
-        controlfile.write("Priority: optional\n")
-        controlfile.write("Architecture: all\n")
-        controlfile.write("Depends: panda3d-runtime\n")
-        controlfile.write("Description: %s\n" % self.fullname)
-        controlfile.close()
-        os.makedirs(os.path.join(tempdir, "usr", "bin"))
-        os.makedirs(os.path.join(tempdir, "usr", "share", "games", self.shortname))
-        if not self.licensefile.empty():
-            os.makedirs(os.path.join(tempdir, "usr", "share", "doc", self.shortname))
-        launcherfile = open(os.path.join(tempdir, "usr", "bin", self.shortname), "w")
-        launcherfile.write("#!/bin/sh\n")
-        launcherfile.write("/usr/bin/env panda3d /usr/share/games/%s/data.p3d\n" % self.shortname)
-        launcherfile.close()
-        os.chmod(os.path.join(tempdir, "usr", "bin", self.shortname), 0755)
-        shutil.copyfile(self.p3dfile.toOsSpecific(), os.path.join(tempdir, "usr", "share", "games", self.shortname, "data.p3d"))
-        if not self.licensefile.empty():
-            shutil.copyfile(self.licensefile.toOsSpecific(), os.path.join(tempdir, "usr", "share", "doc", self.shortname, "copyright"))
-
-        # Create a control.tar.gz file in memory
-        controltargz = CachedFile()
-        controltarfile = tarfile.TarFile.gzopen("control.tar.gz", "w", controltargz, 9)
-        controltarfile.add(os.path.join(tempdir, "control"), "control")
-        controltarfile.close()
-        os.remove(os.path.join(tempdir, "control"))
-
-        # Create the data.tar.gz file in the temporary directory
-        datatargz = CachedFile()
-        datatarfile = tarfile.TarFile.gzopen("data.tar.gz", "w", datatargz, 9)
-        datatarfile.add(tempdir + "/usr", "/usr")
-        datatarfile.close()
-
-        # Open the deb file and write to it. It's actually
-        # just an AR file, which is very easy to make.
-        modtime = int(time.time())
-        if os.path.isfile(debfn):
-            os.remove(debfn)
-        debfile = open(debfn, "wb")
-        debfile.write("!<arch>\x0A")
-        debfile.write("debian-binary   %-12lu0     0     100644  %-10ld\x60\x0A" % (modtime, 4))
-        debfile.write("2.0\x0A")
-        debfile.write("control.tar.gz  %-12lu0     0     100644  %-10ld\x60\x0A" % (modtime, len(controltargz.str)))
-        debfile.write(controltargz.str)
-        if (len(controltargz.str) & 1): debfile.write("\x0A")
-        debfile.write("data.tar.gz     %-12lu0     0     100644  %-10ld\x60\x0A" % (modtime, len(datatargz.str)))
-        debfile.write(datatargz.str)
-        if (len(datatargz.str) & 1): debfile.write("\x0A")
-        debfile.close()
-        shutil.rmtree(tempdir)
-
-    def __buildNSIS(self):
-        # Check if we have makensis first
-        makensis = None
-        if (sys.platform.startswith("win")):
-            for p in os.defpath.split(";") + os.environ["PATH"].split(";"):
-                if os.path.isfile(os.path.join(p, "makensis.exe")):
-                    makensis = os.path.join(p, "makensis.exe")
-            if not makensis:
-                import pandac
-                makensis = os.path.dirname(os.path.dirname(pandac.__file__))
-                makensis = os.path.join(makensis, "nsis", "makensis.exe")
-                if not os.path.isfile(makensis): makensis = None
-        else:
-            for p in os.defpath.split(":") + os.environ["PATH"].split(":"):
-                if os.path.isfile(os.path.join(p, "makensis")):
-                    makensis = os.path.join(p, "makensis")
-        
-        if makensis == None:
-            InstallerMaker.notify.warning("Makensis utility not found, no Windows installer will be built!")
-            return
-        InstallerMaker.notify.info("Creating %s.exe..." % self.shortname)
-
-        tempfile = self.shortname + ".nsi"
-        nsi = open(tempfile, "w")
-
-        # Some global info
-        nsi.write('Name "%s"\n' % self.fullname)
-        nsi.write('OutFile "%s.exe"\n' % self.shortname)
-        nsi.write('InstallDir "$PROGRAMFILES\%s"\n' % self.fullname)
-        nsi.write('SetCompress auto\n')
-        nsi.write('SetCompressor lzma\n')
-        nsi.write('ShowInstDetails nevershow\n')
-        nsi.write('ShowUninstDetails nevershow\n')
-        nsi.write('InstType "Typical"\n')
-
-        # Tell Vista that we require admin rights
-        nsi.write('RequestExecutionLevel admin\n')
-        nsi.write('\n')
-        nsi.write('Function launch\n')
-        nsi.write('  ExecShell "open" "$INSTDIR\%s.bat"\n' % self.shortname)
-        nsi.write('FunctionEnd\n')
-        nsi.write('\n')
-        nsi.write('!include "MUI2.nsh"\n')
-        nsi.write('!define MUI_ABORTWARNING\n')
-        nsi.write('!define MUI_FINISHPAGE_RUN\n')
-        nsi.write('!define MUI_FINISHPAGE_RUN_FUNCTION launch\n')
-        nsi.write('!define MUI_FINISHPAGE_RUN_TEXT "Play %s"\n' % self.fullname)
-        nsi.write('\n')
-        nsi.write('Var StartMenuFolder\n')
-        nsi.write('!insertmacro MUI_PAGE_WELCOME\n')
-        if not self.licensefile.empty():
-            nsi.write('!insertmacro MUI_PAGE_LICENSE "%s"\n' % self.licensefile.toOsSpecific())
-        nsi.write('!insertmacro MUI_PAGE_DIRECTORY\n')
-        nsi.write('!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder\n')
-        nsi.write('!insertmacro MUI_PAGE_INSTFILES\n')
-        nsi.write('!insertmacro MUI_PAGE_FINISH\n')
-        nsi.write('!insertmacro MUI_UNPAGE_WELCOME\n')
-        nsi.write('!insertmacro MUI_UNPAGE_CONFIRM\n')
-        nsi.write('!insertmacro MUI_UNPAGE_INSTFILES\n')
-        nsi.write('!insertmacro MUI_UNPAGE_FINISH\n')
-        nsi.write('!insertmacro MUI_LANGUAGE "English"\n')
-
-        # This section defines the installer.
-        nsi.write('Section "Install"\n')
-        nsi.write('  SetOutPath "$INSTDIR"\n')
-        nsi.write('  WriteUninstaller "$INSTDIR\Uninstall.exe"\n')
-        nsi.write('  ; Start menu items\n')
-        nsi.write('  !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n')
-        nsi.write('    CreateDirectory "$SMPROGRAMS\$StartMenuFolder"\n')
-        nsi.write('    CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe"\n')
-        nsi.write('  !insertmacro MUI_STARTMENU_WRITE_END\n')
-        nsi.write('SectionEnd\n')
-
-        # This section defines the uninstaller.
-        nsi.write('Section "Uninstall"\n')
-        nsi.write('  Delete "$INSTDIR\Uninstall.exe"\n')
-        nsi.write('  RMDir "$INSTDIR"\n')
-        nsi.write('  ; Start menu items\n')
-        nsi.write('  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n')
-        nsi.write('  Delete "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk"\n')
-        nsi.write('  RMDir "$SMPROGRAMS\$StartMenuFolder"\n')
-        nsi.write('SectionEnd')
-        nsi.close()
-
-        options = ["V2"]
-        cmd = makensis
-        for o in options:
-            if sys.platform.startswith("win"):
-                cmd += " /" + o
-            else:
-                cmd += " -" + o
-        cmd += " " + tempfile
-        os.system(cmd)
-
-        os.remove(tempfile)
-

+ 12 - 0
direct/src/p3d/panda3d.pdef

@@ -320,3 +320,15 @@ class pmerge(p3d):
     require('panda3d')
 
     mainModule('direct.p3d.pmerge')
+
+
+class pdeploy(p3d):
+    # This utility can distribute a game in the form of
+    # a standalone executable or a graphical installer.
+
+    config(display_name = "Panda3D Deployment Tool",
+           hidden = True, platform_specific = False,
+           keep_user_env = True)
+    require('panda3d')
+
+    mainModule('direct.p3d.pdeploy')

+ 171 - 101
direct/src/p3d/pdeploy.py

@@ -1,35 +1,35 @@
 #! /usr/bin/env python
 
-"""
+usageText = """
 
-This command will help you to distribute your Panda game, consisting
-of a .p3d file, into an installable package or an HTML webpage.
+This command will help you to distribute your Panda application,
+consisting of a .p3d package, into a standalone executable, graphical
+installer or an HTML webpage. It will attempt to create packages
+for every platform, if possible.
 
 Usage:
 
-  %s [opts] app.p3d installer|web
+  %(prog)s [opts] app.p3d standalone|installer|html
 
 Modes:
 
+  standalone
+    A standalone executable will be created that embeds the given
+    p3d file. The resulting executable will require an
+    internet connection in order to run properly.
+
   installer
     In this mode, installable packages will be created for as many
     platforms as possible. To create Windows installers on
     non-Windows platforms, you need to have the "makensis" utility
     on your system PATH environment variable.
 
-  web
+  html
     An HTML webpage will be generated that can be used to view
     the provided p3d file in a browser.
 
 Options:
 
-  -v version_number
-     This should define the version number of your application
-     or game. In some deploy modes, this argument is required.
-     This should only contain alphanumeric characters, dots and
-     dashes, as the result of the deployment may be in valid
-     on some platforms otherwise.
-
   -n your_app
      Short, lowercase name of the application or game. Can only
      contain alphanumeric characters, underscore or dash. This
@@ -37,112 +37,182 @@ Options:
      If omitted, the basename of the p3d file is used.
 
   -N "Your Application"
-     Full name of the application or game. This one will be used
-     to display to the end-user.
+     Full name of the application or game. This value will
+     be used to display to the end-user.
      If omitted, the short name is used.
 
+  -v version_number
+     This should define the version number of your application
+     or game. In some deploy modes, this argument is required.
+     This should only contain alphanumeric characters, dots and
+     dashes, as otherwise the result of the deployment may be
+     invalid on some platforms.
+
+  -o output_dir
+     Indicates the directory where the output will be stored.
+     Within this directory, subdirectories will be created
+     for every platform, unless -t is provided.
+     If omitted, the current working directory is assumed.
+
+  -t token=value
+     Defines a web token or parameter to pass to the application.
+     Use this to configure how the application will be run.
+     You can pass as many -t options as you need. Examples of
+     tokens are width, height, log_basename, auto_start, hidden.
+
+  -P platform
+     If this option is provided, it should specify a comma-
+     separated list of platforms that the p3d package will be
+     deployed for. If omitted, it will be built for all platforms.
+     This option may be specified multiple times.
+     Examples of valid platforms are win32, linux_amd64 and osx_ppc.
+
+  -c
+     If this option is provided, the -p option is ignored and
+     the p3d package is only deployed for the current platform.
+     Furthermore, no per-platform subdirectories will be created
+     inside the output dirctory.
+
   -l "License Name"
      Specifies the name of the software license that the game
      or application is licensed under.
+     Only relevant when generating a graphical installer.
 
   -L licensefile.txt
      This should point to a file that contains the full text
      describing the software license that the game or application
      is licensed under.
+     Only relevant when generating a graphical installer.
+
+  -h
+     Display this help
 
 """
 
-DEPLOY_MODES = ["installer", "web"]
+DEPLOY_MODES = ["standalone", "installer", "html"]
 
 import sys
 import os
 import getopt
-from direct.p3d import InstallerMaker
-from pandac.PandaModules import Filename
-
-class ArgumentError(StandardError):
-    pass
-
-def deployApp(args):
-    opts, args = getopt.getopt(args, 'l:L:n:N:v:h')
-    
-    version = ""
-    shortname = ""
-    fullname = ""
-    licensename = ""
-    licensefile = Filename()
-    
-    for option, value in opts:
-        if option == '-v':
-            version = value.strip()
-        if option == '-n':
-            shortname = value.strip()
-        elif option == '-L':
-            fullname = value.strip()
-        if option == '-l':
-            licensename = value
-        elif option == '-L':
-            licensefile = Filename.fromOsSpecific(value)
-        elif option == '-h':
-            print __doc__ % (os.path.split(sys.argv[0])[1])
-            sys.exit(1)
-
-    if not args or len(args) < 2:
-        raise ArgumentError, "No target app and/or deploy type specified.  Use:\n%s app.p3d %s" % (os.path.split(sys.argv[0])[1], '|'.join(DEPLOY_MODES))
-
-    if len(args) > 2:
-        raise ArgumentError, "Too many arguments."
-
-    appFilename = Filename.fromOsSpecific(args[0])
-    if appFilename.getExtension().lower() != 'p3d':
-        raise ArgumentError, 'Application filename must end in ".p3d".'
-
-    deploy_mode = args[1].lower()
-    if deploy_mode not in DEPLOY_MODES:
-        raise ArgumentError, 'Invalid deploy type, must be one of "%s".' % '", "'.join(DEPLOY_MODES)
-
-    if shortname.lower() != shortname or ' ' in shortname:
-        raise ArgumentError, 'Provided short name should be lowercase, and may not contain spaces!'
-
-    if shortname == '':
-        shortname = appFilename.getBasenameWoExtension()
-
-    if fullname == '':
-        fullname = shortname
-
-    if version == '' and deploy_mode == 'installer':
-        raise ArgumentError, 'A version number is required in "installer" mode!'
-
-    try:
-        if deploy_mode == 'installer':
-            im = InstallerMaker.InstallerMaker(shortname, fullname, appFilename, version)
-            im.licensename = licensename
-            im.licensefile = licensefile
-            im.build()
-        elif deploy_mode == 'web':
-            print "Creating %s.html..." % shortname
-            html = open(shortname + ".html", "w")
-            html.write("<html>\n")
-            html.write("  <head>\n")
-            html.write("    <title>%s</title>\n" % fullname)
-            html.write("  </head>\n")
-            html.write("  <body>\n")
-            html.write("    <object data=\"%s\" type=\"application/x-panda3d\"></object>\n" % appFilename.getBasename())
-            html.write("  </body>\n")
-            html.write("</html>\n")
-            html.close()
+from direct.p3d.DeploymentTools import Standalone, Installer
+from pandac.PandaModules import Filename, PandaSystem
+
+def usage(code, msg = ''):
+    print >> sys.stderr, usageText % {'prog' : os.path.split(sys.argv[0])[1]}
+    print >> sys.stderr, msg
+    sys.exit(code)
+
+shortname = ""
+fullname = ""
+version = ""
+outputDir = Filename("./")
+tokens = {}
+platforms = []
+currentPlatform = False
+licensename = ""
+licensefile = Filename()
+
+try:
+    opts, args = getopt.getopt(sys.argv[1:], 'n:N:v:o:t:P:cl:L:h')
+except getopt.error, msg:
+    usage(1, msg)
+
+for opt, arg in opts:
+    if opt == '-n':
+        shortname = arg.strip()
+    elif opt == '-N':
+        fullname = arg.strip()
+    elif opt == '-v':
+        version = arg.strip()
+    elif opt == '-o':
+        outputDir = Filename.fromOsSpecific(arg)
+    elif opt == '-t':
+        token = arg.strip().split("=", 1)
+        tokens[token[0]] = token[1]
+    elif opt == '-P':
+        platforms.append(arg)
+    elif opt == '-c':
+        currentPlatform = True
+    elif opt == '-l':
+        licensename = arg.strip()
+    elif opt == '-L':
+        licensefile = Filename.fromOsSpecific(arg)
         
-    except: raise
-    #except InstallerMaker.InstallerMakerError:
-    #    # Just print the error message and exit gracefully.
-    #    inst = sys.exc_info()[1]
-    #    print inst.args[0]
-    #    sys.exit(1)
-
-if __name__ == '__main__':
-    try:
-        deployApp(sys.argv[1:])
-    except ArgumentError, e:
-        print e.args[0]
+    elif opt == '-h':
+        usage(0)
+    else:
+        print 'illegal option: ' + flag
         sys.exit(1)
 
+if not args or len(args) != 2:
+    usage(1)
+
+appFilename = Filename.fromOsSpecific(args[0])
+if appFilename.getExtension().lower() != 'p3d':
+    print 'Application filename must end in ".p3d".'
+    sys.exit(1)
+deploy_mode = args[1].lower()
+
+if not appFilename.exists():
+    print 'Application filename does not exist!'
+    sys.exit(1)
+
+if shortname.lower() != shortname or ' ' in shortname:
+    print '\nProvided short name should be lowercase, and may not contain spaces!\n'
+
+if shortname == '':
+    shortname = appFilename.getBasenameWoExtension()
+
+if fullname == '':
+    fullname = shortname
+
+if version == '' and deploy_mode == 'installer':
+    print '\nA version number is required in "installer" mode.\n'
+    sys.exit(1)
+
+if not outputDir:
+    print '\nYou must name the output directory with the -o parameter.\n'
+    sys.exit(1)
+
+if deploy_mode == 'standalone':
+    s = Standalone(appFilename, tokens)
+    s.basename = shortname
+    
+    if currentPlatform:
+        platform = PandaSystem.getPlatform()
+        if platform.startswith("win"):
+            s.build(Filename(outputDir, shortname + ".exe"), platform)
+        else:
+            s.build(Filename(outputDir, shortname), platform)
+    elif len(platforms) == 0:
+        s.buildAll(outputDir)
+    else:
+        for platform in platforms:
+            if platform.startswith("win"):
+                s.build(Filename(outputDir, platform + "/" + shortname + ".exe"), platform)
+            else:
+                s.build(Filename(outputDir, platform + "/" + shortname), platform)
+
+elif deploy_mode == 'installer':
+    i = Installer(shortname, fullname, appFilename, version)
+    i.licensename = licensename
+    i.licensefile = licensefile
+    i.build()
+elif deploy_mode == 'html':
+    print "Creating %s.html..." % shortname
+    html = open(shortname + ".html", "w")
+    html.write("<html>\n")
+    html.write("  <head>\n")
+    html.write("    <title>%s</title>\n" % fullname)
+    html.write("  </head>\n")
+    html.write("  <body>\n")
+    html.write("    <object data=\"%s\" type=\"application/x-panda3d\"></object>\n" % appFilename.getBasename())
+    html.write("  </body>\n")
+    html.write("</html>\n")
+    html.close()
+else:
+    usage(1, 'Invalid deployment mode!')
+
+# An explicit call to exit() is required to exit the program, when
+# this module is packaged in a p3d file.
+sys.exit(0)