Loads of new improvements to pdeploy. It can now automatically stuff required packages into the installer, so that no internet connection is needed to run the resulting games

This commit is contained in:
rdb 2009-12-30 15:35:01 +00:00
parent 51378e31a0
commit 7363098027
2 changed files with 149 additions and 23 deletions

View File

@ -4,9 +4,9 @@ to build for as many platforms as possible. """
__all__ = ["Standalone", "Installer"]
import os, sys, subprocess, tarfile, shutil, time, zipfile
import os, sys, subprocess, tarfile, shutil, time, zipfile, glob
from direct.directnotify.DirectNotifyGlobal import *
from pandac.PandaModules import PandaSystem, HTTPClient, Filename, VirtualFileSystem
from pandac.PandaModules import PandaSystem, HTTPClient, Filename, VirtualFileSystem, Multifile, readXmlStream
from direct.p3d.HostInfo import HostInfo
from direct.showbase.AppRunnerGlobal import appRunner
@ -27,11 +27,11 @@ class Standalone:
self.tokens = tokens
if appRunner:
self.host = appRunner.getHost("http://runtime.panda3d.org")
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.host = HostInfo("http://runtime.panda3d.org/", hostDir = hostDir, asMirror = False, perPlatform = True)
self.http = HTTPClient.getGlobalPtr()
if not self.host.readContentsFile():
@ -156,11 +156,83 @@ class Installer:
self.shortname = shortname
self.fullname = fullname
self.version = str(version)
self.includeRequires = False
self.licensename = ""
self.authorid = "org.panda3d"
self.authorname = ""
self.licensefile = Filename()
self.standalone = Standalone(p3dfile, tokens)
self.http = self.standalone.http
# Load the p3d file to read out the required packages
mf = Multifile()
if not mf.openRead(p3dfile):
Installer.notify.error("Not a Panda3D application: %s" % (p3dFilename))
return
# Now load the p3dInfo file.
self.hostUrl = PandaSystem.getPackageHostUrl()
if not self.hostUrl:
self.hostUrl = self.standalone.host.hostUrl
self.requirements = []
i = mf.findSubfile('p3d_info.xml')
if i >= 0:
stream = mf.openReadSubfile(i)
p3dInfo = readXmlStream(stream)
mf.closeReadSubfile(stream)
if p3dInfo:
p3dPackage = p3dInfo.FirstChildElement('package')
p3dHost = p3dPackage.FirstChildElement('host')
if p3dHost.Attribute('url'):
self.hostUrl = p3dHost.Attribute('url')
p3dRequires = p3dPackage.FirstChildElement('requires')
while p3dRequires:
self.requirements.append((p3dRequires.Attribute('name'), p3dRequires.Attribute('version')))
p3dRequires = p3dRequires.NextSiblingElement('requires')
def installPackagesInto(self, rootDir, platform):
""" Installs the packages required by the .p3d file into
the specified root directory, for the given platform. """
if not self.includeRequires:
return
host = HostInfo(self.hostUrl, rootDir = rootDir, asMirror = False)
if not host.readContentsFile():
if not host.downloadContentsFile(self.http):
Installer.notify.error("couldn't read host")
return
for name, version in self.requirements:
package = host.getPackage(name, version, 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
# Also install the 'images' package from the same host that p3dembed was downloaded from.
host = HostInfo(self.standalone.host.hostUrl, rootDir = rootDir, asMirror = False)
if not host.readContentsFile():
if not host.downloadContentsFile(self.http):
Installer.notify.error("couldn't read host")
return
for package in host.getPackages(name = "images"):
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
break
# Remove the original multifiles. We don't need them and they take up space.
for mf in glob.glob(Filename(rootDir, "hosts/*/*/*.mf").toOsSpecific()):
os.remove(mf)
for mf in glob.glob(Filename(rootDir, "hosts/*/*/*/*.mf").toOsSpecific()):
os.remove(mf)
def buildAll(self, outputDir = "."):
""" Creates a (graphical) installer for every known platform.
@ -216,27 +288,43 @@ class Installer:
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")
print >>controlfile, "Package: %s" % self.shortname.lower()
print >>controlfile, "Version: %s" % self.version
print >>controlfile, "Section: games"
print >>controlfile, "Priority: optional"
print >>controlfile, "Architecture: %s" % arch
print >>controlfile, "Description: %s" % self.fullname
print >>controlfile, "Depends: libc6, libgcc1, libstdc++6, libx11-6"
controlfile.close()
postrmfile = open(Filename(tempdir, "postrm").toOsSpecific(), "w")
print >>postrmfile, "#!/bin/sh"
print >>postrmfile, "rm -rf /usr/share/%s" % self.shortname.lower()
postrmfile.close()
os.chmod(Filename(tempdir, "postrm").toOsSpecific(), 0755)
Filename(tempdir, "usr/bin/").makeDir()
self.standalone.tokens["root_dir"] = "/usr/share/" + self.shortname.lower()
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())
rootDir = Filename(tempdir, "usr/share/" + self.shortname.lower())
rootDir.makeDir()
Filename(rootDir, "log").makeDir()
Filename(rootDir, "prc").makeDir()
Filename(rootDir, "start").makeDir()
Filename(rootDir, "certs").makeDir()
self.installPackagesInto(rootDir, platform)
# Create a control.tar.gz file in memory
controlfile = Filename(tempdir, "control")
postrmfile = Filename(tempdir, "postrm")
controltargz = CachedFile()
controltarfile = tarfile.TarFile.gzopen("control.tar.gz", "w", controltargz, 9)
controltarfile.add(controlfile.toOsSpecific(), "control")
controltarfile.add(postrmfile.toOsSpecific(), "postrm")
controltarfile.close()
controlfile.unlink()
postrmfile.unlink()
# Create the data.tar.gz file in the temporary directory
datatargz = CachedFile()
@ -272,7 +360,11 @@ class Installer:
# Create the executable for the application bundle
exefile = Filename(output, "Contents/MacOS/" + self.shortname)
exefile.makeDir()
self.standalone.tokens["root_dir"] = "../Resources"
self.standalone.build(exefile, platform)
rootDir = Filename(output, "Contents/Resources/")
rootDir.makeDir()
self.installPackagesInto(rootDir, platform)
# Create the application plist file.
# Although it might make more sense to use Python's plistlib module here,
@ -459,8 +551,14 @@ class Installer:
exefile = Filename(Filename.getTempDirectory(), self.shortname + ".exe")
exefile.unlink()
self.standalone.tokens["root_dir"] = "."
self.standalone.build(exefile, platform)
# Temporary directory to store the rootdir in
rootDir = Filename.temporary("", self.shortname.lower() + "_exe_", "") + "/"
rootDir.makeDir()
self.installPackagesInto(rootDir, platform)
nsifile = Filename(Filename.getTempDirectory(), self.shortname + ".nsi")
nsifile.unlink()
nsi = open(nsifile.toOsSpecific(), "w")
@ -468,7 +566,7 @@ class Installer:
# 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('InstallDir "$PROGRAMFILES\\%s"\n' % self.fullname)
nsi.write('SetCompress auto\n')
nsi.write('SetCompressor lzma\n')
nsi.write('ShowInstDetails nevershow\n')
@ -479,7 +577,7 @@ class Installer:
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(' ExecShell "open" "$INSTDIR\\%s.exe"\n' % self.shortname)
nsi.write('FunctionEnd\n')
nsi.write('\n')
nsi.write('!include "MUI2.nsh"\n')
@ -508,25 +606,37 @@ class Installer:
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')
curdir = ""
for root, dirs, files in os.walk(rootDir.toOsSpecific()):
for name in files:
file = Filename.fromOsSpecific(os.path.join(root, name))
if file.getExtension().lower() == "mf": continue
file.makeAbsolute()
file.makeRelativeTo(rootDir)
outdir = file.getDirname().replace('/', '\\')
if curdir != outdir:
nsi.write(' SetOutPath "$INSTDIR\\%s"\n' % outdir)
curdir = outdir
nsi.write(' File "%s"\n' % os.path.join(root, name))
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(' 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)
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(' Delete "$INSTDIR\\Uninstall.exe"\n')
nsi.write(' RMDir /r "$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(' Delete "$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk"\n')
nsi.write(' RMDir "$SMPROGRAMS\\$StartMenuFolder"\n')
nsi.write('SectionEnd')
nsi.close()
@ -541,4 +651,5 @@ class Installer:
os.system(cmd)
nsifile.unlink()
shutil.rmtree(rootDir.toOsSpecific())
return output

View File

@ -6,6 +6,7 @@ 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.
Note that pdeploy requires an internet connection to run.
Usage:
@ -68,11 +69,22 @@ Options:
Examples of valid platforms are win32, linux_amd64 and osx_ppc.
-c
If this option is provided, the -p option is ignored and
If this option is provided, any -P options are ignored and
the p3d package is only deployed for the current platform.
Furthermore, no per-platform subdirectories will be created
inside the output dirctory.
-s
This option only has effect in 'installer' mode. If it is
provided, the resulting installers will be fully self-contained,
will not require an internet connection to run, and start up
much faster. Note that pdeploy will also take a very long time
to finish when -s is provided.
If it is omitted, pdeploy will finish much quicker, and the
resulting installers will be smaller, but they will require
an internet connection for the first run, and the load time
will be considerably longer.
-l "License Name"
Specifies the name of the software license that the game
or application is licensed under.
@ -121,9 +133,10 @@ licensename = ""
licensefile = Filename()
authorid = ""
authorname = ""
includeRequires = False
try:
opts, args = getopt.getopt(sys.argv[1:], 'n:N:v:o:t:P:cl:L:h')
opts, args = getopt.getopt(sys.argv[1:], 'n:N:v:o:t:P:csl:L:a:A:h')
except getopt.error, msg:
usage(1, msg)
@ -143,6 +156,8 @@ for opt, arg in opts:
platforms.append(arg)
elif opt == '-c':
currentPlatform = True
elif opt == '-s':
includeRequires = True
elif opt == '-l':
licensename = arg.strip()
elif opt == '-L':