Scons

From WxWiki
Jump to: navigation, search

SCons is a software construction tool that aims to replace make and autotools, which have been coined to be somewhat of a problem for both end users and developers. SCons is written in Python and is a very easy tool to learn, specially compared to GNU Make.

The latest online manual can be found here.

The basic idea: an example on Windows

SCons has a function ParseConfig for tools like pkg-config and Wx-Config. On windows, one can use Wx-config_Windows_port (tested with revision 44 and MinGW).

A simple SConstruct looks like:

env = Environment()
env.ParseConfig('wx-config --cflags --libs')

env.StaticLibrary(target = 'my_lib', source = ['foo.cpp'])
env.Program(target = 'my_app', source = ['bar.cpp'],
            LIBS = env['LIBS'] + ['my_lib'], LIBPATH = env['LIBPATH'] + ['.'])

Note that to link my_app with my_lib, I appended my_lib to the LIBS in the environment, instead of just using LIBS=['my_lib'].

Go on to the next section to see how we can adapt this example to work on *nix platforms.

The basic idea on Linux and Mac OS X

The proposed SConstruct file from the last example runs well under Linux for scons v0.98, however the last lines fail on Mac OS X. We will simply remove them, since they are not needed under Linux either if you are not building and linking against you own software library (which is probably the case for more than 90% of the projects out there).

env = Environment()
env.ParseConfig('wx-config --cxxflags --libs')

env.Program(target = 'my_app', source = ['bar.cpp'])

Please note we have also changed the --cflags parameter to --cxxflags, although they should make no difference under Linux.

More robust Wx-Config usage

Wx-Config enables cross-platform building with relative ease. There are some caveats though with different operating systems and versions of wxWidgets. The following SCons python module was tested for MSVC on windows and gcc on Linux and MacOSX and wxWidgets versions 2.4 and 2.6.

#
# SCons python library: wxWidgets configuration
#
# \author Willem van Engen <[email protected]>
# \date created 2006/10/16
# \version v 1.3.2.2 2006/11/24 14:26:47 willem Exp
#
import os, os.path
import re
import sys
import glob

# Runs a system command and returns return value and output
def SystemBacktick(program):
	# Run and return result
	pipe = os.popen(program + ' 2>&1') # TODO: properly redirect stderr to stdout
	output = pipe.read()
	retcode = pipe.close() or 0
	return [retcode, output]

def SystemWXConfig(env, args):
	if sys.platform == 'win32':
		return SystemBacktick(env['wxconfig'] + ' --wxcfg=' + env['ENV']['WXCFG'] + ' ' + args + env['wxconfig_postargs'])
	else:
		return SystemBacktick(env['wxconfig'] + ' ' + args + env['wxconfig_postargs'])

# Check version of wx-config
# It succeeds with a warning if version check failed.
def CheckWXConfigVersion(context, version):
	releaseversion = SystemWXConfig(context.env, '--release')[1]
	try:
		if float(version) > float(releaseversion.strip()):
			return False
	except (ValueError, TypeError):
		context.Message('version check failed, but ok... ')
		return True
	return True

# Check if requested components exist in wxWidgets
def CheckWXConfigComponents(context, libraries):
	# set components, method depending on wxWidgets version
	if CheckWXConfigVersion(context, '2.6'):
		context.env['wxconfig_postargs'] += ' ' + ','.join(libraries)
		return SystemWXConfig(context.env, '--libs ')[0] == 0
	# version 2.4 or below, only gl is an optional component with special flag
	if 'gl' in libraries:
		context.env['wxconfig'] += ' --gl-libs'
		return SystemWXConfig(context.env, '--libs')[0] == 0

	# no optional components can be checked, it should be allright
	return True


# Find wx-config with suitable settings. It tries to use a debug configuration if requested,
# but reverts to release or default when that fails.
def CheckWXConfigWin(context, version, debug):
	context.Message('Checking for wxWidgets >= %s... ' % version)

	# Try to find it in path
	wx_prog = context.env.WhereIs(context.env['wxconfig'])
	if wx_prog == None:
		# You could supply wx-config.exe as a fallback option.
		#wx_prog = os.path.join('scons', 'wx-config')
		context.Message('wx-config not found...')
		return False
	context.env['wxconfig'] = wx_prog

	# Some environment variables are required for wx-config to work, check them.
	if 'WXWIN' not in context.env['ENV']:
		# TODO: maybe try some default location like "C:\Program Files\wxWidgets-*" or registry
		context.Message('please set WXWIN in environment... ')
		return False

	# If there's no WXCFG, either there is only one config or we try to find out.
	if 'WXCFG' not in context.env['ENV']:
		# TODO: try to find one in some sensible order from alternatives
		# Current guess is: visual studio static, non-unicode

		# Try debugging version first if requested, else fallback to release
		if debug:
			context.env['ENV']['WXCFG'] = 'vc_lib\mswd'
			if SystemWXConfig(context.env, '--libs')[0] == 0:
				return CheckWXConfigVersion(context, version)

		# Non-debug
		context.env['ENV']['WXCFG'] = 'vc_lib\msw'
		if SystemWXConfig(context.env, '--libs')[0] == 0:
			# this is the only configuration: use it
			return CheckWXConfigVersion(context, version)

		context.Message('please set WXCFG in environment... ')
		return False

	# WXCFG is in environment, nice.
	# Try a debugging version if requested: postfix WXCFG with 'd'
	if debug:
		oldwxcfg = context.env['ENV']['WXCFG']
		context.env['ENV']['WXCFG'] += 'd'
		if SystemWXConfig(context.env, '--libs')[0] == 0:
			return CheckWXConfigVersion(context, version)
		# Failed: revert to plain WXCFG, use existing environment
		context.env['ENV']['WXCFG'] = oldwxcfg

	if SystemWXConfig(context.env, '--libs')[0] == 0:
		return CheckWXConfigVersion(context, version)

	# Everything failed ... 
	return False


def CheckWXConfigPosixFind(context, debug):
	# Find a wx-config compatible pathname
	# wx*-config --> wx*-[0-9]+\.[0-9]+-config / wx<platform>-<version>-config

	dbglist = []
	rellist = []
	cfgre = re.compile('\/wx(\w+?)(d?)-(\d+\.\d+)-config')
	for dir in context.env['ENV']['PATH'].split(':'):
		for cfgprog in glob.glob(os.path.join(dir, 'wx*-config')):
			m = cfgre.search(cfgprog)
			if m and m.group(1) != 'base':
				# add to either debug or release list
				if m.group(2) == '':
					rellist.append(cfgprog)
				else:
					dbglist.append(cfgprog)

	# TODO: sort on version

	# Now pick the right one
	if debug and len(dbglist) > 0:
		return dbglist[0]

	if len(rellist) > 0:
		return rellist[0]

	# Too bad
	return False


def CheckWXConfigPosix(context, version, debug):
	# TODO: try several wx-config names
	context.Message('Checking for wxWidgets >= %s... ' % version)

	# If supplied wx-config doesn't work, try to find another one
	if SystemWXConfig(context.env, '--libs')[0] != 0:
		wx_prog = CheckWXConfigPosixFind(context, debug)
		if not wx_prog:
			context.Message('not found... ')
			return False
		context.env['wxconfig'] = wx_prog
		
	if not debug:
		return CheckWXConfigVersion(context, version)

	# use `wx-config --debug` if it's in its help
	helpoutput = SystemWXConfig(context.env, '--help')[1]
	if '--debug' in helpoutput:
		context.Message('--debug')
		if SystemWXConfig(context.env, '--debug --libs')[0] == 0:
			context.env['wxconfig'] = context.env['wxconfig'] + ' --debug'
			return CheckWXConfigVersion(context, version)

	# If it's plain wx-config, we may need to look for another one for debugging
	if context.env['wxconfig'] == 'wx-config':
		wx_prog = CheckWXConfigPosixFind(context, debug)
		if wx_prog:
			context.env['wxconfig']  = wx_prog

	# TODO: possibly warning message when using release instead of debug
	return CheckWXConfigVersion(context, version)


def CheckWXConfig(context, version, components, debug = False):
	context.env['wxconfig_postargs'] = ''
	build_platform=context.env['build_platform']
	target_platform=context.env['target_platform']

	# Get wx-config invocation and check version
	if build_platform == 'win32' and target_platform == 'win32':
		res = CheckWXConfigWin(context, version, debug)
	else:
		res = CheckWXConfigPosix(context, version, debug)

	# Make sure we have the required libraries
	if res:
		res = CheckWXConfigComponents(context, components)
		if not res:
			context.Message('not all components found [' + ','.join(components) + ']... ')

	context.Result(res)
	return res

def ParseWXConfig(env):
	build_platform = env['build_platform']
	target_platform = env['target_platform']
	# Windows doesn't work with ParseConfig (yet) :(
	if build_platform == 'win32' and target_platform == 'win32':
		# Use wx-config, yay!
		# ParseConfig() on windows is broken, so the following is done instead
		cflags = SystemWXConfig(env, '--cxxflags')[1]
		env.AppendUnique(CPPFLAGS = cflags.strip().split(' '))
		libs = SystemWXConfig(env, '--libs')[1]
		env.AppendUnique(LINKFLAGS = libs.strip().split(' '))
	elif target_platform == 'darwin':
		# MacOSX doesn't handle '-framework foobar' correctly, do that separately.
		env.ParseConfig(env['wxconfig'] + ' --cxxflags' + env['wxconfig_postargs'])
		env.AppendUnique(LINKFLAGS=SystemWXConfig(env, '--libs' + env['wxconfig_postargs'])[1])
	else:
		# Here ParseConfig should really work
		env.ParseConfig(env['wxconfig'] + ' --cxxflags --libs' + env['wxconfig_postargs'])

To use this module, include something like this in your main SConscript/SConstruct:

### import the module
sys.path.append('my_scons_script_directory')
from wxconfig import *

### setup variables needed, mainly to support cross-compilation
env['build_platform'] = env['PLATFORM']
env['target_platform'] = env['PLATFORM']
# it is recommended to get these from SCons parameters
debug = False
env['wxconfig'] = 'wx-config'

### detect wxWidgets
conf = Configure(env, custom_tests = {'CheckWXConfig' : CheckWXConfig })

# CheckWXConfig(version, componentlist, debug)
#          version: string with minimum version ('x.y')
#    componentlist: list of components needed. This was introduced with
#                   wxWidgets 2.6 if I'm correct. You'll usually need
#                   ['adv', 'core', 'base'], in this order. If you use
#                   wxGLCanvas, prepend 'gl' like below.
if not conf.CheckWXConfig('2.4', ['gl', 'adv', 'core', 'base'], debug):
	print 'wxWidgets library not found.'
	Exit(1)

env = conf.Finish()

### target
ParseWXConfig(env)

env.Program(target = 'my_app', source = ['bar.cpp'])