#!/usr/bin/env ruby
#
# File:         scbuilder
# Contents:     Build script for SuperCollider plugins
# Authors:      Stefan Kersten <sk AT k-hornz DOT de>
#               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 <stdin>"
        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
