Просмотр исходного кода

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

rdb 14 лет назад
Родитель
Сommit
388551f4da
2 измененных файлов с 259 добавлено и 6 удалено
  1. 232 6
      direct/src/p3d/DeploymentTools.py
  2. 27 0
      direct/src/p3d/pdeploy.py

+ 232 - 6
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('<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
 

+ 27 - 0
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: