#! /usr/bin/env python

#
# Configure Script for Sketch
#
# This script tries to figure out how to configure Sketch to work work
# with your python installation.
#
# It is very experimental at the moment. In particular it is not as
# generic as it could be and contains lots of hacks to make it work for
# the current release.
#

import sys, os
import re
import glob
import compileall
import shutil, pipes

from string import split, join, strip


#
# Part 1:
#
# extract config info from Python's modules Setup.
#

rx_comment = re.compile('^[ \t]*#')
rx_macro = re.compile('^.*=')
rx_ignore = re.compile(r'[ \t]*($|\*)')

class ModuleConfig:

    def __init__(self):
        self.files = []
        self.inc_dirs = []
        self.macros = []
        self.lib_dirs = []
        self.libraries = []


def read_target(args, setup):
    while args[-2:] == '\\\n':
        #print '->', args
        line = setup.readline()
        #print line
        if not line:
            break
        if rx_comment.match(line):
            continue
        args = args[:-2] + line

    config = ModuleConfig()

    for item in split(args):
        if item == '#':
            break
        head = item[:2]
        if head == '-I':
            config.inc_dirs.append(item)
        elif head == '-D':
            config.macros.append(item)
        elif head == '-L':
            config.lib_dirs.append(item)
        elif head == '-l':
            config.libraries.append(item)
        else:
            config.files.append(item)
    
    return config


def extract_config(setup):
    configs = {}
    setup = open(setup)

    while 1:
        line = setup.readline()
        if not line:
            break
        #print line
        if rx_comment.match(line):
            #print '==>> is comment'
            continue
        if rx_macro.match(line):
            #print '==>> is macro'
            continue
        if rx_ignore.match(line):
            #print '==>> ignore'
            continue

        #print '==>> is target',
        target, args = split(line, None, 1)
        #print target
        configs[target] = read_target(args, setup)
            

    return configs

    
def print_configs(configs):
    for key, value in configs.items():
        print '********', key
        print 'file    ', value.files
        print 'inc_dirs', value.inc_dirs
        print 'macros  ', value.macros
        print 'lib_dirs', value.lib_dirs
        print 'libraries', value.libraries
    

#
# Part 2:
#
# Configure:
# Convert Sketch's Setup.config to Setup
#

def find_include_dir(dir, header):
    files = os.listdir(dir)
    if header in files:
        return dir
    else:
        for file in files:
            file = os.path.join(dir, file)
            if os.path.isdir(file):
                result = find_include_dir(file, header)
                if result:
                    return result
    return ''


rx_replace = re.compile(r'@([a-zA-Z_0-9]+):([^@]+)@')

setup_comment = '''\
# This file was generated from Setup.config by configure.py
# If you want to edit the configuration by hand, edit Setup.in and
# copy it to Setup
'''

def convert(input, output, configs, flags):
    input = open(input)
    output = open(output, 'w')
    #output = sys.stdout

    output.write(setup_comment)
    write_nl = 0
    while 1:
        line = input.readline()
        if not line:
            break
        if rx_comment.match(line):
            continue
        while 1:
            found = rx_replace.search(line)
            if found:
                config_name = found.group(1)
                if flags.has_key(config_name):
                    flag_name = found.group(2)
                    config_flags = flags[config_name]
                    if config_flags.has_key(flag_name):
                        value = config_flags[flag_name]
                    else:
                        raise ValueError, 'Unknown flag %s:%s' % (config_name,
                                                                  flag_name)
                elif configs.has_key(config_name):
                    config = configs[config_name]
                    items = split(found.group(2), ',')
                    value = []
                    for item in items:
                        if hasattr(config, item):
                            value = value + getattr(config, item)
                        else:
                            raise ValueError, \
                                  'Unknown config item %s:%s' % (config_name,
                                                                 item)
                    value = join(value)
                else:
                    raise ValueError, 'Unknown config type %s' % (config_name,)
                line = line[:found.start(0)] + value + line[found.end(0):]
            else:
                break
        if line[-2:] == '\\\n':
            line = line[:-2]
            write_nl = 1
        output.write(line)
    #print line
    if write_nl:
        output.write('\n')
    write_nl = 0

