Fixes to OSX builds.
[cdist.git] / cdist
diff --git a/cdist b/cdist
index 80c63a6281a5d2e78a1f301873a01eaebde277d8..268a6f49f3d7f0d092000d6a8f56f3e69a3a9b29 100755 (executable)
--- a/cdist
+++ b/cdist
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 
-#    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+#    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
 #
 #    This program is free software; you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License as published by
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
-#
-# Configuration
-#
-
-# Directory to build things in within chroots
-DIR_IN_CHROOT = '/home/carl'
-# Prefix of chroots in the filesystem
-CHROOT_PREFIX = '/home/carl/Environments'
-# Prefix of windows environments
-WINDOWS_ENVIRONMENT_PREFIX = '/home/carl/Environments/windows'
-# Git prefix
-GIT_DIR = 'ssh://houllier/home/carl/git'
-OSX_BUILD_HOST = 'carl@192.168.1.94'
-DIR_ON_HOST = '/Users/carl/cdist'
-OSX_ENVIRONMENT_PREFIX = '/Users/carl/Environments/osx/10.8'
-
-
 import os
 import sys
 import shutil
@@ -42,6 +25,77 @@ import argparse
 import datetime
 import subprocess
 import re
+import copy
+import inspect
+
+TEMPORARY_DIRECTORY = '/tmp'
+
+class Error(Exception):
+    def __init__(self, value):
+        self.value = value
+    def __str__(self):
+        return '\x1b[31m%s\x1b[0m' % repr(self.value)
+    def __repr__(self):
+        return str(self)
+
+#
+# Configuration
+#
+
+class Option(object):
+    def __init__(self, key):
+        self.key = key
+        self.value = None
+
+    def offer(self, key, value):
+        if key == self.key:
+            self.value = value
+
+class BoolOption(object):
+    def __init__(self, key):
+        self.key = key
+        self.value = False
+
+    def offer(self, key, value):
+        if key == self.key:
+            self.value = (value == 'yes' or value == '1' or value == 'true')
+
+class Config:
+    def __init__(self):
+        self.options = [ Option('linux_chroot_prefix'),
+                         Option('windows_environment_prefix'),
+                         Option('mingw_prefix'),
+                         Option('git_prefix'),
+                         Option('osx_build_host'),
+                         Option('osx_environment_prefix'),
+                         Option('osx_sdk_prefix'),
+                         Option('osx_sdk') ]
+
+        try:
+            f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
+            while 1:
+                l = f.readline()
+                if l == '':
+                    break
+
+                if len(l) > 0 and l[0] == '#':
+                    continue
+
+                s = l.strip().split()
+                if len(s) == 2:
+                    for k in self.options:
+                        k.offer(s[0], s[1])
+        except:
+            raise
+
+    def get(self, k):
+        for o in self.options:
+            if o.key == k:
+                return o.value
+
+        raise Error('Required setting %s not found' % k)
+
+config = Config()
 
 #
 # Utility bits
@@ -51,10 +105,6 @@ def log(m):
     if not args.quiet:
         print '\x1b[33m* %s\x1b[0m' % m
 
-def error(e):
-    print '\x1b[31mError: %s\x1b[0m' % e
-    sys.exit(1)
-
 def copytree(a, b):
     log('copy %s -> %s' % (a, b))
     shutil.copytree(a, b)
@@ -75,7 +125,7 @@ def command(c, can_fail=False):
     log(c)
     r = os.system(c)
     if (r >> 8) and not can_fail:
-        error('command %s failed' % c)
+        raise Error('command %s failed' % c)
 
 def command_and_read(c):
     log(c)
@@ -83,6 +133,20 @@ def command_and_read(c):
     f = os.fdopen(os.dup(p.stdout.fileno()))
     return f
 
+def read_wscript_variable(directory, variable):
+    f = open('%s/wscript' % directory, 'r')
+    while 1:
+        l = f.readline()
+        if l == '':
+            break
+        
+        s = l.split()
+        if len(s) == 3 and s[0] == variable:
+            f.close()
+            return s[2][1:-1]
+
+    f.close()
+    return None
 
 #
 # Version
@@ -90,78 +154,129 @@ def command_and_read(c):
 
 class Version:
     def __init__(self, s):
-        self.pre = False
-        self.beta = None
+        self.devel = False
 
         if s.startswith("'"):
             s = s[1:]
         if s.endswith("'"):
             s = s[0:-1]
         
+        if s.endswith('devel'):
+            s = s[0:-5]
+            self.devel = True
+
         if s.endswith('pre'):
             s = s[0:-3]
-            self.pre = True
-
-        b = s.find("beta")
-        if b != -1:
-            self.beta = int(s[b+4:])
-            s = s[0:b]
 
         p = s.split('.')
         self.major = int(p[0])
         self.minor = int(p[1])
+        if len(p) == 3:
+            self.micro = int(p[2])
+        else:
+            self.micro = 0
 
-    def bump(self):
+    def bump_minor(self):
         self.minor += 1
-        self.pre = False
-        self.beta = None
+        self.micro = 0
 
-    def to_pre(self):
-        self.pre = True
-        self.beta = None
+    def bump_micro(self):
+        self.micro += 1
 
-    def bump_and_to_pre(self):
-        self.bump()
-        self.pre = True
-        self.beta = None
+    def to_devel(self):
+        self.devel = True
 
     def to_release(self):
-        self.pre = False
-        self.beta = None
-
-    def bump_beta(self):
-        if self.pre:
-            self.pre = False
-            self.beta = 1
-        elif self.beta is not None:
-            self.beta += 1
-        elif self.beta is None:
-            self.beta = 1
+        self.devel = False
 
     def __str__(self):
-        s = '%d.%02d' % (self.major, self.minor)
-        if self.beta is not None:
-            s += 'beta%d' % self.beta
-        elif self.pre:
-            s += 'pre'
+        s = '%d.%d.%d' % (self.major, self.minor, self.micro)
+        if self.devel:
+            s += 'devel'
 
         return s
 
-
 #
-# Environment
+# Targets
 #
 
-class Environment(object):
-    def __init__(self):
+class Target(object):
+    # @param directory directory to work in; if None we will use a temporary directory
+    # Temporary directories will be removed after use; specified directories will not
+    def __init__(self, platform, parallel, directory=None):
+        self.platform = platform
+        self.parallel = parallel
+
+        if directory is None:
+            self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
+            self.rmdir = True
+        else:
+            self.directory = directory
+            self.rmdir = False
+
+        print 'Working in %s' % self.directory
+
+        # Environment variables that we will use when we call cscripts
         self.variables = {}
+        self.debug = False
+
+    def build_dependencies(self, project):
+        cwd = os.getcwd()
+        if 'dependencies' in project.cscript:
+            for d in project.cscript['dependencies'](self):
+                log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
+                dep = Project(d[0], '.', d[1])
+                dep.checkout(self)
+                self.build_dependencies(dep)
+
+                # Make the options to pass in from the option_defaults of the thing
+                # we are building and any options specified by the parent.
+                options = {}
+                if 'option_defaults' in dep.cscript:
+                    options = dep.cscript['option_defaults']()
+                    if len(d) > 2:
+                        for k, v in d[2].iteritems():
+                            options[k] = v
+
+                self.build(dep, options)
+
+        os.chdir(cwd)
+
+    def build(self, project, options=None):
+        variables = copy.copy(self.variables)
+        if len(inspect.getargspec(project.cscript['build']).args) == 2:
+            project.cscript['build'](self, options)
+        else:
+            project.cscript['build'](self)
+        self.variables = variables
+
+    def package(self, project):
+        project.checkout(self)
+        self.build_dependencies(project)
+        self.build(project)
+        return project.cscript['package'](self, project.version)
+
+    def test(self, project):
+        project.checkout(self)
+        self.build_dependencies(project)
+        self.build(project)
+        project.cscript['test'](self)
 
     def set(self, a, b):
         self.variables[a] = b
 
+    def unset(self, a):
+        del(self.variables[a])
+
     def get(self, a):
         return self.variables[a]
 
+    def append_with_space(self, k, v):
+        if not k in self.variables:
+            self.variables[k] = v
+        else:
+            self.variables[k] = '%s %s' % (self.variables[k], v)
+
     def variables_string(self, escaped_quotes=False):
         e = ''
         for k, v in self.variables.iteritems():
@@ -170,212 +285,183 @@ class Environment(object):
             e += '%s=%s ' % (k, v)
         return e
 
-    def work_dir_cdist(self, sub):
-        assert(false)
-
-    def work_dir_cscript(self):
-        assert(false)
-
-    def build_dependencies(self, target, project):
-        cwd = os.getcwd()
-        if 'dependencies' in project.cscript:
-            for d in project.cscript['dependencies'](target):
-                dep = Project(d[0], '.', d[1])
-                dep.checkout(self)
-                self.build(target, dep)
-        os.chdir(cwd)
+    def cleanup(self):
+        if self.rmdir:
+            rmtree(self.directory)
 
-    def build(self, target, project):
-        project.cscript['build'](self, target)
+# 
+# Windows
+#
 
-    def package(self, target, project):
-        project.checkout(self)
-        if target.platform != 'source':
-            self.build_dependencies(target, project)
-        if target.platform == 'source':
-            command('./waf dist')
-            if project.directory != '.':
-                return os.path.abspath('%s-%s.tar.bz2' % (project.directory, project.version))
-            return os.path.abspath('%s-%s.tar.bz2' % (project.name, project.version))
+class WindowsTarget(Target):
+    def __init__(self, bits, directory=None):
+        super(WindowsTarget, self).__init__('windows', 2, directory)
+        self.bits = bits
+
+        self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
+        if not os.path.exists(self.windows_prefix):
+            raise Error('windows prefix %s does not exist' % self.windows_prefix)
+            
+        if self.bits == 32:
+            self.mingw_name = 'i686'
         else:
-            project.cscript['build'](self, target)
-            return project.cscript['package'](self, target, project.version)
+            self.mingw_name = 'x86_64'
+
+        mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
+        self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
+
+        self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
+        self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
+        self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
+        self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
+        self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
+        self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
+        self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
+        self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
+        cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
+        link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
+        for p in self.mingw_prefixes:
+            cxx += ' -I%s/include' % p
+            link += ' -L%s/lib' % p
+        self.set('CXXFLAGS', '"%s"' % cxx)
+        self.set('LINKFLAGS', '"%s"' % link)
 
-    def cleanup(self):
-        pass
+    def command(self, c):
+        log('host -> %s' % c)
+        command('%s %s' % (self.variables_string(), c))
 
 #
-# ChrootEnvironment
+# Linux
 #
 
-class ChrootEnvironment(Environment):
-    def __init__(self, chroot):
-        super(ChrootEnvironment, self).__init__()
-        self.chroot = chroot
-        self.dir_in_chroot = DIR_IN_CHROOT
-        self.chroot_dir = CHROOT_PREFIX
-
-        # ChrootEnvironments work in dir_in_chroot, and clear
-        # it out before use
-        for g in glob.glob('%s/*' % self.work_dir_cdist()):
-            rmtree(g)
-
-        # Environment variables
-        self.set('CXXFLAGS', '-I%s/include' % self.work_dir_cscript())
-        self.set('LINKFLAGS', '-L%s/lib' % self.work_dir_cscript())
-        self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % self.work_dir_cscript())
-
-    def work_dir_cdist(self):
-        return '%s/%s%s' % (self.chroot_dir, self.chroot, self.dir_in_chroot)
-
-    def work_dir_cscript(self):
-        return self.dir_in_chroot
+class LinuxTarget(Target):
+    def __init__(self, distro, version, bits, directory=None):
+        super(LinuxTarget, self).__init__('linux', 2, directory)
+        self.distro = distro
+        self.version = version
+        self.bits = bits
+        # e.g. ubuntu-14.04-64
+        self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
+        # e.g. /home/carl/Environments/ubuntu-14.04-64
+        self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
+
+        self.set('CXXFLAGS', '-I%s/include' % self.directory)
+        self.set('LINKFLAGS', '-L%s/lib' % self.directory)
+        self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
+        self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
 
     def command(self, c):
-        # Work out the cwd for the chrooted command
-        cwd = os.getcwd()
-        prefix = '%s/%s' % (self.chroot_dir, self.chroot)
-        assert(cwd.startswith(prefix))
-        cwd = cwd[len(prefix):]
-
-        log('schroot [%s] -> %s' % (cwd, c))
-        command('%s schroot -c %s -d %s -p -- %s' % (self.variables_string(), self.chroot, cwd, c))
-
+        command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
 
 #
-# RemoteEnvironment
+# OS X
 #
 
