Try to fix option parsing and add -n.
[cdist.git] / cdist
diff --git a/cdist b/cdist
index 515e81b0e98fd8eeb7c265bd92065b9697a6942f..75230b4b52551579b513a73353b592e6897db287 100755 (executable)
--- a/cdist
+++ b/cdist
@@ -16,6 +16,7 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
+from __future__ import print_function
 import os
 import sys
 import shutil
@@ -120,10 +121,10 @@ class Config:
     def get(self, k):
         for o in self.options:
             if o.key == k:
+                if o.value is None:
+                    raise Error('Required setting %s not found' % k)
                 return o.value
 
-        raise Error('Required setting %s not found' % k)
-
     def set(self, k, v):
         for o in self.options:
             o.offer(k, v)
@@ -305,11 +306,26 @@ class Version:
 
 class Target(object):
     """
-    platform -- platform string (e.g. 'windows', 'linux', 'osx')
-    directory -- directory to work in; if None we will use a temporary directory
-    Temporary directories will be removed after use; specified directories will not.
+    Class representing the target that we are building for.  This is exposed to cscripts,
+    though not all of it is guaranteed 'API'.  cscripts may expect:
+
+    platform: platform string (e.g. 'windows', 'linux', 'osx')
+    parallel: number of parallel jobs to run
+    directory: directory to work in
+    variables: dict of environment variables
+    debug: True to build a debug version, otherwise False
+    set(a, b): set the value of variable 'a' to 'b'
+    unset(a): unset the value of variable 'a'
+    command(c): run the command 'c' in the build environment
+
     """
+
     def __init__(self, platform, directory=None):
+        """
+        platform -- platform string (e.g. 'windows', 'linux', 'osx')
+        directory -- directory to work in; if None we will use a temporary directory
+        Temporary directories will be removed after use; specified directories will not.
+        """
         self.platform = platform
         self.parallel = int(config.get('parallel'))
 
@@ -327,14 +343,15 @@ class Target(object):
 
     def package(self, project, checkout):
         tree = globals.trees.get(project, checkout, self)
-        tree.build_dependencies()
-        tree.build(tree)
+        tree.build_dependencies(args.dry_run)
+        tree.build(args.dry_run)
         return tree.call('package', tree.version), tree.git_commit
 
-    def test(self, tree):
-        tree.build_dependencies()
-        tree.build()
-        return tree.call('test')
+    def test(self, tree, test):
+        """test is the test case to run, or None"""
+        tree.build_dependencies(args.dry_run)
+        tree.build(args.dry_run)
+        return tree.call('test', test)
 
     def set(self, a, b):
         self.variables[a] = b
@@ -367,18 +384,24 @@ class Target(object):
         if self.rmdir:
             rmtree(self.directory)
 
-#
-# Windows
-#
 
 class WindowsTarget(Target):
-    def __init__(self, bits, directory=None):
+    """
+    This target exposes the following additional API:
+
+    version: Windows version ('xp' or None)
+    bits: bitness of Windows (32 or 64)
+    environment_prefix: path to Windows environment
+    mingw_path: path to mingw binaries
+    """
+    def __init__(self, version, bits, directory=None):
         super(WindowsTarget, self).__init__('windows', directory)
+        self.version = version
         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)
+        self.environment_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
+        if not os.path.exists(self.environment_prefix):
+            raise Error('environment prefix %s does not exist' % self.environment_prefix)
 
         if self.bits == 32:
             self.mingw_name = 'i686'
@@ -388,16 +411,16 @@ class WindowsTarget(Target):
         self.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_LIBDIR', '%s/lib/pkgconfig' % self.environment_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, self.mingw_path, os.environ['PATH']))