def make_boot(dir):
    # run 'make -f Makefile.pre.in boot' in dir.
    os.system('cd %s; make -f Makefile.pre.in boot PYTHON=%s'
              % (dir, sys.executable))

def configure(dirs, flags, setup):
    if not flags['sketch'].has_key('imaging-include'):
        print 'option --imaging-include=DIR must be provided'
        sys.exit(1)
    else:
        value = flags['sketch']['imaging-include']
        value = os.path.expanduser(os.path.expandvars(value))
        header = 'Imaging.h'
        print 'looking for include dir for %s under %s' % (header, value)
        dir = find_include_dir(value, header)
        if not dir:
            print header, 'not found under', value, '!'
            sys.exit(1)
        print 'found it in', dir
        flags['sketch']['imaging-include'] = '-I' + dir

    if setup == None:
        setup = os.path.join(sys.prefix, 'lib/python' + sys.version[:3],
                             'config/Setup')
    print 'reading configuration from', setup, '...',
    configs = extract_config(setup)
    print 'done'

    if not configs.has_key('_tkinter'):
        print "Your Python installation doesn't seem to be configured with "\
              "tkinter."
        sys.exit(1)        

    for dir in dirs:
        file = os.path.join(dir, 'Setup.config')
        if os.path.isfile(file):
            out = os.path.splitext(file)[0]
            print 'converting', file, 'to', out, '...',
            convert(file, out, configs, flags)
            print 'done'
        make_boot(dir)

#
# Build
#

def make(dir, make_flags):
    # run 'make' in dir.
    os.system('cd %s; make %s' % (dir, join(make_flags)))


def build(makedirs, make_flags):
    for dir in makedirs:
        print "running 'make' in", dir
        make(dir, make_flags)


#
# Install
#

rx_replace_dir = re.compile(r'@([a-z]+)')

class InstallDirs:

    prefix = ''
    exec_prefix = ''
    executable = ''
    library = ''
    destdir = ''

    def __init__(self, flags):
        self.prefix = flags['standard']['prefix']
        self.destdir = flags['standard']['destdir']

    def fix_dirs(self, progname, version):
        # e.g. progname = 'sketch', version = '0.5.3'
        if not self.exec_prefix:
            self.exec_prefix = self.prefix
        if not self.executable:
            self.executable = os.path.join(self.exec_prefix, 'bin')
        if not self.library:
            self.library = os.path.join(self.prefix, 'lib',
                                        progname + '-' + version)

    def replace_dirs(self, string):
        result = ''
        match = 1
        while match:
            match = rx_replace_dir.search(string)
            if match is not None:
                start, end = match.span(0)
                dir = getattr(self, match.group(1))
                result = result + string[:start] + dir
                string = string[end:]
            else:
                result = result + string
        return result

    def prepend_destdir(self, dir):
        # this may return a filename with multiple consecutive slashes
        # but that shouldn't be problem on Linux.
        if self.destdir:
            return self.destdir + '/' + dir
        return dir


# return the longest common prefix of path1 and path2 that is a
# directory.
def commonbasedir(path1, path2):
    if path1[-1] != os.sep:
	path1 = path1 + os.sep
    return os.path.split(os.path.commonprefix([path1, path2]))[0]



# return the absolute path PATH2 as a path relative to the directory
# PATH1. If commonbasedir(PATH1, PATH2) is '/', return PATH2. Doesn't
# take symbolic links into account...
def relpath(path1, path2):
    #if not os.path.isabs(path2):
    #	return path2
    basedir = commonbasedir(path1, path2)
    if basedir == os.sep:
	return path2
    path2 = path2[len(basedir) + 1 : ]
    curbase = path1
    while curbase != basedir:
	curbase = os.path.split(curbase)[0]
	path2 = os.pardir + os.sep + path2
    return path2

def create_directory(dir):
    if os.path.isdir(dir):
	return
    parent, base = os.path.split(dir)
    if parent:
        create_directory(parent)
    try:
        os.mkdir(dir, 0777)
    except os.error, exc:
        print "can't create directory %s:%s" % (dir, exc)

    
