""" 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, zipfile 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 = {}): 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 = False, perPlatform = True) self.http = HTTPClient.getGlobalPtr() if not self.host.readContentsFile(): 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.warning("No platforms found to build for!") outputDir = Filename(outputDir + "/") outputDir.makeDir() 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 a different platform 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.warning(" -> %s failed for platform %s" % (package.packageName, package.platform)) continue if not package.downloadPackage(self.http): Standalone.notify.warning(" -> %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) if not p3dembed.exists(): Standalone.notify.warning(" -> %s failed for platform %s" % (package.packageName, package.platform)) continue return self.embed(output, p3dembed) 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) def getExtraFiles(self, platform): """ Returns a list of extra files that will need to be included with the standalone executable in order for it to run, such as dependent libraries. The returned paths are full absolute paths. """ package = self.host.getPackages(name = "p3dembed", platform = platform)[0] if not package.downloadDescFile(self.http): Standalone.notify.warning(" -> %s failed for platform %s" % (package.packageName, package.platform)) return [] if not package.downloadPackage(self.http): Standalone.notify.warning(" -> %s failed for platform %s" % (package.packageName, package.platform)) return [] filenames = [] for e in package.extracts: if e.basename not in ["p3dembed", "p3dembed.exe"]: filename = Filename(package.getPackageDir(), e.filename) filename.makeAbsolute() if filename.exists(): filenames.append(filename) else: Standalone.notify.error("%s mentioned in xml, but does not exist" % e.filename) return filenames class Installer: """ This class creates a (graphical) installer from a given .p3d file. """ notify = directNotify.newCategory("Installer") def __init__(self, shortname, fullname, p3dfile, version, tokens = {}): self.shortname = shortname self.fullname = fullname self.version = str(version) self.licensename = "" self.authorid = "org.panda3d" self.authorname = "" self.licensefile = Filename() self.standalone = Standalone(p3dfile, tokens) def buildAll(self, outputDir = "."): """ Creates a (graphical) installer for every known platform. Call this after you have set the desired parameters. """ platforms = set() for package in self.standalone.host.getPackages(name = "p3dembed"): platforms.add(package.platform) if len(platforms) == 0: Installer.notify.warning("No platforms found to build for!") outputDir = Filename(outputDir + "/") outputDir.makeDir() for platform in platforms: output = Filename(outputDir, platform + "/") output.makeDir() self.build(output, platform) def build(self, output, platform = None): """ Builds a (graphical) installer and stores it into the path indicated by the 'output' argument. You can specify to build for a different platform by altering the 'platform' argument. If 'output' is a directory, the installer will be stored in it. """ if platform == None: platform = PandaSystem.getPlatform() if platform == "win32": return self.buildNSIS(output, platform) elif "_" in platform: os, arch = platform.split("_", 1) if os == "linux": return self.buildDEB(output, platform) elif os == "osx": return self.buildPKG(output, platform) elif os == "freebsd": return self.buildDEB(output, platform) Installer.notify.info("Ignoring unknown platform " + platform) def buildDEB(self, output, platform): """ Builds a .deb archive and stores it in the path indicated by the 'output' argument. It will be built for the architecture specified by the 'arch' argument. If 'output' is a directory, the deb file will be stored in it. """ arch = platform.rsplit("_", 1)[-1] output = Filename(output) if output.isDirectory(): output = Filename(output, "%s_%s_%s.deb" % (self.shortname.lower(), self.version, arch)) Installer.notify.info("Creating %s..." % output) # Create a temporary directory and write the control file + launcher to it tempdir = Filename.temporary("", self.shortname.lower() + "_deb_", "") + "/" tempdir.makeDir() controlfile = open(Filename(tempdir, "control").toOsSpecific(), "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.write("Depends: libc6 libgcc1 libstdc++6 libx11-6\n") controlfile.close() Filename(tempdir, "usr/bin/").makeDir() self.standalone.build(Filename(tempdir, "usr/bin/" + self.shortname.lower()), platform) if not self.licensefile.empty(): Filename(tempdir, "usr/share/doc/%s/" % self.shortname.lower()).makeDir() shutil.copyfile(self.licensefile.toOsSpecific(), Filename(tempdir, "usr/share/doc/%s/copyright" % self.shortname.lower()).toOsSpecific()) # Create a control.tar.gz file in memory controlfile = Filename(tempdir, "control") controltargz = CachedFile() controltarfile = tarfile.TarFile.gzopen("control.tar.gz", "w", controltargz, 9) controltarfile.add(controlfile.toOsSpecific(), "control") controltarfile.close() controlfile.unlink() # Create the data.tar.gz file in the temporary directory datatargz = CachedFile() datatarfile = tarfile.TarFile.gzopen("data.tar.gz", "w", datatargz, 9) datatarfile.add(Filename(tempdir, "usr").toOsSpecific(), "/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 output.exists(): output.unlink() debfile = open(output.toOsSpecific(), "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.toOsSpecific()) def buildAPP(self, output, platform): output = Filename(output) if output.isDirectory() and output.getExtension() != 'app': output = Filename(output, "%s.app" % self.fullname) Installer.notify.info("Creating %s..." % output) # Create the executable for the application bundle exefile = Filename(output, "Contents/MacOS/" + self.shortname) exefile.makeDir() self.standalone.build(exefile, platform) # 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(Filename(output, "Contents/Info.plist").toOsSpecific(), "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, '\t%s' % exefile.getBasename() 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() return output def buildPKG(self, output, platform): appfn = self.buildAPP(output, platform) appname = "/Applications/" + appfn.getBasename() output = Filename(output) if output.isDirectory(): output = Filename(output, "%s %s.pkg" % (self.fullname, self.version)) Installer.notify.info("Creating %s..." % output) Filename(output, "Contents/Resources/en.lproj/").makeDir() if self.licensefile: shutil.copyfile(self.licensefile.toOsSpecific(), Filename(output, "Contents/Resources/License.txt").toOsSpecific()) pkginfo = open(Filename(output, "Contents/PkgInfo").toOsSpecific(), "w") pkginfo.write("pkmkrpkg1") pkginfo.close() pkginfo = open(Filename(output, "Contents/Resources/package_version").toOsSpecific(), "w") pkginfo.write("major: 1\nminor: 9") pkginfo.close() # Although it might make more sense to use Python's plistlib here, # it is not available on non-OSX systems before Python 2.6. plist = open(Filename(output, "Contents/Info.plist").toOsSpecific(), "w") plist.write('\n') plist.write('\n') plist.write('\n') plist.write('\n') plist.write('\tCFBundleIdentifier\n') plist.write('\t%s.pkg.%s\n' % (self.authorid, self.shortname)) plist.write('\tCFBundleShortVersionString\n') plist.write('\t%s\n' % self.version) plist.write('\tIFMajorVersion\n') plist.write('\t1\n') plist.write('\tIFMinorVersion\n') plist.write('\t9\n') plist.write('\tIFPkgFlagAllowBackRev\n') plist.write('\t\n') plist.write('\tIFPkgFlagAuthorizationAction\n') plist.write('\tRootAuthorization\n') plist.write('\tIFPkgFlagDefaultLocation\n') plist.write('\t/\n') plist.write('\tIFPkgFlagFollowLinks\n') plist.write('\t\n') plist.write('\tIFPkgFlagIsRequired\n') plist.write('\t\n') plist.write('\tIFPkgFlagOverwritePermissions\n') plist.write('\t\n') plist.write('\tIFPkgFlagRelocatable\n') plist.write('\t\n') plist.write('\tIFPkgFlagRestartAction\n') plist.write('\tNone\n') plist.write('\tIFPkgFlagRootVolumeOnly\n') plist.write('\t\n') plist.write('\tIFPkgFlagUpdateInstalledLanguages\n') plist.write('\t\n') plist.write('\tIFPkgFormatVersion\n') plist.write('\t0.10000000149011612\n') plist.write('\tIFPkgPathMappings\n') plist.write('\t\n') plist.write('\t\t%s\n' % appname) plist.write('\t\t{pkmk-token-2}\n') plist.write('\t\n') plist.write('\n') plist.write('\n') plist.close() plist = open(Filename(output, "Contents/Resources/TokenDefinitions.plist").toOsSpecific(), "w") plist.write('\n') plist.write('\n') plist.write('\n') plist.write('\n') plist.write('\tpkmk-token-2\n') plist.write('\t\n') plist.write('\t\t\n') plist.write('\t\t\tidentifier\n') plist.write('\t\t\t%s.%s\n' % (self.authorid, self.shortname)) plist.write('\t\t\tpath\n') plist.write('\t\t\t%s\n' % appname) plist.write('\t\t\tsearchPlugin\n') plist.write('\t\t\tCommonAppSearch\n') plist.write('\t\t\n') plist.write('\t\n') plist.write('\n') plist.write('\n') plist = open(Filename(output, "Contents/Resources/en.lproj/Description.plist").toOsSpecific(), "w") plist.write('\n') plist.write('\n') plist.write('\n') plist.write('\n') plist.write('\tIFPkgDescriptionDescription\n') plist.write('\t\n') plist.write('\tIFPkgDescriptionTitle\n') plist.write('\t%s\n' % self.fullname) plist.write('\n') plist.write('\n') if hasattr(tarfile, "PAX_FORMAT"): archive = tarfile.open(Filename(output, "Contents/Archive.pax.gz").toOsSpecific(), "w:gz", format = tarfile.PAX_FORMAT) else: archive = tarfile.open(Filename(output, "Contents/Archive.pax.gz").toOsSpecific(), "w:gz") archive.add(appfn.toOsSpecific(), appname) archive.close() # Put the .pkg into a zipfile archive = Filename(output.getDirname(), "%s %s.zip" % (self.fullname, self.version)) dir = Filename(output.getDirname()) dir.makeAbsolute() zip = zipfile.ZipFile(archive.toOsSpecific(), 'w') for root, dirs, files in os.walk(output.toOsSpecific()): for name in files: file = Filename.fromOsSpecific(os.path.join(root, name)) file.makeAbsolute() file.makeRelativeTo(dir) zip.write(os.path.join(root, name), str(file)) zip.close() return output def buildNSIS(self, output, platform): # 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 output = Filename(output) if output.isDirectory(): output = Filename(output, "%s %s.exe" % (self.fullname, self.version)) Installer.notify.info("Creating %s..." % output) output.makeAbsolute() extrafiles = self.standalone.getExtraFiles(platform) exefile = Filename(Filename.getTempDirectory(), self.shortname + ".exe") exefile.unlink() self.standalone.build(exefile, platform) nsifile = Filename(Filename.getTempDirectory(), self.shortname + ".nsi") nsifile.unlink() nsi = open(nsifile.toOsSpecific(), "w") # Some global info nsi.write('Name "%s"\n' % self.fullname) nsi.write('OutFile "%s"\n' % output.toOsSpecific()) 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.exe"\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 "" SecCore\n') nsi.write(' SetOutPath "$INSTDIR"\n') nsi.write(' File "%s"\n' % exefile.toOsSpecific()) for f in extrafiles: nsi.write(' File "%s"\n' % f.toOsSpecific()) 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\%s.exe"\n' % self.shortname) for f in extrafiles: nsi.write(' Delete "%s"\n' % f.getBasename()) 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 += " " + nsifile.toOsSpecific() os.system(cmd) nsifile.unlink() return output