Please note:The SCons wiki is now restored from the attack in March 2013. All old passwords have been invalidated. Please reset your password if you have an account. If you note missing pages, please report them to webmaster@scons.org. Also, new account creation is currently disabled due to an ongoing spam flood (2013/08/27).

Here are some tips for using SCons with Mac OS X.

Building Dynamic Libraries

Just use SharedLibrary as always if you use scons later than 0.96.91. Otherwise you need to do something like this:

   1 env['SHLINKFLAGS'] = '$LINKFLAGS -dynamic'
   2 env['SHLIBSUFFIX'] = '.dylib'

Building Dynamic Plugins

MacOSX makes big difference on dynamic libs and bundles (plugins). Use LoadableModule builder if you use scons later than 0.96.91. Otherwise you could do something like this to enable Plugins:

   1 def XmmsPlugin(self,target,source):
   2     self['SHLINKFLAGS'] = '$LINKFLAGS -bundle -flat_namespace -undefined suppress'
   3     self['SHLIBSUFFIX'] = '.so'
   4     self.SharedLibrary(target, source)

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 *'.)

   1 from SCons.Defaults import SharedCheck, ProgScan
   2 from SCons.Script.SConscript import SConsEnvironment
   3 
   4 def TOOL_BUNDLE(env):
   5     """defines env.LinkBundle() for linking bundles on Darwin/OSX, and
   6        env.MakeBundle() for installing a bundle into its dir.
   7        A bundle has this structure: (filenames are case SENSITIVE)
   8        sapphire.bundle/
   9          Contents/
  10            Info.plist (an XML key->value database; defined by BUNDLE_INFO_PLIST)
  11            PkgInfo (trivially short; defined by value of BUNDLE_PKGINFO)
  12            MacOS/
  13              executable (the executable or shared lib, linked with Bundle())
  14     Resources/
  15          """
  16     if 'BUNDLE' in env['TOOLS']: return
  17     if platform == 'darwin':
  18         if tools_verbose:
  19             print " running tool: TOOL_BUNDLE"
  20         env.Append(TOOLS = 'BUNDLE')
  21         # This is like the regular linker, but uses different vars.
  22         # XXX: NOTE: this may be out of date now, scons 0.96.91 has some bundle linker stuff built in.
  23         # Check the docs before using this.
  24         LinkBundle = SCons.Builder.Builder(action=[SharedCheck, "$BUNDLECOM"],
  25                                            emitter="$SHLIBEMITTER",
  26                                            prefix = '$BUNDLEPREFIX',
  27                                            suffix = '$BUNDLESUFFIX',
  28                                            target_scanner = ProgScan,
  29                                            src_suffix = '$BUNDLESUFFIX',
  30                                            src_builder = 'SharedObject')
  31         env['BUILDERS']['LinkBundle'] = LinkBundle
  32         env['BUNDLEEMITTER'] = None
  33         env['BUNDLEPREFIX'] = ''
  34         env['BUNDLESUFFIX'] = ''
  35         env['BUNDLEDIRSUFFIX'] = '.bundle'
  36         env['FRAMEWORKS'] = ['-framework Carbon', '-framework System']
  37         env['BUNDLE'] = '$SHLINK'
  38         env['BUNDLEFLAGS'] = ' -bundle'
  39         env['BUNDLECOM'] = '$BUNDLE $BUNDLEFLAGS -o ${TARGET} $SOURCES $_LIBDIRFLAGS $_LIBFLAGS $FRAMEWORKS'
  40         # This requires some other tools:
  41         TOOL_WRITE_VAL(env)
  42         TOOL_SUBST(env)
  43         # Common type codes are BNDL for generic bundle and APPL for application.
  44         def MakeBundle(env, bundledir, app,
  45                        key, info_plist,
  46                        typecode='BNDL', creator='SapP',
  47                        icon_file='#macosx-install/sapphire-icon.icns',
  48                        subst_dict=None,
  49                        resources=[]):
  50             """Install a bundle into its dir, in the proper format"""
  51             # Substitute construction vars:
  52             for a in [bundledir, key, info_plist, icon_file, typecode, creator]:
  53                 a = env.subst(a)
  54             if SCons.Util.is_List(app):
  55                 app = app[0]
  56             if SCons.Util.is_String(app):
  57                 app = env.subst(app)
  58                 appbase = basename(app)
  59             else:
  60                 appbase = basename(str(app))
  61             if not ('.' in bundledir):
  62                 bundledir += '.$BUNDLEDIRSUFFIX'
  63             bundledir = env.subst(bundledir) # substitute again
  64             suffix=bundledir[bundledir.rfind('.'):]
  65             if (suffix=='.app' and typecode != 'APPL' or
  66                 suffix!='.app' and typecode == 'APPL'):
  67                 raise Error, "MakeBundle: inconsistent dir suffix %s and type code %s: app bundles should end with .app and type code APPL."%(suffix, typecode)
  68             if subst_dict is None:
  69                 subst_dict={'%SHORTVERSION%': '$VERSION_NUM',
  70                             '%LONGVERSION%': '$VERSION_NAME',
  71                             '%YEAR%': '$COMPILE_YEAR',
  72                             '%BUNDLE_EXECUTABLE%': appbase,
  73                             '%ICONFILE%': basename(icon_file),
  74                             '%CREATOR%': creator,
  75                             '%TYPE%': typecode,
  76                             '%BUNDLE_KEY%': key}
  77             env.Install(bundledir+'/Contents/MacOS', app)
  78             f=env.SubstInFile(bundledir+'/Contents/Info.plist', info_plist,
  79                             SUBST_DICT=subst_dict)
  80             env.Depends(f,SCons.Node.Python.Value(key+creator+typecode+env['VERSION_NUM']+env['VERSION_NAME']))
  81             env.WriteVal(target=bundledir+'/Contents/PkgInfo',
  82                          source=SCons.Node.Python.Value(typecode+creator))
  83             resources.append(icon_file)
  84             for r in resources:
  85                 if SCons.Util.is_List(r):
  86                     env.InstallAs(join(bundledir+'/Contents/Resources',
  87                                                r[1]),
  88                                   r[0])
  89                 else:
  90                     env.Install(bundledir+'/Contents/Resources', r)
  91             return [ SCons.Node.FS.default_fs.Dir(bundledir) ]
  92         # This is not a regular Builder; it's a wrapper function.
  93         # So just make it available as a method of Environment.
  94         SConsEnvironment.MakeBundle = MakeBundle
  95 
  96 def TOOL_WRITE_VAL(env):
  97     if tools_verbose:
  98         print " running tool: TOOL_WRITE_VAL"
  99     env.Append(TOOLS = 'WRITE_VAL')
 100     def write_val(target, source, env):
 101         """Write the contents of the first source into the target.
 102         source is usually a Value() node, but could be a file."""
 103         f = open(str(target[0]), 'wb')
 104         f.write(source[0].get_contents())
 105         f.close()
 106     env['BUILDERS']['WriteVal'] = Builder(action=write_val)

Note: you will need TOOL_SUBST from the wiki page SubstInFileBuilder.

With a few small changes I was able to convert the above into an importable module which lets you define more than one application bundle per build tree. The result is at SconsMacV2.

Compiling Objective C / Objective C++ files

As of version 0.96.90, Objective C / Objective C++ support is built in to SCons. The expected file suffixes are '.m' for Objective C; '.mm' for Objective C++.

Packaging using pkg and disk images

OK, this is way incomplete, but here's something that might help. I haven't toolified any of this yet, it's just inline in my SConscript.

   1     ...
   2     type='pmkr'
   3     creator='pkg1'
   4     pkgdir='Pkg' # dest dir where the .pkg dir will go
   5     srcdir='PrePkg' # src dir where all the stuff for the package lives
   6     pkgname=splitext(basename(SrcDirPath(pkgdir)))[0]
   7     env.Command(target=[join(pkgdir,"Contents/Archive.pax.gz"),
   8                         join(pkgdir,"Contents/Archive.bom")],
   9                 source=srcdir,
  10                 action=["(cd ${SOURCE} ; /Developer/Tools/SplitForks . ; pax -w -x cpio . ) | gzip -9 > $TARGET",
  11                         "mkbom $SOURCE ${TARGETS[1]}"])
  12     env.WriteVal(target=join(pkgdir, 'Contents/PkgInfo'),
  13                  source=SCons.Node.Python.Value(type+creator))
  14     # CUSTOMIZE THIS PART:
  15     v=float(env['VERSION_NUM'])
  16     subst_dict={'%SHORTVERSION%': '$VERSION_NUM',
  17                 '%LONGVERSION%': 'This is my product, version $VERSION_NAME',
  18                 '%PRODUCT%': 'MyProduct version $VERSION_NAME',
  19                 '%MAJOR_VERSION%': str(int(v)),
  20                 '%MINOR_VERSION%': str(int((v - int(v)) * 1000)),
  21                 '%YEAR%': '$COMPILE_YEAR',
  22                 '%CREATOR%': creator,
  23                 '%TYPE%': type,
  24                 '%DESTDIR_TOP%': '/Applications/MyAppTopDir',
  25                 '%BUNDLE_KEY%': 'com.example.something',
  26                 '%BUNDLE_NAME%': 'Super App',
  27                 }
  28     env.SubstInFile(join(pkgdir,'Contents','Info.plist'),
  29                     join('resources','Info.plist.in'),
  30                     SUBST_DICT=subst_dict)

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.

   1 def ensureWritable(nodes):
   2     for node in nodes:
   3         if exists(node.path) and not (stat(node.path)[0] & 0200):
   4            chmod(node.path, 0777)
   5     return nodes
   6 
   7 # Copy given patterns from inDir to outDir
   8 def DFS(root, skip_symlinks = 1):
   9     """Depth first search traversal of directory structure.  Children
  10     are visited in alphabetical order."""
  11     stack = [root]
  12     visited = {}
  13     while stack:
  14         d = stack.pop()
  15         if d not in visited:  ## just to prevent any possible recursive
  16                               ## loops
  17             visited[d] = 1
  18             yield d
  19         stack.extend(subdirs(d, skip_symlinks))
  20 
  21 def subdirs(root, skip_symlinks = 1):
  22     """Given a root directory, returns the first-level subdirectories."""
  23     try:
  24         dirs = [join(root, x) for x in listdir(root)]
  25         dirs = filter(isdir, dirs)
  26         if skip_symlinks:
  27             dirs = filter(lambda x: not islink(x), dirs)
  28         dirs.sort()
  29         return dirs
  30     except OSError, IOError: return []
  31 
  32 def copyFiles (env, outDir, inDir):
  33     inDirNode = env.Dir(inDir)
  34     outDirNode = env.Dir(outDir)
  35     subdirs = DFS (inDirNode.name)
  36     files = []
  37     for subdir in subdirs:
  38         files += glob.glob (join (subdir, '*'))
  39     outputs = []
  40     for f in files:
  41         if isfile (f):
  42             outputs += ensureWritable (env.InstallAs (outDirNode.abspath + '/' + f, env.File (f)))
  43     return outputs
  44 
  45 def InstallBundle (env, target_dir, bundle):
  46     """Move a Mac OS-X bundle to its final destination"""
  47     # check parameters!
  48     if exists(target_dir) and not isdir (target_dir):
  49         raise SCons.Errors.UserError, "InstallBundle: %s needs to be a directory!"%(target_dir)
  50     bundledirs = env.arg2nodes (bundle, env.fs.File)
  51     outputs = []
  52     for bundledir in bundledirs:
  53         suffix = bundledir.name [bundledir.name.rfind ('.'):]
  54         if (exists(bundledir.name) and not isdir (bundledir.name)) or suffix != '.app':
  55             raise SCons.Errors.UserError, "InstallBundle: %s needs to be a directory with a .app suffix!"%(bundledir.name)
  56     # copy all of them to the target dir
  57         outputs += env.copyFiles (target_dir, bundledir)
  58     return outputs

