From 388551f4da6af2ae8bef9720ec00cde867861ac4 Mon Sep 17 00:00:00 2001 From: rdb Date: Sun, 8 Jan 2012 10:40:48 +0000 Subject: [PATCH] add icon support to pdeploy, and write .desktop files on Linux --- direct/src/p3d/DeploymentTools.py | 238 +++++++++++++++++++++++++++++- direct/src/p3d/pdeploy.py | 27 ++++ 2 files changed, 259 insertions(+), 6 deletions(-) diff --git a/direct/src/p3d/DeploymentTools.py b/direct/src/p3d/DeploymentTools.py index c52c7a7c6f..23bc6c79cd 100644 --- a/direct/src/p3d/DeploymentTools.py +++ b/direct/src/p3d/DeploymentTools.py @@ -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('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('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%s' % self.fullname print >>plist, '\tCFBundleExecutable' print >>plist, '\t%s' % exefile.getBasename() + if hasIcon: + print >>plist, '\tCFBundleIconFile' + print >>plist, '\t%s' % self.shortname print >>plist, '\tCFBundleIdentifier' print >>plist, '\t%s.%s' % (self.authorid, self.shortname) print >>plist, '\tCFBundleInfoDictionaryVersion' @@ -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 diff --git a/direct/src/p3d/pdeploy.py b/direct/src/p3d/pdeploy.py index bf141f5956..de4ab10f94 100644 --- a/direct/src/p3d/pdeploy.py +++ b/direct/src/p3d/pdeploy.py @@ -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: