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')
     require('panda3d')
 
 
     mainModule('direct.p3d.pmerge')
     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
 #! /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:
 Usage:
 
 
-  %s [opts] app.p3d installer|web
+  %(prog)s [opts] app.p3d standalone|installer|html
 
 
 Modes:
 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
   installer
     In this mode, installable packages will be created for as many
     In this mode, installable packages will be created for as many
     platforms as possible. To create Windows installers on
     platforms as possible. To create Windows installers on
     non-Windows platforms, you need to have the "makensis" utility
     non-Windows platforms, you need to have the "makensis" utility
     on your system PATH environment variable.
     on your system PATH environment variable.
 
 
-  web
+  html
     An HTML webpage will be generated that can be used to view
     An HTML webpage will be generated that can be used to view
     the provided p3d file in a browser.
     the provided p3d file in a browser.
 
 
 Options:
 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
   -n your_app
      Short, lowercase name of the application or game. Can only
      Short, lowercase name of the application or game. Can only
      contain alphanumeric characters, underscore or dash. This
      contain alphanumeric characters, underscore or dash. This
@@ -37,112 +37,182 @@ Options:
      If omitted, the basename of the p3d file is used.
      If omitted, the basename of the p3d file is used.
 
 
   -N "Your Application"
   -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.
      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"
   -l "License Name"
      Specifies the name of the software license that the game
      Specifies the name of the software license that the game
      or application is licensed under.
      or application is licensed under.
+     Only relevant when generating a graphical installer.
 
 
   -L licensefile.txt
   -L licensefile.txt
      This should point to a file that contains the full text
      This should point to a file that contains the full text
      describing the software license that the game or application
      describing the software license that the game or application
      is licensed under.
      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 sys
 import os
 import os
 import getopt
 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)
         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)