-class RemoteEnvironment(Environment):
-    def __init__(self, host):
-        super(RemoteEnvironment, self).__init__()
-        self.host = host
-        self.dir_on_host = DIR_ON_HOST
-        self.host_mount_dir = tempfile.mkdtemp()
+class OSXTarget(Target):
+    def __init__(self, directory=None):
+        super(OSXTarget, self).__init__('osx', 4, directory)
 
-        # Mount the remote host on host_mount_dir
-        command('sshfs %s:%s %s' % (self.host, self.dir_on_host, self.host_mount_dir))
-        for g in glob.glob('%s/*' % self.host_mount_dir):
-            rmtree(g)
+    def command(self, c):
+        command('%s %s' % (self.variables_string(False), c))
 
-        # Environment variables
-        self.set('CXXFLAGS', '"-I%s/include -I%s/include"' % (self.dir_on_host, OSX_ENVIRONMENT_PREFIX))
-        self.set('LINKFLAGS', '"-L%s/lib -L%s/lib"' % (self.dir_on_host, OSX_ENVIRONMENT_PREFIX))
-        self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig' % (self.dir_on_host, OSX_ENVIRONMENT_PREFIX))
-        self.set('PATH', '$PATH:/usr/local/bin:%s/bin' % OSX_ENVIRONMENT_PREFIX)
 
-    def work_dir_cdist(self):
-        return self.host_mount_dir
+class OSXSingleTarget(OSXTarget):
+    def __init__(self, bits, directory=None):
+        super(OSXSingleTarget, self).__init__(directory)
+        self.bits = bits
 
-    def work_dir_cscript(self):
-        return self.dir_on_host
+        if bits == 32:
+            arch = 'i386'
+        else:
+            arch = 'x86_64'
 
-    def command(self, c):
-        # Work out the cwd for the chrooted command
-        cwd = os.getcwd()
-        assert(cwd.startswith(self.host_mount_dir))
-        cwd = cwd[len(self.host_mount_dir):]
+        flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
+        enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
 
-        log('ssh [%s] -> %s' % (cwd, c))
-        command('ssh %s -- "cd %s%s; %s %s"' % (self.host, self.dir_on_host, cwd, self.variables_string(True), c))
+        # Environment variables
+        self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
+        self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
+        self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
+        self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
+        self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
+        self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
+        self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
 
-    def cleanup(self):
-        os.chdir('/')
-        command('fusermount -u %s' % self.host_mount_dir)
-        rmdir(self.host_mount_dir)
+    def package(self, project):
+        raise Error('cannot package non-universal OS X versions')
 
-#
-# HostEnvironment
-#
 
-class HostEnvironment(Environment):
+class OSXUniversalTarget(OSXTarget):
     def __init__(self, directory=None):
-        super(HostEnvironment, self).__init__()
-        if directory is None:
-            self.directory = tempfile.mkdtemp()
-            self.rmdir = True
-        else:
-            self.directory = directory
-            self.rmdir = False
+        super(OSXUniversalTarget, self).__init__(directory)
+        self.parts = []
+        self.parts.append(OSXSingleTarget(32, os.path.join(self.directory, '32')))
+        self.parts.append(OSXSingleTarget(64, os.path.join(self.directory, '64')))
+
+    def package(self, project):
+        for p in self.parts:
+            project.checkout(p)
+            p.build_dependencies(project)
+            p.build(project)
+
+        return project.cscript['package'](self, project.version)
+    
 
-    def work_dir_cdist(self):
-        return self.directory
+#
+# Source
+#
 
-    def work_dir_cscript(self):
-        return self.directory
+class SourceTarget(Target):
+    def __init__(self):
+        super(SourceTarget, self).__init__('source', 2)
 
     def command(self, c):
         log('host -> %s' % c)
         command('%s %s' % (self.variables_string(), c))
 
     def cleanup(self):
-        if self.rmdir:
-            rmtree(self.directory)
-
+        rmtree(self.directory)
 
-def prepare_for_windows(env, bits):
-    env.windows_prefix = '%s/%d' % (WINDOWS_ENVIRONMENT_PREFIX, bits)
-    if not os.path.exists(env.windows_prefix):
-        error('windows prefix %s does not exist' % env.windows_prefix)
-
-    if bits == 32:
-        mingw_name = 'i686'
-    else:
-        mingw_name = 'x86_64'
-
-    mingw_path = '/mingw/%d/bin' % bits
-    mingw_prefixes = ['/mingw/%d' % bits, '/mingw/%d/%s-w64-mingw32' % (bits, mingw_name)]
-
-    env.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % env.windows_prefix)
-    env.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % env.work_dir_cscript())
-    env.set('PATH', '%s/bin:%s:%s' % (env.windows_prefix, mingw_path, os.environ['PATH']))
-    env.set('CC', '%s-w64-mingw32-gcc' % mingw_name)
-    env.set('CXX', '%s-w64-mingw32-g++' % mingw_name)
-    env.set('LD', '%s-w64-mingw32-ld' % mingw_name)
-    env.set('RANLIB', '%s-w64-mingw32-ranlib' % mingw_name)
-    env.set('WINRC', '%s-w64-mingw32-windres' % mingw_name)
-    cxx = '-I%s/include -I%s/include' % (env.windows_prefix, env.work_dir_cscript())
-    link = '-L%s/lib -L%s/lib' % (env.windows_prefix, env.work_dir_cscript())
-    for p in mingw_prefixes:
-        cxx += ' -I%s/include' % p
-        link += ' -L%s/lib' % p
-    env.set('CXXFLAGS', '"%s"' % cxx)
-    env.set('LINKFLAGS', '"%s"' % link)
+    def package(self, project):
+        project.checkout(self)
+        name = read_wscript_variable(os.getcwd(), 'APPNAME')
+        command('./waf dist')
+        return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
+
+
+# @param s Target string:
+#       windows-{32,64}
+#    or ubuntu-version-{32,64}
+#    or debian-version-{32,64}
+#    or centos-version-{32,64}
+#    or osx-{32,64}
+#    or source      
+# @param debug True to build with debugging symbols (where possible)
+def target_factory(s, debug, work):
+    target = None
+    if s.startswith('windows-'):
+        target = WindowsTarget(int(s.split('-')[1]), work)
+    elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
+        p = s.split('-')
+        if len(p) != 3:
+            print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
+            sys.exit(1)
+        target = LinuxTarget(p[0], p[1], int(p[2]), work)
+    elif s.startswith('osx-'):
+        target = OSXSingleTarget(int(s.split('-')[1]), work)
+    elif s == 'osx':
+        if args.command == 'build':
+            target = OSXSingleTarget(64, work)
+        else:
+            target = OSXUniversalTarget(work)
+    elif s == 'source':
+        target = SourceTarget()
 
+    if target is not None:
+        target.debug = debug
 
-#
-# Target
-#
+    return target
 
-class Target:
-    def __init__(self, name):
-        self.name = name
-        if name.startswith('ubuntu-') or name.startswith('debian-'):
-            self.platform = 'linux'
-            self.version = name.split('-')[1]
-            self.bits = int(name.split('-')[2])
-        elif name.startswith('windows-'):
-            self.platform = 'windows'
-            self.bits = int(name.split('-')[1])
-        elif name == 'osx':
-            self.platform = 'osx'
-        elif name == 'source':
-            self.platform = 'source'
-
-def environment_for_target(target, directory):
-    if target.platform == 'linux':
-        return ChrootEnvironment(target.name)
-    elif target.platform == 'windows':
-        env = HostEnvironment(directory)
-        prepare_for_windows(env, target.bits)
-        return env
-    elif target.platform == 'osx':
-        env = RemoteEnvironment(OSX_BUILD_HOST)
-        return env
-    elif target.platform == 'source':
-        return HostEnvironment()
-
-    return None
 
 #
 # Project
@@ -385,42 +471,34 @@ class Project(object):
     def __init__(self, name, directory, specifier=None):
         self.name = name
         self.directory = directory
-        self.git_dir = GIT_DIR
         self.version = None
         self.specifier = specifier
+        self.git_commit = None
         if self.specifier is None:
             self.specifier = 'master'
 
-    def checkout(self, env):
+    def checkout(self, target):
         flags = ''
         redirect = ''
         if args.quiet:
             flags = '-q'
             redirect = '>/dev/null'
-        command('git clone --depth 0 %s %s/%s.git %s/src/%s' % (flags, self.git_dir, self.name, env.work_dir_cdist(), self.name))
-        os.chdir('%s/src/%s' % (env.work_dir_cdist(), self.name))
+        command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
+        os.chdir('%s/src/%s' % (target.directory, self.name))
         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
