diff --git a/direct/src/p3d/DeploymentTools.py b/direct/src/p3d/DeploymentTools.py new file mode 100644 index 0000000000..11185a684a --- /dev/null +++ b/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("!\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, '' + print >>plist, '' + print >>plist, '' + print >>plist, '' + print >>plist, '\tCFBundleDevelopmentRegion' + print >>plist, '\tEnglish' + print >>plist, '\tCFBundleDisplayName' + print >>plist, '\t%s' % self.fullname + print >>plist, '\tCFBundleExecutable' + print >>plist, '\tlaunch' + print >>plist, '\tCFBundleIdentifier' + print >>plist, '\t%s.%s' % (self.authorid, self.shortname) + print >>plist, '\tCFBundleInfoDictionaryVersion' + print >>plist, '\t6.0' + print >>plist, '\tCFBundleName' + print >>plist, '\t%s' % self.shortname + print >>plist, '\tCFBundlePackageType' + print >>plist, '\tAPPL' + print >>plist, '\tCFBundleShortVersionString' + print >>plist, '\t%s' % self.version + print >>plist, '\tCFBundleVersion' + print >>plist, '\t%s' % self.version + print >>plist, '\tLSHasLocalizedDisplayName' + print >>plist, '\t' + print >>plist, '\tNSAppleScriptEnabled' + print >>plist, '\t' + print >>plist, '\tNSPrincipalClass' + print >>plist, '\tNSApplication' + print >>plist, '' + print >>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) diff --git a/direct/src/p3d/InstallerMaker.py b/direct/src/p3d/InstallerMaker.py deleted file mode 100644 index 1316064b9d..0000000000 --- a/direct/src/p3d/InstallerMaker.py +++ /dev/null @@ -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("!\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) - diff --git a/direct/src/p3d/panda3d.pdef b/direct/src/p3d/panda3d.pdef index d964f4dc48..2aff322b6b 100755 --- a/direct/src/p3d/panda3d.pdef +++ b/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') diff --git a/direct/src/p3d/pdeploy.py b/direct/src/p3d/pdeploy.py index 8f12d98253..14998df5bc 100644 --- a/direct/src/p3d/pdeploy.py +++ b/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 +from direct.p3d.DeploymentTools import Standalone, Installer +from pandac.PandaModules import Filename, PandaSystem -class ArgumentError(StandardError): - pass +def usage(code, msg = ''): + print >> sys.stderr, usageText % {'prog' : os.path.split(sys.argv[0])[1]} + print >> sys.stderr, msg + sys.exit(code) -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) +shortname = "" +fullname = "" +version = "" +outputDir = Filename("./") +tokens = {} +platforms = [] +currentPlatform = False +licensename = "" +licensefile = Filename() - 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)) +try: + opts, args = getopt.getopt(sys.argv[1:], 'n:N:v:o:t:P:cl:L:h') +except getopt.error, msg: + usage(1, msg) - 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("\n") - html.write(" \n") - html.write(" %s\n" % fullname) - html.write(" \n") - html.write(" \n") - html.write(" \n" % appFilename.getBasename()) - html.write(" \n") - html.write("\n") - html.close() +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("\n") + html.write(" \n") + html.write(" %s\n" % fullname) + html.write(" \n") + html.write(" \n") + html.write(" \n" % appFilename.getBasename()) + html.write(" \n") + html.write("\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)