Creating Bundles

A Bundle is OSX's understanding of an application. It's a directory tree of many individual parts, each with a particular use. For instance, the Info.plist file, the icon, resources, PkgInfo, the executable(s), and so on.

Gary Oberbrunner posted this tool on the users mailing list to help with Bundle creation. There are a few references like 'SCons.Node.Python.Value' in here because I import this into my SConscripts, so it doesn't have regular access to all the scons stuff. If you're putting it directly in your SConstruct/script, you could just say Value(). (Note: as of scons 0.97, you can just say 'from SCons.Script import *'.)

#!python fromSCons.DefaultsimportSharedCheck,ProgScanfromSCons.Script.SConscriptimportSConsEnvironmentdefTOOL_BUNDLE(env):"""defines env.LinkBundle() for linking bundles on Darwin/OSX, and env.MakeBundle() for installing a bundle into its dir. A bundle has this structure: (filenames are case SENSITIVE) sapphire.bundle/ Contents/ Info.plist (an XML key->value database; defined by BUNDLE_INFO_PLIST) PkgInfo (trivially short; defined by value of BUNDLE_PKGINFO) MacOS/ executable (the executable or shared lib, linked with Bundle()) Resources/ """if'BUNDLE'inenv['TOOLS']:returnifplatform=='darwin':iftools_verbose:print" running tool: TOOL_BUNDLE"env.Append(TOOLS='BUNDLE')# This is like the regular linker, but uses different vars.# XXX: NOTE: this may be out of date now, scons 0.96.91 has some bundle linker stuff built in.# Check the docs before using this.LinkBundle=SCons.Builder.Builder(action=[SharedCheck,"$BUNDLECOM"],emitter="$SHLIBEMITTER",prefix='$BUNDLEPREFIX',suffix='$BUNDLESUFFIX',target_scanner=ProgScan,src_suffix='$BUNDLESUFFIX',src_builder='SharedObject')env['BUILDERS']['LinkBundle']=LinkBundleenv['BUNDLEEMITTER']=Noneenv['BUNDLEPREFIX']=''env['BUNDLESUFFIX']=''env['BUNDLEDIRSUFFIX']='.bundle'env['FRAMEWORKS']=['-framework Carbon','-framework System']env['BUNDLE']='$SHLINK'env['BUNDLEFLAGS']=' -bundle'env['BUNDLECOM']='$BUNDLE $BUNDLEFLAGS -o ${TARGET} $SOURCES $_LIBDIRFLAGS $_LIBFLAGS $FRAMEWORKS'# This requires some other tools:TOOL_WRITE_VAL(env)TOOL_SUBST(env)# Common type codes are BNDL for generic bundle and APPL for application.defMakeBundle(env,bundledir,app,key,info_plist,typecode='BNDL',creator='SapP',icon_file='#macosx-install/sapphire-icon.icns',subst_dict=None,resources=[]):"""Install a bundle into its dir, in the proper format"""# Substitute construction vars:forain[bundledir,key,info_plist,icon_file,typecode,creator]:a=env.subst(a)ifSCons.Util.is_List(app):app=app[0]ifSCons.Util.is_String(app):app=env.subst(app)appbase=basename(app)else:appbase=basename(str(app))ifnot('.'inbundledir):bundledir+='.$BUNDLEDIRSUFFIX'bundledir=env.subst(bundledir)# substitute againsuffix=bundledir[bundledir.rfind('.'):]if(suffix=='.app'andtypecode!='APPL'orsuffix!='.app'andtypecode=='APPL'):raiseError,"MakeBundle: inconsistent dir suffix %s and type code %s: app bundles should end with .app and type code APPL."%(suffix,typecode)ifsubst_dictisNone:subst_dict={'%SHORTVERSION%':'$VERSION_NUM','%LONGVERSION%':'$VERSION_NAME','%YEAR%':'$COMPILE_YEAR','%BUNDLE_EXECUTABLE%':appbase,'%ICONFILE%':basename(icon_file),'%CREATOR%':creator,'%TYPE%':typecode,'%BUNDLE_KEY%':key}env.Install(bundledir+'/Contents/MacOS',app)f=env.SubstInFile(bundledir+'/Contents/Info.plist',info_plist,SUBST_DICT=subst_dict)env.Depends(f,SCons.Node.Python.Value(key+creator+typecode+env['VERSION_NUM']+env['VERSION_NAME']))env.WriteVal(target=bundledir+'/Contents/PkgInfo',source=SCons.Node.Python.Value(typecode+creator))resources.append(icon_file)forrinresources:ifSCons.Util.is_List(r):env.InstallAs(join(bundledir+'/Contents/Resources',r[1]),r[0])else:env.Install(bundledir+'/Contents/Resources',r)return[SCons.Node.FS.default_fs.Dir(bundledir)]# This is not a regular Builder; it's a wrapper function.# So just make it available as a method of Environment.SConsEnvironment.MakeBundle=MakeBundledefTOOL_WRITE_VAL(env):iftools_verbose:print" running tool: TOOL_WRITE_VAL"env.Append(TOOLS='WRITE_VAL')defwrite_val(target,source,env):"""Write the contents of the first source into the target. source is usually a Value() node, but could be a file."""f=open(str(target[0]),'wb')f.write(source[0].get_contents())f.close()env['BUILDERS']['WriteVal']=Builder(action=write_val)

that more or less works for me, creating a .pkg dir. Then just make a disk image and ship it! :)

Installing Mac Created Bundles

The regular env.Install will not work to install Mac bundles since they are directories. Here's a way to send the output of a env.MakeBundle to this new env.InstallBundle.

EDITED 2/6/06 gary o: This is not a good way to do it. Using glob() only finds files that already exist when the SCons files are read, not ones that will be built. See BuildDirGlob for better ways to glob over Nodes.

#!python defensureWritable(nodes):fornodeinnodes:ifexists(node.path)andnot(stat(node.path)[0]&0200):chmod(node.path,0777)returnnodes# Copy given patterns from inDir to outDirdefDFS(root,skip_symlinks=1):"""Depth first search traversal of directory structure. Children are visited in alphabetical order."""stack=[root]visited={}whilestack:d=stack.pop()ifdnotinvisited:## just to prevent any possible recursive## loopsvisited[d]=1yielddstack.extend(subdirs(d,skip_symlinks))defsubdirs(root,skip_symlinks=1):"""Given a root directory, returns the first-level subdirectories."""try:dirs=[join(root,x)forxinlistdir(root)]dirs=filter(isdir,dirs)ifskip_symlinks:dirs=filter(lambdax:notislink(x),dirs)dirs.sort()returndirsexceptOSError,IOError:return[]defcopyFiles(env,outDir,inDir):inDirNode=env.Dir(inDir)outDirNode=env.Dir(outDir)subdirs=DFS(inDirNode.name)files=[]forsubdirinsubdirs:files+=glob.glob(join(subdir,'*'))outputs=[]forfinfiles:ifisfile(f):outputs+=ensureWritable(env.InstallAs(outDirNode.abspath+'/'+f,env.File(f)))returnoutputsdefInstallBundle(env,target_dir,bundle):"""Move a Mac OS-X bundle to its final destination"""# check parameters!ifexists(target_dir)andnotisdir(target_dir):raiseSCons.Errors.UserError,"InstallBundle: %s needs to be a directory!"%(target_dir)bundledirs=env.arg2nodes(bundle,env.fs.File)outputs=[]forbundledirinbundledirs:suffix=bundledir.name[bundledir.name.rfind('.'):]if(exists(bundledir.name)andnotisdir(bundledir.name))orsuffix!='.app':raiseSCons.Errors.UserError,"InstallBundle: %s needs to be a directory with a .app suffix!"%(bundledir.name)# copy all of them to the target diroutputs+=env.copyFiles(target_dir,bundledir)returnoutputs

To use it, try the following:

#!python prog=env.Program(program,objs+other_objects,LIBS=libs+env['EXTRA_LIBS'],LIBPATH=libpaths+env['SDDAS_LIB'])env.Default(prog)ifenv['PLATFORM']=="darwin"andisNativeOnMac:# I pass in a boolean telling me that# the program is a real Mac app, not a# X11 thing or regular command line exe.env['VERSION_NAME']=program+'.app'env.Append(LINKFLAGS=['-framework','Carbon'])# This is not needed in newer versions of SConsbundle=env.MakeBundle(program+'.app',program,'com.SwRI.'+program,# this key is some sort of Mac-ism,# java style, can be anything?'Info.plist',# Info.plist is an XML thing made with# Property List Editor'APPL',# tells SCons this is an application'SwRI',# Creator code, can be anything?'#/MAC_ICONS/'+program+'.icns')# Icon for the programenv.Default(bundle)inst=env.InstallBundle(env['SDDAS_BIN'],bundle)# env ['SDDAS_BIN'] is the target directoryelse:inst=env.Install(dir=env['SDDAS_BIN'],source=prog)env.AddPostAction(inst,env.Action('strip $TARGET'))

4) When you do an "scons install" (or whatever your alias to do the install), it will install the program in the right place.

Copying files with resource forks

_ Note that as of OS X, resource forks are deprecated and rarely used nowadays; so unless you develop for legacy support, this section should not be relevant._

Python, as of 2.3 I believe, comes with a macostools extension module that has a copy function that deals with resource forks. However, the parameter list is a bit different than what SCons expects for Install, so a small wrapper method is needed.

Update: I actually get permission denied errors when trying to open the copied executables. Can anyone else confirm this erroneous behavior?

-- MichaelKoehmstedt
On Mac OS X, you sometimes have to build files with resource forks. Installing them the usual way with env.Install() won't work, because env.Install() uses cp by default, which doesn't copy resource forks.

Fortunately env.Install() actually calls whatever python function is in env['INSTALL'], so you can replace it like this:

You could enhance this by checking whether dest/rsrc exists, and only use CpMac in that case. dest/rsrc is one way to get to the resource fork of a file; the syntax refers to the file as if it were a directory so it's a little unusual, but it does work.

Get absolute path name in error messages (for Xcode integration)

If you use SCons as external build tool within an Xcode 3.2 through at least 4.5 project, then the parsing of error messages is broken, because Xcode expects absolute path names, while SCons calls the C/C++ compiler with relative path names. Some modifications and intervention is possible so that compiler errors generated through Xcode are 'clickable' and browse to the correct source code location. GCC formats the output in the same way that source files are specified. Therefore, we need to change the way SCons calls the compiler such that it usese absolute path names to source code:

This overrides the default C and C++ compiler action and calls the compiler with absolute path names through the ${SOURCES.abspath} syntax.

The above will fix error reporting for source code directly compiled. It will not fix errors from incuded files ( .h header files ), because the GCC compiler will still report relative paths for those errors. The solution is to call scons through a script that will do stream processing on the stderr output, replacing relative path names to absolute paths.