-        command('git submodule init')
-        command('git submodule update')
+        self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
+        command('git submodule init --quiet')
+        command('git submodule update --quiet')
         os.chdir(self.directory)
 
-        proj = '%s/src/%s/%s' % (env.work_dir_cdist(), self.name, self.directory)
+        proj = '%s/src/%s/%s' % (target.directory, self.name, self.directory)
 
         self.read_cscript('%s/cscript' % proj)
         
         if os.path.exists('%s/wscript' % proj):
-            f = open('%s/wscript' % proj, 'r')
-            version = None
-            while 1:
-                l = f.readline()
-                if l == '':
-                    break
-
-                s = l.split()
-                if len(s) == 3 and s[0] == "VERSION":
-                    self.version = Version(s[2])
-
-            f.close()
+            v = read_wscript_variable(proj, "VERSION");
+            if v is not None:
+                self.version = Version(v)
 
     def read_cscript(self, s):
         self.cscript = {}
@@ -467,79 +545,88 @@ def append_version_to_debian_changelog(version):
 
     command('dch -b -v %s-1 "New upstream release."' % version)
 
+def devel_to_git(project, filename):
+    if project.git_commit is not None:
+        filename = filename.replace('devel', '-%s' % project.git_commit)
+    return filename
+
 #
 # Command-line parser
 #
 
 parser = argparse.ArgumentParser()
 parser.add_argument('command')
-parser.add_argument('-p', '--project', help='project name', required=True)
+parser.add_argument('-p', '--project', help='project name')
 parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
-parser.add_argument('--beta', help='beta release', action='store_true')
-parser.add_argument('--full', help='full release', action='store_true')
+parser.add_argument('--minor', help='minor version number bump', action='store_true')
+parser.add_argument('--micro', help='micro version number bump', action='store_true')
 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
 parser.add_argument('-o', '--output', help='output directory', default='.')
 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
 parser.add_argument('-t', '--target', help='target')
 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
+parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
+parser.add_argument('-w', '--work', help='override default work directory')
 args = parser.parse_args()
 
 args.output = os.path.abspath(args.output)
+if args.work is not None:
+    args.work = os.path.abspath(args.work)
 
-if args.project is None:
-    error('you must specify -p or --project')
+if args.project is None and args.command != 'shell':
+    raise Error('you must specify -p or --project')
 
 project = Project(args.project, args.directory, args.checkout)
 
 if args.command == 'build':
     if args.target is None:
-        error('you must specify -t or --target')
+        raise Error('you must specify -t or --target')
 
-    target = Target(args.target)
-    env = environment_for_target(target, None)
-    project.checkout(env)
-    env.build_dependencies(target, project)
-    env.build(target, project)
-
-    env.cleanup()
+    target = target_factory(args.target, args.debug, args.work)
+    project.checkout(target)
+    target.build_dependencies(project)
+    target.build(project)
+    if not args.keep:
+        target.cleanup()
 
 elif args.command == 'package':
     if args.target is None:
-        error('you must specify -t or --target')
+        raise Error('you must specify -t or --target')
         
-    target = Target(args.target)
-    env = environment_for_target(target, None)
+    target = target_factory(args.target, args.debug, args.work)
 
-    packages = env.package(target, project)
+    packages = target.package(project)
     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
         packages = [packages]
 
     if target.platform == 'linux':
-        out = '%s/%s-%d' % (args.output, target.version, target.bits)
+        out = '%s/%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
         try:
             os.makedirs(out)
         except:
             pass
         for p in packages:
-            copyfile(p, '%s/%s' % (out, os.path.basename(p)))
+            copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(project, p))))
     else:
         for p in packages:
-            copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
+            copyfile(p, '%s/%s' % (args.output, os.path.basename(devel_to_git(project, p))))
 
-    env.cleanup()
+    if not args.keep:
+        target.cleanup()
 
 elif args.command == 'release':
-    if args.full is False and args.beta is False:
-        error('you must specify --full or --beta')
+    if args.minor is False and args.micro is False:
+        raise Error('you must specify --minor or --micro')
 
-    env = HostEnvironment()
-    project.checkout(env)
+    target = SourceTarget()
+    project.checkout(target)
 
     version = project.version
-    if args.full:
-        version.to_release()
+    version.to_release()
+    if args.minor:
+        version.bump_minor()
     else:
-        version.bump_beta()
+        version.bump_micro()
 
     set_version_in_wscript(version)
     append_version_to_changelog(version)
@@ -548,29 +635,27 @@ elif args.command == 'release':
     command('git commit -a -m "Bump version"')
     command('git tag -m "v%s" v%s' % (version, version))
 
-    if args.full:
-        version.bump_and_to_pre()
-        set_version_in_wscript(version)
-        command('git commit -a -m "Bump version"')
-
+    version.to_devel()
+    set_version_in_wscript(version)
+    command('git commit -a -m "Bump version"')
     command('git push')
     command('git push --tags')
 
-    env.cleanup()
+    target.cleanup()
 
 elif args.command == 'pot':
-    env = HostEnvironment()
-    project.checkout(env)
+    target = SourceTarget()
+    project.checkout(target)
 
-    pots = project.cscript['make_pot'](env)
+    pots = project.cscript['make_pot'](target)
     for p in pots:
         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
 
-    env.cleanup()
+    target.cleanup()
 
 elif args.command == 'changelog':
-    env = HostEnvironment()
-    project.checkout(env)
+    target = SourceTarget()
+    project.checkout(target)
 
     text = open('ChangeLog', 'r')
     html = open('%s/changelog.html' % args.output, 'w')
@@ -587,7 +672,8 @@ elif args.command == 'changelog':
         if len(l) > 0 and l[0] == "\t":
             s = l.split()
             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
-                if not "beta" in s[2]:
+                v = Version(s[2])
+                if v.micro == 0:
                     if last is not None and len(changes) > 0:
                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
                         print >>html,"<ul>"
@@ -607,34 +693,37 @@ elif args.command == 'changelog':
                     else:
                         changes[-1] += " " + c
 
-    env.cleanup()
+    target.cleanup()
 
 elif args.command == 'manual':
-    env = HostEnvironment()
-    project.checkout(env)
+    target = SourceTarget()
+    project.checkout(target)
 
-    dirs = project.cscript['make_manual'](env)
-    for d in dirs:
-        copytree(d, '%s/%s' % (args.output, os.path.basename(d)))
+    outs = project.cscript['make_manual'](target)
+    for o in outs:
+        if os.path.isfile(o):
+            copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
+        else:
+            copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
 
-    env.cleanup()
+    target.cleanup()
 
 elif args.command == 'doxygen':
-    env = HostEnvironment()
-    project.checkout(env)
+    target = SourceTarget()
+    project.checkout(target)
 
-    dirs = project.cscript['make_doxygen'](env)
+    dirs = project.cscript['make_doxygen'](target)
     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
         dirs = [dirs]
 
     for d in dirs:
         copytree(d, '%s/%s' % (args.output, 'doc'))
 
-    env.cleanup()
+    target.cleanup()
 
 elif args.command == 'latest':
-    env = HostEnvironment()
-    project.checkout(env)
+    target = SourceTarget()
+    project.checkout(target)
 
     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
     t = f.readline()
@@ -643,20 +732,37 @@ elif args.command == 'latest':
     if m:
         tags = m.group(1).split(', ')
         for t in tags:
+            s = t.split()
+            if len(s) > 1:
+                t = s[1]
             if len(t) > 0 and t[0] == 'v':
                 latest = t[1:]
 
     print latest
-    env.cleanup()
+    target.cleanup()
 
 elif args.command == 'test':
     if args.target is None:
-        error('you must specify -t or --target')
+        raise Error('you must specify -t or --target')
+
+    target = None
+    try:
+        target = target_factory(args.target, args.debug, args.work)
+        target.test(project)
+    except Error as e:
+        if target is not None:
+            target.cleanup()
+        raise
+        
+    if target is not None:
+        target.cleanup()
+
+elif args.command == 'shell':
+    if args.target is None:
+        raise Error('you must specify -t or --target')
 
-    target = Target(args.target)
-    env = environment_for_target(target, '.')
-    project.read_cscript('cscript')
-    env.build(target, project)
+    target = target_factory(args.target, args.debug, args.work)
+    target.command('bash')
 
 else:
-    error('invalid command %s' % args.command)
+    raise Error('invalid command %s' % args.command)