#!/usr/bin/env python # encoding: utf-8 # # Copyright 2009-2014 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 # # https://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. """ transcript2pkg.py Created by Greg Neagle on 2009-06-04. """ import sys import os import stat import plistlib import subprocess import optparse import tempfile # change these to suit yourself packagemaker = "/Developer/usr/bin/packagemaker" t2pkg = "/Users/Shared/bin/t2pkg" pkgidprefix = "com.myorg.pkg." pkgoutputdir = "/Users/Shared/pkgs" def nameAndVersion(s): """ Splits a string into the name and version numbers: 'TextWrangler2.3b1' becomes ('TextWrangler', '2.3b1') 'AdobePhotoshopCS3-11.2.1' becomes ('AdobePhotoshopCS3', '11.2.1') 'MicrosoftOffice2008v12.2.1' becomes ('MicrosoftOffice2008', '12.2.1') """ index = 0 for char in s: if char in "0123456789": possibleVersion = s[index:] if not (" " in possibleVersion or "_" in possibleVersion or "-" in possibleVersion or "v" in possibleVersion): return (s[0:index].rstrip(" .-_v"), possibleVersion) index += 1 # no version number found, just return original string and empty string return (s, '') def makeDMG(pkgpath): print "Making disk image..." cmd = ["/usr/bin/hdiutil", "create", "-srcfolder", pkgpath, pkgpath + ".dmg"] p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: output = p.stdout.readline() if not output and (p.poll() != None): break print output.rstrip("\n") sys.stdout.flush() retcode = p.poll() if retcode: print >>sys.stderr, "Disk image creation failed." return(-1) def main(): # command-line options p = optparse.OptionParser() p.add_option('--makedmg', '-d', action='store_true', help='Makes a disk image containing the package.') p.add_option('--name', '-n', help='Specify a name for the package.') p.add_option('--version', '-v', help='Specify a version number for the package.') p.add_option('--id', help='Specify a package id for the package.') p.add_option('--displayname', help='Specify a display name for the package.') p.add_option('--description', help='Specify a description for the package.') p.add_option('--restart', action='store_true', help='Restart is required on install.') p.add_option('--logout', action='store_true', help='Logout is required to install.') # Get our options and our package names options, transcripts = p.parse_args() if not os.path.exists(packagemaker): print >>sys.stderr, "packagemaker tool not found at %s." % \ packagemaker exit(-1) if not os.path.exists(t2pkg): print >>sys.stderr, "t2pkg tool not found at %s." % t2pkg exit(-1) if len(transcripts) == 0: print >>sys.stderr, "You must specify a transcript!" exit(-1) if len(transcripts) > 1: print >>sys.stderr, "You may convert only one transcript at a time." exit(-1) transcript = transcripts[0] if transcript.find("/") == -1: # transcript name only; we'll prepend the standard # client path transcript = os.path.join("/var/radmind/client", transcript) if not os.path.exists(transcript): print "No transcript file at %s" % transcript exit(-1) mytmpdir = tempfile.mkdtemp() if options.name: pkgname = options.name else: pkgname = os.path.splitext(os.path.basename(transcript))[0] # make packageroot directory using t2pkg packageroot = os.path.join(mytmpdir, pkgname) cmd = [t2pkg, '-d', packageroot, '-r', '/', transcript] p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, err) = p.communicate() if p.returncode != 0: print >>sys.stderr, err exit(-1) if err: print >>sys.stderr, err exit(-1) # fix uid/gid/perms on directories for dirpath, dirnames, filenames in os.walk(packageroot): srcdir = dirpath[len(packageroot):] if srcdir == "": srcdir = "/" os.chown(dirpath, os.stat(srcdir).st_uid, os.stat(srcdir).st_gid) os.chmod(dirpath, stat.S_IMODE(os.stat(srcdir).st_mode)) # some default values (name, versionInName) = nameAndVersion(pkgname) if options.id: pkgid = options.id else: pkgid = pkgidprefix + name.lower() if options.version: pkgvers = options.version elif versionInName: pkgvers = versionInName else: pkgvers = "1.0.0" infopath = "" # look through packageroot dir for Receipts pkgrootreceipts = os.path.join(packageroot, "Library/Receipts") if os.path.exists(pkgrootreceipts): receiptlist = os.listdir(pkgrootreceipts) if len(receiptlist) == 1: receipt = os.path.join(pkgrootreceipts, receiptlist[0]) infopath = os.path.join(receipt,"Contents/Info.plist") if os.path.exists(infopath): print "Using package info from %s" % infopath else: infopath = "" else: print >>sys.stderr, \ "Found multiple receipts, so cannot determine pkgid and version." if not infopath: # look for a single application bundle and get info from that appinfo = "" for dirpath, dirnames, filenames in os.walk(packageroot): if dirpath.endswith('.app'): if not appinfo: appinfo = os.path.join(dirpath, "Contents/Info.plist") if not os.path.exists(appinfo): appinfo = "" else: # crap, found more than one. appinfo = "" print >>sys.stderr, \ "Found multiple application bundles, so cannot determine pkgid and version." break if appinfo: pl = plistlib.readPlist(appinfo) if "CFBundleIdentifier" in pl and not options.id: pkgid = pl["CFBundleIdentifier"] + ".pkg" if "CFBundleShortVersionString" in pl and not options.version: pkgvers = pl["CFBundleShortVersionString"] print "Using pkgid: %s, version: %s from %s" % (pkgid, pkgvers, appinfo) else: # let's look for any Contents/Info.plist infoplist = "" for dirpath, dirnames, filenames in os.walk(packageroot): if dirpath.endswith("/Contents") and \ "Info.plist" in filenames: if not infoplist: infoplist = os.path.join(dirpath, "Info.plist") if not os.path.exists(infoplist): infoplist = "" else: # found more than one Info.plist infoplist = "" break if infoplist: pl = plistlib.readPlist(infoplist) if "CFBundleIdentifier" in pl and not options.id: pkgid = pl["CFBundleIdentifier"] + ".pkg" if "CFBundleShortVersionString" in pl and not options.version: pkgvers = pl["CFBundleShortVersionString"] print "Using pkgid: %s, version: %s from %s" % (pkgid, pkgvers, infoplist) print "Package name: %s" % pkgname newdisplayname = raw_input("Display name [%s]: " % options.displayname) options.displayname = newdisplayname or options.displayname newdescription = raw_input("Description [%s]: " % options.description) options.description = newdescription or options.description newid = raw_input("PackageID [%s]: " % pkgid) pkgid = newid or pkgid newversion = raw_input("Version [%s]: " % pkgvers) pkgvers = newversion or pkgvers print print print "Package name: %s" % pkgname print "Display name: %s" % options.displayname print "Description: %s" % options.description print "PackageID: %s" % pkgid print "Version: %s" % pkgvers print answer = raw_input("Build the package? [y/n] ") if not answer.lower().startswith("y"): exit(0) # build package outputname = os.path.join(pkgoutputdir, pkgname + ".pkg") if os.path.exists(outputname): retcode = subprocess.call(["/bin/rm", "-rf", outputname]) if infopath: cmd = [packagemaker, '--root', packageroot, '--info', infopath, '--no-recommend', '--out', outputname, '--verbose', '--filter', 'Library/Receipts', '--filter', '.DS_Store$', '--filter', 'Library/FA/Applications'] else: cmd = [packagemaker, '--root', packageroot, '--id', pkgid, '--version', pkgvers, '--out', outputname, '--verbose', '--no-recommend', '--filter', 'Library/Receipts', '--filter', '.DS_Store$', '--filter', 'Library/FA/Applications'] print cmd p = subprocess.Popen(cmd, shell=False, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: output = p.stdout.readline() if not output and (p.poll() != None): break print output.rstrip("\n") sys.stdout.flush() retcode = p.poll() if retcode: print >>sys.stderr, "Package creation failed." exit(-1) else: # remove relocatable stuff tokendefinitions = os.path.join(outputname, "Contents/Resources/TokenDefinitions.plist") if os.path.exists(tokendefinitions): os.remove(tokendefinitions) infoplist = os.path.join(outputname, "Contents/Info.plist") pl = plistlib.readPlist(infoplist) if 'IFPkgPathMappings' in pl: del pl['IFPkgPathMappings'] if options.restart: # add restart info to plist pl['IFPkgFlagRestartAction'] = "RequiredRestart" elif options.logout: # add logout info to plist pl['IFPkgFlagRestartAction'] = "RequiredLogout" plistlib.writePlist(pl, infoplist) if options.displayname or options.description: languages = ['en.lproj', 'English.lproj'] for item in languages: lprojpath = os.path.join(outputname, 'Contents/Resources', item) if os.path.exists(lprojpath): descriptionplist = os.path.join(lprojpath, "Description.plist") pl = {} pl['IFPkgDescriptionTitle'] = (options.displayname or pkgname) pl['IFPkgDescriptionDescription'] = (options.description or "") plistlib.writePlist(pl, descriptionplist) break print "Completed package is at %s" % outputname if options.makedmg: makeDMG(outputname) #cleanup temp dir retcode = subprocess.call(["/bin/rm", "-rf", mytmpdir]) if __name__ == '__main__': main()