Source code for waftools.bdist

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Michel Mooij, michel.mooij7@gmail.com

'''
Summary
-------
Creates archives (.tar.gz) and/or installers (e.g. NSIS) for end users 
containing all build results (i.e. artifacts).

Description
-----------
Using this tool, it is possible to create archives and installers containing
all files, libraries and binaries that are the results of the build and 
install process. The structure and prefix will be the same as has been defined
in the *waf* build environment.
At present the following distribution formats are supported:
- windows installers using NSIS 
- compressed tar.bz2 archives
- list; displays all files (including complete path) to be packaged

When selecting NSIS as distribution method, a *.nsi* script may be provided, if not 
a default *.nsi* script will be generated.


Usage
-----
For windows targets platforms installers can be created using the NullSoft
Installable Scripting system (*NSIS*). If no user defined .nsi script is 
provided a default one will be created in the top level directory of the 
build system.

The code snippet below presents a *wscript* that provides support for
creating installers using **NSIS**::

	import waftools

	def options(opt):
		opt.load('compiler_c')
		opt.load('bdist', tooldir=waftools.location)
	
	def configure(conf):
		conf.load('compiler_c')
		conf.load('bdist')

	def build(bld):
		bld.program(target='hello', source='hello.c')

Using this code snippet, a Windows installer can be created using
the following command::

	waf bdist --formats=nsis

'''



import shutil, os, sys, platform
from waflib import Build, Context, Scripting, Logs

BDIST_TYPES_DEFAULT='all'

