view SConstruct @ 11:4706e0194af3

Various improvements to the build process including support for self-contained builds. * Note that despite all of these changes PARPG still does not run because asset paths are not standardized, * Modified the SCons script so that by default running `scons` with no arguments creates a self-contained "build" under a build subdirectory to make in-source testing easier. To install PARPG, use `scons install` instead. * Got rid of the binary launcher and replaced it with a shell script for unix and a batch script for Windows (batch script is untested). The binary turned out to be too much trouble to maintain. * Modified the parpg.settings module and parpg.main entry script so that PARPG searches through several default search paths for configuration file(s). PARPG thus no longer crashes if it can't find a configuration file in any particular search path, but will crash it if can't find any configuration files. * Paths supplied to parpg.main are now appended as search paths for the configuration file(s). * Changed the default configuration file name to "parpg.cfg" to simplify searches. * Created the site_scons directory tree where SCons extensions and tools should be placed. * Created a new SCons builder, CopyRecurse, which can copy only certain files and folders from a directory tree using filters (files and folders that start with a leading dot "." e.g. ".svn" are ignored by default). * Added the CPython SCons tool (stands for Compile-Python - I didn't name it!), which provides the InstallPython builder for pre-compiling python sources before they are installed. However, it is currently broken and only installs the python sources.
author M. George Hansen <technopolitica@gmail.com>
date Tue, 31 May 2011 02:46:20 -0700
parents dd4ed4945411
children d60f1dab8469
line wrap: on
line source

import sys
import os
import platform
import compileall
import fnmatch
from copy import copy
from collections import Sequence
from types import StringType
from multiprocessing import cpu_count

from SCons.Util import is_Sequence, is_Dict
from SCons.Tool.install import copyFunc

#def recursive_glob(topdir_path, include_pattern='*', exclude_pattern=''):
#    """
#    Locate all files matching supplied filename pattern in and below
#    supplied root directory.
#    """
#    for dir_path, subdir_names, file_names in \
#       os.walk(os.path.abspath(topdir_path)):
#        for file_name in fnmatch.filter(file_names, include_pattern):
#            if not fnmatch.fnmatch(file_name, exclude_pattern):
#                file_path = os.path.join(dir_path, file_name)
#                yield File(file_path)
#        for subdir_name in copy(subdir_names):
#            if not fnmatch.fnmatch(subdir_name, include_pattern) or \
#                fnmatch.fnmatch(subdir_name, exclude_pattern):
#                subdir_names.remove(subdir_name)
#            else:
#                subdir_path = os.path.join(dir_path, subdir_name)

def InstallChmod(env, dest, source, mode):
    targets = env.Install(dest, source)
    for target in targets:
        env.AddPostAction(target, Chmod(target, mode))
    return targets

def InstallExecutable(env, dest, source):
    return env.InstallChmod(dest, source, mode=0755)

def InstallReadOnly(env, dest, source):
    if not isinstance(source, Sequence):
        source = [source]
    targets = []
    for entry in map(Entry, source):
        entry.disambiguate()
        if entry.isdir():
            target = env.InstallChmod(dest, entry, mode=0755)
        elif entry.isfile():
            target = env.InstallChmod(dest, entry, mode=0644)
        else:
            # Something really weird happened and entry is not a Dir or a
            # File... (Note: Yes this can happen!)
            error_message = \
                'expected entry to be a Dir or a File, but got {0!r}'
            raise ValueError(error_message.format(entry))
        targets.append(target)
    return targets

def SubstfileEscape(env, *args, **kwargs):
    subst_dict = kwargs.get('SUBST_DICT') or env.get('SUBST_DICT')
    escape_sequences = kwargs.get('ESCAPE_SEQUENCES') or env.get('ESCAPE_SEQUENCES')
    if subst_dict is not None and escape_sequences is not None:
        if not is_Dict(subst_dict) and is_Sequence(subst_dict):
            subst_dict = dict(subst_dict)
        else:
            error_message = 'SUBST_DICT must be dict or sequence'
            raise SCons.Errors.UserError(error_message)
        escaped_subst_dict = {}
        for key, value in subst_dict.items():
            escaped_value = value
            for seq, escaped_seq in escape_sequences.items():
                escaped_value = escaped_value.replace(seq, escaped_seq)
            escaped_subst_dict[key] = escaped_value
        kwargs['SUBST_DICT'] = escaped_subst_dict
    
    target = env.Substfile(*args, **kwargs)
    return target

AddMethod(Environment, InstallChmod)
AddMethod(Environment, InstallExecutable)
AddMethod(Environment, InstallReadOnly)
AddMethod(Environment, SubstfileEscape)

EnsurePythonVersion(2, 6)

AddOption(
    '--stand-alone',
    dest='stand_alone',
    action='store_true',
    default=False,
    help='install the entire program under installation prefix instead of in '
         'various system folders'
)
AddOption(
    '--no-compile',
    dest='compile',
    action='store_false',
    default=True,
    help='don\'t compile any Python modules into .pyc files before installing',
)
SetOption('num_jobs', cpu_count())

def is_abs_path(key, val, env):
    if not os.path.isabs(val):
        error_message = '${key} must be an absolute path'
        raise ValueError(error_message.format(key=key))
    env[key] = os.path.normpath(val)

variables = Variables()
# NOTE M. George Hansen 2011-05-13: Path variables are based on the GNU
#     standards as defined at http://www.gnu.org/prep/standards/html_node/Directory-Variables.html

# Platform-specific variable defaults.
# FIXME M. George Hansen 2011-05-12: Define the MacOS-specific
#     environmental variables (and check version...)
platform_name = platform.system()
if platform_name in ['Linux', 'MacOS']:
    if GetOption('stand_alone'):
        PREFIX_DEFAULT = '/opt'
    else:
        PREFIX_DEFAULT = '/usr/local'
    BIN_DIR_DEFAULT = '$EXEC_PREFIX/bin'
    DATA_ROOT_DIR_DEFAULT = '$PREFIX/share/$PROJECT_NAME'
    SYS_CONF_DIR_DEFAULT = '$PREFIX/etc/$PROJECT_NAME'
    INCLUDE_DIR_DEFAULT = '$PREFIX/include/$PROJECT_NAME'
    DOC_DIR_DEFAULT = '$DATA_ROOT_DIR/doc/$PROJECT_NAME'
    LIB_DIR_DEFAULT = '$EXEC_PREFIX/lib' 
    dist_name, dist_version, dist_id = \
        platform.linux_distribution(full_distribution_name=False)
    if dist_name in ['debian', 'Ubuntu']:
        # Debian uses dist-packages instead of site-packages for Python
        #     versions > 2.5.
        PY_PACKAGES_DIR_DEFAULT = '$EXEC_PREFIX/lib/python$PY_VERSION_SHORT/' \
                                 'dist-packages'
    else:
        PY_PACKAGES_DIR_DEFAULT = '$EXEC_PREFIX/lib/python$PY_VERSION_SHORT/' \
                                 'site-packages'
    PY_HEADERS_DIR_DEFAULT = '/usr/include/python$PY_VERSION_SHORT'
    PY_LIB_DIR_DEFAULT = '/usr/lib'
    PY_LIB_NAME = 'python$PY_VERSION_SHORT'
elif platform_name == 'Windows':
    try:
        PREFIX_DEFAULT = os.environ['PROGRAMFILES'] + r'\$PROGRAM_NAME'
    except KeyError:
        PREFIX_DEFAULT = ''
        error_message = '%PROGRAMFILES% environmental variable is not ' \
                        'set, unable to determine path to Program Files ' \
                        'folder'
        raise SConfWarning(error_message)
    BIN_DIR_DEFAULT = '$EXEC_PREFIX'
    DATA_ROOT_DIR_DEFAULT = '$PREFIX/data'
    SYS_CONF_DIR_DEFAULT = '$PREFIX/config'
    INCLUDE_DIR_DEFAULT = '$PREFIX/include'
    DOC_DIR_DEFAULT = '$PREFIX/doc/'
    LIB_DIR_DEFAULT = '$EXEC_PREFIX/lib'
    # FIXME M. George Hansen 2011-05-12: Does sys.prefix include the
    #     PythonX.Y part on Windows?
    python_prefix = sys.prefix
    if GetOption('stand_alone'):
        PY_PACKAGES_DIR_DEFAULT = '$PREFIX/lib'
    else:
        PY_PACKAGES_DIR_DEFAULT = \
            os.path.join(python_prefix, 'Lib', 'site-packages')
    PY_HEADERS_DIR_DEFAULT = os.path.join(python_prefix, 'include')
    PY_LIB_DIR_DEFAULT = os.path.join(python_prefix, 'libs')
    PY_LIB_NAME = 'python$PY_VERSION_MAJOR$PY_VERSION_MINOR'

# Platform-independant variables:
variables.AddVariables(
    PathVariable(
        'PREFIX',
        'directory under which most or all of the program components should '
            'be installed',
        PREFIX_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'EXEC_PREFIX',
        'directory under which machine-specific compiled libraries and '
            'objects should be installed',
        '$PREFIX',
        is_abs_path,
    ),
    PathVariable(
        'BIN_DIR',
        'directory where program executables should be installed',
        BIN_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'DATA_ROOT_DIR',
        'directory under which read-only, architecture-independant data files '
            'should be installed',
        DATA_ROOT_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'DATA_DIR',
        'directory where read-only, architecture-independant data files '
            'should be installed',
        '$DATA_ROOT_DIR',
        is_abs_path,
    ),
    PathVariable(
        'SYS_CONF_DIR',
        'directory where read-only, machine-specific data files should be '
            'installed',
        SYS_CONF_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'INCLUDE_DIR',
        'directory where C/C++ header files should be installed',
        INCLUDE_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'DOC_DIR',
        'directory where program documentation should be installed',
        DOC_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'LIB_DIR',
        'directory where platform-dependant, compiled library and object '
            'files should be installed',
        LIB_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'PY_PACKAGES_DIR',
        'directory where pure Python modules and packages should be installed',
        PY_PACKAGES_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'INCLUDE_DIR',
        'directory where C/C++ header files should be installed',
        INCLUDE_DIR_DEFAULT,
        is_abs_path,
    ),
    PathVariable(
        'PY_HEADERS_DIR',
        'directory where Python.h can be found',
        PY_HEADERS_DIR_DEFAULT,
    ),
    PathVariable(
        'PY_LIB_DIR',
        'directory where the Python shared library can be found',
        PY_LIB_DIR_DEFAULT,
    ),
    BoolVariable(
        'DEBUG',
        'if True, compile the program launcher executable with debugging '
            'symbols',
        False,
    ),
)

python_version_tuple = platform.python_version_tuple()

environment = Environment(
    tools=['default', 'cpython', 'copyrecurse', 'textfile', 'packaging'],
    variables=variables,
    PROJECT_NAME='parpg',
    PROJECT_VERSION_MAJOR=0,
    PROJECT_VERSION_MINOR=2,
    PROJECT_VERSION_PATCH=0,
    PROJECT_VERSION_SHORT='${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MAJOR}',
    PROJECT_VERSION_LONG='${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.'
                         '${PROJECT_VERSION_PATCH}',
    PYTHON=sys.executable,
    PY_VERSION_MAJOR=python_version_tuple[0],
    PY_VERSION_MINOR=python_version_tuple[1],
    PY_VERSION_PATCH=python_version_tuple[2],
    PY_VERSION_SHORT='${PY_VERSION_MAJOR}.${PY_VERSION_MINOR}',
    PY_VERSION_LONG='${PY_VERSION_MAJOR}.${PY_VERSION_MINOR}.'
                    '${PY_VERSION_PATCH}',
    PY_LIB_NAME=PY_LIB_NAME,
    BUILD_DIR='build',
)
if environment['DEBUG']:
    if platform_name == 'Windows':
        environment['CCPDBFLAGS'] = '/Z7 /Od'
        environment.AppendUnique(LINKFLAGS='/DEBUG')
    else:
        environment.AppendUnique(CCFLAGS=['-gdwarf-2', '-g3'])

Help(variables.GenerateHelpText(environment))

config_dict = [('@{0}@'.format(key), environment.Dictionary()[key]) for key in
               ('PREFIX', 'LIB_DIR', 'PY_PACKAGES_DIR', 'BIN_DIR',
                'SYS_CONF_DIR', 'DATA_DIR', 'PYTHON')]

copy_py_packages = environment.Install(
    '$BUILD_DIR',
    'src/parpg',
)
install_py_packages = environment.InstallPython(
    '$PY_PACKAGES_DIR',
    copy_py_packages,
    PYTHON_COMPILE=GetOption('compile'),
)

copy_data = environment.CopyRecurse(
    '$BUILD_DIR',
    'data',
)

subst_config_file = environment.Substfile(
    '$BUILD_DIR/parpg.cfg',
    'parpg.cfg.in',
    SUBST_DICT=config_dict,
)
install_config_files = environment.InstallChmod(
    '$SYS_CONF_DIR',
    subst_config_file,
    mode=0755,
)
Requires(install_config_files, subst_config_file)

if platform_name == 'Windows':
    launcher_name = 'parpg.bat'
else:
    launcher_name = 'parpg.sh'
# FIXME M. George Hansen 2011-05-20: Do any other sequences need to be escaped?
launcher = environment.SubstfileEscape(
    'build/$LAUNCHER_NAME',
    'bin/${LAUNCHER_NAME}.in',
    LAUNCHER_NAME=launcher_name,
    SUBST_DICT=config_dict,
    ESCAPE_SEQUENCES={'\\': r'\\\\', '"': r'\\"'},
)
if platform_name == 'Windows':
    install_launcher = environment.InstallExecutable(
        '$BIN_DIR',
        launcher,
    )
else:
    # Remove the .sh suffix, since it isn't needed on unix platforms.
    install_launcher = environment.InstallAs(
        '$BIN_DIR/parpg',
        launcher,
    )

# TODO M. George Hansen 2011-05-12: Implement package builder.
#package = environment.Package(
#    NAME='parpg',
#    VERSION='0.2.0',
#    PACKAGEVERSION=0,
#    LICENSE='gpl',
#    SUMMARY='',
#    DESCRIPTION='',
#    X_RPM_GROUP='Application/parpg',
#)

build = Alias('build', [launcher, subst_config_file, copy_py_packages,
                        copy_data])
install = Alias('install', [build, install_launcher, install_py_packages,
                install_config_files])

Default(build)