To use it, try the following:

   1     prog = env.Program (program, objs + other_objects,
   2                         LIBS = libs + env ['EXTRA_LIBS'],
   3                         LIBPATH = libpaths + env ['SDDAS_LIB'])
   4     env.Default (prog)
   5     if env ['PLATFORM'] == "darwin" and isNativeOnMac:    # I pass in a boolean telling me that
   6                                                           # the program is a real Mac app, not a
   7                                                           # X11 thing or regular command line exe.
   8        env ['VERSION_NAME'] = program + '.app'
   9        env.Append (LINKFLAGS = ['-framework', 'Carbon'])  # This is not needed in newer versions of SCons
  10        bundle = env.MakeBundle (program + '.app', program,
  11                                 'com.SwRI.' + program,    # this key is some sort of Mac-ism,
  12                                                           # java style, can be anything?
  13                                 'Info.plist',             # Info.plist is an XML thing made with
  14                                                           # Property List Editor
  15                                 'APPL',                   # tells SCons this is an application
  16                                 'SwRI',                   # Creator code, can be anything?
  17                                 '#/MAC_ICONS/' + program + '.icns')  # Icon for the program
  18        env.Default (bundle)
  19        inst = env.InstallBundle (env ['SDDAS_BIN'], bundle)   # env ['SDDAS_BIN'] is the target directory
  20     else:
  21        inst = env.Install (dir=env ['SDDAS_BIN'], source=prog)
  22        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.

   1 def osx_copy( dest, source, env ):
   2     from macostools import copy
   3     copy( source, dest )

Remember to set the INSTALL variable for your environment:

   1 env['INSTALL'] = osx_copy

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

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:

   1 import os, os.path, shutil
   2 def copyFunc_with_resources(dest, source, env):
   3     """Install a source file into a destination by copying it (and its
   4     permission/mode bits, AND MAC RESOURCE STUFF)."""
   5     st = os.stat(source)
   6     if sys.platform == 'darwin' and os.path.exists('/Developer/Tools/CpMac'):
   7        if os.path.dirname(str(dest)) and \
   8               not os.path.exists(os.path.dirname(str(dest))):
   9            os.makedirs(os.path.dirname(str(dest)))
  10        cmd='/Developer/Tools/CpMac "%s" "%s"'%(str(source), str(dest))
  11        # print "(Using Dev Tools to copy: cmd=%s)"%cmd
  12        os.system(cmd)
  13     else:
  14        shutil.copy2(source, dest)
  15     os.chmod(dest, stat.S_IMODE(st.st_mode) | stat.S_IWRITE)
  16     return 0

Then when you're creating your environment, do something like this:

   1 env['INSTALL'] = copyFunc_with_resources

Then Install() will copy resources.

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:

  env['CXXCOM'] = string.replace(env['CXXCOM'], '$SOURCES', '${SOURCES.abspath}')
  env['CCCOM']  = string.replace(env['CCCOM'],  '$SOURCES', '${SOURCES.abspath}')

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.

Here is one such method:

#!/bin/bash
# Call this build_script from XCode as 'external build system'
# Macro Definitions:
# $SCONS_EXEC : path to scons command
# $SOURCE_DIR : base path to project source
# $1          : argument passed to script from XCode, i.e. build target

cd $SOURCE_DIR

( $SCONS_EXEC $1 ) 2> >( sed -E "s|^([^/][a-zA-Z/_]+\.h)|$SOURCE_DIR/\1|;s| ([^/][a-zA-Z/_]+\.h)| $SOURCE_DIR/\1|g" >&2 )

# This passes the stderr output from scons (and GCC) through sed to change identified relative path .h files to absolute path.  Note the filename matching criteria [a-zA-Z/_] may need some modification for special characters / numbers in filename.

MacOSX (last edited 2012-12-31 17:35:53 by c-76-22-66-115)