add icon support to pdeploy, and write .desktop files on Linux
This commit is contained in:
parent
3fd427f0c9
commit
388551f4da
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue