#!/usr/bin/env ruby # # File: scbuilder # Contents: Build script for SuperCollider plugins # Authors: Stefan Kersten # nescivi AT gmail DOT com # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation; either version 2, or (at your option) any later version. # # This software is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., 675 # Mass Ave, Cambridge, MA 02139, USA. # # ====================================================================== # Usage: # # This script compiles `simple' SuperCollider plugins, i.e. plugins that # consist of only one source file. Source files may be C++ implementations or # a Faust[1] DSP specification. # # Source file paths are passed either in a file (specified with the # PLUGIN_SOURCES option) or via standard input. # # E.g., when source file paths are in a file called sources.txt: # # $ sc-build-plugins PLUGIN_SOURCES=sources.txt # # Alternatively, source file paths can be passed via standard input: # # $ my-command | sc-build-plugins # # ====================================================================== # ====================================================================== # Bootstrap (ruby): Execute scons with content after __END__ require 'tempfile' Tempfile.open("r") { |f| f << DATA.read f.flush system("scons", "-f", f.path, *ARGV) } __END__ # ====================================================================== # NOTE: Please use an indentation level of 4 spaces, i.e. no mixing of # tabs and spaces. # ====================================================================== # ====================================================================== # setup # ====================================================================== EnsureSConsVersion(0,96) EnsurePythonVersion(2,3) SConsignFile() # ====================================================================== # imports # ====================================================================== import glob import os import re import sys import subprocess import types import tarfile # ====================================================================== # constants # ====================================================================== PACKAGE = 'SuperCollider' def short_cpu_name(cpu): if cpu == 'Power Macintosh': cpu = 'ppc' return cpu.lower() PLATFORM = os.uname()[0].lower() CPU = short_cpu_name(os.uname()[4]) if PLATFORM == 'darwin': PLATFORM_SYMBOL = 'SC_DARWIN' PLUGIN_EXT = '.scx' DEFAULT_PREFIX = '/' elif PLATFORM == 'freebsd': PLATFORM_SYMBOL = 'SC_FREEBSD' PLUGIN_EXT = '.so' DEFAULT_PREFIX = '/usr/local' elif PLATFORM == 'linux': PLATFORM_SYMBOL = 'SC_LINUX' PLUGIN_EXT = '.so' DEFAULT_PREFIX = '/usr/local' elif PLATFORM == 'windows': PLATFORM_SYMBOL = 'SC_WIN32' PLUGIN_EXT = '.scx' DEFAULT_PREFIX = '/' else: print 'Unknown platform: %s' % PLATFORM Exit(1) if CPU == 'ppc': DEFAULT_OPT_ARCH = '7450' elif CPU in [ 'i586', 'i686' ]: # FIXME: better detection DEFAULT_OPT_ARCH = CPU else: DEFAULT_OPT_ARCH = None # ====================================================================== # util # ====================================================================== def make_os_env(*keys): env = os.environ res = {} for key in keys: if env.has_key(key): res[key] = env[key] return res def CheckPKGConfig(context, version): context.Message( 'Checking for pkg-config... ' ) ret = context.TryAction('pkg-config --atleast-pkgconfig-version=%s' % version)[0] context.Result( ret ) return ret def CheckPKG(context, name): context.Message('Checking for %s... ' % name) ret = context.TryAction('pkg-config --exists \'%s\'' % name)[0] res = None if ret: res = Environment(ENV = make_os_env('PATH', 'PKG_CONFIG_PATH')) res.ParseConfig('pkg-config --cflags --libs \'%s\'' % name) res['PKGCONFIG'] = name context.Result(ret) return (ret, res) def get_new_pkg_env(): return Environment(ENV = make_os_env('PATH', 'PKG_CONFIG_PATH')) def merge_lib_info(env, *others): for other in others: env.AppendUnique(CCFLAGS = other.get('CCFLAGS', [])) env.AppendUnique(CPPDEFINES = other.get('CPPDEFINES', [])) env.AppendUnique(CPPPATH = other.get('CPPPATH', [])) env.AppendUnique(CXXFLAGS = other.get('CXXFLAGS', [])) env.AppendUnique(LIBS = other.get('LIBS', [])) env.AppendUnique(LIBPATH = other.get('LIBPATH', [])) env['LINKFLAGS'] = env['LINKFLAGS'] + other.get('LINKFLAGS', "") def flatten_dir(dir): res = [] for root, dirs, files in os.walk(dir): if 'CVS' in dirs: dirs.remove('CVS') if '.svn' in dirs: dirs.remove('.svn') for f in files: res.append(os.path.join(root, f)) return res def install_dir(env, src_dir, dst_dir, filter_re, strip_levels=0): nodes = [] for root, dirs, files in os.walk(src_dir): src_paths = [] dst_paths = [] if 'CVS' in dirs: dirs.remove('CVS') if '.svn' in dirs: dirs.remove('.svn') for d in dirs[:]: if filter_re.match(d): src_paths += flatten_dir(os.path.join(root, d)) dirs.remove(d) for f in files: if filter_re.match(f): src_paths.append(os.path.join(root, f)) dst_paths += map( lambda f: os.path.join( dst_dir, *f.split(os.path.sep)[strip_levels:]), src_paths) nodes += env.InstallAs(dst_paths, src_paths) return nodes def is_installing(): pat = re.compile('^install.*$') for x in COMMAND_LINE_TARGETS: if pat.match(x): return True return False def bin_dir(prefix): return os.path.join(prefix, 'bin') def lib_dir(prefix): return os.path.join(prefix, 'lib') def pkg_data_dir(prefix, *args): if PLATFORM == 'darwin': base = os.path.join(prefix, 'Library/Application Support') else: base = os.path.join(prefix, 'share') return os.path.join(base, PACKAGE, *args) def pkg_doc_dir(prefix, *args): if PLATFORM == 'darwin': base = os.path.join(prefix, 'Library/Documentation') else: base = os.path.join(prefix, 'share', 'doc') return os.path.join(base, PACKAGE, *args) def pkg_include_dir(prefix, *args): return os.path.join(prefix, 'include', PACKAGE, *args) def pkg_lib_dir(prefix, *args): return os.path.join(lib_dir(prefix), PACKAGE, *args) def pkg_classlib_dir(prefix, *args): return pkg_data_dir(prefix, 'SCClassLibrary', *args) def pkg_extension_dir(prefix, *args): return pkg_data_dir(prefix, 'Extensions', *args) def make_opt_flags(env): flags = [ "-O3", ## "-fomit-frame-pointer", # can behave strangely for sclang "-ffast-math", "-fstrength-reduce" ] arch = env.get('OPT_ARCH') if arch: if CPU == 'ppc': flags.extend([ "-mcpu=%s" % (arch,) ]) else: flags.extend([ "-march=%s" % (arch,) ]) if CPU == 'ppc': flags.extend([ "-fsigned-char", "-mhard-float", ## "-mpowerpc-gpopt", # crashes sqrt "-mpowerpc-gfxopt" ]) return flags # ====================================================================== # Faust builder # ====================================================================== def faustInitEnvironment(env): dsp = Builder( action = 'faust -a supercollider.cpp -o $TARGET $SOURCE', suffix = '.cpp', src_suffix = '.dsp') xml = Builder( action = ['faust -o /dev/null -xml $SOURCE', Move('$TARGET', '${SOURCE}.xml')], suffix = '.dsp.xml', src_suffix = '.dsp') sc = Builder( action = '$FAUST2SC --prefix="$FAUST2SC_PREFIX" -o $TARGET $SOURCE', suffix = '.sc', src_suffix = '.dsp.xml') env.Append(BUILDERS = { 'Faust' : dsp, 'FaustXML' : xml, 'FaustSC' : sc }) # ====================================================================== # Command line options # ====================================================================== opts = Options('scache.conf', ARGUMENTS) opts.AddOptions( BoolOption('ALTIVEC', 'Build with Altivec support', 1), PathOption('BUILD_DIR', 'Directory to build temporary files in', '.', PathOption.PathIsDirCreate), BoolOption('BUILD_SC', 'Build SuperCollider class files', 1), BoolOption('BUILD_XML', 'Build Faust XML files', 1), ('CC', 'C compiler executable'), ('CCFLAGS', 'C compiler flags'), ('CXX', 'C++ compiler executable'), ('CXXFLAGS', 'C++ compiler flags'), BoolOption('CROSSCOMPILE', 'Crosscompile for another platform (does not do SSE support check)', 0), BoolOption('DEBUG', 'Build with debugging information', 0), PathOption('DESTDIR', 'Intermediate installation prefix for packaging', '/'), PathOption('FAUST2SC', 'Path to faust2sc script', 'faust2sc', PathOption.PathAccept), ( 'FAUST2SC_PREFIX', 'Prefix for SC classes generated from Faust definitions', ''), PathOption('PREFIX', 'Installation prefix', DEFAULT_PREFIX), PathOption('INSTALLDIR', 'Installation directory', '', PathOption.PathAccept), BoolOption('SSE', 'Build with SSE support', 1), ( 'OPT_ARCH', 'Architecture to optimize for', DEFAULT_OPT_ARCH), PathOption('PLUGIN_SOURCES', 'File containing plugin sources (C++ or Faust)', None, PathOption.PathAccept), PathOption('SC_SOURCE_DIR', 'SuperCollider source directory', '../supercollider', PathOption.PathAccept), ) if PLATFORM == 'darwin': opts.AddOptions( BoolOption('UNIVERSAL', 'Build universal binaries (see UNIVERSAL_ARCHS)', 1), ListOption('UNIVERSAL_ARCHS', 'Architectures to build for', 'all', ['ppc', 'i386']) ) # ====================================================================== # basic environment # ====================================================================== env = Environment(options = opts, ENV = make_os_env('PATH', 'PKG_CONFIG_PATH'), PACKAGE = PACKAGE, URL = 'http://supercollider.sourceforge.net') env.Append(PATH = ['/usr/local/bin', '/usr/bin', '/bin']) faustInitEnvironment(env) # checks for DISTCC and CCACHE as used in modern linux-distros: if os.path.exists('/usr/lib/distcc/bin'): os.environ['PATH'] = '/usr/lib/distcc/bin:' + os.environ['PATH'] env['ENV']['DISTCC_HOSTS'] = os.environ['DISTCC_HOSTS'] if os.path.exists('/usr/lib/ccache/bin'): os.environ['PATH'] = '/usr/lib/ccache/bin:' + os.environ['PATH'] env['ENV']['CCACHE_DIR'] = os.environ['CCACHE_DIR'] env['ENV']['PATH'] = os.environ['PATH'] env['ENV']['HOME'] = os.environ['HOME'] # ====================================================================== # installation directories # ====================================================================== FINAL_PREFIX = '$PREFIX' INSTALL_PREFIX = os.path.join('$DESTDIR', '$PREFIX') # ====================================================================== # configuration # ====================================================================== def make_conf(env): return Configure(env, custom_tests = { 'CheckPKGConfig' : CheckPKGConfig, 'CheckPKG' : CheckPKG }) def isDefaultBuild(): return not env.GetOption('clean') and not 'debian' in COMMAND_LINE_TARGETS conf = make_conf(env) # libraries libraries = { } features = { } if isDefaultBuild(): if not conf.CheckPKGConfig('0'): print 'pkg-config not found.' Exit(1) # SC includes and flags success, libraries['scplugin'] = conf.CheckPKG('libscplugin') if not success: success, libraries['scplugin'] = conf.CheckPKG('libscsynth') if success: libraries['scplugin']['LIBS'] = [] else: src_dir = env['SC_SOURCE_DIR'] if os.path.isdir(src_dir): libraries['scplugin'] = Environment( CPPDEFINES = [PLATFORM_SYMBOL], CPPPATH = [os.path.join(src_dir, 'Headers/common'), os.path.join(src_dir, 'Headers/plugin_interface'), os.path.join(src_dir, 'Headers/server')]) else: print "Please specify the SC_SOURCE_DIR option." Exit(1) else: libraries['scplugin'] = Environment() # only _one_ Configure context can be alive at a time env = conf.Finish() # altivec if env['ALTIVEC']: if PLATFORM == 'darwin': altivec_flags = [ '-faltivec' ] else: altivec_flags = [ '-maltivec', '-mabi=altivec' ] libraries['altivec'] = env.Copy() libraries['altivec'].Append( CCFLAGS = altivec_flags, CPPDEFINES = [('SC_MEMORY_ALIGNMENT', 16)]) altiConf = Configure(libraries['altivec']) features['altivec'] = altiConf.CheckCHeader('altivec.h') altiConf.Finish() else: features['altivec'] = False # sse if env['SSE']: libraries['sse'] = env.Copy() libraries['sse'].Append( CCFLAGS = ['-msse', '-mfpmath=sse'], CPPDEFINES = [('SC_MEMORY_ALIGNMENT', 16)]) sseConf = Configure(libraries['sse']) hasSSEHeader = sseConf.CheckCHeader('xmmintrin.h') if env['CROSSCOMPILE']: build_host_supports_sse = True print 'NOTICE: cross compiling for another platform: assuming SSE support' else: build_host_supports_sse = False if CPU != 'ppc': if PLATFORM == 'freebsd': machine_info = os.popen ("sysctl -a hw.instruction_sse").read()[:-1] x86_flags = 'no' if "1" in [x for x in machine_info]: build_host_supports_sse = True x86_flags = 'sse' elif PLATFORM != 'darwin': flag_line = os.popen ("cat /proc/cpuinfo | grep '^flags'").read()[:-1] x86_flags = flag_line.split (": ")[1:][0].split () else: machine_info = os.popen ("sysctl -a machdep.cpu").read()[:-1] x86_flags = machine_info.split() if "sse" in [x.lower() for x in x86_flags]: build_host_supports_sse = True print 'NOTICE: CPU has SSE support' else: print 'NOTICE: CPU does not have SSE support' features['sse'] = hasSSEHeader and build_host_supports_sse sseConf.Finish() else: features['sse'] = False opts.Save('scache.conf', env) Help(opts.GenerateHelpText(env)) # defines and compiler flags env.Append( CPPDEFINES = [ '_REENTRANT' ], CCFLAGS = [ '-Wno-unknown-pragmas' ], CXXFLAGS = [ '-Wno-deprecated' ] ) # debugging flags if env['DEBUG']: env.Append(CCFLAGS = '-g') else: env.Append( CCFLAGS = make_opt_flags(env), CPPDEFINES = ['NDEBUG']) # platform specific if PLATFORM == 'darwin': env.Append( CCFLAGS = '-fvisibility=hidden' ) # vectorization if features['altivec']: merge_lib_info(env, libraries['altivec']) elif features['sse']: merge_lib_info(env, libraries['sse']) else: env.AppendUnique(CPPDEFINES = [('SC_MEMORY_ALIGNMENT', 1)]) # ====================================================================== # Source/plugins # ====================================================================== pluginEnv = env.Copy( SHLIBPREFIX = '', SHLIBSUFFIX = PLUGIN_EXT) if PLATFORM == 'darwin': pluginEnv['SHLINKFLAGS'] = '$LINKFLAGS -bundle' merge_lib_info(pluginEnv, libraries['scplugin']) def get_sources(env): if env.has_key('PLUGIN_SOURCES') and env['PLUGIN_SOURCES']: fd = open(env['PLUGIN_SOURCES'], "r") sources = fd.readlines() fd.close() else: print "Reading PLUGIN_SOURCES from " sources = sys.stdin.readlines() return map(lambda x: x.strip(), sources) def make_build_file(env, path, ext=""): newpath = os.path.splitext(os.path.basename(path))[0] + ext return os.path.join(env['BUILD_DIR'], newpath) def make_plugin_target(env, path): name, ext = os.path.splitext(path) if ext == ".dsp": src = env.Faust(make_build_file(env, path), path) else: src = path return env.SharedLibrary(make_build_file(env, path), src) def make_xml_target(env, path): name, ext = os.path.splitext(path) if ext == '.dsp': if env['BUILD_SC']: xml = env.FaustXML(make_build_file(env, path), path) sc = env.FaustSC(make_build_file(env, path), xml) if env['BUILD_XML']: return [sc, xml] else: return sc elif env['BUILD_XML']: return env.FaustXML(make_build_file(env, path), path) return [] sources = get_sources(env) plugins = map(lambda x: make_plugin_target(pluginEnv, x), sources) xml = map(lambda x: make_xml_target(pluginEnv, x), sources) # ====================================================================== # installation # ====================================================================== env.Alias('install', env.Install( os.path.join(pkg_extension_dir(INSTALL_PREFIX), env['INSTALLDIR']), plugins + xml)) # ====================================================================== # cleanup # ====================================================================== env.Clean('scrub', Split('config.log scache.conf .sconf_temp .sconsign.dblite')) # ====================================================================== # configuration summary # ====================================================================== def yesorno(p): if p: return 'yes' else: return 'no' print '------------------------------------------------------------------------' print ' ALTIVEC: %s' % yesorno(features['altivec']) print ' BUILD_SC: %s' % yesorno(env['BUILD_SC']) print ' BUILD_XML: %s' % yesorno(env['BUILD_XML']) print ' FAUST2SC: %s' % env['FAUST2SC'] if env['INSTALLDIR']: print ' INSTALLDIR: %s' % env['INSTALLDIR'] print ' DEBUG: %s' % yesorno(env['DEBUG']) if env.has_key('PLUGIN_SOURCES'): print ' PLUGIN_SOURCES: %s' % env['PLUGIN_SOURCES'] else: print ' PLUGIN_SOURCES: %s' % 'stdin' print ' PREFIX: %s' % env['PREFIX'] print ' SC_SOURCE_DIR %s' % env['SC_SOURCE_DIR'] print ' SSE: %s' % yesorno(features['sse']) print ' CROSSCOMPILE: %s' % yesorno(env['CROSSCOMPILE']) print '------------------------------------------------------------------------' # ====================================================================== # EOF