def link_file(source, dest):
    if os.path.isfile(dest) or os.path.islink(dest):
        # XXX should we really remove this
        try:
            os.unlink(dest)
        except os.error, exc:
            print "can't create remove %s:%s" % (dest, exc)
    try:
        os.symlink(source, dest)
    except os.error, exc:
        print "can't create symbolic link %s in %s:%s" % (source, dest, exc)

def install_file(srcfile, dest, flags, dirs, verbose = 1, noop = 0):
    # srcfile must be a relative pathname, dest a directory
    srcdir, basename = os.path.split(srcfile)
    if 'recursive' in flags and not os.path.isabs(srcdir):
        destdir = os.path.join(dest, srcdir)
    else:
        destdir = os.path.normpath(dest)
    if not noop:
        create_directory(destdir)
    if 'link' in flags:
        # symlink
        # XXX should the link be relative if the directories have a
        # common prefix?
        if basename[-3:] == '.py':
            # XXX hack
            basename = basename[:-3]
        destfile = os.path.join(destdir, basename)
        if 'relative' in flags:
            # make srcfile a filename relative to destdir. Strip the
            # value of --dest-dir.
            d = destdir
            if dirs.destdir:
                normalized = os.path.normpath(dirs.destdir)
                length = len(normalized)
                if normalized == destdir[:length]:
                    d = destdir[length:]
                    if d[0] != '/':
                        d = '/' + d
            srcfile = relpath(d, srcfile)
        if verbose:
            print 'create symlink %s in %s' % (srcfile, destfile)
        if not noop:
            link_file(srcfile, destfile)
    else:
        # copy file
        destfile = os.path.join(destdir, basename)
        if verbose:
            print 'copying %s to %s' % (srcfile, destfile)
        if not noop:
            shutil.copy(srcfile, destfile)
        
def bytecompile(dir):
    compileall.compile_dir(os.path.join(os.getcwd(), dir))

def install(config, dirs):
    # 
    # config is a list of tuples. Each tuple is of the form
    # (PATTERN, DEST)  or  (PATTERN, DEST, FLAGS)
    #
    for item in config:
        if len(item) == 2:
            pattern, dest = item
            flags = ()
        else:
            pattern, dest, flags = item
            if type(flags) == type(''):
                flags = (flags,)
        pattern = dirs.replace_dirs(pattern)
        dest = dirs.prepend_destdir(dirs.replace_dirs(dest))
        files = glob.glob(pattern)
        #print pattern, dest, files
        if not files and 'link' in flags:
            # hack for symlinks. The source may not exist during tests
            files = (pattern,)
        
        for file in files:
            install_file(file, dest, flags, dirs)
            #print 'install', file, 'in', dest
#
# Part 3:
#
# Drivers
#

def get_version():
    version = strip(open('Sketch/VERSION').read())
    return version

def parse_cmd_line():
    setup = None
    argv = sys.argv[1:]
    flags = {}
    flags['standard'] = {'prefix': '/usr/local/', 'destdir':''}
    flags['pax'] = {'XSHM': ''}
    flags['intl'] = {'files': ''}
    flags['sketch'] = {'imaging-include':
                       os.path.join(sys.prefix, 'include',
                                    'python' + sys.version[:3])}
    flags['make_defs'] = []
    if len(argv) == 0:
        command = 'help'
    else:
        command = argv[0]
        if command in ('-h', '--help'):
            command = 'help'
        del argv[0]
    for arg in argv:
        if '=' in arg:
            arg, value = split(arg, '=')
        else:
            value = None
        if arg == '--prefix':
            if value is None:
                print 'Value required for option --prefix'
                sys.exit(1)
            flags['standard']['prefix'] = value
        elif arg == '--dest-dir':
            flags['standard']['destdir'] = value
        elif arg == '--python-setup':
            setup = value
        elif arg == '--pax-no-xshm':
            flags['pax']['XSHM'] = '-DPAX_NO_XSHM'
        elif arg == '--imaging-include':
            if value is None:
                print 'Value required for option --imaging-include'
                sys.exit(1)
            flags['sketch']['imaging-include'] = value
        elif arg == '--with-nls':
            flags['intl']['files'] = 'intl intl.c'
        elif arg in ('-h', '--help'):
            command = 'help'
        elif arg[0] != '-' and value:
            flags['make_defs'].append(pipes.quote(arg + '=' + value))
        else:
            sys.stderr.write('Unknown option %s\n' % arg)

    return command, flags, setup

