#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2009 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#      http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
makepkginfo

Created by Greg Neagle on 2008-11-25.
Creates a managed install pkg info plist given an Installer item:
a .pkg, a .mpkg, or a .dmg containing a .pkg or .mpkg
at the root of the mounted disk image.

You may also pass additional items that are installed by the package. These
are added to the 'installs' key of the catalog item plist and are used when 
processing the catalog to check if the package needs to be installed or 
reinstalled.

The generated plist is printed to STDOUT.

Usage: makepkginfo /path/to/package_or_dmg [-f /path/to/item/it/installs ...]
"""

import sys
import os
import re
import optparse
from distutils import version
import subprocess
import hashlib

from munkilib import munkicommon
from munkilib import FoundationPlist

def getCatalogInfoFromDmg(dmgpath):
    """
    * Mounts a disk image 
    * Gets catalog info for the first installer item found at the root level.
    * Unmounts the disk image
    
    To-do: handle multiple installer items on a disk image
    """
    cataloginfo = None
    mountpoints = munkicommon.mountdmg(dmgpath)
    for mountpoint in mountpoints:
        for fsitem in os.listdir(mountpoint):
            itempath = os.path.join(mountpoint, fsitem)
            if itempath.endswith('.pkg') or itempath.endswith('.mpkg'):
                cataloginfo = munkicommon.getPackageMetaData(itempath)
                # get out of fsitem loop
                break
        if cataloginfo:
            # get out of mountpoint loop
            break
            
    #unmount all the mountpoints from the dmg
    for mountpoint in mountpoints:
        munkicommon.unmountdmg(mountpoint)
    return cataloginfo   


def getBundleInfo(path):
    """
    Returns Info.plist data if available
    for bundle at path
    """
    infopath = os.path.join(path, "Contents", "Info.plist")
    if not os.path.exists(infopath):
        infopath = os.path.join(path, "Resources", "Info.plist")
        
    if os.path.exists(infopath):
        try:
            pl = FoundationPlist.readPlist(infopath)
            return pl
        except:
            pass

    return None


def getmd5hash(filename):
    """
    Returns hex of MD5 checksum of a file
    """
    if not os.path.isfile(filename):
        return "NOT A FILE"

    f = open(filename, 'rb')
    m = hashlib.md5()
    while 1:
        chunk = f.read(2**16)
        if not chunk:
            break
        m.update(chunk)
    f.close()
    return m.hexdigest()


def getiteminfo(itempath):
    """
    Gets info for filesystem items passed to makecatalog item, to be used for
    the "installs" key.
    Determines if the item is an application, bundle, Info.plist, or a file or directory
    and gets additional metadata for later comparison.
    """
    infodict = {}
    if itempath.endswith('.app'):
        infodict['type'] = 'application'
        infodict['path'] = itempath
        pl = getBundleInfo(itempath)
        if 'CFBundleName' in pl:
            infodict['CFBundleName'] = pl['CFBundleName']
        if 'CFBundleIdentifier' in pl:
            infodict['CFBundleIdentifier'] = pl['CFBundleIdentifier']
        if 'CFBundleShortVersionString' in pl:
            infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
        elif 'CFBundleVersion' in pl:
            infodict['CFBundleShortVersionString'] = pl['CFBundleVersion']
        if 'LSMinimumSystemVersion' in pl:
            infodict['minosversion'] = pl['LSMinimumSystemVersion']
        elif 'SystemVersionCheck:MinimumSystemVersion' in pl:
            infodict['minosversion'] = pl['SystemVersionCheck:MinimumSystemVersion']
            
    elif os.path.exists(os.path.join(itempath,'Contents','Info.plist')) or os.path.exists(os.path.join(itempath,'Resources','Info.plist')):
        infodict['type'] = 'bundle'
        infodict['path'] = itempath
        pl = getBundleInfo(itempath)
        if 'CFBundleShortVersionString' in pl:
            infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
        elif 'CFBundleVersion' in pl:
            infodict['CFBundleShortVersionString'] = pl['CFBundleVersion']
            
    elif itempath.endswith("Info.plist") or itempath.endswith("version.plist"):
        infodict['type'] = 'plist'
        infodict['path'] = itempath
        try:
            pl = FoundationPlist.readPlist(itempath)
            if 'CFBundleShortVersionString' in pl:
                infodict['CFBundleShortVersionString'] = pl['CFBundleShortVersionString']
            elif 'CFBundleVersion' in pl:
                infodict['CFBundleShortVersionString'] = pl['CFBundleVersion']
        except:
            pass
        
    if not 'CFBundleShortVersionString' in infodict:
        infodict['type'] = 'file'
        infodict['path'] = itempath
        if os.path.isfile(itempath):
            infodict['md5checksum'] = getmd5hash(itempath)
    return infodict
        


def main():
    usage = "usage: %prog [options] /path/to/installeritem"
    p = optparse.OptionParser(usage=usage)
    p.add_option('--file', '-f', action="append",
                    help='Path to a filesystem item installed by this package. Can be specified multiple times.')
    options, arguments = p.parse_args()
    if len(arguments) == 0:
        print >>sys.stderr, "Need to specify an installer item (.pkg, .mpkg, .dmg)!"
        exit(-1)
    
    if len(arguments) > 1:
        print >>sys.stderr, "Can process only one installer item at a time. Ignoring additional installer items."
        
    item = arguments[0].rstrip("/")
    if os.path.exists(item):
        # get size of installer item
        itemsize = 0 
        if os.path.isfile(item):
            itemsize = int(os.path.getsize(item))
        if os.path.isdir(item):
            # need to walk the dir and add it all up
            for (path, dirs, files) in os.walk(item):
                for f in files:
                    filename = os.path.join(path, f)
                    # use os.lstat so we don't follow symlinks
                    itemsize += int(os.lstat(filename).st_size)
        
        if item.endswith('.dmg'):
            catinfo = getCatalogInfoFromDmg(item)
            if not catinfo:
                print >>sys.stderr, "Could not find a supported installer item in %s!" % item
                exit(-1)
            
        elif item.endswith('.pkg') or item.endswith('.mpkg'):
            catinfo = munkicommon.getPackageMetaData(item)
            if not catinfo:
                print >>sys.stderr, "%s doesn't appear to be a valid installer item!" % item
                exit(-1)
            
        else:
            print >>sys.stderr, "%s is not an installer package!" % item
            exit(-1)
            
        if catinfo:
            catinfo['installer_item_size'] = int(itemsize/1024)
            minosversion = ""
            if options.file:
                installs = []           
                for fitem in options.file:
                    # no trailing slashes, please.
                    fitem = fitem.rstrip('/')
                    if fitem.startswith('/Library/Receipts'):
                        # no receipts, please!
                        print >>sys.stderr, "Item %s appears to be a receipt. Skipping." % fitem
                        continue
                    if os.path.exists(fitem):
                        iteminfodict = getiteminfo(fitem)
                        if 'minosversion' in iteminfodict:
                            thisminosversion = iteminfodict.pop('minosversion')
                            if not minosversion:
                                minosversion = thisminosversion
                            elif version.LooseVersion(thisminosversion) < version.LooseVersion(minosversion):
                                minosversion = thisminosversion
                        installs.append(iteminfodict)
                    else:
                        print >>sys.stderr, "Item %s doesn't exist. Skipping." % fitem
                catinfo['installs'] = installs   
                    
            # try to generate the correct item location
            temppath = item
            location = ""
            while len(temppath) > 4:
                if temppath.endswith('/pkgs'):
                    location = item[len(temppath)+1:]
                    break
                else:
                    temppath = os.path.dirname(temppath)
                    
            if not location:
                #just the filename
                location = os.path.split(item)[1]
            catinfo['installer_item_location'] = location
            
            if minosversion:
                catinfo['minimum_os_version'] = minosversion
            else:
                catinfo['minimum_os_version'] = "10.4.0"
                
            # some metainfo
            catinfo['catalogs'] = ['testing']
            catinfo['uninstallable'] = True
            catinfo['uninstall_method'] = "removepackages"
            
            # and now, what we've all been waiting for...
            print FoundationPlist.writePlistToString(catinfo)


if __name__ == '__main__':
	main()

