#! /usr/bin/env python import os import sys import shutil import platform import tempfile from optparse import OptionParser import subprocess try: from hashlib import sha1 as sha except ImportError: from sha import sha usage = """ This command creates a graphical installer for the Panda3D plugin and runtime environment. %prog [opts]""" parser = OptionParser(usage = usage) parser.add_option('-n', '--short', dest = 'short_name', help = 'The product short name', default = 'Panda3D') parser.add_option('-N', '--long', dest = 'long_name', help = 'The product long name', default = 'Panda3D Game Engine') parser.add_option('-v', '--version', dest = 'version', help = 'The product version', default = None) parser.add_option('-p', '--publisher', dest = 'publisher', help = 'The name of the publisher', default = 'Carnegie Mellon Entertainment Technology Center') parser.add_option('', '--install', dest = 'install_dir', help = "The install directory on the user's machine (Windows only)", default = '$PROGRAMFILES\\Panda3D') parser.add_option('-l', '--license', dest = 'license', help = 'A file containing the license or EULA text', default = None) parser.add_option('-w', '--website', dest = 'website', help = 'The product website', default = 'https://www.panda3d.org') parser.add_option('', '--start', dest = 'start', help = 'Specify this option to add a start menu', action = 'store_true', default = False) parser.add_option('', '--welcome_image', dest = 'welcome_image', help = 'The image to display on the installer, 170x312 BMP', default = None) parser.add_option('', '--install_icon', dest = 'install_icon', help = 'The icon to give to the installer', default = None) parser.add_option('', '--nsis', dest = 'nsis', help = 'The path to the makensis executable', default = None) parser.add_option('', '--cab', dest = 'cab', help = 'Generate an ActiveX CAB file (Windows only). If --spc and --pvk are not also specified, the CAB file will be unsigned.', action = 'store_true', default = False) parser.add_option('', '--spc', dest = 'spc', help = 'Sign the CAB file generated by --cab with the indicated spc file (Windows only). You must also specify --pvk.', default = None) parser.add_option('', '--pvk', dest = 'pvk', help = 'Specifies the private key to be used in conjuction with --spc to sign a CAB file (Windows only).', default = None) parser.add_option('', '--mssdk', dest = 'mssdk', help = 'The path to the MS Platform SDK directory (Windows only). mssdk/bin should contain cabarc.exe and signcode.exe.', default = None) parser.add_option('', '--regview', dest = 'regview', help = 'Which registry view to use, 64 or 32.', default = None) (options, args) = parser.parse_args() this_dir = os.path.split(sys.argv[0])[0] assert options.version, "A version number must be supplied!" ############################################################################## # # This Info.plist file is used only for the OSX 10.4 version of packagemaker. # ############################################################################## Info_plist = """ CFBundleIdentifier %(package_id)s CFBundleShortVersionString %(version)s IFPkgFlagRelocatable IFPkgFlagAuthorizationAction RootAuthorization """ ############################################################################## # # This Description.plist file is used only for the OSX 10.4 version of packagemaker. # ############################################################################## Description_plist = """ IFPkgDescriptionDescription IFPkgDescriptionTitle %(long_name)s """ ############################################################################## # # Locate the relevant files. # ############################################################################## def findExecutable(filename): """ Searches for the named .exe or .dll file along the system PATH and returns its full path if found, or None if not found. """ if sys.platform == "win32": for p in os.defpath.split(";") + os.environ["PATH"].split(";"): if os.path.isfile(os.path.join(p, filename)): return os.path.join(p, filename) else: for p in os.defpath.split(":") + os.environ["PATH"].split(":"): if os.path.isfile(os.path.join(p, filename)): return os.path.join(p, filename) return None if not options.nsis: makensis = findExecutable('makensis.exe') if sys.platform == "win32": if not makensis: try: import pandac makensis = os.path.dirname(os.path.dirname(pandac.__file__)) makensis = os.path.join(makensis, "nsis", "makensis.exe") if not os.path.isfile(makensis): makensis = None except ImportError: pass if not makensis: thirdparty = os.environ.get("MAKEPANDA_THIRDPARTY", "thirdparty") makensis = os.path.join(thirdparty, "win-nsis", "makensis.exe") if not os.path.isfile(makensis): makensis = None options.nsis = makensis if not options.license: try: import pandac options.license = os.path.join(os.path.dirname(os.path.dirname(pandac.__file__)), "LICENSE") if not os.path.isfile(options.license): options.license = None except: pass if not options.license: options.license = os.path.join("doc", "LICENSE") if not os.path.isfile(options.license): options.license = None if options.license: options.license = os.path.abspath(options.license) if sys.platform == "win32" and not options.welcome_image: filename = os.path.join('models', 'plugin_images', 'installer.bmp') if not os.path.exists(filename): sys.exit("Couldn't find installer.bmp for welcome_image.") options.welcome_image = os.path.abspath(filename) def parseDependenciesWindows(tempFile): """ Reads the indicated temporary file, the output from dumpbin /dependents, to determine the list of dll's this executable file depends on. """ lines = open(tempFile, 'rU').readlines() li = 0 while li < len(lines): line = lines[li] li += 1 if line.find(' has the following dependencies') != -1: break if li < len(lines): line = lines[li] if line.strip() == '': # Skip a blank line. li += 1 # Now we're finding filenames, until the next blank line. filenames = [] while li < len(lines): line = lines[li] li += 1 line = line.strip() if line == '': # We're done. return filenames filenames.append(line) # Hmm, we ran out of data. Oh well. if not filenames: # Some parse error. return None # At least we got some data. return filenames def parseDependenciesUnix(tempFile): """ Reads the indicated temporary file, the output from otool -XL or ldd, to determine the list of dll's this executable file depends on. """ lines = open(tempFile, 'rU').readlines() filenames = [] for l in lines: filenames.append(l.strip().split(' ', 1)[0]) return filenames def addDependencies(path, pathname, file, pluginDependencies, dependentFiles, required=True): """ Checks the named file for DLL dependencies, and adds any appropriate dependencies found into pluginDependencies and dependentFiles. """ tempFile = tempfile.mktemp('.txt', 'p3d_') if sys.platform == "darwin": command = 'otool -XL "%s" >"%s"' elif sys.platform == "win32": command = 'dumpbin /dependents "%s" >"%s"' else: command = 'ldd "%s" >"%s"' command = command % (pathname, tempFile) try: os.system(command) except: pass filenames = None if os.path.isfile(tempFile): if sys.platform == "win32": filenames = parseDependenciesWindows(tempFile) else: filenames = parseDependenciesUnix(tempFile) os.unlink(tempFile) if filenames is None: sys.exit("Unable to determine dependencies from %s" % (pathname)) # Look for MSVC[RP]*.dll, and MFC*.dll. These dependent files # have to be included too. Also, any Panda-based libraries, or # the Python DLL, should be included, in case panda3d.exe wasn't # built static. The Panda-based libraries begin with "lib" and # are all lowercase, or start with libpanda/libp3d. for dfile in filenames: dfilelower = dfile.lower() if dfilelower not in dependentFiles: if dfilelower.startswith('msvc') or \ dfilelower.startswith('mfc') or \ dfilelower.startswith('zlib1') or \ (dfile.startswith('lib') and dfile == dfilelower) or \ dfilelower.startswith('libpanda') or \ dfilelower.startswith('libp3d') or \ dfilelower.startswith('python'): pathname = None for pitem in path: pathname = os.path.join(pitem, dfile) if os.path.exists(pathname): break pathname = None if not pathname: if required: sys.exit("Couldn't find %s." % (dfile)) sys.stderr.write("Warning: couldn't find %s." % (dfile)) continue pathname = os.path.abspath(pathname) dependentFiles[dfilelower] = pathname # Also recurse. addDependencies(path, pathname, file, pluginDependencies, dependentFiles) if dfilelower in dependentFiles and dfilelower not in pluginDependencies[file]: pluginDependencies[file].append(dfilelower) def getDllVersion(filename): """ Returns the DLL version number in the indicated DLL, as a string of comma-separated integers. Windows only. """ # This relies on the VBScript program in the same directory as # this script. thisdir = os.path.split(sys.argv[0])[0] versionInfo = os.path.join(thisdir, 'VersionInfo.vbs') tempfile = 'tversion.txt' tempdata = open(tempfile, 'w+') cmd = 'cscript //nologo "%s" "%s"' % (versionInfo, filename) print(cmd) result = subprocess.call(cmd, stdout = tempdata) if result: sys.exit(result) tempdata.seek(0) data = tempdata.read() tempdata.close() os.unlink(tempfile) return ','.join(data.strip().split('.')) def makeCabFile(ocx, pluginDependencies): """ Creates an ActiveX CAB file. Windows only. """ ocxFullpath = findExecutable(ocx) cabFilename = os.path.splitext(ocx)[0] + '.cab' cabarc = 'cabarc' signcode = 'signcode' if options.mssdk: cabarc = options.mssdk + '/bin/cabarc' signcode = options.mssdk + '/bin/signcode' # First, we must generate an INF file. infFile = 'temp.inf' inf = open(infFile, 'w') info.write('[Add.Code]\n%s=%s\n%s=%s\n' % (infFile, infFile, ocx, ocx)) dependencies = pluginDependencies[ocx] for filename in dependencies: inf.write('%s=%s\n' % (filename, filename)) inf.write('\n[%s]\nfile=thiscab\n' % (infFile)) inf.write('\n[%s]\nfile=thiscab\nclsid={924B4927-D3BA-41EA-9F7E-8A89194AB3AC}\nRegisterServer=yes\nFileVersion=%s\n' % (ocx, getDllVersion(ocxFullpath))) fullpaths = [] for filename in dependencies: fullpath = findExecutable(filename) fullpaths.append(fullpath) inf.write('\n[%s]\nfile=thiscab\nDestDir=11\nRegisterServer=yes\nFileVersion=%s\n' % (filename, getDllVersion(fullpath))) inf.close() # Now process the inf file with cabarc. try: os.unlink(cabFilename) except OSError: pass cmd = '"%s" -s 6144 n "%s"' % (cabarc, cabFilename) for fullpath in fullpaths: cmd += ' "%s"' % (fullpath) cmd += ' "%s" %s' % (ocxFullpath, infFile) print(cmd) result = subprocess.call(cmd) if result: sys.exit(result) if not os.path.exists(cabFilename): print("Couldn't generate %s" % (cabFilename)) sys.exit(1) print("Successfully generated %s" % (cabFilename)) if options.spc and options.pvk: # Now we have to sign the cab file. cmd = '"%s" -spc "%s" -k "%s" "%s"' % (signcode, options.spc, options.pvk, cabFilename) print(cmd) result = subprocess.call(cmd) if result: sys.exit(result) def makeInstaller(): # Locate the plugin(s). pluginFiles = {} pluginDependencies = {} dependentFiles = {} # These are the primary files that make # up the plugin/runtime. if sys.platform == "darwin": npapi = 'nppanda3d.plugin' panda3d = 'panda3d' panda3dapp = 'Panda3D.app' baseFiles = [npapi, panda3d, panda3dapp] elif sys.platform == 'win32': ocx = 'p3dactivex.ocx' npapi = 'nppanda3d.dll' panda3d = 'panda3d.exe' panda3dw = 'panda3dw.exe' baseFiles = [ocx, npapi, panda3d, panda3dw] else: baseFiles = [] path = [] pathsep = ':' if sys.platform == "win32": pathsep = ';' if 'PATH' in os.environ: path += os.environ['PATH'].split(pathsep) if sys.platform != "win32" and 'LD_LIBRARY_PATH' in os.environ: path += os.environ['LD_LIBRARY_PATH'].split(pathsep) if sys.platform == "darwin" and 'DYLD_LIBRARY_PATH' in os.environ: path += os.environ['DYLD_LIBRARY_PATH'].split(pathsep) path += os.defpath.split(pathsep) for file in baseFiles: pathname = None for pitem in path: pathname = os.path.join(pitem, file) if os.path.exists(pathname): break pathname = None if not pathname: sys.exit("Couldn't find %s." % (file)) pathname = os.path.abspath(pathname) pluginFiles[file] = pathname pluginDependencies[file] = [] if sys.platform == "win32": # Also look for the dll's that these plugins reference. addDependencies(path, pathname, file, pluginDependencies, dependentFiles) if sys.platform == "darwin": tmproot = "/var/tmp/Panda3D Runtime/" # Apparently, we have to rename this package with each # version, or Snow Leopard won't think the versions are # increasing. I don't really understand why this is so. It # might be related to the use of the Tiger PackageMaker's # output being run on Snow Leopard. Whatever. Numbering the # package files works around the problem and doesn't seem to # cause additional problems, so we'll do that. pkgname = 'p3d-setup-%s.pkg' % (options.version) if os.path.exists(tmproot): shutil.rmtree(tmproot) if os.path.isfile(pkgname): os.remove(pkgname) elif os.path.isdir(pkgname): shutil.rmtree(pkgname) if not os.path.exists(tmproot): os.makedirs(tmproot) dst_npapi = os.path.join(tmproot, "Library", "Internet Plug-Ins", npapi) dst_panda3d = os.path.join(tmproot, "usr", "local", "bin", panda3d) dst_panda3dapp = os.path.join(tmproot, "Applications", panda3dapp) if not os.path.exists(dst_npapi): os.makedirs(os.path.dirname(dst_npapi)) if not os.path.exists(dst_panda3d): os.makedirs(os.path.dirname(dst_panda3d)) if not os.path.exists(dst_panda3dapp): os.makedirs(os.path.dirname(dst_panda3dapp)) shutil.copytree(pluginFiles[npapi], dst_npapi) shutil.copyfile(pluginFiles[panda3d], dst_panda3d) os.chmod(dst_panda3d, 493) # 0o755 shutil.copytree(pluginFiles[panda3dapp], dst_panda3dapp) tmpresdir = tempfile.mktemp('', 'p3d-resources') if os.path.exists(tmpresdir): shutil.rmtree(tmpresdir) os.makedirs(tmpresdir) if options.license: shutil.copyfile(options.license, os.path.join(tmpresdir, "License.txt")) package_id = 'org.panda3d.pkg.runtime' #TODO: maybe more customizable? infoFilename = None descriptionFilename = None packagemaker = "/Applications/Xcode.app/Contents/Applications/PackageMaker.app/Contents/MacOS/PackageMaker" if not os.path.exists(packagemaker): packagemaker = "/Developer/usr/bin/packagemaker" if os.path.exists(packagemaker): # PackageMaker 3.0 or better, e.g. OSX 10.5. CMD = packagemaker CMD += ' --id "%s"' % package_id CMD += ' --version "%s"' % options.version CMD += ' --title "%s"' % options.long_name CMD += ' --out "%s"' % (pkgname) CMD += ' --target 10.5' # The earliest version of OSX supported by Panda CMD += ' --domain system' CMD += ' --root "%s"' % tmproot CMD += ' --resources "%s"' % tmpresdir CMD += ' --no-relocate' else: # PackageMaker 2.0, e.g. OSX 10.4. packagemaker = "/Developer/Tools/packagemaker" infoFilename = '/tmp/Info_plist' info = open(infoFilename, 'w') info.write(Info_plist % { 'package_id' : package_id, 'version' : options.version, }) info.close() descriptionFilename = '/tmp/Description_plist' description = open(descriptionFilename, 'w') description.write(Description_plist % { 'long_name' : options.long_name, 'short_name' : options.short_name, }) description.close() CMD = packagemaker CMD += ' -build' CMD += ' -f "%s"' % (tmproot) CMD += ' -r "%s"' % (tmpresdir) CMD += ' -p "%s"' % (pkgname) CMD += ' -i "%s"' % (infoFilename) CMD += ' -d "%s"' % (descriptionFilename) print("") print(CMD) # Don't check the exit status of packagemaker; it's not always # reliable. subprocess.call(CMD, shell = True) shutil.rmtree(tmproot) if infoFilename: os.unlink(infoFilename) if descriptionFilename: os.unlink(descriptionFilename) if os.path.exists(tmpresdir): shutil.rmtree(tmpresdir) if not os.path.exists(pkgname): print("Unable to create %s." % (pkgname)) sys.exit(1) # Pack the .pkg into a .dmg if not os.path.exists(tmproot): os.makedirs(tmproot) if os.path.isdir(pkgname): shutil.copytree(pkgname, os.path.join(tmproot, pkgname)) else: shutil.copyfile(pkgname, os.path.join(tmproot, pkgname)) tmpdmg = tempfile.mktemp('', 'p3d-setup') + ".dmg" CMD = 'hdiutil create "%s" -srcfolder "%s"' % (tmpdmg, tmproot) print("") print(CMD) result = subprocess.call(CMD, shell = True) if result: sys.exit(result) shutil.rmtree(tmproot) # Compress the .dmg (and make it read-only) if os.path.exists("p3d-setup.dmg"): os.remove("p3d-setup.dmg") CMD = 'hdiutil convert "%s" -format UDBZ -o "p3d-setup.dmg"' % tmpdmg print("") print(CMD) result = subprocess.call(CMD, shell = True) if result: sys.exit(result) elif sys.platform == 'win32': # Now build the NSIS command. CMD = "\"" + options.nsis + "\" /V3 " CMD += '/DPRODUCT_NAME="' + options.long_name + '" ' CMD += '/DPRODUCT_NAME_SHORT="' + options.short_name + '" ' CMD += '/DPRODUCT_PUBLISHER="' + options.publisher + '" ' CMD += '/DPRODUCT_WEB_SITE="' + options.website + '" ' CMD += '/DPRODUCT_VERSION="' + options.version + '" ' CMD += '/DINSTALL_DIR="' + options.install_dir + '" ' CMD += '/DLICENSE_FILE="' + options.license + '" ' CMD += '/DOCX="' + ocx + '" ' CMD += '/DOCX_PATH="' + pluginFiles[ocx] + '" ' CMD += '/DNPAPI="' + npapi + '" ' CMD += '/DNPAPI_PATH="' + pluginFiles[npapi] + '" ' CMD += '/DPANDA3D="' + panda3d + '" ' CMD += '/DPANDA3D_PATH="' + pluginFiles[panda3d] + '" ' CMD += '/DPANDA3DW="' + panda3dw + '" ' CMD += '/DPANDA3DW_PATH="' + pluginFiles[panda3dw] + '" ' if options.regview: CMD += '/DREGVIEW=%s ' % (options.regview) for i, dep in enumerate(dependentFiles.items()): CMD += '/DDEP%s="%s" ' % (i, dep[0]) CMD += '/DDEP%sP="%s" ' % (i, dep[1]) for i, dep in enumerate(pluginDependencies[npapi]): CMD += '/DNPAPI_DEP%s="%s" ' % (i, dep) if options.start: CMD += '/DADD_START_MENU ' if options.welcome_image: CMD += '/DMUI_WELCOMEFINISHPAGE_BITMAP="' + options.welcome_image + '" ' CMD += '/DMUI_UNWELCOMEFINISHPAGE_BITMAP="' + options.welcome_image + '" ' if options.install_icon: CMD += '/DINSTALL_ICON="' + options.install_icon + '" ' CMD += '"' + this_dir + '\\p3d_installer.nsi"' print("") print(CMD) print("packing...") result = subprocess.call(CMD) if result: sys.exit(result) if options.cab: # Generate a CAB file and optionally sign it. makeCabFile(ocx, pluginDependencies) makeInstaller()