def print_help():
    setup = os.path.join(sys.prefix, 'lib/python' + sys.version[:3],
                         'config/Setup')
    print help_message % {'version': get_version(), 
                          'pyprefix': sys.prefix,
                          'pyversion': sys.version[:3],
                          'pysetup': setup}

help_message = """\
Usage: setup.py COMMAND [options...]

setup.py configures, builds and installs Sketch. COMMAND is one of:

        configure         configure Sketch
        build             compile the C extension modules
        install           install Sketch on your system

Generic options:
  -h, --help              print this help message
Options for configure:
  --imaging-include=DIR   Look (recursively) under DIR for the header files
                          of PIL (Python Imaging Library)
                          [%(pyprefix)s/include/python%(pyversion)s]
  --pax-no-xshm           Compile Pax (a module for direct access to Xlib)
                          without support for the X Shared Memory extension.
  --with-nls              Enable national language support for messages, menus,
                          etc. You need the gettext library for this.
  --python-setup=FILE     The python Setup file to parse.
                          [%(pysetup)s]
Options for build:
  <VAR>=<VALUE>           Options like this are passed through to make
                          to let you override variables like CC or OPT.
                          See the generated Makefiles for more details.
Options for install:
  --prefix=PREFIX         Install files in PREFIX/lib/sketch-%(version)s/ and
                          PREFIX/bin [/usr/local]
"""

    
make_dirs = ('Pax', 'Filter', 'Sketch/Modules')

lib = '@library'
bin = '@executable'
install_config = \
[
    ('sketch.py', lib, 'executable'),
    ('sk2ps.py', lib, 'executable'),
    ('Plugins/*/*.py', lib, 'recursive'),
    ('Sketch/*.py', lib, 'recursive'),
    ('Sketch/VERSION', lib, 'recursive'),
    ('Sketch/*/*.py', lib, 'recursive'),
    ('Sketch/*/*.so', lib, 'recursive'),
    ('Sketch/*/*.xbm', lib, 'recursive'),
    ('Sketch/*/*/*.xbm', lib, 'recursive'),
    ('Sketch/*/*.gif', lib, 'recursive'),
    ('Script/*.py', lib, 'recursive'),
    ('Resources/Fontmetrics/*', lib, 'recursive'),
    ('Resources/Misc/*', lib, 'recursive'),
    ('Pax/*.so', os.path.join(lib, 'Lib')),
    ('Pax/*.py', os.path.join(lib, 'Lib')),
    ('Filter/*.so', os.path.join(lib, 'Lib')),
    (os.path.join(lib, 'sketch.py'), bin, ('link', 'relative')),
    (os.path.join(lib, 'sk2ps.py'), bin, ('link', 'relative')),
]
progname = 'sketch'
version = None

def intl_available():
    sys.path.insert(0, os.path.join(sys.path[0], 'Pax'))
    try:
        import intl
        #print 'intl available'
        return 1
    except:
        #print 'intl not available'
        return 0

def main():
    global version
    version = get_version()
    command, flags, setup = parse_cmd_line()
    if command == 'help':
        print_help()
    elif command == 'configure':
        configure(make_dirs, flags, setup)
    elif command == 'build':
        build(make_dirs, flags['make_defs'])
    elif command == 'install':
        dirs = InstallDirs(flags)
        dirs.fix_dirs(progname = progname, version = version)
        install(install_config, dirs)
        if intl_available():
            install([('Resources/Messages/*/*/*.mo', lib, 'recursive')], dirs)
        for dir in ('Sketch', 'Plugins', 'Lib', 'Script'):
            dir = dirs.prepend_destdir(os.path.join(dirs.library, dir))
            bytecompile(dir)
    else:
        print 'unknown command', command
        print_help()



if __name__ == '__main__':
    main()