[docs]def options(opt): '''Adds command line options to the *waf* build environment :param opt: Options context from the *waf* build environment. :type opt: waflib.Options.OptionsContext ''' opt.add_option('--bdist', dest='bdist_types', default=BDIST_TYPES_DEFAULT, action='store', help='bdist types to create (default=all)') opt.add_option('--bdist_cleanup', dest='bdist_cleanup', default=False, action='store_true', help='cleanup bdist staging area') opt.add_option('--nsis_script', dest='nsis_script', default='install.nsi', action='store', help='nsis install script (default=install.nsi)')
[docs]def configure(conf): '''Method that will be invoked by *waf* when configuring the build environment. :param conf: Configuration context from the *waf* build environment. :type conf: waflib.Configure.ConfigurationContext ''' conf.env.BDIST_TYPES = conf.options.bdist_types.split(',') conf.env.NSIS_SCRIPT = conf.options.nsis_script conf.find_program('makensis', var='NSIS', mandatory=False)
[docs]class BDistContext(Build.InstallContext): '''creates installers (e.g. NSIS) for end users.''' cmd = 'bdist' fun = 'build'
[docs] def init_dirs(self, *k, **kw): super(BDistContext, self).init_dirs(*k, **kw) self._pkgroot = self.bldnode.make_node('.bdist') try: shutil.rmtree(self._pkgroot.abspath()) except: pass if os.path.exists(self._pkgroot.abspath()): self.fatal('Could not remove temporary directory %r' % self._pkgroot) self._pkgroot.mkdir() self.options.destdir = self._pkgroot.abspath()
[docs] def execute(self, *k, **kw): '''Executes normal 'install' into a special, temporary, bdist directory and creates a bdist (e.g. tar.bz2 or rpm) file containing all installed files into the bdist directory. ''' super(BDistContext, self).execute(*k, **kw) prefix = str(self.env.PREFIX) i = prefix.find(':') if i >= 0 and (i+1) < len(prefix): prefix = prefix[i+1:] self._bdist = self._pkgroot.find_or_declare(prefix) version = getattr(Context.g_module, Context.VERSION, self.top_dir) appname = getattr(Context.g_module, Context.APPNAME, self.top_dir) variant = self.variant if self.variant else '' pkgtype = self.env.BDIST_TYPES files = self.get_files() pkg = self.options.bdist_types if pkg != BDIST_TYPES_DEFAULT: pkgtype = pkg.split(',') if set(pkgtype) & set(['all', 'ls']): self.bdist_ls(appname, variant, version, files) if set(pkgtype) & set(['all', 'tar.bz2']): self.bdist_tar_bz2(appname, variant, version) if set(pkgtype) & set(['all', 'nsis']): self.bdist_nsis(appname, version) if self.options.bdist_cleanup: shutil.rmtree(self._bdist.abspath())
[docs] def get_files(self): '''returns a list of file names to be packaged from which the PREFIX path has been stripped. ''' prefix = self._bdist.relpath().replace('\\','/') files = [] for node in self._bdist.ant_glob('**'): f = node.relpath().replace('\\','/') f = f[len(prefix):] files.append(f) return files
[docs] def bdist_ls(self, appname, variant, version, files): '''Lists all files that will be packaged. :param appname: Functional application and bdist name :type appname: str :param variant: Name of the build variant (if any) :type variant: str or None :param version: Current version of the application being packaged :type version: str :param files: List of file nodes :type files: list ''' p = Logs.info p('') p('=======================') p('BDIST = ls') p('APPNAME = %s' % appname) p('VERSION = %s' % version) p('VARIANT = %s' % variant) p('PREFIX = %s' % self.env.PREFIX) p('=======================') p('') for f in files: p('$PREFIX%s' % f) p('-----------------------')
[docs] def bdist_tar_bz2(self, appname, variant, version): '''Creates a tar.gz archive. :param appname: Functional application and bdist name :type appname: str :param variant: Name of the build variant (if any) :type variant: str or None :param version: Current version of the application being packaged :type version: str ''' name = '%s%s-%s' % (appname, variant, version) p = Logs.info p('') p('=======================') p('BDIST (tar.bz2)') p('=======================') p('PREFIX=%s' % self.env.PREFIX) ctx = Scripting.Dist() ctx.arch_name = '%s.tar.bz2' % (name) ctx.files = self._bdist.ant_glob('**') ctx.tar_prefix = '' ctx.base_path = self._pkgroot ctx.archive() p('-----------------------')
[docs] def bdist_nsis(self, appname, version): '''Creates an installer for Windows hosts using NSIS. If the install script does not exist, a default install script will be created by this module. :param appname: Functional application and bdist name :type appname: str :param version: Current version of the application being packaged :type version: str ''' nsis = self.env.NSIS if isinstance(nsis, list): if not len(nsis): Logs.warn('NSIS not available, skipping') return nsis = nsis[0] fname = self.env.NSIS_SCRIPT script = self.path.find_node(fname) if not script: script = self.nsis_create_script(fname, appname) args = [] args.append('/V4') args.append('/NOCD') args.append('/DVERSION=%s' % version) v = version.split('.') try: v_major = int(v[0]) args.append('/DVER_MAJOR=%s' % v_major) except: pass try: v_minor = int(v[1]) args.append('/DVER_MINOR=%s' % v_minor) except: pass try: v_patch = int(v[2]) args.append('/DVER_PATCH=%s' % v_patch) except: pass arch = platform.architecture() if sys.platform == 'win32' and arch[0] == '32bit': outfile = '%s-%s-win32-setup.exe' % (appname, version) else: outfile = '%s-%s-win64-setup.exe' % (appname, version) outfile = os.path.join(self.path.abspath(), outfile) args.append('/DINSTALLER=%s' % outfile) if sys.platform != 'win32': args = [a.replace('/','-',1) for a in args] cmd = '%s %s %s' % (nsis, ' '.join(args), script.abspath()) cwd = self._bdist.abspath() stdout = self.cmd_and_log(cmd, output=Context.STDOUT, quiet=Context.STDOUT, cwd=cwd) Logs.info(stdout)
[docs] def nsis_create_script(self, fname, appname): '''Creates and returns a NSIS installer script using the buildin template. :param fname: Name of the script file (including path). :type fname: str :param appname: Functional application and bdist name :type appname: str ''' script = self.path.make_node(fname) s = _nsis_template % appname script.write(s) return script
_nsis_template = r''' !include "MUI2.nsh" !include "x64.nsh" !ifndef VER_MAJOR !define VER_MAJOR 0 !endif !ifndef VER_MINOR !define VER_MINOR 0 !endif !ifndef VER_PATCH !define VER_PATCH 0 !endif !ifndef VERSION !define VERSION "${VER_MAJOR}.${VER_MINOR}.${VER_PATCH}" !endif !ifndef APPNAME !define APPNAME "%s" !endif !ifndef INSTALLER !ifdef RunningX64 !define INSTALLER "${APPNAME}-${VERSION}-win64-setup.exe" !else !define INSTALLER "${APPNAME}-${VERSION}-win32-setup.exe" !endif !endif !define REGKEY "Software\${APPNAME}" !define UNINSTALL_REGKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" Name "${APPNAME} ${VERSION}" OutFile "${INSTALLER}" InstallDir "$PROGRAMFILES\${APPNAME}" InstallDirRegKey HKCU "${REGKEY}" "" RequestExecutionLevel admin AutoCloseWindow false ShowInstDetails show ShowUnInstDetails show CRCCheck On !define MUI_ABORTWARNING !define MUI_STARTMENUPAGE_REGISTRY_ROOT HKCU !define MUI_STARTMENUPAGE_REGISTRY_KEY "${REGKEY}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" !define MUI_VERSION "${VERSION}" !define MUI_PRODUCT "${APPNAME} ${MUI_VERSION}" !define MUI_BRANDINGTEXT "" Var StartMenuFolder !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_WELCOME !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_COMPONENTS !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH !insertmacro MUI_LANGUAGE "English" Section "-Install" Section0 SetOutPath "$INSTDIR" File /r *.* SectionEnd Section "-Post install" Section1 ${If} ${RunningX64} SetRegView 64 ${EndIf} WriteRegStr HKCU "${REGKEY}" "" $INSTDIR WriteRegStr HKCU "${UNINSTALL_REGKEY}" "DisplayName" "${APPNAME}" WriteRegStr HKCU "${UNINSTALL_REGKEY}" "DisplayVersion" "${VERSION}" WriteRegStr HKCU "${UNINSTALL_REGKEY}" "InstallLocation" "$INSTDIR" WriteRegStr HKCU "${UNINSTALL_REGKEY}" "Publisher" "" WriteRegStr HKCU "${UNINSTALL_REGKEY}" "UninstallString" "$INSTDIR\Uninstall.exe" WriteRegDWORD HKCU "${UNINSTALL_REGKEY}" "VersionMajor" ${VER_MAJOR} WriteRegDWORD HKCU "${UNINSTALL_REGKEY}" "VersionMinor" ${VER_MINOR} WriteUninstaller "$INSTDIR\Uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateDirectory "$SMPROGRAMS\$StartMenuFolder" CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" "$INSTDIR\Uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_END SectionEnd Section Uninstall Delete "$INSTDIR\Uninstall.exe" RMDir /r /REBOOTOK "$INSTDIR" !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder Delete "$SMPROGRAMS\$StartMenuFolder\Uninstall.lnk" RMDir "$SMPROGRAMS\$StartMenuFolder" DeleteRegKey /ifempty HKCU "${REGKEY}" DeleteRegKey HKCU "${UNINSTALL_REGKEY}" SectionEnd Function .onInit ${IfNot} ${RunningX64} MessageBox MB_ICONSTOP "This $(^Name) installer is suitable for 64-bit Windows only!" Abort ${EndIf} FunctionEnd Function un.onInit MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2 Abort FunctionEnd Function un.onUninstSuccess HideWindow MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer." FunctionEnd '''