+        self.set('PATH', '%s/bin:%s:%s' % (self.environment_prefix, self.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)
+        cxx = '-I%s/include -I%s/include' % (self.environment_prefix, self.directory)
+        link = '-L%s/lib -L%s/lib' % (self.environment_prefix, self.directory)
         for p in self.mingw_prefixes:
             cxx += ' -I%s/include' % p
             link += ' -L%s/lib' % p
@@ -406,12 +429,23 @@ class WindowsTarget(Target):
         self.set('LINKFLAGS', '"%s"' % link)
         self.set('LDFLAGS', '"%s"' % link)
 
+        # This is for backwards-compatibility
+        self.windows_prefix = self.environment_prefix
+
     def command(self, c):
         log('host -> %s' % c)
         command('%s %s' % (self.variables_string(), c))
 
 class LinuxTarget(Target):
-    """Parent for Linux targets"""
+    """
+    Parent for Linux targets.
+    This target exposes the following additional API:
+
+    distro: distribution ('debian', 'ubuntu', 'centos' or 'fedora')
+    version: distribution version (e.g. '12.04', '8', '6.5')
+    bits: bitness of the distribution (32 or 64)
+    """
+
     def __init__(self, distro, version, bits, directory=None):
         super(LinuxTarget, self).__init__('linux', directory)
         self.distro = distro
@@ -421,7 +455,8 @@ class LinuxTarget(Target):
         self.set('CXXFLAGS', '-I%s/include' % self.directory)
         self.set('CPPFLAGS', '')
         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
-        self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
+        self.set('PKG_CONFIG_PATH',
+                 '%s/lib/pkgconfig:%s/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig' % (self.directory, self.directory))
         self.set('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin')
 
 class ChrootTarget(LinuxTarget):
@@ -448,15 +483,34 @@ class HostTarget(LinuxTarget):
     def command(self, c):
         command('%s %s' % (self.variables_string(), c))
 
-#
-# OS X
-#
+
+class DockerTarget(Target):
+    """
+    Build a Docker image.
+
+    This target exposes the following additional API:
+
+    deb: path to Debian 8 .deb
+    """
+    def __init__(self, directory=None):
+        super(DockerTarget, self).__init__('docker', directory)
+        self.debian = ChrootTarget('debian', '8', 64, directory)
+
+    def command(self, c):
+        log('host -> %s' % c)
+        command('%s %s' % (self.variables_string(), c))
+
+    def package(self, project, checkout):
+        self.deb = self.debian.package(project, checkout)
+        return globals.trees.get(project, checkout, self).call('package', tree.version), tree.git_commit
+
 
 class OSXTarget(Target):
     def __init__(self, directory=None):
         super(OSXTarget, self).__init__('osx', directory)
         self.sdk = config.get('osx_sdk')
         self.sdk_prefix = config.get('osx_sdk_prefix')
+        self.environment_prefix = config.get('osx_environment_prefix')
 
     def command(self, c):
         command('%s %s' % (self.variables_string(False), c))
@@ -498,8 +552,8 @@ class OSXUniversalTarget(OSXTarget):
         for b in [32, 64]:
             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
             tree = globals.trees.get(project, checkout, target)
-            tree.build_dependencies()
-            tree.build()
+            tree.build_dependencies(False)
+            tree.build(False)
 
         tree = globals.trees.get(project, checkout, self)
         with TreeDirectory(tree):
@@ -536,25 +590,39 @@ class SourceTarget(Target):
 def target_factory(s, debug, work):
     target = None
     if s.startswith('windows-'):
-        target = WindowsTarget(int(s.split('-')[1]), work)
+        x = s.split('-')
+        if len(x) == 2:
+            target = WindowsTarget(None, int(x[1]), work)
+        elif len(x) == 3:
+            target = WindowsTarget(x[1], int(x[2]), work)
+        else:
+            raise Error("Bad Windows target name `%s'")
     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
         p = s.split('-')
         if len(p) != 3:
             raise Error("Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s)
         target = ChrootTarget(p[0], p[1], int(p[2]), work)
+    elif s.startswith('arch-'):
+        p = s.split('-')
+        if len(p) != 2:
+            raise Error("Bad Arch target name `%s'; must be arch-32 or arch-64")
+        target = ChrootTarget(p[0], None, p[1], work)
     elif s == 'raspbian':
         target = ChrootTarget(s, None, None, work)
     elif s == 'host':
+        if command_and_read('uname -m').read().strip() == 'x86_64':
+            bits = 64
+        else:
+            bits = 32
         try:
             f = open('/etc/fedora-release', 'r')
             l = f.readline().strip().split()
-            if command_and_read('uname -m').read().strip() == 'x86_64':
-                bits = 64
-            else:
-                bits = 32
             target = HostTarget("fedora", l[2], bits, work)
         except Exception as e:
-            raise Error("could not identify distribution for `host' target (%s)" % e)
+            if os.path.exists('/etc/arch-release'):
+                target = HostTarget("arch", None, bits, work)
+            else:
+                raise Error("could not identify distribution for `host' target (%s)" % e)
     elif s.startswith('osx-'):
         target = OSXSingleTarget(int(s.split('-')[1]), work)
     elif s == 'osx':
@@ -564,6 +632,8 @@ def target_factory(s, debug, work):
             target = OSXUniversalTarget(work)
     elif s == 'source':
         target = SourceTarget()
+    elif s == 'docker':
+        target = DockerTarget()
 
     if target is None:
         raise Error("Bad target `%s'" % s)
@@ -618,7 +688,7 @@ class Tree(object):
         proj = '%s/src/%s' % (target.directory, self.name)
 
         self.cscript = {}
-        execfile('%s/cscript' % proj, self.cscript)
+        exec(open('%s/cscript' % proj).read(), self.cscript)
 
         if os.path.exists('%s/wscript' % proj):
             v = read_wscript_variable(proj, "VERSION");
@@ -631,34 +701,42 @@ class Tree(object):
         with TreeDirectory(self):
             return self.cscript[function](self.target, *args)
 
-    def build_dependencies(self):
+    def build_dependencies(self, dry_run, options=None):
         if 'dependencies' in self.cscript:
-            for d in self.cscript['dependencies'](self.target):
+            if len(inspect.getargspec(self.cscript['dependencies']).args) == 2:
+                deps = self.call('dependencies', options)
+            else:
+                deps = self.call('dependencies')
+
+            for d in deps:
                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
                 dep = globals.trees.get(d[0], d[1], self.target)
-                dep.build_dependencies()
 
                 # 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].items():
-                            options[k] = v
+                    for k, v in dep.cscript['option_defaults'].items():
+                        options[k] = v
+
+                if len(d) > 2:
+                    for k, v in d[2].items():
+                        options[k] = v
 
-                dep.build(options)
+                dep.build_dependencies(dry_run, options)
+                dep.build(dry_run, options)
 
-    def build(self, options=None):
+    def build(self, dry_run, options=None):
         if self.built:
             return
 
         variables = copy.copy(self.target.variables)
 
-        if len(inspect.getargspec(self.cscript['build']).args) == 2:
-            self.call('build', options)
-        else:
-            self.call('build')
+        if not dry_run:
+            if len(inspect.getargspec(self.cscript['build']).args) == 2:
+                self.call('build', options)
+            else:
+                self.call('build')
 
         self.target.variables = variables
         self.built = True
@@ -704,6 +782,8 @@ def main():
     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
     parser.add_argument('-w', '--work', help='override default work directory')
     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
+    parser.add_argument('--test', help='name of test to run (with `test''), defaults to all')
+    parser.add_argument('-n', '--dry-run', help='run the process without building anything')
     args = parser.parse_args()
 
     # Override configured stuff
@@ -738,8 +818,8 @@ def main():
 
         target = target_factory(args.target, args.debug, args.work)
         tree = globals.trees.get(args.project, args.checkout, target)
-        tree.build_dependencies()
-        tree.build()
+        tree.build_dependencies(dry_run)
+        tree.build(dry_run)
         if not args.keep:
             target.cleanup()
 
@@ -915,7 +995,7 @@ def main():
             target = target_factory(args.target, args.debug, args.work)
             tree = globals.trees.get(args.project, args.checkout, target)
             with TreeDirectory(tree):
-                target.test(tree)
+                target.test(tree, args.test)
         except Error as e:
             if target is not None:
                 target.cleanup()