add icon support to pdeploy, and write .desktop files on Linux

This commit is contained in:
rdb 2012-01-08 10:40:48 +00:00
parent 3fd427f0c9
commit 388551f4da
2 changed files with 259 additions and 6 deletions

View File

@ -4,12 +4,14 @@ to build for as many platforms as possible. """
__all__ = ["Standalone", "Installer"]
import os, sys, subprocess, tarfile, shutil, time, zipfile, glob, socket, getpass
import os, sys, subprocess, tarfile, shutil, time, zipfile, glob, socket, getpass, struct
from cStringIO import StringIO
from direct.directnotify.DirectNotifyGlobal import *
from direct.showbase.AppRunnerGlobal import appRunner
from pandac.PandaModules import PandaSystem, HTTPClient, Filename, VirtualFileSystem, Multifile
from pandac.PandaModules import TiXmlDocument, TiXmlDeclaration, TiXmlElement, readXmlStream
from pandac.PandaModules import PNMImage, PNMFileTypeRegistry
from direct.stdpy.file import *
from direct.p3d.HostInfo import HostInfo
# This is important for some reason
import encodings
@ -126,7 +128,8 @@ class Standalone:
# Figure out where p3dembed might be now.
if package.platform.startswith("win"):
# Use p3dembedw unless console_environment was set.
if extraTokens.get("console_environment", self.tokens.get("console_environment", 0)) != 0:
cEnv = extraTokens.get("console_environment", self.tokens.get("console_environment", 0))
if cEnv != "" and int(cEnv) != 0:
p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembed.exe" % package.platform)
else:
p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembedw.exe" % package.platform)
@ -287,6 +290,155 @@ class PackageTree:
return package
class Icon:
""" This class is used to create an icon for various platforms. """
notify = directNotify.newCategory("Icon")
def __init__(self):
self.images = {}
def addImage(self, image):
""" Adds an image to the icon. Returns False on failure, True on success.
Only one image per size can be loaded, and the image size must be square. """
if not isinstance(image, PNMImage):
fn = image
if not isinstance(fn, Filename):
fn = Filename.fromOsSpecific(fn)
image = PNMImage()
if not image.read(fn):
Icon.notify.warning("Image '%s' could not be read" % fn.getBasename())
return False
if image.getXSize() != image.getYSize():
Icon.notify.warning("Ignoring image without square size")
return False
self.images[image.getXSize()] = image
return True
def makeICO(self, fn):
""" Writes the images to a Windows ICO file. Returns True on success. """
if not isinstance(fn, Filename):
fn = Filename.fromOsSpecific(fn)
fn.setBinary()
count = 0
for size in self.images.keys():
if size <= 256:
count += 1
ico = open(fn, 'wb')
ico.write(struct.pack('<HHH', 0, 1, count))
# Write the directory
for size, image in self.images.items():
if size == 256:
ico.write('\0\0')
else:
ico.write(struct.pack('<BB', size, size))
bpp = 32 if image.hasAlpha() else 24
ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, 0, 0))
# Now write the actual icons
ptr = 14
for size, image in self.images.items():
loc = ico.tell()
bpp = 32 if image.hasAlpha() else 24
ico.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
# XOR mask
if bpp == 24:
# Align rows to 4-byte boundary
rowalign = '\0' * (-(size * 3) & 3)
for y in xrange(size):
for x in xrange(size):
r, g, b = image.getXel(x, size - y - 1)
ico.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
ico.write(rowalign)
else:
for y in xrange(size):
for x in xrange(size):
r, g, b, a = image.getXelA(x, size - y - 1)
ico.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
# Empty AND mask, aligned to 4-byte boundary
#TODO: perhaps we should convert alpha into an AND mask
# to support older versions of Windows that don't support alpha.
ico.write('\0' * (size * (size / 8 + (-((size / 8) * 3) & 3))))
# Go back to write the location
dataend = ico.tell()
ico.seek(ptr)
ico.write(struct.pack('<II', dataend - loc, loc))
ico.seek(dataend)
ptr += 16
ico.close()
return True
def makeICNS(self, fn):
""" Writes the images to an Apple ICNS file. Returns True on success. """
if not isinstance(fn, Filename):
fn = Filename.fromOsSpecific(fn)
fn.setBinary()
vfs = VirtualFileSystem.getGlobalPtr()
stream = vfs.openWriteFile(fn, False, True)
icns = open(stream, 'wb')
icns.write('icns\0\0\0\0')
icon_types = {16: 'is32', 32: 'il32', 48: 'ih32', 128: 'it32'}
mask_types = {16: 's8mk', 32: 'l8mk', 48: 'h8mk', 128: 't8mk'}
png_types = {256: 'ic08', 512: 'ic09'}
pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
for size, image in self.images.items():
if size in png_types:
if pngtype is None:
continue
icns.write(png_types[size])
icns.write('\0\0\0\0')
start = icns.tell()
image.write(stream, "", pngtype)
pngsize = icns.tell() - start
icns.seek(start - 4)
icns.write(struct.pack('>I', pngsize + 8))
icns.seek(start + pngsize)
elif size in icon_types:
icns.write(icon_types[size])
icns.write(struct.pack('>I', size * size * 4 + 8))
for y in xrange(size):
for x in xrange(size):
r, g, b = image.getXel(x, y)
icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
if not image.hasAlpha():
continue
icns.write(mask_types[size])
icns.write(struct.pack('>I', size * size + 8))
for y in xrange(size):
for x in xrange(size):
icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
length = icns.tell()
icns.seek(4)
icns.write(struct.pack('>I', length))
icns.close()
return True
class Installer:
""" This class creates a (graphical) installer from a given .p3d file. """
notify = directNotify.newCategory("Installer")
@ -303,6 +455,7 @@ class Installer:
self.authorid = "org.panda3d"
self.authorname = os.environ.get("DEBFULLNAME", "")
self.authoremail = os.environ.get("DEBEMAIL", "")
self.icon = None
# Try to determine a default author name ourselves.
uname = None
@ -413,6 +566,7 @@ class Installer:
mf.repack()
# If we have no subfiles left, we can just remove the multifile.
#XXX rdb: it seems that removing it causes trouble, so let's not.
#if mf.getNumSubfiles() == 0:
# Installer.notify.info("Removing empty archive %s" % (package.uncompressedArchive.filename))
# mf.close()
@ -505,8 +659,51 @@ class Installer:
shutil.copyfile(self.licensefile.toOsSpecific(), Filename(tempdir, "usr/share/doc/%s/copyright" % self.shortname.lower()).toOsSpecific())
shutil.copyfile(self.licensefile.toOsSpecific(), Filename(tempdir, "usr/share/doc/%s/LICENSE" % self.shortname.lower()).toOsSpecific())
# Add an image file to /usr/share/pixmaps/
iconFile = None
if self.icon is not None:
iconImage = None
if 48 in self.icon.images:
iconImage = self.icon.images[48]
elif 64 in self.icon.images:
iconImage = self.icon.images[64]
elif 32 in self.icon.images:
iconImage = self.icon.images[32]
else:
Installer.notify.warning("No suitable icon image for Linux provided, should preferably be 48x48 in size")
if iconImage is not None:
iconFile = Filename(tempdir, "usr/share/pixmaps/%s.png" % self.shortname)
iconFile.setBinary()
iconFile.makeDir()
if not iconImage.write(iconFile):
Installer.notify.warning("Failed to write icon file for Linux")
iconFile.unlink()
iconFile = None
# Write a .desktop file to /usr/share/applications/
desktopFile = Filename(tempdir, "usr/share/applications/%s.desktop" % self.shortname.lower())
desktopFile.setText()
desktopFile.makeDir()
desktop = open(desktopFile.toOsSpecific(), 'w')
print >>desktop, "[Desktop Entry]"
print >>desktop, "Name=%s" % self.fullname
print >>desktop, "Exec=%s" % self.shortname.lower()
if iconFile is not None:
print >>desktop, "Icon=%s" % iconFile.getBasename()
# Set the "Terminal" option based on whether or not a console env is requested
cEnv = self.standalone.tokens.get("console_environment", "")
if cEnv == "" or int(cEnv) == 0:
print >>desktop, "Terminal=false"
else:
print >>desktop, "Terminal=true"
print >>desktop, "Type=Application"
desktop.close()
if self.includeRequires:
hostDir = Filename(tempdir, "usr/lib/" + self.shortname.lower())
hostDir = Filename(tempdir, "usr/lib/%s/" % self.shortname.lower())
hostDir.makeDir()
self.installPackagesInto(hostDir, platform)
@ -654,6 +851,11 @@ class Installer:
hostDir.makeDir()
self.installPackagesInto(hostDir, platform)
hasIcon = False
if self.icon is not None:
Installer.notify.info("Generating %s.icns..." % self.shortname)
hasIcon = self.icon.writeICNS(Filename(hostDir, "%s.icns" % self.shortname))
# 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.
@ -668,6 +870,9 @@ class Installer:
print >>plist, '\t<string>%s</string>' % self.fullname
print >>plist, '\t<key>CFBundleExecutable</key>'
print >>plist, '\t<string>%s</string>' % exefile.getBasename()
if hasIcon:
print >>plist, '\t<key>CFBundleIconFile</key>'
print >>plist, '\t<string>%s</string>' % self.shortname
print >>plist, '\t<key>CFBundleIdentifier</key>'
print >>plist, '\t<string>%s.%s</string>' % (self.authorid, self.shortname)
print >>plist, '\t<key>CFBundleInfoDictionaryVersion</key>'
@ -860,6 +1065,16 @@ class Installer:
hostDir.makeDir()
self.installPackagesInto(hostDir, platform)
# See if we can generate an icon
icofile = None
if self.icon is not None:
icofile = Filename(Filename.getTempDirectory(), self.shortname + ".ico")
icofile.unlink()
Installer.notify.info("Generating %s.ico..." % self.shortname)
if not self.icon.writeICO(icofile):
icofile = None
# Create the .nsi installer script
nsifile = Filename(Filename.getTempDirectory(), self.shortname + ".nsi")
nsifile.unlink()
nsi = open(nsifile.toOsSpecific(), "w")
@ -882,7 +1097,10 @@ class Installer:
print >>nsi, 'FunctionEnd'
print >>nsi
print >>nsi, 'Function desktopshortcut'
print >>nsi, ' CreateShortcut "$DESKTOP\\%s.lnk" "$INSTDIR\\%s.exe"' % (self.fullname, self.shortname)
if icofile is None:
print >>nsi, ' CreateShortcut "$DESKTOP\\%s.lnk" "$INSTDIR\\%s.exe"' % (self.fullname, self.shortname)
else:
print >>nsi, ' CreateShortcut "$DESKTOP\\%s.lnk" "$INSTDIR\\%s.exe" "" "$INSTDIR\\%s.ico"' % (self.fullname, self.shortname, self.shortname)
print >>nsi, 'FunctionEnd'
print >>nsi
print >>nsi, '!include "MUI2.nsh"'
@ -914,6 +1132,8 @@ class Installer:
print >>nsi, 'Section "" SecCore'
print >>nsi, ' SetOutPath "$INSTDIR"'
print >>nsi, ' File "%s"' % exefile.toOsSpecific()
if icofile is not None:
print >>nsi, ' File "%s"' % icofile.toOsSpecific()
for f in extrafiles:
print >>nsi, ' File "%s"' % f.toOsSpecific()
curdir = ""
@ -931,7 +1151,10 @@ class Installer:
print >>nsi, ' ; Start menu items'
print >>nsi, ' !insertmacro MUI_STARTMENU_WRITE_BEGIN Application'
print >>nsi, ' CreateDirectory "$SMPROGRAMS\\$StartMenuFolder"'
print >>nsi, ' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s.exe"' % (self.fullname, self.shortname)
if icofile is None:
print >>nsi, ' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s.exe"' % (self.fullname, self.shortname)
else:
print >>nsi, ' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s.exe" "" "$INSTDIR\\%s.ico"' % (self.fullname, self.shortname, self.shortname)
print >>nsi, ' CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk" "$INSTDIR\\Uninstall.exe"'
print >>nsi, ' !insertmacro MUI_STARTMENU_WRITE_END'
print >>nsi, 'SectionEnd'
@ -939,6 +1162,8 @@ class Installer:
# This section defines the uninstaller.
print >>nsi, 'Section Uninstall'
print >>nsi, ' Delete "$INSTDIR\\%s.exe"' % self.shortname
if icofile is not None:
print >>nsi, ' Delete "$INSTDIR\\%s.ico"' % self.shortname
for f in extrafiles:
print >>nsi, ' Delete "%s"' % f.getBasename()
print >>nsi, ' Delete "$INSTDIR\\Uninstall.exe"'
@ -966,7 +1191,8 @@ class Installer:
self.notify.warning("Unable to invoke NSIS command.")
nsifile.unlink()
# TODO: should empty hostDir here to clean up after ourselves.
if icofile is not None:
icofile.unlink()
return output

View File

@ -119,6 +119,18 @@ Options:
is username@hostname.
Only relevant when generating a graphical installer.
-i "path/icon32.png" -i "path/icon48.png" -i "path/icon128.png"
This option should be repeated several times with different square
image sizes. These images will then be combined to form an icon
file that will be used to represent the installed application.
To support all platforms, it is recommended to supply images of
the sizes 16x16, 32x32, 48x48, 128x128, 256x256, and 512x512.
The larger icon sizes can be omitted can safely be omitted if
you cannot provide images in that resolution.
It is recommended to use .png images for correct transparency.
If no images are provided, no icon will be generated.
Only relevant when generating a graphical installer.
-h
Display this help
@ -150,6 +162,7 @@ licensefile = Filename()
authorid = ""
authorname = ""
authoremail = ""
iconFiles = []
includeRequires = False
try:
@ -185,6 +198,8 @@ for opt, arg in opts:
authorname = arg.strip()
elif opt == '-e':
authoremail = arg.strip()
elif opt == '-i':
iconFiles.append(Filename.fromOsSpecific(arg))
elif opt == '-h':
usage(0)
@ -264,6 +279,16 @@ elif deploy_mode == 'installer':
print "Using author \"%s\" <%s> with ID %s" % \
(i.authorname, i.authoremail, i.authorid)
# Add the supplied icon images
failed = False
for iconFile in iconFiles:
if not i.icon.addImage(iconFile):
print '\nFailed to add icon image "%s"!\n' % iconFile
failed = True
if failed:
sys.exit(1)
# Now build for the requested platforms.
if currentPlatform:
platform = PandaSystem.getPlatform()
if platform.startswith("win"):
@ -278,6 +303,8 @@ elif deploy_mode == 'installer':
output.makeDir()
i.build(output, platform)
del i
elif deploy_mode == 'html':
w, h = tokens.get("width", 640), tokens.get("height", 480)
if "data" not in tokens: