Import pymake into Mozilla. This is not used by default (yet), but this snapshot should be good enough for doing relative-srcdir builds on Windows. r=ted
authorBenjamin Smedberg <benjamin@smedbergs.us>
Thu, 19 Mar 2009 10:19:38 -0400
changeset 26359 80da01543b565978817c19ad3c739fda73784a79
parent 26358 cc39595b8ee7fcb9f40552c3e57b377e9819a87e
child 26360 ac92990fb908a6d5905357805490b558442bce57
push idunknown
push userunknown
push dateunknown
reviewersted
milestone1.9.2a1pre
Import pymake into Mozilla. This is not used by default (yet), but this snapshot should be good enough for doing relative-srcdir builds on Windows. r=ted
build/pymake/.hg_archival.txt
build/pymake/.hgignore
build/pymake/LICENSE
build/pymake/README
build/pymake/make.py
build/pymake/mkparse.py
build/pymake/pymake/__init__.py
build/pymake/pymake/builtins.py
build/pymake/pymake/command.py
build/pymake/pymake/data.py
build/pymake/pymake/functions.py
build/pymake/pymake/globrelative.py
build/pymake/pymake/parser.py
build/pymake/pymake/parserdata.py
build/pymake/pymake/process.py
build/pymake/pymake/util.py
build/pymake/pymake/win32process.py
build/pymake/tests/automatic-variables.mk
build/pymake/tests/bad-command-continuation.mk
build/pymake/tests/call.mk
build/pymake/tests/commandmodifiers.mk
build/pymake/tests/comment-parsing.mk
build/pymake/tests/datatests.py
build/pymake/tests/default-target.mk
build/pymake/tests/default-target2.mk
build/pymake/tests/define-directive.mk
build/pymake/tests/depfailed.mk
build/pymake/tests/depfailedj.mk
build/pymake/tests/diamond-deps.mk
build/pymake/tests/dotslash.mk
build/pymake/tests/doublecolon-exists.mk
build/pymake/tests/doublecolon-remake.mk
build/pymake/tests/dynamic-var.mk
build/pymake/tests/empty-with-deps.mk
build/pymake/tests/eof-continuation.mk
build/pymake/tests/escape-chars.mk
build/pymake/tests/escaped-continuation.mk
build/pymake/tests/eval-duringexecute.mk
build/pymake/tests/eval.mk
build/pymake/tests/exit-code.mk
build/pymake/tests/file-functions-symlinks.mk
build/pymake/tests/file-functions.mk
build/pymake/tests/func-refs.mk
build/pymake/tests/functions.mk
build/pymake/tests/ifdefs-nesting.mk
build/pymake/tests/ifdefs.mk
build/pymake/tests/ignore-error.mk
build/pymake/tests/implicit-chain.mk
build/pymake/tests/implicit-dir.mk
build/pymake/tests/implicit-terminal.mk
build/pymake/tests/implicitsubdir.mk
build/pymake/tests/include-dynamic.mk
build/pymake/tests/include-file.inc
build/pymake/tests/include-notfound.mk
build/pymake/tests/include-regen.mk
build/pymake/tests/include-test.mk
build/pymake/tests/line-continuations.mk
build/pymake/tests/link-search.mk
build/pymake/tests/matchany.mk
build/pymake/tests/matchany2.mk
build/pymake/tests/matchany3.mk
build/pymake/tests/no-remake.mk
build/pymake/tests/nosuchfile.mk
build/pymake/tests/notargets.mk
build/pymake/tests/parallel-dep-resolution.mk
build/pymake/tests/parallel-rule-execution.mk
build/pymake/tests/parallel-simple.mk
build/pymake/tests/parallel-submake.mk
build/pymake/tests/parallel-toserial.mk
build/pymake/tests/parallel-waiting.mk
build/pymake/tests/parsertests.py
build/pymake/tests/patsubst.mk
build/pymake/tests/recursive-set.mk
build/pymake/tests/recursive-set2.mk
build/pymake/tests/runtests.py
build/pymake/tests/serial-dep-resolution.mk
build/pymake/tests/serial-rule-execution.mk
build/pymake/tests/serial-rule-execution2.mk
build/pymake/tests/shellfunc.mk
build/pymake/tests/specified-target.mk
build/pymake/tests/static-pattern.mk
build/pymake/tests/static-pattern2.mk
build/pymake/tests/submake.makefile2
build/pymake/tests/submake.mk
build/pymake/tests/tab-intro.mk
build/pymake/tests/target-specific.mk
build/pymake/tests/unterminated-dollar.mk
build/pymake/tests/var-change-flavor.mk
build/pymake/tests/var-commandline.mk
build/pymake/tests/var-overrides.mk
build/pymake/tests/var-ref.mk
build/pymake/tests/var-set.mk
build/pymake/tests/var-substitutions.mk
build/pymake/tests/vpath-directive-dynamic.mk
build/pymake/tests/vpath-directive.mk
build/pymake/tests/vpath.mk
build/pymake/tests/wildcards.mk
new file mode 100644
--- /dev/null
+++ b/build/pymake/.hg_archival.txt
@@ -0,0 +1,2 @@
+repo: f5ab154deef2ffa97f1b2139589ae4a1962090a4
+node: b48239b1191839491b6e37c03fcdfcd62024c745
new file mode 100644
--- /dev/null
+++ b/build/pymake/.hgignore
@@ -0,0 +1,3 @@
+\.pyc$
+\.pyo$
+~$
new file mode 100644
--- /dev/null
+++ b/build/pymake/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009 The Mozilla Foundation <http://www.mozilla.org/>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
new file mode 100644
--- /dev/null
+++ b/build/pymake/README
@@ -0,0 +1,64 @@
+INTRODUCTION
+
+make.py (and the pymake modules that support it) are an implementation of the make tool
+which are mostly compatible with makefiles written for GNU make.
+
+PURPOSE
+
+The Mozilla project inspired this tool with several goals:
+
+* Improve build speeds, especially on Windows. This can be done by reducing the total number
+  of processes that are launched, especially MSYS shell processes which are expensive.
+
+* Allow writing some complicated build logic directly in Python instead of in shell.
+
+* Allow computing dependencies for special targets, such as members within ZIP files.
+
+* Enable experiments with build system. By writing a makefile parser, we can experiment
+  with converting in-tree makefiles to another build system, such as SCons, waf, ant, ...insert
+  your favorite build tool here. Or we could experiment along the lines of makepp, keeping
+  our existing makefiles, but change the engine to build a global dependency graph.
+
+KNOWN INCOMPATIBILITIES
+
+* Order-only prerequisites are not yet supported
+
+* Secondary expansion is not yet supported.
+
+* Target-specific variables behave differently than in GNU make: in pymake, the target-specific
+  variable only applies to the specific target that is mentioned, and does not apply recursively
+  to all dependencies which are remade. This is an intentional change: the behavior of GNU make
+  is neither deterministic nor intuitive.
+
+* $(eval) is only supported during the parse phase. Any attempt to recursively expand
+  an $(eval) function during command execution will fail. This is an intentional incompatibility.
+
+* There is a subtle difference in execution order that can cause unexpected changes in the
+  following circumstance:
+** A file `foo.c` exists on the VPATH
+** A rule for `foo.c` exists with a dependency on `tool` and no commands
+** `tool` is remade for some other reason earlier in the file
+  In this case, pymake resets the VPATH of `foo.c`, while GNU make does not. This shouldn't
+  happen in the real world, since a target found on the VPATH without commands is silly. But
+  mozilla/js/src happens to have a rule, which I'm patching.
+
+* pymake does not implement any of the builtin implicit rules or the related variables. Mozilla
+  only cares because pymake doesn't implicitly define $(RM), which I'm also fixing in the Mozilla
+  code.
+
+ISSUES
+
+* Speed is a problem.
+
+FUTURE WORK
+
+* implement a new type of command which is implemented in python. This would allow us
+to replace the current `nsinstall` binary (and execution costs for the shell and binary) with an
+in-process python solution.
+
+AUTHOR
+
+Initial code was written by Benjamin Smedberg <benjamin@smedbergs.us>. For future releases see
+http://benjamin.smedbergs.us/pymake/
+
+See the LICENSE file for license information (MIT license)
new file mode 100755
--- /dev/null
+++ b/build/pymake/make.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+
+"""
+make.py
+
+A drop-in or mostly drop-in replacement for GNU make.
+"""
+
+import sys, os
+import pymake.command, pymake.process
+
+pymake.command.main(sys.argv[1:], os.environ, os.getcwd(), context=None, cb=sys.exit)
+pymake.process.ParallelContext.spin()
+assert False, "Not reached"
new file mode 100755
--- /dev/null
+++ b/build/pymake/mkparse.py
@@ -0,0 +1,8 @@
+import sys
+import pymake.parser
+
+for f in sys.argv[1:]:
+    print "Parsing %s" % f
+    fd = open(f)
+    stmts = pymake.parser.parsestream(fd, f)
+    print stmts
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/builtins.py
@@ -0,0 +1,10 @@
+"""
+Implicit variables; perhaps in the future this will also include some implicit
+rules, at least match-anything cancellation rules.
+"""
+
+variables = {
+    'RM': 'rm -f',
+    '.LIBPATTERNS': 'lib%.so lib%.a',
+    '.PYMAKE': '1',
+    }
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/command.py
@@ -0,0 +1,232 @@
+"""
+Makefile execution.
+
+Multiple `makes` can be run within the same process. Each one has an entirely data.Makefile and .Target
+structure, environment, and working directory. Typically they will all share a parallel execution context,
+except when a submake specifies -j1 when the parent make is building in parallel.
+"""
+
+import os, subprocess, sys, logging, time, traceback
+from optparse import OptionParser
+import data, parserdata, process, util
+
+# TODO: If this ever goes from relocatable package to system-installed, this may need to be
+# a configured-in path.
+
+makepypath = os.path.normpath(os.path.join(os.path.dirname(__file__), '../make.py'))
+
+def parsemakeflags(env):
+    """
+    Parse MAKEFLAGS from the environment into a sequence of command-line arguments.
+    """
+
+    makeflags = env.get('MAKEFLAGS', '')
+    makeflags = makeflags.strip()
+
+    if makeflags == '':
+        return []
+
+    if makeflags[0] not in ('-', ' '):
+        makeflags = '-' + makeflags
+
+    opts = []
+    curopt = ''
+
+    i = 0
+    while i < len(makeflags):
+        c = makeflags[i]
+        if c.isspace():
+            opts.append(curopt)
+            curopt = ''
+            i += 1
+            while i < len(makeflags) and makeflags[i].isspace():
+                i += 1
+            continue
+
+        if c == '\\':
+            i += 1
+            if i == len(makeflags):
+                raise data.DataError("MAKEFLAGS has trailing backslash")
+            c = makeflags[i]
+            
+        curopt += c
+        i += 1
+
+    if curopt != '':
+        opts.append(curopt)
+
+    return opts
+
+def _version(*args):
+    print """pymake: GNU-compatible make program
+Copyright (C) 2009 The Mozilla Foundation <http://www.mozilla.org/>
+This is free software; see the source for copying conditions.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE."""
+
+_log = logging.getLogger('pymake.execution')
+
+def main(args, env, cwd, context, cb):
+    """
+    Start a single makefile execution, given a command line, working directory, and environment.
+
+    @param cb a callback to notify with an exit code when make execution is finished.
+    """
+
+    try:
+        makelevel = int(env.get('MAKELEVEL', '0'))
+
+        op = OptionParser()
+        op.add_option('-f', '--file', '--makefile',
+                      action='append',
+                      dest='makefiles',
+                      default=[])
+        op.add_option('-d',
+                      action="store_true",
+                      dest="verbose", default=False)
+        op.add_option('--debug-log',
+                      dest="debuglog", default=None)
+        op.add_option('-C', '--directory',
+                      dest="directory", default=None)
+        op.add_option('-v', '--version', action="store_true",
+                      dest="printversion", default=False)
+        op.add_option('-j', '--jobs', type="int",
+                      dest="jobcount", default=1)
+        op.add_option('--no-print-directory', action="store_false",
+                      dest="printdir", default=True)
+
+        options, arguments1 = op.parse_args(parsemakeflags(env))
+        options, arguments2 = op.parse_args(args, values=options)
+
+        arguments = arguments1 + arguments2
+
+        if options.printversion:
+            _version()
+            cb(0)
+            return
+
+        shortflags = []
+        longflags = []
+
+        loglevel = logging.WARNING
+        if options.verbose:
+            loglevel = logging.DEBUG
+            shortflags.append('d')
+
+        logkwargs = {}
+        if options.debuglog:
+            logkwargs['filename'] = options.debuglog
+            longflags.append('--debug-log=%s' % options.debuglog)
+
+        if options.directory is None:
+            workdir = cwd
+        else:
+            workdir = os.path.join(cwd, options.directory)
+
+        shortflags.append('j%i' % (options.jobcount,))
+
+        makeflags = ''.join(shortflags) + ' ' + ' '.join(longflags)
+
+        logging.basicConfig(level=loglevel, **logkwargs)
+
+        if context is not None and context.jcount > 1 and options.jobcount == 1:
+            _log.debug("-j1 specified, creating new serial execution context")
+            context = process.getcontext(options.jobcount)
+            subcontext = True
+        elif context is None:
+            _log.debug("Creating new execution context, jobcount %s", options.jobcount)
+            context = process.getcontext(options.jobcount)
+            subcontext = True
+        else:
+            _log.debug("Using parent execution context")
+            subcontext = False
+
+        if options.printdir:
+            print "make.py[%i]: Entering directory '%s'" % (makelevel, workdir)
+            sys.stdout.flush()
+
+        if len(options.makefiles) == 0:
+            if os.path.exists(os.path.join(workdir, 'Makefile')):
+                options.makefiles.append('Makefile')
+            else:
+                print "No makefile found"
+                cb(2)
+                return
+
+        overrides, targets = parserdata.parsecommandlineargs(arguments)
+
+        def makecb(error, didanything, makefile, realtargets, tstack, i, firsterror):
+            if error is not None:
+                print error
+                if firsterror is None:
+                    firsterror = error
+
+            if i == len(realtargets):
+                if subcontext:
+                    context.finish()
+
+                if options.printdir:
+                    print "make.py[%i]: Leaving directory '%s'" % (makelevel, workdir)
+                sys.stdout.flush()
+
+                context.defer(cb, firsterror and 2 or 0)
+            else:
+                deferredmake = process.makedeferrable(makecb, makefile=makefile,
+                                                      realtargets=realtargets, tstack=tstack, i=i+1, firsterror=firsterror)
+
+                makefile.gettarget(realtargets[i]).make(makefile, tstack, [], cb=deferredmake)
+                                                                                  
+
+        def remakecb(remade, restarts, makefile):
+            if remade:
+                if restarts > 0:
+                    _log.info("make.py[%i]: Restarting makefile parsing", makelevel)
+                makefile = data.Makefile(restarts=restarts, make='%s %s' % (sys.executable.replace('\\', '/'), makepypath.replace('\\', '/')),
+                                         makeflags=makeflags, makelevel=makelevel, workdir=workdir,
+                                         context=context, env=env)
+
+                try:
+                    overrides.execute(makefile)
+                    for f in options.makefiles:
+                        makefile.include(f)
+                    makefile.finishparsing()
+                    makefile.remakemakefiles(process.makedeferrable(remakecb, restarts=restarts + 1, makefile=makefile))
+
+                except util.MakeError, e:
+                    print e
+                    context.defer(cb, 2)
+                    return
+
+                return
+
+            if len(targets) == 0:
+                if makefile.defaulttarget is None:
+                    print "No target specified and no default target found."
+                    context.defer(cb, 2)
+                    return
+
+                _log.info("Making default target %s", makefile.defaulttarget)
+                realtargets = [makefile.defaulttarget]
+                tstack = ['<default-target>']
+            else:
+                realtargets = targets
+                tstack = ['<command-line>']
+
+            deferredmake = process.makedeferrable(makecb, makefile=makefile,
+                                                  realtargets=realtargets, tstack=tstack, i=1, firsterror=None)
+            makefile.gettarget(realtargets[0]).make(makefile, tstack, [], cb=deferredmake)
+
+        context.defer(remakecb, True, 0, None)
+
+    except (util.MakeError), e:
+        print e
+        if options.printdir:
+            print "make.py[%i]: Leaving directory '%s'" % (makelevel, workdir)
+        sys.stdout.flush()
+        cb(2)
+        return
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/data.py
@@ -0,0 +1,1390 @@
+"""
+A representation of makefile data structures.
+"""
+
+import logging, re, os
+import parserdata, parser, functions, process, util, builtins
+from cStringIO import StringIO
+
+_log = logging.getLogger('pymake.data')
+
+class DataError(util.MakeError):
+    pass
+
+class ResolutionError(DataError):
+    """
+    Raised when dependency resolution fails, either due to recursion or to missing
+    prerequisites.This is separately catchable so that implicit rule search can try things
+    without having to commit.
+    """
+    pass
+
+def withoutdups(it):
+    r = set()
+    for i in it:
+        if not i in r:
+            r.add(i)
+            yield i
+
+def mtimeislater(deptime, targettime):
+    """
+    Is the mtime of the dependency later than the target?
+    """
+
+    if deptime is None:
+        return True
+    if targettime is None:
+        return False
+    return deptime > targettime
+
+def getmtime(path):
+    try:
+        s = os.stat(path)
+        return s.st_mtime
+    except OSError:
+        return None
+
+def stripdotslash(s):
+    if s.startswith('./'):
+        return s[2:]
+    return s
+
+def stripdotslashes(sl):
+    for s in sl:
+        yield stripdotslash(s)
+
+def getindent(stack):
+    return ''.ljust(len(stack) - 1)
+
+def _if_else(c, t, f):
+    if c:
+        return t()
+    return f()
+
+class StringExpansion(object):
+    __slots__ = ('s',)
+    loc = None
+    simple = True
+    
+    def __init__(self, s):
+        assert isinstance(s, str)
+        self.s = s
+
+    def lstrip(self):
+        self.s = self.s.lstrip()
+
+    def rstrip(self):
+        self.s = self.s.rstrip()
+
+    def isempty(self):
+        return self.s == ''
+
+    def resolve(self, i, j, fd, k=None):
+        fd.write(self.s)
+
+    def resolvestr(self, i, j, k=None):
+        return self.s
+
+    def resolvesplit(self, i, j, k=None):
+        return self.s.split()
+
+    def clone(self):
+        e = Expansion()
+        e.appendstr(self.s)
+        return e
+
+    def __len__(self):
+        return 1
+
+    def __getitem__(self, i):
+        assert i == 0
+        return self.s, False
+
+class Expansion(list):
+    """
+    A representation of expanded data, such as that for a recursively-expanded variable, a command, etc.
+    """
+
+    __slots__ = ('loc', 'hasfunc')
+    simple = False
+
+    def __init__(self, loc=None):
+        # A list of (element, isfunc) tuples
+        # element is either a string or a function
+        self.loc = loc
+        self.hasfunc = False
+
+    @staticmethod
+    def fromstring(s):
+        return StringExpansion(s)
+
+    def clone(self):
+        e = Expansion()
+        e.extend(self)
+        return e
+
+    def appendstr(self, s):
+        assert isinstance(s, str)
+        if s == '':
+            return
+
+        self.append((s, False))
+
+    def appendfunc(self, func):
+        assert isinstance(func, functions.Function)
+        self.append((func, True))
+        self.hasfunc = True
+
+    def concat(self, o):
+        """Concatenate the other expansion on to this one."""
+        if o.simple:
+            self.appendstr(o.s)
+        else:
+            self.extend(o)
+            self.hasfunc = self.hasfunc or o.hasfunc
+
+    def isempty(self):
+        return (not len(self)) or self[0] == ('', False)
+
+    def lstrip(self):
+        """Strip leading literal whitespace from this expansion."""
+        while True:
+            i, isfunc = self[0]
+            if isfunc:
+                return
+
+            i = i.lstrip()
+            if i != '':
+                self[0] = i, False
+                return
+
+            del self[0]
+
+    def rstrip(self):
+        """Strip trailing literal whitespace from this expansion."""
+        while True:
+            i, isfunc = self[-1]
+            if isfunc:
+                return
+
+            i = i.rstrip()
+            if i != '':
+                self[-1] = i, False
+                return
+
+            del self[-1]
+
+    def finish(self):
+        if self.hasfunc:
+            return self
+
+        return StringExpansion(''.join([i for i, isfunc in self]))
+
+    def resolve(self, makefile, variables, fd, setting=[]):
+        """
+        Resolve this variable into a value, by interpolating the value
+        of other variables.
+
+        @param setting (Variable instance) the variable currently
+               being set, if any. Setting variables must avoid self-referential
+               loops.
+        """
+        assert isinstance(makefile, Makefile)
+        assert isinstance(variables, Variables)
+        assert isinstance(setting, list)
+
+        for e, isfunc in self:
+            if isfunc:
+                e.resolve(makefile, variables, fd, setting)
+            else:
+                assert isinstance(e, str)
+                fd.write(e)
+                    
+    def resolvestr(self, makefile, variables, setting=[]):
+        fd = StringIO()
+        self.resolve(makefile, variables, fd, setting)
+        return fd.getvalue()
+
+    def resolvesplit(self, makefile, variables, setting=[]):
+        return self.resolvestr(makefile, variables, setting).split()
+
+    def __repr__(self):
+        return "<Expansion with elements: %r>" % ([e for e, isfunc in self],)
+
+class Variables(object):
+    """
+    A mapping from variable names to variables. Variables have flavor, source, and value. The value is an 
+    expansion object.
+    """
+
+    __slots__ = ('parent', '_map')
+
+    FLAVOR_RECURSIVE = 0
+    FLAVOR_SIMPLE = 1
+    FLAVOR_APPEND = 2
+
+    SOURCE_OVERRIDE = 0
+    SOURCE_COMMANDLINE = 1
+    SOURCE_MAKEFILE = 2
+    SOURCE_ENVIRONMENT = 3
+    SOURCE_AUTOMATIC = 4
+    SOURCE_IMPLICIT = 5
+
+    def __init__(self, parent=None):
+        self._map = {} # vname -> flavor, source, valuestr, valueexp
+        self.parent = parent
+
+    def readfromenvironment(self, env):
+        for k, v in env.iteritems():
+            self.set(k, self.FLAVOR_SIMPLE, self.SOURCE_ENVIRONMENT, v)
+
+    def get(self, name, expand=True):
+        """
+        Get the value of a named variable. Returns a tuple (flavor, source, value)
+
+        If the variable is not present, returns (None, None, None)
+
+        @param expand If true, the value will be returned as an expansion. If false,
+        it will be returned as an unexpanded string.
+        """
+        flavor, source, valuestr, valueexp = self._map.get(name, (None, None, None, None))
+        if flavor is not None:
+            if expand and flavor != self.FLAVOR_SIMPLE and valueexp is None:
+                d = parser.Data.fromstring(valuestr, parserdata.Location("Expansion of variables '%s'" % (name,), 1, 0))
+                valueexp, t, o = parser.parsemakesyntax(d, 0, (), parser.iterdata)
+                self._map[name] = flavor, source, valuestr, valueexp
+
+            if flavor == self.FLAVOR_APPEND:
+                if self.parent:
+                    pflavor, psource, pvalue = self.parent.get(name, expand)
+                else:
+                    pflavor, psource, pvalue = None, None, None
+
+                if pvalue is None:
+                    flavor = self.FLAVOR_RECURSIVE
+                    # fall through
+                else:
+                    if source > psource:
+                        # TODO: log a warning?
+                        return pflavor, psource, pvalue
+
+                    if not expand:
+                        return pflavor, psource, pvalue + ' ' + valuestr
+
+                    pvalue = pvalue.clone()
+                    pvalue.appendstr(' ')
+                    pvalue.concat(valueexp)
+
+                    return pflavor, psource, pvalue
+                    
+            if not expand:
+                return flavor, source, valuestr
+
+            if flavor == self.FLAVOR_RECURSIVE:
+                val = valueexp
+            else:
+                val = Expansion.fromstring(valuestr)
+
+            return flavor, source, val
+
+        if self.parent is not None:
+            return self.parent.get(name, expand)
+
+        return (None, None, None)
+
+    def set(self, name, flavor, source, value):
+        assert flavor in (self.FLAVOR_RECURSIVE, self.FLAVOR_SIMPLE)
+        assert source in (self.SOURCE_OVERRIDE, self.SOURCE_COMMANDLINE, self.SOURCE_MAKEFILE, self.SOURCE_ENVIRONMENT, self.SOURCE_AUTOMATIC, self.SOURCE_IMPLICIT)
+        assert isinstance(value, str), "expected str, got %s" % type(value)
+
+        prevflavor, prevsource, prevvalue = self.get(name)
+        if prevsource is not None and source > prevsource:
+            # TODO: give a location for this warning
+            _log.info("not setting variable '%s', set by higher-priority source to value '%s'" % (name, prevvalue))
+            return
+
+        self._map[name] = flavor, source, value, None
+
+    def append(self, name, source, value, variables, makefile):
+        assert source in (self.SOURCE_OVERRIDE, self.SOURCE_MAKEFILE, self.SOURCE_AUTOMATIC)
+        assert isinstance(value, str)
+
+        def expand():
+            try:
+                d = parser.Data.fromstring(value, parserdata.Location("Expansion of variable '%s'" % (name,), 1, 0))
+                valueexp, t, o = parser.parsemakesyntax(d, 0, (), parser.iterdata)
+                return valueexp, None
+            except parser.SyntaxError, e:
+                return None, e
+        
+        if name not in self._map:
+            self._map[name] = self.FLAVOR_APPEND, source, value, None
+            return
+
+        prevflavor, prevsource, prevvalue, valueexp = self._map[name]
+        if source > prevsource:
+            # TODO: log a warning?
+            return
+
+        if prevflavor == self.FLAVOR_SIMPLE:
+            d = parser.Data.fromstring(value, parserdata.Location("Expansion of variables '%s'" % (name,), 1, 0))
+            valueexp, t, o = parser.parsemakesyntax(d, 0, (), parser.iterdata)
+
+            val = valueexp.resolvestr(makefile, variables, [name])
+            self._map[name] = prevflavor, prevsource, prevvalue + ' ' + val, None
+            return
+
+        newvalue = prevvalue + ' ' + value
+        self._map[name] = prevflavor, prevsource, newvalue, None
+
+    def merge(self, other):
+        assert isinstance(other, Variables)
+        for k, flavor, source, value in other:
+            self.set(k, flavor, source, value)
+
+    def __iter__(self):
+        for k, (flavor, source, value, valueexp) in self._map.iteritems():
+            yield k, flavor, source, value
+
+    def __contains__(self, item):
+        return item in self._map
+
+class Pattern(object):
+    """
+    A pattern is a string, possibly with a % substitution character. From the GNU make manual:
+
+    '%' characters in pattern rules can be quoted with precending backslashes ('\'). Backslashes that
+    would otherwise quote '%' charcters can be quoted with more backslashes. Backslashes that
+    quote '%' characters or other backslashes are removed from the pattern before it is compared t
+    file names or has a stem substituted into it. Backslashes that are not in danger of quoting '%'
+    characters go unmolested. For example, the pattern the\%weird\\%pattern\\ has `the%weird\' preceding
+    the operative '%' character, and 'pattern\\' following it. The final two backslashes are left alone
+    because they cannot affect any '%' character.
+
+    This insane behavior probably doesn't matter, but we're compatible just for shits and giggles.
+    """
+
+    __slots__ = ('data')
+
+    def __init__(self, s):
+        r = []
+        i = 0
+        while i < len(s):
+            c = s[i]
+            if c == '\\':
+                nc = s[i + 1]
+                if nc == '%':
+                    r.append('%')
+                    i += 1
+                elif nc == '\\':
+                    r.append('\\')
+                    i += 1
+                else:
+                    r.append(c)
+            elif c == '%':
+                self.data = (''.join(r), s[i+1:])
+                return
+            else:
+                r.append(c)
+            i += 1
+
+        # This is different than (s,) because \% and \\ have been unescaped. Parsing patterns is
+        # context-sensitive!
+        self.data = (''.join(r),)
+
+    def ismatchany(self):
+        return self.data == ('','')
+
+    def ispattern(self):
+        return len(self.data) == 2
+
+    def __hash__(self):
+        return self.data.__hash__()
+
+    def __eq__(self, o):
+        assert isinstance(o, Pattern)
+        return self.data == o.data
+
+    def gettarget(self):
+        assert not self.ispattern()
+        return self.data[0]
+
+    def hasslash(self):
+        return self.data[0].find('/') != -1 or self.data[1].find('/') != -1
+
+    def match(self, word):
+        """
+        Match this search pattern against a word (string).
+
+        @returns None if the word doesn't match, or the matching stem.
+                      If this is a %-less pattern, the stem will always be ''
+        """
+        d = self.data
+        if len(d) == 1:
+            if word == d[0]:
+                return word
+            return None
+
+        d0, d1 = d
+        l1 = len(d0)
+        l2 = len(d1)
+        if len(word) >= l1 + l2 and word.startswith(d0) and word.endswith(d1):
+            if l2 == 0:
+                return word[l1:]
+            return word[l1:-l2]
+
+        return None
+
+    def resolve(self, dir, stem):
+        if self.ispattern():
+            return dir + self.data[0] + stem + self.data[1]
+
+        return self.data[0]
+
+    def subst(self, replacement, word, mustmatch):
+        """
+        Given a word, replace the current pattern with the replacement pattern, a la 'patsubst'
+
+        @param mustmatch If true and this pattern doesn't match the word, throw a DataError. Otherwise
+                         return word unchanged.
+        """
+        assert isinstance(replacement, str)
+
+        stem = self.match(word)
+        if stem is None:
+            if mustmatch:
+                raise DataError("target '%s' doesn't match pattern" % (word,))
+            return word
+
+        if not self.ispattern():
+            # if we're not a pattern, the replacement is not parsed as a pattern either
+            return replacement
+
+        return Pattern(replacement).resolve('', stem)
+
+    def __repr__(self):
+        return "<Pattern with data %r>" % (self.data,)
+
+    _backre = re.compile(r'[%\\]')
+    def __str__(self):
+        if not self.ispattern:
+            return self._backre.sub(r'\\\1', self.data[0])
+
+        return self._backre.sub(r'\\\1', self.data[0]) + '%' + self.data[1]
+
+MAKESTATE_NONE = 0
+MAKESTATE_FINISHED = 1
+MAKESTATE_WORKING = 2
+
+class Target(object):
+    """
+    An actual (non-pattern) target.
+
+    It holds target-specific variables and a list of rules. It may also point to a parent
+    PatternTarget, if this target is being created by an implicit rule.
+
+    The rules associated with this target may be Rule instances or, in the case of static pattern
+    rules, PatternRule instances.
+    """
+
+    def __init__(self, target, makefile):
+        assert isinstance(target, str)
+        self.target = target
+        self.vpathtarget = None
+        self.rules = []
+        self.variables = Variables(makefile.variables)
+        self.explicit = False
+        self._state = MAKESTATE_NONE
+
+    def addrule(self, rule):
+        assert isinstance(rule, (Rule, PatternRuleInstance))
+        if len(self.rules) and rule.doublecolon != self.rules[0].doublecolon:
+            raise DataError("Cannot have single- and double-colon rules for the same target. Prior rule location: %s" % self.rules[0].loc, rule.loc)
+
+        if isinstance(rule, PatternRuleInstance):
+            if len(rule.prule.targetpatterns) != 1:
+                raise DataError("Static pattern rules must only have one target pattern", rule.prule.loc)
+            if rule.prule.targetpatterns[0].match(self.target) is None:
+                raise DataError("Static pattern rule doesn't match target '%s'" % self.target, rule.loc)
+
+        self.rules.append(rule)
+
+    def isdoublecolon(self):
+        return self.rules[0].doublecolon
+
+    def isphony(self, makefile):
+        """Is this a phony target? We don't check for existence of phony targets."""
+        phony = makefile.gettarget('.PHONY').hasdependency(self.target)
+
+    def hasdependency(self, t):
+        for rule in self.rules:
+            if t in rule.prerequisites:
+                return True
+
+        return False
+
+    def resolveimplicitrule(self, makefile, targetstack, rulestack):
+        """
+        Try to resolve an implicit rule to build this target.
+        """
+        # The steps in the GNU make manual Implicit-Rule-Search.html are very detailed. I hope they can be trusted.
+
+        indent = getindent(targetstack)
+
+        _log.info("%sSearching for implicit rule to make '%s'", indent, self.target)
+
+        dir, s, file = util.strrpartition(self.target, '/')
+        dir = dir + s
+
+        candidates = [] # list of PatternRuleInstance
+
+        hasmatch = util.any((r.hasspecificmatch(file) for r in makefile.implicitrules))
+
+        for r in makefile.implicitrules:
+            if r in rulestack:
+                _log.info("%s %s: Avoiding implicit rule recursion", indent, r.loc)
+                continue
+
+            if not len(r.commands):
+                continue
+
+            for ri in r.matchesfor(dir, file, hasmatch):
+                candidates.append(ri)
+            
+        newcandidates = []
+
+        for r in candidates:
+            depfailed = None
+            for p in r.prerequisites:
+                t = makefile.gettarget(p)
+                t.resolvevpath(makefile)
+                if not t.explicit and t.mtime is None:
+                    depfailed = p
+                    break
+
+            if depfailed is not None:
+                if r.doublecolon:
+                    _log.info("%s Terminal rule at %s doesn't match: prerequisite '%s' not mentioned and doesn't exist.", indent, r.loc, depfailed)
+                else:
+                    newcandidates.append(r)
+                continue
+
+            _log.info("%sFound implicit rule at %s for target '%s'", indent, r.loc, self.target)
+            self.rules.append(r)
+            return
+
+        # Try again, but this time with chaining and without terminal (double-colon) rules
+
+        for r in newcandidates:
+            newrulestack = rulestack + [r.prule]
+
+            depfailed = None
+            for p in r.prerequisites:
+                t = makefile.gettarget(p)
+                try:
+                    t.resolvedeps(makefile, targetstack, newrulestack, True)
+                except ResolutionError:
+                    depfailed = p
+                    break
+
+            if depfailed is not None:
+                _log.info("%s Rule at %s doesn't match: prerequisite '%s' could not be made.", indent, r.loc, depfailed)
+                continue
+
+            _log.info("%sFound implicit rule at %s for target '%s'", indent, r.loc, self.target)
+            self.rules.append(r)
+            return
+
+        _log.info("%sCouldn't find implicit rule to remake '%s'", indent, self.target)
+
+    def ruleswithcommands(self):
+        "The number of rules with commands"
+        return reduce(lambda i, rule: i + (len(rule.commands) > 0), self.rules, 0)
+
+    def resolvedeps(self, makefile, targetstack, rulestack, recursive):
+        """
+        Resolve the actual path of this target, using vpath if necessary.
+
+        Recursively resolve dependencies of this target. This means finding implicit
+        rules which match the target, if appropriate.
+
+        Figure out whether this target needs to be rebuild, and set self.outofdate
+        appropriately.
+
+        @param targetstack is the current stack of dependencies being resolved. If
+               this target is already in targetstack, bail to prevent infinite
+               recursion.
+        @param rulestack is the current stack of implicit rules being used to resolve
+               dependencies. A rule chain cannot use the same implicit rule twice.
+        """
+        assert makefile.parsingfinished
+
+        if self.target in targetstack:
+            raise ResolutionError("Recursive dependency: %s -> %s" % (
+                    " -> ".join(targetstack), self.target))
+
+        targetstack = targetstack + [self.target]
+        
+        indent = getindent(targetstack)
+
+        _log.info("%sConsidering target '%s'", indent, self.target)
+
+        self.resolvevpath(makefile)
+
+        # Sanity-check our rules. If we're single-colon, only one rule should have commands
+        ruleswithcommands = self.ruleswithcommands()
+        if len(self.rules) and not self.isdoublecolon():
+            if ruleswithcommands > 1:
+                # In GNU make this is a warning, not an error. I'm going to be stricter.
+                # TODO: provide locations
+                raise DataError("Target '%s' has multiple rules with commands." % self.target)
+
+        if ruleswithcommands == 0:
+            self.resolveimplicitrule(makefile, targetstack, rulestack)
+
+        # If a target is mentioned, but doesn't exist, has no commands and no
+        # prerequisites, it is special and exists just to say that targets which
+        # depend on it are always out of date. This is like .FORCE but more
+        # compatible with other makes.
+        # Otherwise, we don't know how to make it.
+        if not len(self.rules) and self.mtime is None and not util.any((len(rule.prerequisites) > 0
+                                                                        for rule in self.rules)):
+            raise ResolutionError("No rule to make target '%s' needed by %r" % (self.target,
+                                                                                targetstack))
+
+        if recursive:
+            for r in self.rules:
+                newrulestack = rulestack + [r]
+                for d in r.prerequisites:
+                    dt = makefile.gettarget(d)
+                    if dt.explicit:
+                        continue
+
+                    dt.resolvedeps(makefile, targetstack, newrulestack, True)
+
+        for v in makefile.getpatternvariablesfor(self.target):
+            self.variables.merge(v)
+
+    def resolvevpath(self, makefile):
+        if self.vpathtarget is not None:
+            return
+
+        if self.isphony(makefile):
+            self.vpathtarget = self.target
+            self.mtime = None
+            return
+
+        if self.target.startswith('-l'):
+            stem = self.target[2:]
+            f, s, e = makefile.variables.get('.LIBPATTERNS')
+            if e is not None:
+                libpatterns = [Pattern(stripdotslash(s)) for s in e.resolvesplit(makefile, makefile.variables)]
+                if len(libpatterns):
+                    searchdirs = ['']
+                    searchdirs.extend(makefile.getvpath(self.target))
+
+                    for lp in libpatterns:
+                        if not lp.ispattern():
+                            raise DataError('.LIBPATTERNS contains a non-pattern')
+
+                        libname = lp.resolve('', stem)
+
+                        for dir in searchdirs:
+                            libpath = os.path.join(dir, libname).replace('\\', '/')
+                            fspath = os.path.join(makefile.workdir, libpath)
+                            mtime = getmtime(fspath)
+                            if mtime is not None:
+                                self.vpathtarget = libpath
+                                self.mtime = mtime
+                                return
+
+                    self.vpathtarget = self.target
+                    self.mtime = None
+                    return
+
+        search = [self.target]
+        if not os.path.isabs(self.target):
+            search += [os.path.join(dir, self.target).replace('\\', '/')
+                       for dir in makefile.getvpath(self.target)]
+
+        for t in search:
+            fspath = os.path.join(makefile.workdir, t).replace('\\', '/')
+            mtime = getmtime(fspath)
+            if mtime is not None:
+                self.vpathtarget = t
+                self.mtime = mtime
+                return
+
+        self.vpathtarget = self.target
+        self.mtime = None
+        
+    def _beingremade(self):
+        """
+        When we remake ourself, we need to reset our mtime and vpathtarget.
+
+        We store our old mtime so that $? can calculate out-of-date prerequisites.
+        """
+        self.realmtime = self.mtime
+        self.mtime = None
+        self.vpathtarget = self.target
+
+    def _notifydone(self, makefile):
+        assert self._state == MAKESTATE_WORKING
+
+        self._state = MAKESTATE_FINISHED
+        for cb in self._callbacks:
+            makefile.context.defer(cb, error=self._makeerror, didanything=self._didanything)
+        del self._callbacks 
+
+    def make(self, makefile, targetstack, rulestack, cb, avoidremakeloop=False):
+        """
+        If we are out of date, asynchronously make ourself. This is a multi-stage process, mostly handled
+        by enclosed functions:
+
+        * resolve dependencies (synchronous)
+        * gather a list of rules to execute and related dependencies (synchronous)
+        * for each rule (rulestart)
+        ** remake dependencies (asynchronous, toplevel, callback to start each dependency is `depstart`,
+           callback when each is finished is `depfinished``
+        ** build list of commands to execute (synchronous, in `runcommands`)
+        ** execute each command (asynchronous, runcommands.commandcb)
+        * asynchronously notify rulefinished when each rule is complete
+
+        @param cb A callback function to notify when remaking is finished. It is called
+               thusly: callback(error=exception/None, didanything=True/False/None)
+               If there is no asynchronous activity to perform, the callback may be called directly.
+        """
+
+        serial = makefile.context.jcount == 1
+        
+        if self._state == MAKESTATE_FINISHED:
+            cb(error=self._makeerror, didanything=self._didanything)
+            return
+            
+        if self._state == MAKESTATE_WORKING:
+            assert not serial
+            self._callbacks.append(cb)
+            return
+
+        assert self._state == MAKESTATE_NONE
+
+        self._state = MAKESTATE_WORKING
+        self._callbacks = [cb]
+        self._makeerror = None
+        self._didanything = False
+
+        indent = getindent(targetstack)
+
+        if serial:
+            def rulefinished():
+                if self._makeerror is not None:
+                    self._notifydone(makefile)
+                    return
+
+                if len(rulelist):
+                    rule, deps = rulelist.pop(0)
+                    makefile.context.defer(startrule, rule, deps)
+                else:
+                    self._notifydone(makefile)
+        else:
+            makeclosure = util.makeobject(('rulesremaining',))
+            def rulefinished():
+                makeclosure.rulesremaining -= 1
+                if makeclosure.rulesremaining == 0:
+                    self._notifydone(makefile)
+
+        def startrule(r, deps):
+            if serial:
+                def depfinished(error, didanything):
+                    if error is not None:
+                        self._makeerror = error
+                        self._notifydone(makefile)
+                        return
+
+                    self._didanything = didanything or self._didanything
+
+                    if len(deplist):
+                        dep = deplist.pop(0)
+                        makefile.context.defer(startdep, dep)
+                    else:
+                        runcommands()
+            else:
+                ruleclosure = util.makeobject(('depsremaining',), depsremaining=len(deps))
+                def depfinished(error, didanything):
+                    if error is not None:
+                        self._makeerror = error
+                    else:
+                        self._didanything = didanything or self._didanything
+
+                    ruleclosure.depsremaining -= 1
+                    if ruleclosure.depsremaining == 0:
+                        if self._makeerror is not None:
+                            rulefinished()
+                        else:
+                            runcommands()
+
+            def startdep(dep):
+                if self._makeerror is not None:
+                    depfinished(None, False)
+                    return
+
+                dep.make(makefile, targetstack, [], cb=depfinished)
+
+            def runcommands():
+                """
+                Asynchronous dependency-making is finished. Now run our commands (if any).
+                """
+
+                if r is None or not len(r.commands):
+                    if self.mtime is None:
+                        self._beingremade()
+                    else:
+                        for d in deps:
+                            if mtimeislater(d.mtime, self.mtime):
+                                self._beingremade()
+                                break
+                    rulefinished()
+                    return
+
+                def commandcb(error):
+                    if error is not None:
+                        self._makeerror = error
+                        rulefinished()
+                        return
+
+                    if len(commands):
+                        commands.pop(0)(commandcb)
+                    else:
+                        rulefinished()
+
+                remake = False
+                if self.mtime is None:
+                    remake = True
+                    _log.info("%sRemaking %s using rule at %s: target doesn't exist or is a forced target", indent, self.target, r.loc)
+
+                if not remake:
+                    if r.doublecolon:
+                        if len(deps) == 0:
+                            if avoidremakeloop:
+                                _log.info("%sNot remaking %s using rule at %s because it would introduce an infinite loop.", indent, self.target, r.loc)
+                            else:
+                                _log.info("%sRemaking %s using rule at %s because there are no prerequisites listed for a double-colon rule.", indent, self.target, r.loc)
+                                remake = True
+
+                if not remake:
+                    for d in deps:
+                        if mtimeislater(d.mtime, self.mtime):
+                            _log.info("%sRemaking %s using rule at %s because %s is newer.", indent, self.target, r.loc, d.target)
+                            remake = True
+                            break
+
+                if remake:
+                    self._beingremade()
+                    self._didanything = True
+                    try:
+                        commands = [c for c in r.getcommands(self, makefile)]
+                    except util.MakeError, e:
+                        self._makeerror = e
+                        rulefinished()
+                        return
+                    commandcb(None)
+                else:
+                    rulefinished()
+
+            if not len(deps):
+                runcommands()
+            else:
+                if serial:
+                    deplist = list(deps)
+                    startdep(deplist.pop(0))
+                else:
+                    ruleclosure.depsremaining = len(deps)
+                    for d in deps:
+                        makefile.context.defer(startdep, d)
+
+        try:
+            self.resolvedeps(makefile, targetstack, rulestack, False)
+        except util.MakeError, e:
+            self._makeerror = e
+            self._notifydone(makefile)
+            return
+
+        assert self.vpathtarget is not None, "Target was never resolved!"
+        if not len(self.rules):
+            self._notifydone(makefile)
+            return
+
+        if self.isdoublecolon():
+            rulelist = [(r, [makefile.gettarget(p) for p in r.prerequisites]) for r in self.rules]
+        else:
+            alldeps = []
+
+            commandrule = None
+            for r in self.rules:
+                rdeps = [makefile.gettarget(p) for p in r.prerequisites]
+                if len(r.commands):
+                    assert commandrule is None
+                    commandrule = r
+                    # The dependencies of the command rule are resolved before other dependencies,
+                    # no matter the ordering of the other no-command rules
+                    alldeps[0:0] = rdeps
+                else:
+                    alldeps.extend(rdeps)
+
+            rulelist = [(commandrule, alldeps)]
+
+        targetstack = targetstack + [self.target]
+
+        if serial:
+            rulefinished()
+        else:
+            makeclosure.rulesremaining = len(rulelist)
+            for r, deps in rulelist:
+                makefile.context.defer(startrule, r, deps)
+
+def dirpart(p):
+    d, s, f = util.strrpartition(p, '/')
+    if d == '':
+        return '.'
+
+    return d
+
+def filepart(p):
+    d, s, f = util.strrpartition(p, '/')
+    return f
+
+def setautomatic(v, name, plist):
+    v.set(name, Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, ' '.join(plist))
+    v.set(name + 'D', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, ' '.join((dirpart(p) for p in plist)))
+    v.set(name + 'F', Variables.FLAVOR_SIMPLE, Variables.SOURCE_AUTOMATIC, ' '.join((filepart(p) for p in plist)))
+
+def setautomaticvariables(v, makefile, target, prerequisites):
+    prtargets = [makefile.gettarget(p) for p in prerequisites]
+    prall = [pt.vpathtarget for pt in prtargets]
+    proutofdate = [pt.vpathtarget for pt in withoutdups(prtargets)
+                   if target.realmtime is None or mtimeislater(pt.mtime, target.realmtime)]
+    
+    setautomatic(v, '@', [target.vpathtarget])
+    if len(prall):
+        setautomatic(v, '<', [prall[0]])
+
+    setautomatic(v, '?', proutofdate)
+    setautomatic(v, '^', list(withoutdups(prall)))
+    setautomatic(v, '+', prall)
+
+def splitcommand(command):
+    """
+    Using the esoteric rules, split command lines by unescaped newlines.
+    """
+    start = 0
+    i = 0
+    while i < len(command):
+        c = command[i]
+        if c == '\\':
+            i += 1
+        elif c == '\n':
+            yield command[start:i]
+            i += 1
+            start = i
+            continue
+
+        i += 1
+
+    if i > start:
+        yield command[start:i]
+
+def findmodifiers(command):
+    """
+    Find any of +-@ prefixed on the command.
+    @returns (command, isHidden, isRecursive, ignoreErrors)
+    """
+
+    isHidden = False
+    isRecursive = False
+    ignoreErrors = False
+
+    realcommand = command.lstrip(' \t\n@+-')
+    modset = set(command[:-len(realcommand)])
+    return realcommand, '@' in modset, '+' in modset, '-' in modset
+
+class _CommandWrapper(object):
+    def __init__(self, cline, ignoreErrors, loc, context, **kwargs):
+        self.ignoreErrors = ignoreErrors
+        self.loc = loc
+        self.cline = cline
+        self.kwargs = kwargs
+        self.context = context
+
+    def _cb(self, res):
+        if res != 0 and not self.ignoreErrors:
+            self.usercb(error=DataError("command '%s' failed, return code %s" % (self.cline, res), self.loc))
+        else:
+            self.usercb(error=None)
+
+    def __call__(self, cb):
+        self.usercb = cb
+        process.call(self.cline, loc=self.loc, cb=self._cb, context=self.context, **self.kwargs)
+
+def getcommandsforrule(rule, target, makefile, prerequisites, stem):
+    v = Variables(parent=target.variables)
+    setautomaticvariables(v, makefile, target, prerequisites)
+    if stem is not None:
+        setautomatic(v, '*', [stem])
+
+    env = makefile.getsubenvironment(v)
+
+    for c in rule.commands:
+        cstring = c.resolvestr(makefile, v)
+        for cline in splitcommand(cstring):
+            cline, isHidden, isRecursive, ignoreErrors = findmodifiers(cline)
+            if isHidden:
+                echo = None
+            else:
+                echo = "%s$ %s" % (c.loc, cline)
+            yield _CommandWrapper(cline, ignoreErrors=ignoreErrors, env=env, cwd=makefile.workdir, loc=c.loc, context=makefile.context,
+                                 echo=echo)
+
+class Rule(object):
+    """
+    A rule contains a list of prerequisites and a list of commands. It may also
+    contain rule-specific variables. This rule may be associated with multiple targets.
+    """
+
+    def __init__(self, prereqs, doublecolon, loc):
+        self.prerequisites = prereqs
+        self.doublecolon = doublecolon
+        self.commands = []
+        self.loc = loc
+
+    def addcommand(self, c):
+        assert isinstance(c, (Expansion, StringExpansion))
+        self.commands.append(c)
+
+    def getcommands(self, target, makefile):
+        assert isinstance(target, Target)
+
+        return getcommandsforrule(self, target, makefile, self.prerequisites, stem=None)
+        # TODO: $* in non-pattern rules?
+
+class PatternRuleInstance(object):
+    """
+    A pattern rule instantiated for a particular target. It has the same API as Rule, but
+    different internals, forwarding most information on to the PatternRule.
+    """
+    def __init__(self, prule, dir, stem, ismatchany):
+        assert isinstance(prule, PatternRule)
+
+        self.dir = dir
+        self.stem = stem
+        self.prule = prule
+        self.prerequisites = prule.prerequisitesforstem(dir, stem)
+        self.doublecolon = prule.doublecolon
+        self.loc = prule.loc
+        self.ismatchany = ismatchany
+        self.commands = prule.commands
+
+    def getcommands(self, target, makefile):
+        assert isinstance(target, Target)
+        return getcommandsforrule(self, target, makefile, self.prerequisites, stem=self.dir + self.stem)
+
+    def __str__(self):
+        return "Pattern rule at %s with stem '%s', matchany: %s doublecolon: %s" % (self.loc,
+                                                                                    self.dir + self.stem,
+                                                                                    self.ismatchany,
+                                                                                    self.doublecolon)
+
+class PatternRule(object):
+    """
+    An implicit rule or static pattern rule containing target patterns, prerequisite patterns,
+    and a list of commands.
+    """
+
+    def __init__(self, targetpatterns, prerequisites, doublecolon, loc):
+        self.targetpatterns = targetpatterns
+        self.prerequisites = prerequisites
+        self.doublecolon = doublecolon
+        self.loc = loc
+        self.commands = []
+
+    def addcommand(self, c):
+        assert isinstance(c, (Expansion, StringExpansion))
+        self.commands.append(c)
+
+    def ismatchany(self):
+        return util.any((t.ismatchany() for t in self.targetpatterns))
+
+    def hasspecificmatch(self, file):
+        for p in self.targetpatterns:
+            if not p.ismatchany() and p.match(file) is not None:
+                return True
+
+        return False
+
+    def matchesfor(self, dir, file, skipsinglecolonmatchany):
+        """
+        Determine all the target patterns of this rule that might match target t.
+        @yields a PatternRuleInstance for each.
+        """
+
+        for p in self.targetpatterns:
+            matchany = p.ismatchany()
+            if matchany:
+                if skipsinglecolonmatchany and not self.doublecolon:
+                    continue
+
+                yield PatternRuleInstance(self, dir, file, True)
+            else:
+                stem = p.match(dir + file)
+                if stem is not None:
+                    yield PatternRuleInstance(self, '', stem, False)
+                else:
+                    stem = p.match(file)
+                    if stem is not None:
+                        yield PatternRuleInstance(self, dir, stem, False)
+
+    def prerequisitesforstem(self, dir, stem):
+        return [p.resolve(dir, stem) for p in self.prerequisites]
+
+class Makefile(object):
+    """
+    The top-level data structure for makefile execution. It holds Targets, implicit rules, and other
+    state data.
+    """
+
+    def __init__(self, workdir=None, env=None, restarts=0, make=None, makeflags=None, makelevel=0, context=None):
+        self.defaulttarget = None
+
+        if env is None:
+            env = os.environ
+        self.env = env
+
+        self.variables = Variables()
+        self.variables.readfromenvironment(env)
+
+        self.context = context
+        self.exportedvars = set()
+        self.overrides = []
+        self._targets = {}
+        self._patternvariables = [] # of (pattern, variables)
+        self.implicitrules = []
+        self.parsingfinished = False
+
+        self._patternvpaths = [] # of (pattern, [dir, ...])
+
+        if workdir is None:
+            workdir = os.getcwd()
+        workdir = os.path.realpath(workdir)
+        self.workdir = workdir
+        self.variables.set('CURDIR', Variables.FLAVOR_SIMPLE,
+                           Variables.SOURCE_AUTOMATIC, workdir.replace('\\','/'))
+
+        # the list of included makefiles, whether or not they existed
+        self.included = []
+
+        self.variables.set('MAKE_RESTARTS', Variables.FLAVOR_SIMPLE,
+                           Variables.SOURCE_AUTOMATIC, restarts > 0 and str(restarts) or '')
+
+        if make is not None:
+            self.variables.set('MAKE', Variables.FLAVOR_SIMPLE,
+                               Variables.SOURCE_MAKEFILE, make)
+
+        if makeflags is not None:
+            self.variables.set('MAKEFLAGS', Variables.FLAVOR_SIMPLE,
+                               Variables.SOURCE_MAKEFILE, makeflags)
+
+        self.makelevel = makelevel
+        self.variables.set('MAKELEVEL', Variables.FLAVOR_SIMPLE,
+                           Variables.SOURCE_MAKEFILE, str(makelevel))
+
+        for vname, val in builtins.variables.iteritems():
+            self.variables.set(vname, Variables.FLAVOR_SIMPLE,
+                               Variables.SOURCE_IMPLICIT, val)
+
+    def foundtarget(self, t):
+        """
+        Inform the makefile of a target which is a candidate for being the default target,
+        if there isn't already a default target.
+        """
+        if self.defaulttarget is None:
+            self.defaulttarget = t
+
+    def getpatternvariables(self, pattern):
+        assert isinstance(pattern, Pattern)
+
+        for p, v in self._patternvariables:
+            if p == pattern:
+                return v
+
+        v = Variables()
+        self._patternvariables.append( (pattern, v) )
+        return v
+
+    def getpatternvariablesfor(self, target):
+        for p, v in self._patternvariables:
+            if p.match(target):
+                yield v
+
+    def hastarget(self, target):
+        return target in self._targets
+
+    def gettarget(self, target):
+        assert isinstance(target, str)
+
+        target = target.rstrip('/')
+
+        assert target != '', "empty target?"
+
+        if target.find('*') != -1 or target.find('?') != -1 or target.find('[') != -1:
+            raise DataError("wildcards should have been expanded by the parser: '%s'" % (target,))
+
+        t = self._targets.get(target, None)
+        if t is None:
+            t = Target(target, self)
+            self._targets[target] = t
+        return t
+
+    def appendimplicitrule(self, rule):
+        assert isinstance(rule, PatternRule)
+        self.implicitrules.append(rule)
+
+    def finishparsing(self):
+        """
+        Various activities, such as "eval", are not allowed after parsing is
+        finished. In addition, various warnings and errors can only be issued
+        after the parsing data model is complete. All dependency resolution
+        and rule execution requires that parsing be finished.
+        """
+        self.parsingfinished = True
+
+        flavor, source, value = self.variables.get('GPATH')
+        if value is not None and value.resolvestr(self, self.variables, ['GPATH']).strip() != '':
+            raise DataError('GPATH was set: pymake does not support GPATH semantics')
+
+        flavor, source, value = self.variables.get('VPATH')
+        if value is None:
+            self._vpath = []
+        else:
+            self._vpath = filter(lambda e: e != '',
+                                 re.split('[%s\s]+' % os.pathsep,
+                                          value.resolvestr(self, self.variables, ['VPATH'])))
+
+        targets = list(self._targets.itervalues())
+        for t in targets:
+            t.explicit = True
+            for r in t.rules:
+                for p in r.prerequisites:
+                    self.gettarget(p).explicit = True
+
+    def include(self, path, required=True, loc=None):
+        """
+        Include the makefile at `path`.
+        """
+        fspath = os.path.join(self.workdir, path)
+        if os.path.exists(fspath):
+            self.included.append(path)
+            stmts = parser.parsefile(fspath)
+            self.variables.append('MAKEFILE_LIST', Variables.SOURCE_AUTOMATIC, path, None, self)
+            stmts.execute(self)
+            self.gettarget(path).explicit = True
+        elif required:
+            raise DataError("Attempting to include file '%s' which doesn't exist." % (path,), loc)
+
+    def addvpath(self, pattern, dirs):
+        """
+        Add a directory to the vpath search for the given pattern.
+        """
+        self._patternvpaths.append((pattern, dirs))
+
+    def clearvpath(self, pattern):
+        """
+        Clear vpaths for the given pattern.
+        """
+        self._patternvpaths = [(p, dirs)
+                               for p, dirs in self._patternvpaths
+                               if not p.match(pattern)]
+
+    def clearallvpaths(self):
+        self._patternvpaths = []
+
+    def getvpath(self, target):
+        vp = list(self._vpath)
+        for p, dirs in self._patternvpaths:
+            if p.match(target):
+                vp.extend(dirs)
+
+        return withoutdups(vp)
+
+    def remakemakefiles(self, cb):
+        reparse = False
+
+        serial = self.context.jcount == 1
+
+        def remakedone():
+            for t, oldmtime in mlist:
+                if t.mtime != oldmtime:
+                    cb(remade=True)
+                    return
+            cb(remade=False)
+
+        mlist = []
+        for f in self.included:
+            t = self.gettarget(f)
+            t.explicit = True
+            t.resolvevpath(self)
+            oldmtime = t.mtime
+
+            mlist.append((t, oldmtime))
+
+        if serial:
+            remakelist = [self.gettarget(f) for f in self.included]
+            def remakecb(error, didanything):
+                if error is not None:
+                    print "Error remaking makefiles (ignored): ", error
+
+                if len(remakelist):
+                    t = remakelist.pop(0)
+                    t.make(self, [], [], avoidremakeloop=True, cb=remakecb)
+                else:
+                    remakedone()
+
+            remakelist.pop(0).make(self, [], [], avoidremakeloop=True, cb=remakecb)
+        else:
+            o = util.makeobject(('remakesremaining',), remakesremaining=len(self.included))
+            def remakecb(error, didanything):
+                if error is not None:
+                    print "Error remaking makefiles (ignored): ", error
+
+                o.remakesremaining -= 1
+                if o.remakesremaining == 0:
+                    remakedone()
+
+            for t, mtime in mlist:
+                t.make(self, [], [], avoidremakeloop=True, cb=remakecb)
+
+    flagescape = re.compile(r'([\s\\])')
+
+    def getsubenvironment(self, variables):
+        env = dict(self.env)
+        for vname in self.exportedvars:
+            flavor, source, val = variables.get(vname)
+            if val is None:
+                strval = ''
+            else:
+                strval = val.resolvestr(self, variables, [vname])
+            env[vname] = strval
+
+        makeflags = ''
+
+        flavor, source, val = variables.get('MAKEFLAGS')
+        if val is not None:
+            flagsval = val.resolvestr(self, variables, ['MAKEFLAGS'])
+            if flagsval != '':
+                makeflags = flagsval
+
+        makeflags += ' -- '
+        makeflags += ' '.join((self.flagescape.sub(r'\\\1', o) for o in self.overrides))
+
+        env['MAKEFLAGS'] = makeflags
+
+        env['MAKELEVEL'] = str(self.makelevel + 1)
+        return env
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/functions.py
@@ -0,0 +1,621 @@
+"""
+Makefile functions.
+"""
+
+import parser, data, util
+import subprocess, os, logging
+from globrelative import glob
+from cStringIO import StringIO
+
+log = logging.getLogger('pymake.data')
+
+class Function(object):
+    """
+    An object that represents a function call. This class is always subclassed
+    with the following methods and attributes:
+
+    minargs = minimum # of arguments
+    maxargs = maximum # of arguments (0 means unlimited)
+
+    def resolve(self, makefile, variables, fd, setting)
+        Calls the function
+        calls fd.write() with strings
+    """
+    def __init__(self, loc):
+        self._arguments = []
+        self.loc = loc
+        assert self.minargs > 0
+
+    def __getitem__(self, key):
+        return self._arguments[key]
+
+    def setup(self):
+        argc = len(self._arguments)
+
+        if argc < self.minargs:
+            raise data.DataError("Not enough arguments to function %s, requires %s" % (self.name, self.minargs), self.loc)
+
+        assert self.maxargs == 0 or argc <= self.maxargs, "Parser screwed up, gave us too many args"
+
+    def append(self, arg):
+        assert isinstance(arg, (data.Expansion, data.StringExpansion))
+        self._arguments.append(arg)
+
+    def __len__(self):
+        return len(self._arguments)
+
+class VariableRef(Function):
+    def __init__(self, loc, vname):
+        self.loc = loc
+        assert isinstance(vname, (data.Expansion, data.StringExpansion))
+        self.vname = vname
+        
+    def setup(self):
+        assert False, "Shouldn't get here"
+
+    def resolve(self, makefile, variables, fd, setting):
+        vname = self.vname.resolvestr(makefile, variables, setting)
+        if vname in setting:
+            raise data.DataError("Setting variable '%s' recursively references itself." % (vname,), self.loc)
+
+        flavor, source, value = variables.get(vname)
+        if value is None:
+            log.debug("%s: variable '%s' was not set" % (self.loc, vname))
+            return
+
+        value.resolve(makefile, variables, fd, setting + [vname])
+
+class SubstitutionRef(Function):
+    """$(VARNAME:.c=.o) and $(VARNAME:%.c=%.o)"""
+    def __init__(self, loc, varname, substfrom, substto):
+        self.loc = loc
+        self.vname = varname
+        self.substfrom = substfrom
+        self.substto = substto
+
+    def setup(self):
+        assert False, "Shouldn't get here"
+
+    def resolve(self, makefile, variables, fd, setting):
+        vname = self.vname.resolvestr(makefile, variables, setting)
+        if vname in setting:
+            raise data.DataError("Setting variable '%s' recursively references itself." % (vname,), self.loc)
+
+        substfrom = self.substfrom.resolvestr(makefile, variables, setting)
+        substto = self.substto.resolvestr(makefile, variables, setting)
+
+        flavor, source, value = variables.get(vname)
+        if value is None:
+            log.debug("%s: variable '%s' was not set" % (self.loc, vname))
+            return
+
+        f = data.Pattern(substfrom)
+        if not f.ispattern():
+            f = data.Pattern('%' + substfrom)
+            substto = '%' + substto
+
+        fd.write(' '.join([f.subst(substto, word, False)
+                           for word in value.resolvesplit(makefile, variables, setting + [vname])]))
+
+class SubstFunction(Function):
+    name = 'subst'
+    minargs = 3
+    maxargs = 3
+
+    def resolve(self, makefile, variables, fd, setting):
+        s = self._arguments[0].resolvestr(makefile, variables, setting)
+        r = self._arguments[1].resolvestr(makefile, variables, setting)
+        d = self._arguments[2].resolvestr(makefile, variables, setting)
+        fd.write(d.replace(s, r))
+
+class PatSubstFunction(Function):
+    name = 'patsubst'
+    minargs = 3
+    maxargs = 3
+
+    def resolve(self, makefile, variables, fd, setting):
+        s = self._arguments[0].resolvestr(makefile, variables, setting)
+        r = self._arguments[1].resolvestr(makefile, variables, setting)
+
+        p = data.Pattern(s)
+        fd.write(' '.join([p.subst(r, word, False)
+                           for word in self._arguments[2].resolvesplit(makefile, variables, setting)]))
+
+class StripFunction(Function):
+    name = 'strip'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        util.joiniter(fd, self._arguments[0].resolvesplit(makefile, variables, setting))
+
+class FindstringFunction(Function):
+    name = 'findstring'
+    minargs = 2
+    maxargs = 2
+
+    def resolve(self, makefile, variables, fd, setting):
+        s = self._arguments[0].resolvestr(makefile, variables, setting)
+        r = self._arguments[1].resolvestr(makefile, variables, setting)
+        if r.find(s) == -1:
+            return
+        fd.write(s)
+
+class FilterFunction(Function):
+    name = 'filter'
+    minargs = 2
+    maxargs = 2
+
+    def resolve(self, makefile, variables, fd, setting):
+        plist = [data.Pattern(p)
+                 for p in self._arguments[0].resolvesplit(makefile, variables, setting)]
+
+        fd.write(' '.join([w for w in self._arguments[1].resolvesplit(makefile, variables, setting)
+                           if util.any((p.match(w) for p in plist))]))
+
+class FilteroutFunction(Function):
+    name = 'filter-out'
+    minargs = 2
+    maxargs = 2
+
+    def resolve(self, makefile, variables, fd, setting):
+        plist = [data.Pattern(p)
+                 for p in self._arguments[0].resolvesplit(makefile, variables, setting)]
+
+        fd.write(' '.join([w for w in self._arguments[1].resolvesplit(makefile, variables, setting)
+                           if not util.any((p.match(w) for p in plist))]))
+
+class SortFunction(Function):
+    name = 'sort'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        d = list(self._arguments[0].resolvesplit(makefile, variables, setting))
+        d.sort()
+        util.joiniter(fd, d)
+
+class WordFunction(Function):
+    name = 'word'
+    minargs = 2
+    maxargs = 2
+
+    def resolve(self, makefile, variables, fd, setting):
+        n = self._arguments[0].resolvestr(makefile, variables, setting)
+        # TODO: provide better error if this doesn't convert
+        n = int(n)
+        words = list(self._arguments[1].resolvesplit(makefile, variables, setting))
+        if n < 1 or n > len(words):
+            return
+        fd.write(words[n - 1])
+
+class WordlistFunction(Function):
+    name = 'wordlist'
+    minargs = 3
+    maxargs = 3
+
+    def resolve(self, makefile, variables, fd, setting):
+        nfrom = self._arguments[0].resolvestr(makefile, variables, setting)
+        nto = self._arguments[1].resolvestr(makefile, variables, setting)
+        # TODO: provide better errors if this doesn't convert
+        nfrom = int(nfrom)
+        nto = int(nto)
+
+        words = list(self._arguments[2].resolvesplit(makefile, variables, setting))
+
+        if nfrom < 1:
+            nfrom = 1
+        if nto < 1:
+            nto = 1
+
+        util.joiniter(fd, words[nfrom - 1:nto])
+
+class WordsFunction(Function):
+    name = 'words'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        fd.write(str(len(self._arguments[0].resolvesplit(makefile, variables, setting))))
+
+class FirstWordFunction(Function):
+    name = 'firstword'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        l = self._arguments[0].resolvesplit(makefile, variables, setting)
+        if len(l):
+            fd.write(l[0])
+
+class LastWordFunction(Function):
+    name = 'lastword'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        l = self._arguments[0].resolvesplit(makefile, variables, setting)
+        if len(l):
+            fd.write(l[-1])
+
+def pathsplit(path, default='./'):
+    """
+    Splits a path into dirpart, filepart on the last slash. If there is no slash, dirpart
+    is ./
+    """
+    dir, slash, file = util.strrpartition(path, '/')
+    if dir == '':
+        return default, file
+
+    return dir + slash, file
+
+class DirFunction(Function):
+    name = 'dir'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        fd.write(' '.join([pathsplit(path)[0]
+                           for path in self._arguments[0].resolvesplit(makefile, variables, setting)]))
+
+class NotDirFunction(Function):
+    name = 'notdir'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        fd.write(' '.join([pathsplit(path)[1]
+                           for path in self._arguments[0].resolvesplit(makefile, variables, setting)]))
+
+class SuffixFunction(Function):
+    name = 'suffix'
+    minargs = 1
+    maxargs = 1
+
+    @staticmethod
+    def suffixes(words):
+        for w in words:
+            dir, file = pathsplit(w)
+            base, dot, suffix = util.strrpartition(file, '.')
+            if base != '':
+                yield dot + suffix
+
+    def resolve(self, makefile, variables, fd, setting):
+        util.joiniter(fd, self.suffixes(self._arguments[0].resolvesplit(makefile, variables, setting)))
+
+class BasenameFunction(Function):
+    name = 'basename'
+    minargs = 1
+    maxargs = 1
+
+    @staticmethod
+    def basenames(words):
+        for w in words:
+            dir, file = pathsplit(w, '')
+            base, dot, suffix = util.strrpartition(file, '.')
+            if dot == '':
+                base = suffix
+
+            yield dir + base
+
+    def resolve(self, makefile, variables, fd, setting):
+        util.joiniter(fd, self.basenames(self._arguments[0].resolvesplit(makefile, variables, setting)))
+
+class AddSuffixFunction(Function):
+    name = 'addprefix'
+    minargs = 2
+    maxargs = 2
+
+    def resolve(self, makefile, variables, fd, setting):
+        suffix = self._arguments[0].resolvestr(makefile, variables, setting)
+
+        fd.write(' '.join([w + suffix for w in self._arguments[1].resolvesplit(makefile, variables, setting)]))
+
+class AddPrefixFunction(Function):
+    name = 'addsuffix'
+    minargs = 2
+    maxargs = 2
+
+    def resolve(self, makefile, variables, fd, setting):
+        prefix = self._arguments[0].resolvestr(makefile, variables, setting)
+
+        fd.write(' '.join([prefix + w for w in self._arguments[1].resolvesplit(makefile, variables, setting)]))
+
+class JoinFunction(Function):
+    name = 'join'
+    minargs = 2
+    maxargs = 2
+
+    @staticmethod
+    def iterjoin(l1, l2):
+        for i in xrange(0, max(len(l1), len(l2))):
+            i1 = i < len(l1) and l1[i] or ''
+            i2 = i < len(l2) and l2[i] or ''
+            yield i1 + i2
+
+    def resolve(self, makefile, variables, fd, setting):
+        list1 = list(self._arguments[0].resolvesplit(makefile, variables, setting))
+        list2 = list(self._arguments[1].resolvesplit(makefile, variables, setting))
+
+        util.joiniter(fd, self.iterjoin(list1, list2))
+
+class WildcardFunction(Function):
+    name = 'wildcard'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        patterns = self._arguments[0].resolvesplit(makefile, variables, setting)
+
+        fd.write(' '.join([x.replace('\\','/')
+                           for p in patterns
+                           for x in glob(makefile.workdir, p)]))
+
+class RealpathFunction(Function):
+    name = 'realpath'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        fd.write(' '.join([os.path.realpath(os.path.join(makefile.workdir, path)).replace('\\', '/')
+                           for path in self._arguments[0].resolvesplit(makefile, variables, setting)]))
+
+class AbspathFunction(Function):
+    name = 'abspath'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        assert os.path.isabs(makefile.workdir)
+        fd.write(' '.join([os.path.join(makefile.workdir, path).replace('\\', '/')
+                           for path in self._arguments[0].resolvesplit(makefile, variables, setting)]))
+
+class IfFunction(Function):
+    name = 'if'
+    minargs = 1
+    maxargs = 3
+
+    def setup(self):
+        Function.setup(self)
+        self._arguments[0].lstrip()
+        self._arguments[0].rstrip()
+
+    def resolve(self, makefile, variables, fd, setting):
+        condition = self._arguments[0].resolvestr(makefile, variables, setting)
+
+        if len(condition):
+            self._arguments[1].resolve(makefile, variables, fd, setting)
+        elif len(self._arguments) > 2:
+            return self._arguments[2].resolve(makefile, variables, fd, setting)
+
+class OrFunction(Function):
+    name = 'or'
+    minargs = 1
+    maxargs = 0
+
+    def resolve(self, makefile, variables, fd, setting):
+        for arg in self._arguments:
+            r = arg.resolvestr(makefile, variables, setting)
+            if r != '':
+                fd.write(r)
+                return
+
+class AndFunction(Function):
+    name = 'and'
+    minargs = 1
+    maxargs = 0
+
+    def resolve(self, makefile, variables, fd, setting):
+        r = ''
+
+        for arg in self._arguments:
+            r = arg.resolvestr(makefile, variables, setting)
+            if r == '':
+                return
+
+        fd.write(r)
+
+class ForEachFunction(Function):
+    name = 'foreach'
+    minargs = 3
+    maxargs = 3
+
+    def resolve(self, makefile, variables, fd, setting):
+        vname = self._arguments[0].resolvestr(makefile, variables, setting)
+        e = self._arguments[2]
+
+        v = data.Variables(parent=variables)
+        firstword = True
+
+        for w in self._arguments[1].resolvesplit(makefile, variables, setting):
+            if firstword:
+                firstword = False
+            else:
+                fd.write(' ')
+
+            v.set(vname, data.Variables.FLAVOR_SIMPLE, data.Variables.SOURCE_AUTOMATIC, w)
+            e.resolve(makefile, v, fd, setting)
+
+class CallFunction(Function):
+    name = 'call'
+    minargs = 1
+    maxargs = 0
+
+    def resolve(self, makefile, variables, fd, setting):
+        vname = self._arguments[0].resolvestr(makefile, variables, setting)
+        if vname in setting:
+            raise data.DataError("Recursively setting variable '%s'" % (vname,))
+
+        v = data.Variables(parent=variables)
+        v.set('0', data.Variables.FLAVOR_SIMPLE, data.Variables.SOURCE_AUTOMATIC, vname)
+        for i in xrange(1, len(self._arguments)):
+            param = self._arguments[i].resolvestr(makefile, variables, setting)
+            v.set(str(i), data.Variables.FLAVOR_SIMPLE, data.Variables.SOURCE_AUTOMATIC, param)
+
+        flavor, source, e = variables.get(vname)
+
+        if e is None:
+            return
+
+        if flavor == data.Variables.FLAVOR_SIMPLE:
+            log.warning("%s: calling variable '%s' which is simply-expanded" % (self.loc, vname))
+
+        # but we'll do it anyway
+        e.resolve(makefile, v, fd, setting + [vname])
+
+class ValueFunction(Function):
+    name = 'value'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        varname = self._arguments[0].resolvestr(makefile, variables, setting)
+
+        flavor, source, value = variables.get(varname, expand=False)
+        if value is not None:
+            fd.write(value)
+
+class EvalFunction(Function):
+    name = 'eval'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        if makefile.parsingfinished:
+            # GNU make allows variables to be set by recursive expansion during
+            # command execution. This seems really dumb to me, so I don't!
+            raise data.DataError("$(eval) not allowed via recursive expansion after parsing is finished", self.loc)
+
+        text = StringIO(self._arguments[0].resolvestr(makefile, variables, setting))
+        stmts = parser.parsestream(text, 'evaluation from %s' % self.loc)
+        stmts.execute(makefile)
+
+class OriginFunction(Function):
+    name = 'origin'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        vname = self._arguments[0].resolvestr(makefile, variables, setting)
+
+        flavor, source, value = variables.get(vname)
+        if source is None:
+            r = 'undefined'
+        elif source == data.Variables.SOURCE_OVERRIDE:
+            r = 'override'
+
+        elif source == data.Variables.SOURCE_MAKEFILE:
+            r = 'file'
+        elif source == data.Variables.SOURCE_ENVIRONMENT:
+            r = 'environment'
+        elif source == data.Variables.SOURCE_COMMANDLINE:
+            r = 'command line'
+        elif source == data.Variables.SOURCE_AUTOMATIC:
+            r = 'automatic'
+
+        fd.write(r)
+
+class FlavorFunction(Function):
+    name = 'flavor'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        varname = self._arguments[0].resolvestr(makefile, variables, setting)
+        
+        flavor, source, value = variables.get(varname)
+        if flavor is None:
+            r = 'undefined'
+        elif flavor == data.Variables.FLAVOR_RECURSIVE:
+            r = 'recursive'
+        elif flavor == data.Variables.FLAVOR_SIMPLE:
+            r = 'simple'
+        fd.write(r)
+
+class ShellFunction(Function):
+    name = 'shell'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        #TODO: call this once up-front somewhere and save the result?
+        shell, msys = util.checkmsyscompat()
+        cline = self._arguments[0].resolvestr(makefile, variables, setting)
+
+        log.debug("%s: running shell command '%s'" % (self.loc, cline))
+        if msys:
+            cline = [shell, "-c", cline]
+        p = subprocess.Popen(cline, shell=not msys, stdout=subprocess.PIPE, cwd=makefile.workdir)
+        stdout, stderr = p.communicate()
+
+        stdout = stdout.replace('\r\n', '\n')
+        if len(stdout) > 1 and stdout[-1] == '\n':
+            stdout = stdout[:-1]
+        stdout = stdout.replace('\n', ' ')
+
+        fd.write(stdout)
+
+class ErrorFunction(Function):
+    name = 'error'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        v = self._arguments[0].resolvestr(makefile, variables, setting)
+        raise data.DataError(v, self.loc)
+
+class WarningFunction(Function):
+    name = 'warning'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        v = self._arguments[0].resolvestr(makefile, variables, setting)
+        log.warning(v)
+
+class InfoFunction(Function):
+    name = 'info'
+    minargs = 1
+    maxargs = 1
+
+    def resolve(self, makefile, variables, fd, setting):
+        v = self._arguments[0].resolvestr(makefile, variables, setting)
+        log.info(v)
+
+functionmap = {
+    'subst': SubstFunction,
+    'patsubst': PatSubstFunction,
+    'strip': StripFunction,
+    'findstring': FindstringFunction,
+    'filter': FilterFunction,
+    'filter-out': FilteroutFunction,
+    'sort': SortFunction,
+    'word': WordFunction,
+    'wordlist': WordlistFunction,
+    'words': WordsFunction,
+    'firstword': FirstWordFunction,
+    'lastword': LastWordFunction,
+    'dir': DirFunction,
+    'notdir': NotDirFunction,
+    'suffix': SuffixFunction,
+    'basename': BasenameFunction,
+    'addsuffix': AddSuffixFunction,
+    'addprefix': AddPrefixFunction,
+    'join': JoinFunction,
+    'wildcard': WildcardFunction,
+    'realpath': RealpathFunction,
+    'abspath': AbspathFunction,
+    'if': IfFunction,
+    'or': OrFunction,
+    'and': AndFunction,
+    'foreach': ForEachFunction,
+    'call': CallFunction,
+    'value': ValueFunction,
+    'eval': EvalFunction,
+    'origin': OriginFunction,
+    'flavor': FlavorFunction,
+    'shell': ShellFunction,
+    'error': ErrorFunction,
+    'warning': WarningFunction,
+    'info': InfoFunction,
+}
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/globrelative.py
@@ -0,0 +1,67 @@
+"""
+Filename globbing like the python glob module with minor differences:
+
+* glob relative to an arbitrary directory
+* include . and ..
+* check that link targets exist, not just links
+"""
+
+import os, re, fnmatch
+
+_globcheck = re.compile('[[*?]')
+
+def hasglob(p):
+    return _globcheck.search(p) is not None
+
+def glob(fsdir, path):
+    """
+    Yield paths matching the path glob. Sorts as a bonus. Excludes '.' and '..'
+    """
+
+    dir, leaf = os.path.split(path)
+    if dir == '':
+        return globpattern(fsdir, leaf)
+
+    if hasglob(dir):
+        dirsfound = glob(fsdir, dir)
+    else:
+        dirsfound = [dir]
+
+    r = []
+
+    for dir in dirsfound:
+        fspath = os.path.join(fsdir, dir)
+        if not os.path.isdir(fspath):
+            continue
+
+        r.extend((os.path.join(dir, found) for found in globpattern(fspath, leaf)))
+
+    return r
+
+def globpattern(dir, pattern):
+    """
+    Return leaf names in the specified directory which match the pattern.
+    """
+
+    if not hasglob(pattern):
+        if pattern == '':
+            if os.path.isdir(dir):
+                return ['']
+            return []
+
+        if os.path.exists(os.path.join(dir, pattern)):
+            return [pattern]
+        return []
+
+    leaves = os.listdir(dir) + ['.', '..']
+
+    # "hidden" filenames are a bit special
+    if not pattern.startswith('.'):
+        leaves = [leaf for leaf in leaves
+                  if not leaf.startswith('.')]
+
+    leaves = fnmatch.filter(leaves, pattern)
+    leaves = filter(lambda l: os.path.exists(os.path.join(dir, l)), leaves)
+
+    leaves.sort()
+    return leaves
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/parser.py
@@ -0,0 +1,904 @@
+"""
+Module for parsing Makefile syntax.
+
+Makefiles use a line-based parsing system. Continuations and substitutions are handled differently based on the
+type of line being parsed:
+
+Lines with makefile syntax condense continuations to a single space, no matter the actual trailing whitespace
+of the first line or the leading whitespace of the continuation. In other situations, trailing whitespace is
+relevant.
+
+Lines with command syntax do not condense continuations: the backslash and newline are part of the command.
+(GNU Make is buggy in this regard, at least on mac).
+
+Lines with an initial tab are commands if they can be (there is a rule or a command immediately preceding).
+Otherwise, they are parsed as makefile syntax.
+
+This file parses into the data structures defined in the parserdata module. Those classes are what actually
+do the dirty work of "executing" the parsed data into a data.Makefile.
+
+Four iterator functions are available:
+* iterdata
+* itermakefilechars
+* itercommandchars
+* iterdefinechars
+
+The iterators handle line continuations and comments in different ways, but share a common calling
+convention:
+
+Called with (data, startoffset, tokenlist)
+
+yield 4-tuples (flatstr, token, tokenoffset, afteroffset)
+flatstr is data, guaranteed to have no tokens (may be '')
+token, tokenoffset, afteroffset *may be None*. That means there is more text
+coming.
+"""
+
+import logging, re, os, bisect
+import data, functions, util, parserdata
+
+_log = logging.getLogger('pymake.parser')
+
+class SyntaxError(util.MakeError):
+    pass
+
+class LazyLocation(object):
+    __slots__ = ('data', 'offset', 'loc')
+
+    def __init__(self, data, offset):
+        self.data = data
+        self.offset = offset
+        self.loc = None
+
+    def _resolve(self):
+        if self.loc is None:
+            self.loc = self.data.resolveloc(self.offset)
+
+        return self.loc
+
+    def __add__(self, o):
+        return self._resolve().__add__(o)
+
+    def __str__(self):
+        return self._resolve().__str__()
+    
+class Data(object):
+    """
+    A single virtual "line", which can be multiple source lines joined with
+    continuations.
+    """
+
+    __slots__ = ('data', '_offsets', '_locs')
+
+    def __init__(self):
+        self.data = ""
+
+        # _offsets and _locs are matched lists
+        self._offsets = []
+        self._locs = []
+
+    @staticmethod
+    def fromstring(str, loc):
+        d = Data()
+        d.append(str, loc)
+        return d
+
+    def append(self, data, loc):
+        self._offsets.append(len(self.data))
+        self._locs.append(loc)
+        self.data += data
+
+    def resolveloc(self, offset):
+        """
+        Get the location of an offset within data.
+        """
+        if offset is None or offset >= len(self.data):
+            offset = len(self.data) - 1
+
+        if offset == -1:
+            offset = 0
+
+        idx = bisect.bisect_right(self._offsets, offset) - 1
+        loc = self._locs[idx]
+        begin = self._offsets[idx]
+        return loc + self.data[begin:offset]
+
+    def getloc(self, offset):
+        return LazyLocation(self, offset)
+
+    def skipwhitespace(self, offset):
+        """
+        Return the offset into data after skipping whitespace.
+        """
+        while offset < len(self.data):
+            c = self.data[offset]
+            if not c.isspace():
+                break
+            offset += 1
+        return offset
+
+    def findtoken(self, o, tlist, skipws):
+        """
+        Check data at position o for any of the tokens in tlist followed by whitespace
+        or end-of-data.
+
+        If a token is found, skip trailing whitespace and return (token, newoffset).
+        Otherwise return None, oldoffset
+        """
+        assert isinstance(tlist, TokenList)
+
+        if skipws:
+            m = tlist.wslist.match(self.data, pos=o)
+            if m is not None:
+                return m.group(1), m.end(0)
+        else:
+            m = tlist.simplere.match(self.data, pos=o)
+            if m is not None:
+                return m.group(0), m.end(0)
+
+        return None, o
+
+class DynamicData(Data):
+    """
+    If we're reading from a stream, allows reading additional data dynamically.
+    """
+    def __init__(self, lineiter, path):
+        Data.__init__(self)
+        self.lineiter = lineiter
+        self.path = path
+
+    def readline(self):
+        try:
+            lineno, line = self.lineiter.next()
+            self.append(line, parserdata.Location(self.path, lineno, 0))
+            return True
+        except StopIteration:
+            return False
+
+_makefiletokensescaped = [r'\\\\#', r'\\#', '\\\\\n', '\\\\\\s+\\\\\n', r'\\.', '#', '\n']
+_continuationtokensescaped = ['\\\\\n', r'\\.', '\n']
+
+class TokenList(object):
+    """
+    A list of tokens to search. Because these lists are static, we can perform
+    optimizations (such as escaping and compiling regexes) on construction.
+    """
+
+    __slots__ = ('tlist', 'emptylist', 'escapedlist', 'simplere', 'makefilere', 'continuationre', 'wslist')
+
+    def __init__(self, tlist):
+        self.tlist = tlist
+        self.emptylist = len(tlist) == 0
+        self.escapedlist = [re.escape(t) for t in tlist]
+
+    def __getattr__(self, name):
+        if name == 'simplere':
+            self.simplere = re.compile('|'.join(self.escapedlist))
+            return self.simplere
+
+        if name == 'makefilere':
+            self.makefilere = re.compile('|'.join(self.escapedlist + _makefiletokensescaped))
+            return self.makefilere
+
+        if name == 'continuationre':
+            self.continuationre = re.compile('|'.join(self.escapedlist + _continuationtokensescaped))
+            return self.continuationre
+
+        if name == 'wslist':
+            self.wslist = re.compile('(' + '|'.join(self.escapedlist) + ')' + r'(\s+|$)')
+            return self.wslist
+
+        raise AttributeError(name)
+
+    _imap = {}
+
+    @staticmethod
+    def get(s):
+        i = TokenList._imap.get(s, None)
+        if i is None:
+            i = TokenList(s)
+            TokenList._imap[s] = i
+
+        return i
+
+_emptytokenlist = TokenList.get('')
+
+def iterdata(d, offset, tokenlist):
+    """
+    Iterate over flat data without line continuations, comments, or any special escaped characters.
+
+    Typically used to parse recursively-expanded variables.
+    """
+
+    if tokenlist.emptylist:
+        yield d.data, None, None, None
+        return
+
+    s = tokenlist.simplere
+    datalen = len(d.data)
+
+    while offset < datalen:
+        m = s.search(d.data, pos=offset)
+        if m is None:
+            yield d.data[offset:], None, None, None
+            return
+
+        yield d.data[offset:m.start(0)], m.group(0), m.start(0), m.end(0)
+        offset = m.end(0)
+
+def itermakefilechars(d, offset, tokenlist):
+    """
+    Iterate over data in makefile syntax. Comments are found at unescaped # characters, and escaped newlines
+    are converted to single-space continuations.
+    """
+
+    s = tokenlist.makefilere
+
+    while offset < len(d.data):
+        m = s.search(d.data, pos=offset)
+        if m is None:
+            yield d.data[offset:], None, None, None
+            return
+
+        token = m.group(0)
+        start = m.start(0)
+        end = m.end(0)
+
+        if token == '\n':
+            assert end == len(d.data)
+            yield d.data[offset:start], None, None, None
+            return
+
+        if token == '#':
+            yield d.data[offset:start], None, None, None
+            for s in itermakefilechars(d, end, _emptytokenlist): pass
+            return
+
+        if token == '\\\\#':
+            # see escape-chars.mk VARAWFUL
+            yield d.data[offset:start + 1], None, None, None
+            for s in itermakefilechars(d, end, _emptytokenlist): pass
+            return
+
+        if token == '\\\n':
+            yield d.data[offset:start].rstrip() + ' ', None, None, None
+            d.readline()
+            offset = d.skipwhitespace(end)
+            continue
+
+        if token.startswith('\\') and token.endswith('\n'):
+            assert end == len(d.data)
+            yield d.data[offset:start] + '\\ ', None, None, None
+            d.readline()
+            offset = d.skipwhitespace(end)
+            continue
+
+        if token == '\\#':
+            yield d.data[offset:start] + '#', None, None, None
+        elif token.startswith('\\'):
+            if token[1:] in tokenlist.tlist:
+                yield d.data[offset:start + 1], token[1:], start + 1, end
+            else:
+                yield d.data[offset:end], None, None, None
+        else:
+            yield d.data[offset:start], token, start, end
+
+        offset = end
+
+def itercommandchars(d, offset, tokenlist):
+    """
+    Iterate over command syntax. # comment markers are not special, and escaped newlines are included
+    in the output text.
+    """
+
+    s = tokenlist.continuationre
+
+    while offset < len(d.data):
+        m = s.search(d.data, pos=offset)
+        if m is None:
+            yield d.data[offset:], None, None, None
+            return
+
+        token = m.group(0)
+        start = m.start(0)
+        end = m.end(0)
+
+        if token == '\n':
+            assert end == len(d.data)
+            yield d.data[offset:start], None, None, None
+            return
+
+        if token == '\\\n':
+            yield d.data[offset:end], None, None, None
+            d.readline()
+            offset = end
+            if offset < len(d.data) and d.data[offset] == '\t':
+                offset += 1
+            continue
+        
+        if token.startswith('\\'):
+            if token[1:] in tokenlist.tlist:
+                yield d.data[offset:start + 1], token[1:], start + 1, end
+            else:
+                yield d.data[offset:end], None, None, None
+        else:
+            yield d.data[offset:start], token, start, end
+
+        offset = end
+
+_definestokenlist = TokenList.get(('define', 'endef'))
+
+def iterdefinechars(d, offset, tokenlist):
+    """
+    Iterate over define blocks. Most characters are included literally. Escaped newlines are treated
+    as they would be in makefile syntax. Internal define/endef pairs are ignored.
+    """
+
+    def checkfortoken(o):
+        """
+        Check for a define or endef token on the line starting at o.
+        Return an integer for the direction of definecount.
+        """
+        if o >= len(d.data):
+            return 0
+
+        if d.data[o] == '\t':
+            return 0
+
+        o = d.skipwhitespace(o)
+        token, o = d.findtoken(o, _definestokenlist, True)
+        if token == 'define':
+            return 1
+
+        if token == 'endef':
+            return -1
+        
+        return 0
+
+    startoffset = offset
+    definecount = 1 + checkfortoken(offset)
+    if definecount == 0:
+        return
+
+    s = tokenlist.continuationre
+
+    while offset < len(d.data):
+        m = s.search(d.data, pos=offset)
+        if m is None:
+            yield d.data[offset:], None, None, None
+            break
+
+        token = m.group(0)
+        start = m.start(0)
+        end = m.end(0)
+
+        if token == '\\\n':
+            yield d.data[offset:start].rstrip() + ' ', None, None, None
+            d.readline()
+            offset = d.skipwhitespace(end)
+            continue
+
+        if token == '\n':
+            assert end == len(d.data)
+            d.readline()
+            definecount += checkfortoken(end)
+            if definecount == 0:
+                yield d.data[offset:start], None, None, None
+                return
+
+            yield d.data[offset:end], None, None, None
+        elif token.startswith('\\'):
+            if token[1:] in tokenlist.tlist:
+                yield d.data[offset:start + 1], token[1:], start + 1, end
+            else:
+                yield d.data[offset:end], None, None, None
+        else:
+            yield d.data[offset:start], token, start, end
+
+        offset = end
+
+    # Unlike the other iterators, if you fall off this one there is an unterminated
+    # define.
+    raise SyntaxError("Unterminated define", d.getloc(startoffset))
+
+def _iterflatten(iter, data, offset):
+    return ''.join((str for str, t, o, oo in iter(data, offset, _emptytokenlist)))
+
+def _ensureend(d, offset, msg, ifunc=itermakefilechars):
+    """
+    Ensure that only whitespace remains in this data.
+    """
+
+    for c, t, o, oo in ifunc(d, offset, _emptytokenlist):
+        if c != '' and not c.isspace():
+            raise SyntaxError(msg, d.getloc(o))
+
+_eqargstokenlist = TokenList.get(('(', "'", '"'))
+
+def ifeq(d, offset):
+    # the variety of formats for this directive is rather maddening
+    token, offset = d.findtoken(offset, _eqargstokenlist, False)
+    if token is None:
+        raise SyntaxError("No arguments after conditional", d.getloc(offset))
+
+    if token == '(':
+        arg1, t, offset = parsemakesyntax(d, offset, (',',), itermakefilechars)
+        if t is None:
+            raise SyntaxError("Expected two arguments in conditional", d.getloc(offset))
+
+        arg1.rstrip()
+
+        offset = d.skipwhitespace(offset)
+        arg2, t, offset = parsemakesyntax(d, offset, (')',), itermakefilechars)
+        if t is None:
+            raise SyntaxError("Unexpected text in conditional", d.getloc(offset))
+
+        _ensureend(d, offset, "Unexpected text after conditional")
+    else:
+        arg1, t, offset = parsemakesyntax(d, offset, (token,), itermakefilechars)
+        if t is None:
+            raise SyntaxError("Unexpected text in conditional", d.getloc(offset))
+
+        offset = d.skipwhitespace(offset)
+        if offset == len(d.data):
+            raise SyntaxError("Expected two arguments in conditional", d.getloc(offset))
+
+        token = d.data[offset]
+        if token not in '\'"':
+            raise SyntaxError("Unexpected text in conditional", d.getloc(offset))
+
+        arg2, t, offset = parsemakesyntax(d, offset + 1, (token,), itermakefilechars)
+
+        _ensureend(d, offset, "Unexpected text after conditional")
+
+    return parserdata.EqCondition(arg1, arg2)
+
+def ifneq(d, offset):
+    c = ifeq(d, offset)
+    c.expected = False
+    return c
+
+def ifdef(d, offset):
+    e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+    e.rstrip()
+
+    return parserdata.IfdefCondition(e)
+
+def ifndef(d, offset):
+    c = ifdef(d, offset)
+    c.expected = False
+    return c
+
+_conditionkeywords = {
+    'ifeq': ifeq,
+    'ifneq': ifneq,
+    'ifdef': ifdef,
+    'ifndef': ifndef
+    }
+
+_conditiontokens = tuple(_conditionkeywords.iterkeys())
+_directivestokenlist = TokenList.get(_conditiontokens + \
+    ('else', 'endif', 'define', 'endef', 'override', 'include', '-include', 'vpath', 'export', 'unexport'))
+_conditionkeywordstokenlist = TokenList.get(_conditiontokens)
+
+_varsettokens = (':=', '+=', '?=', '=')
+
+_parsecache = {} # realpath -> (mtime, Statements)
+
+def parsefile(pathname):
+    """
+    Parse a filename into a parserdata.StatementList. A cache is used to avoid re-parsing
+    makefiles that have already been parsed and have not changed.
+    """
+
+    pathname = os.path.realpath(pathname)
+
+    mtime = os.path.getmtime(pathname)
+
+    if pathname in _parsecache:
+        oldmtime, stmts = _parsecache[pathname]
+
+        if mtime == oldmtime:
+            _log.debug("Using '%s' from the parser cache.", pathname)
+            return stmts
+
+        _log.debug("Not using '%s' from the parser cache, mtimes don't match: was %s, now %s", pathname, oldmtime, mtime)
+
+    stmts = parsestream(open(pathname, "rU"), pathname)
+    _parsecache[pathname] = mtime, stmts
+    return stmts
+
+def parsestream(fd, filename):
+    """
+    Parse a stream of makefile into a parserdata.StatementList. To parse a file system file, use
+    parsefile instead of this method.
+
+    @param fd A file-like object containing the makefile data.
+    """
+
+    currule = False
+    condstack = [parserdata.StatementList()]
+
+    fdlines = enumerate(fd)
+
+    while True:
+        assert len(condstack) > 0
+
+        d = DynamicData(fdlines, filename)
+        if not d.readline():
+            break
+
+        if len(d.data) > 0 and d.data[0] == '\t' and currule:
+            e, t, o = parsemakesyntax(d, 1, (), itercommandchars)
+            assert t == None
+            condstack[-1].append(parserdata.Command(e))
+        else:
+            # To parse Makefile syntax, we first strip leading whitespace and
+            # look for initial keywords. If there are no keywords, it's either
+            # setting a variable or writing a rule.
+
+            offset = d.skipwhitespace(0)
+
+            kword, offset = d.findtoken(offset, _directivestokenlist, True)
+            if kword == 'endif':
+                _ensureend(d, offset, "Unexpected data after 'endif' directive")
+                if len(condstack) == 1:
+                    raise SyntaxError("unmatched 'endif' directive",
+                                      d.getloc(offset))
+
+                condstack.pop()
+                continue
+            
+            if kword == 'else':
+                if len(condstack) == 1:
+                    raise SyntaxError("unmatched 'else' directive",
+                                      d.getloc(offset))
+
+                kword, offset = d.findtoken(offset, _conditionkeywordstokenlist, True)
+                if kword is None:
+                    _ensureend(d, offset, "Unexpected data after 'else' directive.")
+                    condstack[-1].addcondition(d.getloc(offset), parserdata.ElseCondition())
+                else:
+                    if kword not in _conditionkeywords:
+                        raise SyntaxError("Unexpected condition after 'else' directive.",
+                                          d.getloc(offset))
+
+                    c = _conditionkeywords[kword](d, offset)
+                    condstack[-1].addcondition(d.getloc(offset), c)
+                continue
+
+            if kword in _conditionkeywords:
+                c = _conditionkeywords[kword](d, offset)
+                cb = parserdata.ConditionBlock(d.getloc(0), c)
+                condstack[-1].append(cb)
+                condstack.append(cb)
+                continue
+
+            if kword == 'endef':
+                raise SyntaxError("Unmatched endef", d.getloc(offset))
+
+            if kword == 'define':
+                currule = False
+                vname, t, i = parsemakesyntax(d, offset, (), itermakefilechars)
+                vname.rstrip()
+
+                d = DynamicData(fdlines, filename)
+                d.readline()
+
+                value = _iterflatten(iterdefinechars, d, 0)
+                condstack[-1].append(parserdata.SetVariable(vname, value=value, valueloc=d.getloc(0), token='=', targetexp=None))
+                continue
+
+            if kword in ('include', '-include'):
+                currule = False
+                incfile, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+                condstack[-1].append(parserdata.Include(incfile, kword == 'include'))
+
+                continue
+
+            if kword == 'vpath':
+                currule = False
+                e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+                condstack[-1].append(parserdata.VPathDirective(e))
+                continue
+
+            if kword == 'override':
+                currule = False
+                vname, token, offset = parsemakesyntax(d, offset, _varsettokens, itermakefilechars)
+                vname.lstrip()
+                vname.rstrip()
+
+                if token is None:
+                    raise SyntaxError("Malformed override directive, need =", d.getloc(offset))
+
+                value = _iterflatten(itermakefilechars, d, offset).lstrip()
+
+                condstack[-1].append(parserdata.SetVariable(vname, value=value, valueloc=d.getloc(offset), token=token, targetexp=None, source=data.Variables.SOURCE_OVERRIDE))
+                continue
+
+            if kword == 'export':
+                currule = False
+                e, token, offset = parsemakesyntax(d, offset, _varsettokens, itermakefilechars)
+                e.lstrip()
+                e.rstrip()
+
+                if token is None:
+                    condstack[-1].append(parserdata.ExportDirective(e, single=False))
+                else:
+                    condstack[-1].append(parserdata.ExportDirective(e, single=True))
+
+                    value = _iterflatten(itermakefilechars, d, offset).lstrip()
+                    condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=None))
+
+                continue
+
+            if kword == 'unexport':
+                raise SyntaxError("unexporting variables is not supported", d.getloc(offset))
+
+            assert kword is None, "unexpected kword: %r" % (kword,)
+
+            e, token, offset = parsemakesyntax(d, offset, _varsettokens + ('::', ':'), itermakefilechars)
+            if token is None:
+                e.rstrip()
+                e.lstrip()
+                if not e.isempty():
+                    condstack[-1].append(parserdata.EmptyDirective(e))
+                continue
+
+            # if we encountered real makefile syntax, the current rule is over
+            currule = False
+
+            if token in _varsettokens:
+                e.lstrip()
+                e.rstrip()
+
+                value = _iterflatten(itermakefilechars, d, offset).lstrip()
+
+                condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=None))
+            else:
+                doublecolon = token == '::'
+
+                # `e` is targets or target patterns, which can end up as
+                # * a rule
+                # * an implicit rule
+                # * a static pattern rule
+                # * a target-specific variable definition
+                # * a pattern-specific variable definition
+                # any of the rules may have order-only prerequisites
+                # delimited by |, and a command delimited by ;
+                targets = e
+
+                e, token, offset = parsemakesyntax(d, offset,
+                                                   _varsettokens + (':', '|', ';'),
+                                                   itermakefilechars)
+                if token in (None, ';'):
+                    condstack[-1].append(parserdata.Rule(targets, e, doublecolon))
+                    currule = True
+
+                    if token == ';':
+                        offset = d.skipwhitespace(offset)
+                        e, t, offset = parsemakesyntax(d, offset, (), itercommandchars)
+                        condstack[-1].append(parserdata.Command(e))
+
+                elif token in _varsettokens:
+                    e.lstrip()
+                    e.rstrip()
+
+                    value = _iterflatten(itermakefilechars, d, offset).lstrip()
+                    condstack[-1].append(parserdata.SetVariable(e, value=value, valueloc=d.getloc(offset), token=token, targetexp=targets))
+                elif token == '|':
+                    raise SyntaxError('order-only prerequisites not implemented', d.getloc(offset))
+                else:
+                    assert token == ':'
+                    # static pattern rule
+
+                    pattern = e
+
+                    deps, token, offset = parsemakesyntax(d, offset, (';',), itermakefilechars)
+
+                    condstack[-1].append(parserdata.StaticPatternRule(targets, pattern, deps, doublecolon))
+                    currule = True
+
+                    if token == ';':
+                        offset = d.skipwhitespace(offset)
+                        e, token, offset = parsemakesyntax(d, offset, (), itercommandchars)
+                        condstack[-1].append(parserdata.Command(e))
+
+    if len(condstack) != 1:
+        raise SyntaxError("Condition never terminated with endif", condstack[-1].loc)
+
+    return condstack[0]
+
+_PARSESTATE_TOPLEVEL = 0    # at the top level
+_PARSESTATE_FUNCTION = 1    # expanding a function call
+_PARSESTATE_VARNAME = 2     # expanding a variable expansion.
+_PARSESTATE_SUBSTFROM = 3   # expanding a variable expansion substitution "from" value
+_PARSESTATE_SUBSTTO = 4     # expanding a variable expansion substitution "to" value
+_PARSESTATE_PARENMATCH = 5  # inside nested parentheses/braces that must be matched
+
+class ParseStackFrame(object):
+    __slots__ = ('parsestate', 'parent', 'expansion', 'tokenlist', 'openbrace', 'closebrace', 'function', 'loc', 'varname', 'substfrom')
+
+    def __init__(self, parsestate, parent, expansion, tokenlist, openbrace, closebrace, function=None, loc=None):
+        self.parsestate = parsestate
+        self.parent = parent
+        self.expansion = expansion
+        self.tokenlist = tokenlist
+        self.openbrace = openbrace
+        self.closebrace = closebrace
+        self.function = function
+        self.loc = loc
+
+_functiontokenlist = None
+
+_matchingbrace = {
+    '(': ')',
+    '{': '}',
+    }
+
+def parsemakesyntax(d, startat, stopon, iterfunc):
+    """
+    Given Data, parse it into a data.Expansion.
+
+    @param stopon (sequence)
+        Indicate characters where toplevel parsing should stop.
+
+    @param iterfunc (generator function)
+        A function which is used to iterate over d, yielding (char, offset, loc)
+        @see iterdata
+        @see itermakefilechars
+        @see itercommandchars
+ 
+    @return a tuple (expansion, token, offset). If all the data is consumed,
+    token and offset will be None
+    """
+
+    # print "parsemakesyntax(%r)" % d.data
+
+    global _functiontokenlist
+    if _functiontokenlist is None:
+        functiontokens = list(functions.functionmap.iterkeys())
+        functiontokens.sort(key=len, reverse=True)
+        _functiontokenlist = TokenList.get(tuple(functiontokens))
+
+    assert callable(iterfunc)
+
+    stacktop = ParseStackFrame(_PARSESTATE_TOPLEVEL, None, data.Expansion(loc=d.getloc(startat)),
+                               tokenlist=TokenList.get(stopon + ('$',)),
+                               openbrace=None, closebrace=None)
+
+    di = iterfunc(d, startat, stacktop.tokenlist)
+    while True: # this is not a for loop because `di` changes during the function
+        assert stacktop is not None
+        try:
+            s, token, tokenoffset, offset = di.next()
+        except StopIteration:
+            break
+
+        stacktop.expansion.appendstr(s)
+        if token is None:
+            continue
+
+        parsestate = stacktop.parsestate
+
+        if token == '$':
+            if len(d.data) == offset:
+                # an unterminated $ expands to nothing
+                break
+
+            loc = d.getloc(tokenoffset)
+
+            c = d.data[offset]
+            if c == '$':
+                stacktop.expansion.appendstr('$')
+                offset = offset + 1
+            elif c in ('(', '{'):
+                closebrace = _matchingbrace[c]
+
+                # look forward for a function name
+                fname, offset = d.findtoken(offset + 1, _functiontokenlist, True)
+                if fname is not None:
+                    fn = functions.functionmap[fname](loc)
+                    e = data.Expansion()
+                    if len(fn) + 1 == fn.maxargs:
+                        tokenlist = TokenList.get((c, closebrace, '$'))
+                    else:
+                        tokenlist = TokenList.get((',', c, closebrace, '$'))
+
+                    stacktop = ParseStackFrame(_PARSESTATE_FUNCTION, stacktop,
+                                               e, tokenlist, function=fn,
+                                               openbrace=c, closebrace=closebrace)
+                    di = iterfunc(d, offset, tokenlist)
+                    continue
+
+                e = data.Expansion()
+                tokenlist = TokenList.get((':', c, closebrace, '$'))
+                stacktop = ParseStackFrame(_PARSESTATE_VARNAME, stacktop,
+                                           e, tokenlist,
+                                           openbrace=c, closebrace=closebrace, loc=loc)
+                di = iterfunc(d, offset, tokenlist)
+                continue
+            else:
+                e = data.Expansion.fromstring(c)
+                stacktop.expansion.appendfunc(functions.VariableRef(loc, e))
+                offset += 1
+        elif token in ('(', '{'):
+            assert token == stacktop.openbrace
+
+            stacktop.expansion.appendstr(token)
+            stacktop = ParseStackFrame(_PARSESTATE_PARENMATCH, stacktop,
+                                       stacktop.expansion,
+                                       TokenList.get((token, stacktop.closebrace,)),
+                                       openbrace=token, closebrace=stacktop.closebrace, loc=d.getloc(tokenoffset))
+        elif parsestate == _PARSESTATE_PARENMATCH:
+            assert token == stacktop.closebrace
+            stacktop.expansion.appendstr(token)
+            stacktop = stacktop.parent
+        elif parsestate == _PARSESTATE_TOPLEVEL:
+            assert stacktop.parent is None
+            return stacktop.expansion.finish(), token, offset
+        elif parsestate == _PARSESTATE_FUNCTION:
+            if token == ',':
+                stacktop.function.append(stacktop.expansion.finish())
+
+                stacktop.expansion = data.Expansion()
+                if len(stacktop.function) + 1 == stacktop.function.maxargs:
+                    tokenlist = TokenList.get((stacktop.openbrace, stacktop.closebrace, '$'))
+                    stacktop.tokenlist = tokenlist
+            elif token in (')', '}'):
+                fn = stacktop.function
+                fn.append(stacktop.expansion.finish())
+                fn.setup()
+                
+                stacktop = stacktop.parent
+                stacktop.expansion.appendfunc(fn)
+            else:
+                assert False, "Not reached, _PARSESTATE_FUNCTION"
+        elif parsestate == _PARSESTATE_VARNAME:
+            if token == ':':
+                stacktop.varname = stacktop.expansion
+                stacktop.parsestate = _PARSESTATE_SUBSTFROM
+                stacktop.expansion = data.Expansion()
+                stacktop.tokenlist = TokenList.get(('=', stacktop.openbrace, stacktop.closebrace, '$'))
+            elif token in (')', '}'):
+                fn = functions.VariableRef(stacktop.loc, stacktop.expansion.finish())
+                stacktop = stacktop.parent
+                stacktop.expansion.appendfunc(fn)
+            else:
+                assert False, "Not reached, _PARSESTATE_VARNAME"
+        elif parsestate == _PARSESTATE_SUBSTFROM:
+            if token == '=':
+                stacktop.substfrom = stacktop.expansion
+                stacktop.parsestate = _PARSESTATE_SUBSTTO
+                stacktop.expansion = data.Expansion()
+                stacktop.tokenlist = TokenList.get((stacktop.openbrace, stacktop.closebrace, '$'))
+            elif token in (')', '}'):
+                # A substitution of the form $(VARNAME:.ee) is probably a mistake, but make
+                # parses it. Issue a warning. Combine the varname and substfrom expansions to
+                # make the compatible varname. See tests/var-substitutions.mk SIMPLE3SUBSTNAME
+                _log.warning("%s: Variable reference looks like substitution without =", stacktop.loc)
+                stacktop.varname.appendstr(':')
+                stacktop.varname.concat(stacktop.expansion)
+                fn = functions.VariableRef(stacktop.loc, stacktop.varname.finish())
+                stacktop = stacktop.parent
+                stacktop.expansion.appendfunc(fn)
+            else:
+                assert False, "Not reached, _PARSESTATE_SUBSTFROM"
+        elif parsestate == _PARSESTATE_SUBSTTO:
+            assert token in  (')','}'), "Not reached, _PARSESTATE_SUBSTTO"
+
+            fn = functions.SubstitutionRef(stacktop.loc, stacktop.varname.finish(),
+                                           stacktop.substfrom.finish(), stacktop.expansion.finish())
+            stacktop = stacktop.parent
+            stacktop.expansion.appendfunc(fn)
+        else:
+            assert False, "Unexpected parse state %s" % stacktop.parsestate
+
+        di = iterfunc(d, offset, stacktop.tokenlist)
+
+    if stacktop.parent is not None:
+        raise SyntaxError("Unterminated function call", d.getloc(offset))
+
+    assert stacktop.parsestate == _PARSESTATE_TOPLEVEL
+
+    return stacktop.expansion.finish(), None, None
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/parserdata.py
@@ -0,0 +1,444 @@
+import logging, re, os
+import data, functions, util, parser
+from cStringIO import StringIO
+from pymake.globrelative import hasglob, glob
+
+_log = logging.getLogger('pymake.data')
+_tabwidth = 4
+
+class Location(object):
+    """
+    A location within a makefile.
+
+    For the moment, locations are just path/line/column, but in the future
+    they may reference parent locations for more accurate "included from"
+    or "evaled at" error reporting.
+    """
+    __slots__ = ('path', 'line', 'column')
+
+    def __init__(self, path, line, column):
+        self.path = path
+        self.line = line
+        self.column = column
+
+    def __add__(self, data):
+        """
+        Returns a new location on the same line offset by
+        the specified string.
+        """
+        column = self.column
+        i = 0
+        while True:
+            j = data.find('\t', i)
+            if j == -1:
+                column += len(data) - i
+                break
+
+            column += j - i
+            column += _tabwidth
+            column -= column % _tabwidth
+            i = j + 1
+
+        if column == self.column:
+            return self
+        return Location(self.path, self.line, column)
+
+    def __str__(self):
+        return "%s:%s:%s" % (self.path, self.line, self.column)
+
+def _expandwildcards(makefile, tlist):
+    for t in tlist:
+        if not hasglob(t):
+            yield t
+        else:
+            l = glob(makefile.workdir, t)
+            for r in l:
+                yield r
+
+def parsecommandlineargs(args):
+    """
+    Given a set of arguments from a command-line invocation of make,
+    parse out the variable definitions and return (stmts, arglist)
+    """
+
+    stmts = StatementList()
+    r = []
+    for i in xrange(0, len(args)):
+        a = args[i]
+
+        vname, t, val = util.strpartition(a, ':=')
+        if t == '':
+            vname, t, val = util.strpartition(a, '=')
+        if t != '':
+            stmts.append(Override(a))
+
+            vname = vname.strip()
+            vnameexp = data.Expansion.fromstring(vname)
+
+            stmts.append(SetVariable(vnameexp, token=t,
+                                     value=val, valueloc=Location('<command-line>', i, len(vname) + len(t)),
+                                     targetexp=None, source=data.Variables.SOURCE_COMMANDLINE))
+        else:
+            r.append(a)
+
+    return stmts, r
+
+class Statement(object):
+    """
+    A statement is an abstract object representing a single "chunk" of makefile syntax. Subclasses
+    must implement the following method:
+
+    def execute(self, makefile, context)
+    """
+
+class Override(Statement):
+    def __init__(self, s):
+        self.s = s
+
+    def execute(self, makefile, context):
+        makefile.overrides.append(self.s)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "Override: %r" % (self.s,)
+
+class DummyRule(object):
+    def addcommand(self, r):
+        pass
+
+class Rule(Statement):
+    def __init__(self, targetexp, depexp, doublecolon):
+        assert isinstance(targetexp, (data.Expansion, data.StringExpansion))
+        assert isinstance(depexp, (data.Expansion, data.StringExpansion))
+        
+        self.targetexp = targetexp
+        self.depexp = depexp
+        self.doublecolon = doublecolon
+
+    def execute(self, makefile, context):
+        atargets = data.stripdotslashes(self.targetexp.resolvesplit(makefile, makefile.variables))
+        targets = [data.Pattern(p) for p in _expandwildcards(makefile, atargets)]
+
+        if not len(targets):
+            context.currule = DummyRule()
+            return
+
+        ispatterns = set((t.ispattern() for t in targets))
+        if len(ispatterns) == 2:
+            raise data.DataError("Mixed implicit and normal rule", self.targetexp.loc)
+        ispattern, = ispatterns
+
+        deps = [p for p in _expandwildcards(makefile, data.stripdotslashes(self.depexp.resolvesplit(makefile, makefile.variables)))]
+        if ispattern:
+            rule = data.PatternRule(targets, map(data.Pattern, deps), self.doublecolon, loc=self.targetexp.loc)
+            makefile.appendimplicitrule(rule)
+        else:
+            rule = data.Rule(deps, self.doublecolon, loc=self.targetexp.loc)
+            for t in targets:
+                makefile.gettarget(t.gettarget()).addrule(rule)
+            makefile.foundtarget(targets[0].gettarget())
+
+        context.currule = rule
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "Rule %s: %s" % (self.targetexp, self.depexp)
+
+class StaticPatternRule(Statement):
+    def __init__(self, targetexp, patternexp, depexp, doublecolon):
+        assert isinstance(targetexp, (data.Expansion, data.StringExpansion))
+        assert isinstance(patternexp, (data.Expansion, data.StringExpansion))
+        assert isinstance(depexp, (data.Expansion, data.StringExpansion))
+
+        self.targetexp = targetexp
+        self.patternexp = patternexp
+        self.depexp = depexp
+        self.doublecolon = doublecolon
+
+    def execute(self, makefile, context):
+        targets = list(_expandwildcards(makefile, data.stripdotslashes(self.targetexp.resolvesplit(makefile, makefile.variables))))
+
+        if not len(targets):
+            context.currule = DummyRule()
+            return
+
+        patterns = list(data.stripdotslashes(self.patternexp.resolvesplit(makefile, makefile.variables)))
+        if len(patterns) != 1:
+            raise data.DataError("Static pattern rules must have a single pattern", self.patternexp.loc)
+        pattern = data.Pattern(patterns[0])
+
+        deps = [data.Pattern(p) for p in _expandwildcards(makefile, data.stripdotslashes(self.depexp.resolvesplit(makefile, makefile.variables)))]
+
+        rule = data.PatternRule([pattern], deps, self.doublecolon, loc=self.targetexp.loc)
+
+        for t in targets:
+            if data.Pattern(t).ispattern():
+                raise data.DataError("Target '%s' of a static pattern rule must not be a pattern" % (t,), self.targetexp.loc)
+            stem = pattern.match(t)
+            if stem is None:
+                raise data.DataError("Target '%s' does not match the static pattern '%s'" % (t, pattern), self.targetexp.loc)
+            makefile.gettarget(t).addrule(data.PatternRuleInstance(rule, '', stem, pattern.ismatchany()))
+
+        makefile.foundtarget(targets[0])
+        context.currule = rule
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "StaticPatternRule %r: %r: %r" % (self.targetexp, self.patternexp, self.depexp)
+
+class Command(Statement):
+    def __init__(self, exp):
+        assert isinstance(exp, (data.Expansion, data.StringExpansion))
+        self.exp = exp
+
+    def execute(self, makefile, context):
+        assert context.currule is not None
+        context.currule.addcommand(self.exp)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "Command %r" % (self.exp,)
+
+class SetVariable(Statement):
+    def __init__(self, vnameexp, token, value, valueloc, targetexp, source=None):
+        assert isinstance(vnameexp, (data.Expansion, data.StringExpansion))
+        assert isinstance(value, str)
+        assert targetexp is None or isinstance(targetexp, (data.Expansion, data.StringExpansion))
+
+        if source is None:
+            source = data.Variables.SOURCE_MAKEFILE
+
+        self.vnameexp = vnameexp
+        self.token = token
+        self.value = value
+        self.valueloc = valueloc
+        self.targetexp = targetexp
+        self.source = source
+
+    def execute(self, makefile, context):
+        vname = self.vnameexp.resolvestr(makefile, makefile.variables)
+        if len(vname) == 0:
+            raise data.DataError("Empty variable name", self.vnameexp.loc)
+
+        if self.targetexp is None:
+            setvariables = [makefile.variables]
+        else:
+            setvariables = []
+
+            targets = [data.Pattern(t) for t in data.stripdotslashes(self.targetexp.resolvesplit(makefile, makefile.variables))]
+            for t in targets:
+                if t.ispattern():
+                    setvariables.append(makefile.getpatternvariables(t))
+                else:
+                    setvariables.append(makefile.gettarget(t.gettarget()).variables)
+
+        for v in setvariables:
+            if self.token == '+=':
+                v.append(vname, self.source, self.value, makefile.variables, makefile)
+                continue
+
+            if self.token == '?=':
+                flavor = data.Variables.FLAVOR_RECURSIVE
+                oldflavor, oldsource, oldval = v.get(vname, expand=False)
+                if oldval is not None:
+                    continue
+                value = self.value
+            elif self.token == '=':
+                flavor = data.Variables.FLAVOR_RECURSIVE
+                value = self.value
+            else:
+                assert self.token == ':='
+
+                flavor = data.Variables.FLAVOR_SIMPLE
+                d = parser.Data.fromstring(self.value, self.valueloc)
+                e, t, o = parser.parsemakesyntax(d, 0, (), parser.iterdata)
+                value = e.resolvestr(makefile, makefile.variables)
+
+            v.set(vname, flavor, self.source, value)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "SetVariable %r value=%r" % (self.vnameexp, self.value)
+
+class Condition(object):
+    """
+    An abstract "condition", either ifeq or ifdef, perhaps negated. Subclasses must implement:
+
+    def evaluate(self, makefile)
+    """
+
+class EqCondition(Condition):
+    expected = True
+
+    def __init__(self, exp1, exp2):
+        assert isinstance(exp1, (data.Expansion, data.StringExpansion))
+        assert isinstance(exp2, (data.Expansion, data.StringExpansion))
+
+        self.exp1 = exp1
+        self.exp2 = exp2
+
+    def evaluate(self, makefile):
+        r1 = self.exp1.resolvestr(makefile, makefile.variables)
+        r2 = self.exp2.resolvestr(makefile, makefile.variables)
+        return (r1 == r2) == self.expected
+
+    def __str__(self):
+        return "ifeq (expected=%s) %r %r" % (self.expected, self.exp1, self.exp2)
+
+class IfdefCondition(Condition):
+    expected = True
+
+    def __init__(self, exp):
+        assert isinstance(exp, (data.Expansion, data.StringExpansion))
+        self.exp = exp
+
+    def evaluate(self, makefile):
+        vname = self.exp.resolvestr(makefile, makefile.variables)
+        flavor, source, value = makefile.variables.get(vname, expand=False)
+
+        if value is None:
+            return not self.expected
+
+        return (len(value) > 0) == self.expected
+
+    def __str__(self):
+        return "ifdef (expected=%s) %r" % (self.expected, self.exp)
+
+class ElseCondition(Condition):
+    def evaluate(self, makefile):
+        return True
+
+    def __str__(self):
+        return "else"
+
+class ConditionBlock(Statement):
+    """
+    A list of conditions: each condition has an associated list of statements.
+    """
+    def __init__(self, loc, condition):
+        self.loc = loc
+        self._groups = []
+        self.addcondition(loc, condition)
+
+    def getloc(self):
+        return self._groups[0][0].loc
+
+    def addcondition(self, loc, condition):
+        assert isinstance(condition, Condition)
+
+        if len(self._groups) and isinstance(self._groups[-1][0], ElseCondition):
+            raise parser.SyntaxError("Multiple else conditions for block starting at %s" % self.loc, loc)
+
+        self._groups.append((condition, StatementList()))
+
+    def append(self, statement):
+        self._groups[-1][1].append(statement)
+
+    def execute(self, makefile, context):
+        i = 0
+        for c, statements in self._groups:
+            if c.evaluate(makefile):
+                _log.debug("Condition at %s met by clause #%i", self.loc, i)
+                statements.execute(makefile, context)
+                return
+
+            i += 1
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "ConditionBlock"
+
+        indent1 = indent + ' '
+        indent2 = indent + '  '
+        for c, statements in self._groups:
+            print >>fd, indent1, "Condition %s" % (c,)
+            for s in statements:
+                s.dump(fd, indent2)
+        print >>fd, indent, "~ConditionBlock"
+
+class Include(Statement):
+    def __init__(self, exp, required):
+        assert isinstance(exp, (data.Expansion, data.StringExpansion))
+        self.exp = exp
+        self.required = required
+
+    def execute(self, makefile, context):
+        files = self.exp.resolvesplit(makefile, makefile.variables)
+        for f in files:
+            makefile.include(f, self.required, loc=self.exp.loc)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "Include %r" % (self.exp,)
+
+class VPathDirective(Statement):
+    def __init__(self, exp):
+        assert isinstance(exp, (data.Expansion, data.StringExpansion))
+        self.exp = exp
+
+    def execute(self, makefile, context):
+        words = list(data.stripdotslashes(self.exp.resolvesplit(makefile, makefile.variables)))
+        if len(words) == 0:
+            makefile.clearallvpaths()
+        else:
+            pattern = data.Pattern(words[0])
+            mpaths = words[1:]
+
+            if len(mpaths) == 0:
+                makefile.clearvpath(pattern)
+            else:
+                dirs = []
+                for mpath in mpaths:
+                    dirs.extend((dir for dir in mpath.split(os.pathsep)
+                                 if dir != ''))
+                if len(dirs):
+                    makefile.addvpath(pattern, dirs)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "VPath %r" % (self.exp,)
+
+class ExportDirective(Statement):
+    def __init__(self, exp, single):
+        assert isinstance(exp, (data.Expansion, data.StringExpansion))
+        self.exp = exp
+        self.single = single
+
+    def execute(self, makefile, context):
+        if self.single:
+            vlist = [self.exp.resolvestr(makefile, makefile.variables)]
+        else:
+            vlist = list(self.exp.resolvesplit(makefile, makefile.variables))
+            if not len(vlist):
+                raise data.DataError("Exporting all variables is not supported", self.exp.loc)
+
+        for v in vlist:
+            makefile.exportedvars.add(v)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "Export (single=%s) %r" % (self.single, self.exp)
+
+class EmptyDirective(Statement):
+    def __init__(self, exp):
+        assert isinstance(exp, (data.Expansion, data.StringExpansion))
+        self.exp = exp
+
+    def execute(self, makefile, context):
+        v = self.exp.resolvestr(makefile, makefile.variables)
+        if v.strip() != '':
+            raise data.DataError("Line expands to non-empty value", self.exp.loc)
+
+    def dump(self, fd, indent):
+        print >>fd, indent, "EmptyDirective: %r" % self.exp
+
+class StatementList(list):
+    def append(self, statement):
+        assert isinstance(statement, Statement)
+        list.append(self, statement)
+
+    def execute(self, makefile, context=None):
+        if context is None:
+            context = util.makeobject('currule')
+
+        for s in self:
+            s.execute(makefile, context)
+
+    def __str__(self):
+        fd = StringIO()
+        print >>fd, "StatementList"
+        for s in self:
+            s.dump(fd, ' ')
+        print >>fd, "~StatementList"
+        return fd.getvalue()
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/process.py
@@ -0,0 +1,212 @@
+"""
+Skipping shell invocations is good, when possible. This wrapper around subprocess does dirty work of
+parsing command lines into argv and making sure that no shell magic is being used.
+"""
+
+import subprocess, shlex, re, logging, sys, traceback, os
+import command, util
+if sys.platform=='win32':
+    import win32process
+
+_log = logging.getLogger('pymake.process')
+
+_blacklist = re.compile(r'[$><;*?[{~`|&]|\\\n')
+def clinetoargv(cline):
+    """
+    If this command line can safely skip the shell, return an argv array.
+    @returns argv, badchar
+    """
+
+    m = _blacklist.search(cline)
+    if m is not None:
+        return None, m.group(0)
+
+    args = shlex.split(cline, comments=True)
+
+    if len(args) and args[0].find('=') != -1:
+        return None, '='
+
+    return args, None
+
+shellwords = (':', '.', 'break', 'cd', 'continue', 'exec', 'exit', 'export',
+              'getopts', 'hash', 'pwd', 'readonly', 'return', 'shift', 
+              'test', 'times', 'trap', 'umask', 'unset', 'alias',
+              'set', 'bind', 'builtin', 'caller', 'command', 'declare',
+              'echo', 'enable', 'help', 'let', 'local', 'logout', 
+              'printf', 'read', 'shopt', 'source', 'type', 'typeset',
+              'ulimit', 'unalias', 'set')
+
+def call(cline, env, cwd, loc, cb, context, echo):
+    #TODO: call this once up-front somewhere and save the result?
+    shell, msys = util.checkmsyscompat()
+
+    shellreason = None
+    if msys and cline.startswith('/'):
+        shellreason = "command starts with /"
+    else:
+        argv, badchar = clinetoargv(cline)
+        if argv is None:
+            shellreason = "command contains shell-special character '%s'" % (badchar,)
+        elif len(argv) and argv[0] in shellwords:
+            shellreason = "command starts with shell primitive '%s'" % (argv[0],)
+
+    if shellreason is not None:
+        _log.debug("%s: using shell: %s: '%s'", loc, shellreason, cline)
+        if msys:
+            if len(cline) > 3 and cline[1] == ':' and cline[2] == '/':
+                cline = '/' + cline[0] + cline[2:]
+            cline = [shell, "-c", cline]
+        context.call(cline, shell=not msys, env=env, cwd=cwd, cb=cb, echo=echo)
+        return
+
+    if not len(argv):
+        cb(res=0)
+        return
+
+    if argv[0] == command.makepypath:
+        command.main(argv[1:], env, cwd, context, cb)
+        return
+
+    if argv[0:2] == [sys.executable.replace('\\', '/'),
+                     command.makepypath.replace('\\', '/')]:
+        command.main(argv[2:], env, cwd, context, cb)
+        return
+
+    if argv[0].find('/') != -1:
+        executable = os.path.join(cwd, argv[0])
+    else:
+        executable = None
+
+    context.call(argv, executable=executable, shell=False, env=env, cwd=cwd, cb=cb, echo=echo)
+
+def statustoresult(status):
+    """
+    Convert the status returned from waitpid into a prettier numeric result.
+    """
+    sig = status & 0xFF
+    if sig:
+        return -sig
+
+    return status >>8
+
+def getcontext(jcount):
+    assert jcount > 0
+    return ParallelContext(jcount)
+
+class ParallelContext(object):
+    """
+    Manages the parallel execution of processes.
+    """
+
+    _allcontexts = set()
+
+    def __init__(self, jcount):
+        self.jcount = jcount
+        self.exit = False
+
+        self.pending = [] # list of (cb, args, kwargs)
+        self.running = [] # list of (subprocess, cb)
+
+        self._allcontexts.add(self)
+
+    def finish(self):
+        assert len(self.pending) == 0 and len(self.running) == 0, "pending: %i running: %i" % (len(self.pending), len(self.running))
+        self._allcontexts.remove(self)
+
+    def run(self):
+        while len(self.pending) and len(self.running) < self.jcount:
+            cb, args, kwargs = self.pending.pop(0)
+            cb(*args, **kwargs)
+
+    def defer(self, cb, *args, **kwargs):
+        assert self.jcount > 1 or not len(self.pending), "Serial execution error defering %r %r %r: currently pending %r" % (cb, args, kwargs, self.pending)
+        self.pending.append((cb, args, kwargs))
+
+    def _docall(self, argv, executable, shell, env, cwd, cb, echo):
+            if echo is not None:
+                print echo
+            try:
+                p = subprocess.Popen(argv, executable=executable, shell=shell, env=env, cwd=cwd)
+            except OSError, e:
+                print >>sys.stderr, e
+                cb(-127)
+                return
+
+            self.running.append((p, cb))
+
+    def call(self, argv, shell, env, cwd, cb, echo, executable=None):
+        """
+        Asynchronously call the process
+        """
+
+        self.defer(self._docall, argv, executable, shell, env, cwd, cb, echo)
+
+    if sys.platform == 'win32':
+        @staticmethod
+        def _waitany():
+            return win32process.WaitForAnyProcess([p for c in ParallelContext._allcontexts for p, cb in c.running])
+
+        @staticmethod
+        def _comparepid(pid, process):
+            return pid == process
+
+    else:
+        @staticmethod
+        def _waitany():
+            return os.waitpid(-1, 0)
+
+        @staticmethod
+        def _comparepid(pid, process):
+            return pid == process.pid
+
+    @staticmethod
+    def spin():
+        """
+        Spin the 'event loop', and never return.
+        """
+
+        while True:
+            clist = list(ParallelContext._allcontexts)
+            for c in clist:
+                c.run()
+
+            # In python 2.4, subprocess instances wait on child processes under the hood when they are created... this
+            # unfortunate behavior means that before using os.waitpid, we need to check the status using .poll()
+            # see http://bytes.com/groups/python/675403-os-wait-losing-child
+            found = False
+            for c in clist:
+                for i in xrange(0, len(c.running)):
+                    p, cb = c.running[i]
+                    result = p.poll()
+                    if result != None:
+                        del c.running[i]
+                        cb(result)
+                        found = True
+                        break
+
+                if found: break
+            if found: continue
+
+            dowait = util.any((len(c.running) for c in ParallelContext._allcontexts))
+
+            if dowait:
+                pid, status = ParallelContext._waitany()
+                result = statustoresult(status)
+
+                for c in ParallelContext._allcontexts:
+                    for i in xrange(0, len(c.running)):
+                        p, cb = c.running[i]
+                        if ParallelContext._comparepid(pid, p):
+                            del c.running[i]
+                            cb(result)
+                            found = True
+                            break
+
+                    if found: break
+
+def makedeferrable(usercb, **userkwargs):
+    def cb(*args, **kwargs):
+        kwargs.update(userkwargs)
+        return usercb(*args, **kwargs)
+
+    return cb
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/util.py
@@ -0,0 +1,89 @@
+import os
+
+def makeobject(proplist, **kwargs):
+    class P(object):
+        __slots__ = proplist
+
+    p = P()
+    for k, v in kwargs.iteritems():
+        setattr(p, k, v)
+    return p
+
+class MakeError(Exception):
+    def __init__(self, message, loc=None):
+        self.message = message
+        self.loc = loc
+
+    def __str__(self):
+        locstr = ''
+        if self.loc is not None:
+            locstr = "%s:" % (self.loc,)
+
+        return "%s%s" % (locstr, self.message)
+
+def joiniter(fd, it):
+    """
+    Given an iterator that returns strings, write the words with a space in between each.
+    """
+    
+    it = iter(it)
+    for i in it:
+        fd.write(i)
+        break
+
+    for i in it:
+        fd.write(' ')
+        fd.write(i)
+
+def checkmsyscompat():
+    """For msys compatibility on windows, honor the SHELL environment variable,
+    and if $MSYSTEM == MINGW32, run commands through $SHELL -c instead of
+    letting Python use the system shell."""
+    if 'SHELL' in os.environ:
+        shell = os.environ['SHELL']
+    elif 'COMSPEC' in os.environ:
+        shell = os.environ['COMSPEC']
+    else:
+        raise DataError("Can't find a suitable shell!")
+
+    msys = False
+    if 'MSYSTEM' in os.environ and os.environ['MSYSTEM'] == 'MINGW32':
+        msys = True
+        if not shell.lower().endswith(".exe"):
+            shell += ".exe"
+    return (shell, msys)
+
+if hasattr(str, 'partition'):
+    def strpartition(str, token):
+        return str.partition(token)
+
+    def strrpartition(str, token):
+        return str.rpartition(token)
+
+else:
+    def strpartition(str, token):
+        """Python 2.4 compatible str.partition"""
+
+        offset = str.find(token)
+        if offset == -1:
+            return str, '', ''
+
+        return str[:offset], token, str[offset + len(token):]
+
+    def strrpartition(str, token):
+        """Python 2.4 compatible str.rpartition"""
+
+        offset = str.rfind(token)
+        if offset == -1:
+            return '', '', str
+
+        return str[:offset], token, str[offset + len(token):]
+
+try:
+    from __builtin__ import any
+except ImportError:
+    def any(it):
+        for i in it:
+            if i:
+                return True
+        return False
new file mode 100644
--- /dev/null
+++ b/build/pymake/pymake/win32process.py
@@ -0,0 +1,28 @@
+from ctypes import windll, POINTER, byref, WinError
+from ctypes.wintypes import WINFUNCTYPE, HANDLE, DWORD, BOOL
+
+INFINITE = -1
+WAIT_FAILED = 0xFFFFFFFF
+
+LPDWORD = POINTER(DWORD)
+_GetExitCodeProcessProto = WINFUNCTYPE(BOOL, HANDLE, LPDWORD)
+_GetExitCodeProcess = _GetExitCodeProcessProto(("GetExitCodeProcess", windll.kernel32))
+def GetExitCodeProcess(h):
+    exitcode = DWORD()
+    r = _GetExitCodeProcess(h, byref(exitcode))
+    if r is 0:
+        raise WinError()
+    return exitcode.value
+
+_WaitForMultipleObjectsProto = WINFUNCTYPE(DWORD, DWORD, POINTER(HANDLE), BOOL, DWORD)
+_WaitForMultipleObjects = _WaitForMultipleObjectsProto(("WaitForMultipleObjects", windll.kernel32))
+
+def WaitForAnyProcess(processes):
+    arrtype = HANDLE * len(processes)
+    harray = arrtype(*(int(p._handle) for p in processes))
+
+    r = _WaitForMultipleObjects(len(processes), harray, False, INFINITE)
+    if r == WAIT_FAILED:
+        raise WinError()
+
+    return processes[r], GetExitCodeProcess(int(processes[r]._handle)) <<8
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/automatic-variables.mk
@@ -0,0 +1,79 @@
+$(shell \
+mkdir -p src/subd; \
+mkdir subd; \
+touch dummy; \
+sleep 2; \
+touch subd/test.out src/subd/test.in2; \
+sleep 2; \
+touch subd/test.out2 src/subd/test.in; \
+sleep 2; \
+touch subd/host_test.out subd/host_test.out2; \
+sleep 2; \
+touch host_prog; \
+)
+
+VPATH = src
+
+all: prog host_prog prog dir/
+	test "$@" = "all"
+	test "$<" = "prog"
+	test "$^" = "prog host_prog dir"
+	test "$?" = "prog host_prog dir"
+	test "$+" = "prog host_prog prog dir"
+	test "$(@D)" = "."
+	test "$(@F)" = "all"
+	test "$(<D)" = "."
+	test "$(<F)" = "prog"
+	test "$(^D)" = ". . ."
+	test "$(^F)" = "prog host_prog dir"
+	test "$(?D)" = ". . ."
+	test "$(?F)" = "prog host_prog dir"
+	test "$(+D)" = ". . . ."
+	test "$(+F)" = "prog host_prog prog dir"
+	@echo TEST-PASS
+
+dir/:
+	test "$@" = "dir"
+	test "$<" = ""
+	test "$^" = ""
+	test "$(@D)" = "."
+	test "$(@F)" = "dir"
+	mkdir $@
+
+prog: subd/test.out subd/test.out2
+	test "$@" = "prog"
+	test "$<" = "subd/test.out"
+	test "$^" = "subd/test.out subd/test.out2" # ^
+	test "$?" = "subd/test.out subd/test.out2" # ?
+	cat $<
+	test "$$(cat $<)" = "remade"
+	test "$$(cat $(word 2,$^))" = ""
+
+host_prog: subd/host_test.out subd/host_test.out2
+	@echo TEST-FAIL No need to remake
+
+%.out: %.in dummy
+	test "$@" = "subd/test.out"
+	test "$*" = "subd/test"              # *
+	test "$<" = "src/subd/test.in"       # <
+	test "$^" = "src/subd/test.in dummy" # ^
+	test "$?" = "src/subd/test.in"       # ?
+	test "$+" = "src/subd/test.in dummy" # +
+	test "$(@D)" = "subd"
+	test "$(@F)" = "test.out"
+	test "$(*D)" = "subd"
+	test "$(*F)" = "test"
+	test "$(<D)" = "src/subd"
+	test "$(<F)" = "test.in"
+	test "$(^D)" = "src/subd ."          # ^D
+	test "$(^F)" = "test.in dummy"
+	test "$(?D)" = "src/subd"
+	test "$(?F)" = "test.in"
+	test "$(+D)" = "src/subd ."          # +D
+	test "$(+F)" = "test.in dummy"
+	printf "remade" >$@
+
+%.out2: %.in2 dummy
+	@echo TEST_FAIL No need to remake
+
+.PHONY: all
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/bad-command-continuation.mk
@@ -0,0 +1,3 @@
+all:
+	echo 'hello'\
+TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/call.mk
@@ -0,0 +1,12 @@
+test = $0
+reverse = $2 $1
+twice = $1$1
+sideeffect = $(shell echo "called$1:" >>dummyfile)
+
+all:
+	test "$(call test)" = "test"
+	test "$(call reverse,1,2)" = "2 1"
+# expansion happens *before* substitution, thank sanity
+	test "$(call twice,$(sideeffect))" = ""
+	test `cat dummyfile` = "called:"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/commandmodifiers.mk
@@ -0,0 +1,21 @@
+define COMMAND
+$(1)
+ 	$(1)
+
+endef
+
+all:
+	$(call COMMAND,@true #TEST-FAIL)
+	$(call COMMAND,-exit 4)
+	$(call COMMAND,@-exit 1 # TEST-FAIL)
+	$(call COMMAND,-@exit 1 # TEST-FAIL)
+	$(call COMMAND,+exit 0)
+	$(call COMMAND,+-exit 1)
+	$(call COMMAND,@+exit 0 # TEST-FAIL)
+	$(call COMMAND,+@exit 0 # TEST-FAIL)
+	$(call COMMAND,-+@exit 1 # TEST-FAIL)
+	$(call COMMAND,+-@exit 1 # TEST-FAIL)
+	$(call COMMAND,@+-exit 1 # TEST-FAIL)
+	$(call COMMAND,@+-@+-exit 1 # TEST-FAIL)
+	$(call COMMAND,@@++exit 0 # TEST-FAIL)
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/comment-parsing.mk
@@ -0,0 +1,17 @@
+# where do comments take effect?
+
+VAR = val1 # comment
+VAR2 = lit2\#hash
+VAR3 = val3
+VAR4 = lit4\\#backslash
+VAR5 = lit5\char
+# This comment extends to the next line \
+VAR3 = ignored
+
+all:
+	test "$(VAR)" = "val1 "
+	test "$(VAR2)" = "lit2#hash"
+	test "$(VAR3)" = "val3"
+	test '$(VAR4)' = 'lit4\'
+	test '$(VAR5)' = 'lit5\char'
+	@echo "TEST-PASS"
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/datatests.py
@@ -0,0 +1,45 @@
+import pymake.data, pymake.util
+import unittest
+import re
+from cStringIO import StringIO
+
+def multitest(cls):
+    for name in cls.testdata.iterkeys():
+        def m(self, name=name):
+            return self.runSingle(*self.testdata[name])
+
+        setattr(cls, 'test_%s' % name, m)
+    return cls
+
+class SplitWordsTest(unittest.TestCase):
+    testdata = (
+        (' test test.c test.o ', ['test', 'test.c', 'test.o']),
+        ('\ttest\t  test.c \ntest.o', ['test', 'test.c', 'test.o']),
+    )
+
+    def runTest(self):
+        for s, e in self.testdata:
+            w = s.split()
+            self.assertEqual(w, e, 'splitwords(%r)' % (s,))
+
+class GetPatSubstTest(unittest.TestCase):
+    testdata = (
+        ('%.c', '%.o', ' test test.c test.o ', 'test test.o test.o'),
+        ('%', '%.o', ' test.c test.o ', 'test.c.o test.o.o'),
+        ('foo', 'bar', 'test foo bar', 'test bar bar'),
+        ('foo', '%bar', 'test foo bar', 'test %bar bar'),
+        ('%', 'perc_%', 'path', 'perc_path'),
+        ('\\%', 'sub%', 'p %', 'p sub%'),
+        ('%.c', '\\%%.o', 'foo.c bar.o baz.cpp', '%foo.o bar.o baz.cpp'),
+    )
+
+    def runTest(self):
+        for s, r, d, e in self.testdata:
+            words = d.split()
+            p = pymake.data.Pattern(s)
+            a = ' '.join((p.subst(r, word, False)
+                          for word in words))
+            self.assertEqual(a, e, 'Pattern(%r).subst(%r, %r)' % (s, r, d))
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/default-target.mk
@@ -0,0 +1,12 @@
+test: VAR = value
+
+%.do:
+	@echo TEST-FAIL: ran target "$@", should have run "all"
+
+all:
+	@echo TEST-PASS: the default target is all
+
+test:
+	@echo TEST-FAIL: ran target "$@", should have run "all"
+
+test.do:
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/default-target2.mk
@@ -0,0 +1,6 @@
+test.foo: %.foo:
+	test "$@" = "test.foo"
+	@echo TEST-PASS made test.foo by default
+
+all:
+	@echo TEST-FAIL made $@, should have made test.foo
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/define-directive.mk
@@ -0,0 +1,59 @@
+define COMMANDS
+shellvar=hello
+test "$$shellvar" != "hello"
+endef
+
+define COMMANDS2
+shellvar=hello; \
+  test "$$shellvar" = "hello"
+endef
+
+define VARWITHCOMMENT # comment
+value
+endef
+
+define TEST3
+  whitespace
+endef
+
+define TEST4
+define TEST5
+random
+endef
+  endef
+
+ifdef TEST5
+$(error TEST5 should not be set)
+endif
+
+define TEST6
+  define TEST7
+random
+endef
+endef
+
+ifdef TEST7
+$(error TEST7 should not be set)
+endif
+
+define TEST8
+is this # a comment?
+endef
+
+ifneq ($(TEST8),is this \# a comment?)
+$(error TEST8 value not expected: $(TEST8))
+endif
+
+# A backslash continuation "hides" the endef
+define TEST9
+value \
+endef
+endef
+
+all:
+	$(COMMANDS)
+	$(COMMANDS2)
+	test '$(VARWITHCOMMENT)' = 'value'
+	test '$(COMMANDS2)' = 'shellvar=hello; test "$$shellvar" = "hello"'
+	test "$(TEST3)" = "  whitespace"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/depfailed.mk
@@ -0,0 +1,4 @@
+#T returncode: 2
+
+all: foo.out foo.in
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/depfailedj.mk
@@ -0,0 +1,10 @@
+#T returncode: 2
+#T commandline: ['-j4']
+
+$(shell touch foo.in)
+
+all: foo.in foo.out missing
+	@echo TEST-PASS
+
+%.out: %.in
+	cp $< $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/diamond-deps.mk
@@ -0,0 +1,13 @@
+# If the dependency graph includes a diamond dependency, we should only remake
+# once!
+
+all: depA depB
+	cat testfile
+	test `cat testfile` = "data";
+	@echo TEST-PASS
+
+depA: testfile
+depB: testfile
+
+testfile:
+	printf "data" >>$@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/dotslash.mk
@@ -0,0 +1,9 @@
+$(shell touch foo.in)
+
+all: foo.out
+	test "$(wildcard ./*.in)" = "./foo.in"
+	@echo TEST-PASS
+
+./%.out: %.in
+	test "$@" = "foo.out"
+	cp $< $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/doublecolon-exists.mk
@@ -0,0 +1,16 @@
+$(shell touch foo.testfile1 foo.testfile2)
+
+# when a rule has commands and no prerequisites, should it be executed?
+# double-colon: yes
+# single-colon: no
+
+all: foo.testfile1 foo.testfile2
+	test "$$(cat foo.testfile1)" = ""
+	test "$$(cat foo.testfile2)" = "remade:foo.testfile2"
+	@echo TEST-PASS
+
+foo.testfile1:
+	@echo TEST-FAIL
+
+foo.testfile2::
+	printf "remade:$@"> $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/doublecolon-remake.mk
@@ -0,0 +1,4 @@
+$(shell touch somefile)
+
+all:: somefile
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/dynamic-var.mk
@@ -0,0 +1,18 @@
+# The *name* of variables can be constructed dynamically.
+
+VARNAME = FOOBAR
+
+$(VARNAME) = foovalue
+$(VARNAME)2 = foo2value
+
+$(VARNAME:%BAR=%BAM) = foobam
+
+all:
+	test "$(FOOBAR)" = "foovalue"
+	test "$(flavor FOOBAZ)" = "undefined"
+	test "$(FOOBAR2)" = "bazvalue"
+	test "$(FOOBAM)" = "foobam"
+	@echo TEST-PASS
+
+VARNAME = FOOBAZ
+FOOBAR2 = bazvalue
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/empty-with-deps.mk
@@ -0,0 +1,4 @@
+default.test: default.c
+
+default.c:
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/eof-continuation.mk
@@ -0,0 +1,5 @@
+all:
+	test '$(TESTVAR)' = 'testval\'
+	@echo TEST-PASS
+
+TESTVAR = testval\
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/escape-chars.mk
@@ -0,0 +1,26 @@
+space = $(NULL) $(NULL)
+hello$(space)world$(space) = hellovalue
+
+A = aval
+
+VAR = value1\\
+VARAWFUL = value1\\#comment
+VAR2 = value2
+VAR3 = test\$A
+VAR4 = value4\\value5
+
+VAR5 = value1\\ \  \
+	value2
+
+EPERCENT = \%
+
+all:
+	test "$(hello world )" = "hellovalue"
+	test "$(VAR)" = "value1\\"
+	test '$(VARAWFUL)' = 'value1\'
+	test "$(VAR2)" = "value2"
+	test "$(VAR3)" = "test\aval"
+	test "$(VAR4)" = "value4\\value5"
+	test "$(VAR5)" = "value1\\ \ value2"
+	test "$(EPERCENT)" = "\%"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/escaped-continuation.mk
@@ -0,0 +1,6 @@
+#T returncode: 2
+
+all:
+	echo "Hello" \\
+	test "world" = "not!"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/eval-duringexecute.mk
@@ -0,0 +1,12 @@
+#T returncode: 2
+
+# Once parsing is finished, recursive expansion in commands are not allowed to create any new rules (it may only set variables)
+
+define MORERULE
+all:
+	@echo TEST-FAIL
+endef
+
+all:
+	$(eval $(MORERULE))
+	@echo done
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/eval.mk
@@ -0,0 +1,7 @@
+TESTVAR = val1
+
+$(eval TESTVAR = val2)
+
+all:
+	test "$(TESTVAR)" = "val2"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/exit-code.mk
@@ -0,0 +1,5 @@
+#T returncode: 2
+
+all:
+	exit 1
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/file-functions-symlinks.mk
@@ -0,0 +1,22 @@
+#T returncode-on: {'win32': 2}
+$(shell \
+touch test.file; \
+ln -s test.file test.symlink; \
+ln -s test.missing missing.symlink; \
+touch .testhidden; \
+mkdir foo; \
+touch foo/testfile; \
+ln -s foo symdir; \
+)
+
+all:
+	test "$(abspath test.file test.symlink)" = "$(CURDIR)/test.file $(CURDIR)/test.symlink"
+	test "$(realpath test.file test.symlink)" = "$(CURDIR)/test.file $(CURDIR)/test.file"
+	test "$(sort $(wildcard *))" = "foo symdir test.file test.symlink"
+	test "$(sort $(wildcard .*))" = ". .. .testhidden"
+	test "$(sort $(wildcard test*))" = "test.file test.symlink"
+	test "$(sort $(wildcard foo/*))" = "foo/testfile"
+	test "$(sort $(wildcard ./*))" = "./foo ./symdir ./test.file ./test.symlink"
+	test "$(sort $(wildcard f?o/*))" = "foo/testfile"
+	test "$(sort $(wildcard */*))" = "foo/testfile symdir/testfile"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/file-functions.mk
@@ -0,0 +1,19 @@
+$(shell \
+touch test.file; \
+touch .testhidden; \
+mkdir foo; \
+touch foo/testfile; \
+)
+
+all:
+	test "$(abspath test.file)" = "$(CURDIR)/test.file"
+	test "$(realpath test.file)" = "$(CURDIR)/test.file"
+	test "$(sort $(wildcard *))" = "foo test.file"
+# commented out because GNU make matches . and .. while python doesn't, and I don't
+# care enough
+#	test "$(sort $(wildcard .*))" = ". .. .testhidden"
+	test "$(sort $(wildcard test*))" = "test.file"
+	test "$(sort $(wildcard foo/*))" = "foo/testfile"
+	test "$(sort $(wildcard ./*))" = "./foo ./test.file"
+	test "$(sort $(wildcard f?o/*))" = "foo/testfile"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/func-refs.mk
@@ -0,0 +1,10 @@
+unknown var = uval
+
+all:
+	test "$(subst a,b,value)" = "vblue"
+	test "${subst a,b,va)lue}" = "vb)lue"
+	test "$(subst /,\,ab/c)" = "ab\c"
+	test "$( subst a,b,value)" = ""
+	test "$(Subst a,b,value)" = ""
+	test "$(unknown var)" = "uval"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/functions.mk
@@ -0,0 +1,36 @@
+all:
+	test "$(subst e,EE,hello)" = "hEEllo"
+	test "$(strip $(NULL)  test data  )" = "test data"
+	test "$(findstring hell,hello)" = "hell"
+	test "$(findstring heaven,hello)" = ""
+	test "$(filter foo/%.c b%,foo/a.c b.c foo/a.o)" = "foo/a.c b.c"
+	test "$(filter foo,foo bar)" = "foo"
+	test "$(filter-out foo/%.c b%,foo/a.c b.c foo/a.o)" = "foo/a.o"
+	test "$(filter-out %.c,foo,bar.c foo,bar.o)" = "foo,bar.o"
+	test "$(sort .go a b aa A c cc)" = ".go A a aa b c cc"
+	test "$(word 1, hello )" = "hello"
+	test "$(word 2, hello )" = ""
+	test "$(wordlist 1, 2, foo bar baz )" = "foo bar"
+	test "$(words 1 2 3)" = "3"
+	test "$(words )" = "0"
+	test "$(firstword $(NULL) foo bar baz)" = "foo"
+	test "$(firstword )" = ""
+	test "$(dir foo.c path/foo.o dir/dir2/)" = "./ path/ dir/dir2/"
+	test "$(notdir foo.c path/foo.o dir/dir2/)" = "foo.c foo.o "
+	test "$(suffix src/foo.c dir/my.dir/foo foo.o)" = ".c .o"
+	test "$(basename src/foo.c dir/my.dir/foo foo.c .c)" = "src/foo dir/my.dir/foo foo "
+	test "$(addprefix src/,foo bar.c dir/foo)" = "src/foo src/bar.c src/dir/foo"
+	test "$(addsuffix .c,foo dir/bar)" = "foo.c dir/bar.c"
+	test "$(join a b c, 1 2 3)" = "a1 b2 c3"
+	test "$(join a b, 1 2 3)" = "a1 b2 3"
+	test "$(join a b c, 1 2)" = "a1 b2 c"
+	test "$(if $(NULL) ,yes)" = ""
+	test "$(if 1,yes,no)" = "yes"
+	test "$(if ,yes,no )" = "no "
+	test "$(if ,$(error Short-circuit problem))" = ""
+	test "$(or $(NULL),1)" = "1"
+	test "$(or $(NULL),2,$(warning TEST-FAIL bad or short-circuit))" = "2"
+	test "$(and ,$(warning TEST-FAIL bad and short-circuit))" = ""
+	test "$(and 1,2)" = "2"
+	test "$(foreach i,foo bar,found:$(i))" = "found:foo found:bar"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/ifdefs-nesting.mk
@@ -0,0 +1,13 @@
+ifdef RANDOM
+ifeq (,$(error Not evaluated!))
+endif
+endif
+
+ifdef RANDOM
+ifeq (,)
+else ifeq (,$(error Not evaluated!))
+endif
+endif
+
+all:
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/ifdefs.mk
@@ -0,0 +1,127 @@
+ifdef FOO
+$(error FOO is not defined!)
+endif
+
+FOO = foo
+FOOFOUND = false
+BARFOUND = false
+BAZFOUND = false
+
+ifdef FOO
+FOOFOUND = true
+else ifdef BAR
+BARFOUND = true
+else
+BAZFOUND = true
+endif
+
+BAR2 = bar2
+FOO2FOUND = false
+BAR2FOUND = false
+BAZ2FOUND = false
+
+ifdef FOO2
+FOO2FOUND = true
+else ifdef BAR2
+BAR2FOUND = true
+else
+BAZ2FOUND = true
+endif
+
+FOO3FOUND = false
+BAR3FOUND = false
+BAZ3FOUND = false
+
+ifdef FOO3
+FOO3FOUND = true
+else ifdef BAR3
+BAR3FOUND = true
+else
+BAZ3FOUND = true
+endif
+
+ifdef RANDOM
+CONTINUATION = \
+else           \
+endif
+endif
+
+ifndef ASDFJK
+else
+$(error ASFDJK was not set)
+endif
+
+TESTSET =
+
+ifdef TESTSET
+$(error TESTSET was not set)
+endif
+
+TESTEMPTY = $(NULL)
+ifndef TESTEMPTY
+$(error TEST-FAIL TESTEMPTY was probably expanded!)
+endif
+
+# ifneq ( a,a)
+# $(error Arguments to ifeq should be stripped before evaluation)
+# endif
+
+XSPACE = x # trick
+
+ifneq ($(NULL),$(NULL))
+$(error TEST-FAIL ifneq)
+endif
+
+ifneq (x , x)
+$(error argument-stripping1)
+endif
+
+ifeq ( x,x )
+$(error argument-stripping2)
+endif
+
+ifneq ($(XSPACE), x )
+$(error argument-stripping3)
+endif
+
+ifeq 'x ' ' x'
+$(error TEST-FAIL argument-stripping4)
+endif
+
+all:
+	test $(FOOFOUND) = true   # FOOFOUND
+	test $(BARFOUND) = false  # BARFOUND
+	test $(BAZFOUND) = false  # BAZFOUND
+	test $(FOO2FOUND) = false # FOO2FOUND
+	test $(BAR2FOUND) = true  # BAR2FOUND
+	test $(BAZ2FOUND) = false # BAZ2FOUND
+	test $(FOO3FOUND) = false # FOO3FOUND
+	test $(BAR3FOUND) = false # BAR3FOUND
+	test $(BAZ3FOUND) = true  # BAZ3FOUND
+ifneq ($(FOO),foo)
+	echo TEST-FAIL 'FOO neq foo: "$(FOO)"'
+endif
+ifneq ($(FOO), foo) # Whitespace after the comma is stripped
+	echo TEST-FAIL 'FOO plus whitespace'
+endif
+ifeq ($(FOO), foo ) # But not trailing whitespace
+	echo TEST-FAIL 'FOO plus trailing whitespace'
+endif
+ifeq ( $(FOO),foo) # Not whitespace after the paren
+	echo TEST-FAIL 'FOO with leading whitespace'
+endif
+ifeq ($(FOO),$(NULL) foo) # Nor whitespace after expansion
+	echo TEST-FAIL 'FOO with embedded ws'
+endif
+ifeq ($(BAR2),bar)
+	echo TEST-FAIL 'BAR2 eq bar'
+endif
+ifeq '$(BAR3FOUND)' 'false'
+	echo BAR3FOUND is ok
+else
+	echo TEST-FAIL BAR3FOUND is not ok
+endif
+ifndef FOO
+	echo TEST-FAIL "foo not defined?"
+endif
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/ignore-error.mk
@@ -0,0 +1,13 @@
+all:
+	-rm foo
+	+-rm bar
+	-+rm baz
+	@-rm bah
+	-@rm humbug
+	+-@rm sincere
+	+@-rm flattery
+	@+-rm will
+	@-+rm not
+	-+@rm save
+	-@+rm you
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/implicit-chain.mk
@@ -0,0 +1,12 @@
+all: test.prog
+	test "$$(cat $<)" = "Program: Object: Source: test.source"
+	@echo TEST-PASS
+
+%.prog: %.object
+	printf "Program: %s" "$$(cat $<)" > $@
+
+%.object: %.source
+	printf "Object: %s" "$$(cat $<)" > $@
+
+%.source:
+	printf "Source: %s" $@ > $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/implicit-dir.mk
@@ -0,0 +1,16 @@
+# Implicit rules have special instructions to deal with directories, so that a pattern rule which doesn't directly apply
+# may still be used.
+
+all: dir/host_test.otest
+
+host_%.otest: %.osource extra.file
+	@echo making $@ from $<
+
+test.osource:
+	@echo TEST-FAIL should have made dir/test.osource
+
+dir/test.osource:
+	@echo TEST-PASS made the correct dependency
+
+extra.file:
+	@echo building $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/implicit-terminal.mk
@@ -0,0 +1,16 @@
+#T returncode: 2
+
+# the %.object rule is "terminal". This means that additional implicit rules cannot be chained to it.
+
+all: test.prog
+	test "$$(cat $<)" = "Program: Object: Source: test.source"
+	@echo TEST-FAIL
+
+%.prog: %.object
+	printf "Program: %s" "$$(cat $<)" > $@
+
+%.object:: %.source
+	printf "Object: %s" "$$(cat $<)" > $@
+
+%.source:
+	printf "Source: %s" $@ > $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/implicitsubdir.mk
@@ -0,0 +1,12 @@
+$(shell \
+mkdir foo; \
+touch test.in \
+)
+
+all: foo/test.out
+	@echo TEST-PASS
+
+foo/%.out: %.in
+	cp $< $@
+
+
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/include-dynamic.mk
@@ -0,0 +1,21 @@
+$(shell \
+if ! test -f include-dynamic.inc; then \
+  echo "TESTVAR = oldval" > include-dynamic.inc; \
+  sleep 2; \
+  echo "TESTVAR = newval" > include-dynamic.inc.in; \
+fi \
+)
+
+# before running the 'all' rule, we should be rebuilding include-dynamic.inc,
+# because there is a rule to do so
+
+all:
+	test $(TESTVAR) = newval
+	test "$(MAKE_RESTARTS)" = 1
+	@echo TEST-PASS
+
+include-dynamic.inc: include-dynamic.inc.in
+	test "$(MAKE_RESTARTS)" = ""
+	cp $< $@
+
+include include-dynamic.inc
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/include-file.inc
@@ -0,0 +1,1 @@
+INCLUDED = yes
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/include-notfound.mk
@@ -0,0 +1,19 @@
+ifdef __WIN32__
+PS:=\\#
+else
+PS:=/
+endif
+
+ifneq ($(strip $(MAKEFILE_LIST)),$(NATIVE_TESTPATH)$(PS)include-notfound.mk)
+$(error MAKEFILE_LIST incorrect: '$(MAKEFILE_LIST)' (expected '$(NATIVE_TESTPATH)$(PS)include-notfound.mk'))
+endif
+
+-include notfound.inc-dummy
+
+ifneq ($(strip $(MAKEFILE_LIST)),$(NATIVE_TESTPATH)$(PS)include-notfound.mk)
+$(error MAKEFILE_LIST incorrect: '$(MAKEFILE_LIST)' (expected '$(NATIVE_TESTPATH)$(PS)include-notfound.mk'))
+endif
+
+all:
+	@echo TEST-PASS
+
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/include-regen.mk
@@ -0,0 +1,9 @@
+# make should only consider -included makefiles for remaking if they actually exist:
+
+-include notfound.mk
+
+all:
+	@echo TEST-PASS
+
+notfound.mk::
+	@echo TEST-FAIL
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/include-test.mk
@@ -0,0 +1,8 @@
+$(shell echo "INCLUDED2 = yes" >local-include.inc)
+
+include $(TESTPATH)/include-file.inc local-include.inc
+
+all:
+	test "$(INCLUDED)" = "yes"
+	test "$(INCLUDED2)" = "yes"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/line-continuations.mk
@@ -0,0 +1,20 @@
+VAR = val1 	 \
+  	  val2  
+
+VAR2 = val1space\
+val2
+
+all: otarget test.target
+	test "$(VAR)" = "val1 val2  "
+	test "$(VAR2)" = "val1space val2"
+	test "hello \
+	  world" = "hello   world"
+	test "hello" = \
+"hello"
+	@echo TEST-PASS
+
+otarget: ; test "hello\
+	world" = "helloworld"
+
+test.target: %.target: ; test "hello\
+	world" = "helloworld"
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/link-search.mk
@@ -0,0 +1,7 @@
+$(shell \
+touch libfoo.so \
+)
+
+all: -lfoo
+	test "$<" = "libfoo.so"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/matchany.mk
@@ -0,0 +1,14 @@
+#T returncode: 2
+
+# we should fail to make foo.ooo from foo.ooo.test
+all: foo.ooo
+	@echo TEST-FAIL
+
+%.ooo:
+
+# this match-anything pattern should not apply to %.ooo
+%: %.test
+	cp $< $@
+
+foo.ooo.test:
+	touch $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/matchany2.mk
@@ -0,0 +1,13 @@
+# we should succeed in making foo.ooo from foo.ooo.test
+all: foo.ooo
+	@echo TEST-PASS
+
+%.ooo: %.ccc
+	exit 1
+
+# this match-anything rule is terminal, and therefore applies
+%:: %.test
+	cp $< $@
+
+foo.ooo.test:
+	touch $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/matchany3.mk
@@ -0,0 +1,10 @@
+$(shell \
+echo "target" > target.in; \
+)
+
+all: target
+	test "$$(cat $^)" = "target"
+	@echo TEST-PASS
+
+%: %.in
+	cp $< $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/no-remake.mk
@@ -0,0 +1,7 @@
+$(shell date >testfile)
+
+all: testfile
+	@echo TEST-PASS
+
+testfile:
+	@echo TEST-FAIL "We shouldn't have remade this!"
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/nosuchfile.mk
@@ -0,0 +1,4 @@
+#T returncode: 2
+
+all:
+	reallythereisnosuchcommand
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/notargets.mk
@@ -0,0 +1,5 @@
+$(NULL): foo.c
+	@echo TEST-FAIL
+
+all:
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-dep-resolution.mk
@@ -0,0 +1,8 @@
+#T commandline: ['-j3']
+#T returncode: 2
+
+all: t1 t2
+
+t1:
+	sleep 1
+	touch t1 t2
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-rule-execution.mk
@@ -0,0 +1,9 @@
+#T commandline: ['-j3']
+#T returncode: 2
+
+all::
+	sleep 1
+	touch somefile
+
+all:: somefile
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-simple.mk
@@ -0,0 +1,25 @@
+#T commandline: ['-j2']
+
+define SLOWMAKE
+printf "$@:0:" >>results
+sleep 0.5
+printf "$@:1:" >>results
+sleep 0.5
+printf "$@:2:" >>results
+endef
+
+EXPECTED = target1:0:target2:0:target1:1:target2:1:target1:2:target2:2:
+
+all:: target1 target2
+	cat results
+	test "$$(cat results)" = "$(EXPECTED)"
+	@echo TEST-PASS
+
+target1:
+	$(SLOWMAKE)
+
+target2:
+	sleep 0.1
+	$(SLOWMAKE)
+
+.PHONY: all
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-submake.mk
@@ -0,0 +1,17 @@
+#T commandline: ['-j2']
+
+# A submake shouldn't return control to the parent until it has actually finished doing everything.
+
+all:
+	-$(MAKE) -f $(TESTPATH)/parallel-submake.mk subtarget
+	cat results
+	test "$$(cat results)" = "0123"
+	@echo TEST-PASS
+
+subtarget: succeed-slowly fail-quickly
+
+succeed-slowly:
+	printf 0 >>results; sleep 1; printf 1 >>results; sleep 1; printf 2 >>results; sleep 1; printf 3 >>results
+
+fail-quickly:
+	exit 1
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-toserial.mk
@@ -0,0 +1,31 @@
+#T commandline: ['-j4']
+
+# Test that -j1 in a submake has the proper effect.
+
+define SLOWCMD
+printf "$@:0:" >>$(RFILE)
+sleep 0.5
+printf "$@:1:" >>$(RFILE)
+endef
+
+all: p1 p2
+subtarget: s1 s2
+
+p1 p2: RFILE = presult
+s1 s2: RFILE = sresult
+
+p1 s1:
+	$(SLOWCMD)
+
+p2 s2:
+	sleep 0.1
+	$(SLOWCMD)
+
+all:
+	$(MAKE) -j1 -f $(TESTPATH)/parallel-toserial.mk subtarget
+	printf "presult: %s\n" "$$(cat presult)"
+	test "$$(cat presult)" = "p1:0:p2:0:p1:1:p2:1:"
+	printf "sresult: %s\n" "$$(cat sresult)"
+	test "$$(cat sresult)" = "s1:0:s1:1:s2:0:s2:1:"
+	@echo TEST-PASS
+
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-waiting.mk
@@ -0,0 +1,21 @@
+#T commandline: ['-j2']
+
+EXPECTED = target1:before:target2:1:target2:2:target2:3:target1:after
+
+all:: target1 target2
+	cat results
+	test "$$(cat results)" = "$(EXPECTED)"
+	@echo TEST-PASS
+
+target1:
+	printf "$@:before:" >>results
+	sleep 4
+	printf "$@:after" >>results
+
+target2:
+	sleep 0.2
+	printf "$@:1:" >>results
+	sleep 0.1
+	printf "$@:2:" >>results
+	sleep 0.1
+	printf "$@:3:" >>results
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parsertests.py
@@ -0,0 +1,294 @@
+import pymake.data, pymake.parser, pymake.parserdata, pymake.functions
+import unittest
+import logging
+
+from cStringIO import StringIO
+
+def multitest(cls):
+    for name in cls.testdata.iterkeys():
+        def m(self, name=name):
+            return self.runSingle(*self.testdata[name])
+
+        setattr(cls, 'test_%s' % name, m)
+    return cls
+
+class TestBase(unittest.TestCase):
+    def assertEqual(self, a, b, msg=""):
+        """Actually print the values which weren't equal, if things don't work out!"""
+        unittest.TestCase.assertEqual(self, a, b, "%s got %r expected %r" % (msg, a, b))
+
+class DataTest(TestBase):
+    testdata = (
+        ((("He\tllo", "f", 1, 0),),
+         ((0, "f", 1, 0), (2, "f", 1, 2), (3, "f", 1, 4))),
+        ((("line1 ", "f", 1, 4), ("l\tine2", "f", 2, 11)),
+         ((0, "f", 1, 4), (5, "f", 1, 9), (6, "f", 2, 11), (7, "f", 2, 12), (8, "f", 2, 16))),
+    )
+
+    def runTest(self):
+        for datas, results in self.testdata:
+            d = pymake.parser.Data()
+            for line, file, lineno, col in datas:
+                d.append(line, pymake.parserdata.Location(file, lineno, col))
+            for pos, file, lineno, col in results:
+                loc = d.getloc(pos)
+                self.assertEqual(loc.path, file, "data file")
+                self.assertEqual(loc.line, lineno, "data line")
+                self.assertEqual(loc.column, col, "data %r col, got %i expected %i" % (d.data, loc.column, col))
+
+class TokenTest(TestBase):
+    testdata = {
+        'wsmatch': ('  ifdef FOO', 2, ('ifdef', 'else'), True, 'ifdef', 8),
+        'wsnomatch': ('  unexpected FOO', 2, ('ifdef', 'else'), True, None, 2),
+        'wsnows': ('  ifdefFOO', 2, ('ifdef', 'else'), True, None, 2),
+        'paren': (' "hello"', 1, ('(', "'", '"'), False, '"', 2),
+        }
+
+    def runSingle(self, s, start, tlist, needws, etoken, eoffset):
+        d = pymake.parser.Data.fromstring(s, None)
+        tl = pymake.parser.TokenList.get(tlist)
+        atoken, aoffset = d.findtoken(start, tl, needws)
+        self.assertEqual(atoken, etoken)
+        self.assertEqual(aoffset, eoffset)
+multitest(TokenTest)
+
+class IterTest(TestBase):
+    testdata = {
+        'plaindata': (
+            pymake.parser.iterdata,
+            "plaindata # test\n",
+            "plaindata # test\n"
+            ),
+        'makecomment': (
+            pymake.parser.itermakefilechars,
+            "VAR = val # comment",
+            "VAR = val "
+            ),
+        'makeescapedcomment': (
+            pymake.parser.itermakefilechars,
+            "VAR = val \# escaped hash\n",
+            "VAR = val # escaped hash"
+            ),
+        'makeescapedslash': (
+            pymake.parser.itermakefilechars,
+            "VAR = val\\\\\n",
+            "VAR = val\\\\",
+            ),
+        'makecontinuation': (
+            pymake.parser.itermakefilechars,
+            "VAR = VAL  \\\n  continuation # comment \\\n  continuation",
+            "VAR = VAL continuation "
+            ),
+        'makecontinuation2': (
+            pymake.parser.itermakefilechars,
+            "VAR = VAL  \\  \\\n continuation",
+            "VAR = VAL  \\ continuation"
+            ),
+        'makeawful': (
+            pymake.parser.itermakefilechars,
+            "VAR = VAL  \\\\# comment\n",
+            "VAR = VAL  \\"
+            ),
+        'command': (
+            pymake.parser.itercommandchars,
+            "echo boo # comment\n",
+            "echo boo # comment",
+            ),
+        'commandcomment': (
+            pymake.parser.itercommandchars,
+            "echo boo \# comment\n",
+            "echo boo \# comment",
+            ),
+        'commandcontinue': (
+            pymake.parser.itercommandchars,
+            "echo boo # \\\n\t  command 2\n",
+            "echo boo # \\\n  command 2"
+            ),
+        'define': (
+            pymake.parser.iterdefinechars,
+            "endef",
+            ""
+            ),
+        'definenesting': (
+            pymake.parser.iterdefinechars,
+            """define BAR # comment
+random text
+endef not what you think!
+endef # comment is ok\n""",
+            """define BAR # comment
+random text
+endef not what you think!"""
+            ),
+        'defineescaped': (
+            pymake.parser.iterdefinechars,
+            """value   \\
+endef
+endef\n""",
+            "value endef"
+        ),
+    }
+
+    def runSingle(self, ifunc, idata, expected):
+        fd = StringIO(idata)
+        lineiter = enumerate(fd)
+
+        d = pymake.parser.DynamicData(lineiter, 'PlainIterTest-data')
+        d.readline()
+
+        actual = ''.join( (c for c, t, o, oo in ifunc(d, 0, pymake.parser._emptytokenlist)) )
+        self.assertEqual(actual, expected)
+
+        self.assertRaises(StopIteration, lambda: fd.next())
+multitest(IterTest)
+
+class MakeSyntaxTest(TestBase):
+    # (string, startat, stopat, stopoffset, expansion
+    testdata = {
+        'text': ('hello world', 0, (), None, ['hello world']),
+        'singlechar': ('hello $W', 0, (), None,
+                       ['hello ',
+                        {'type': 'VariableRef',
+                         '.vname': ['W']}
+                        ]),
+        'stopat': ('hello: world', 0, (':', '='), 6, ['hello']),
+        'funccall': ('h $(flavor FOO)', 0, (), None,
+                     ['h ',
+                      {'type': 'FlavorFunction',
+                       '[0]': ['FOO']}
+                      ]),
+        'escapedollar': ('hello$$world', 0, (), None, ['hello$world']),
+        'varref': ('echo $(VAR)', 0, (), None,
+                   ['echo ',
+                    {'type': 'VariableRef',
+                     '.vname': ['VAR']}
+                    ]),
+        'dynamicvarname': ('echo $($(VARNAME):.c=.o)', 0, (':',), None,
+                           ['echo ',
+                            {'type': 'SubstitutionRef',
+                             '.vname': [{'type': 'VariableRef',
+                                         '.vname': ['VARNAME']}
+                                        ],
+                             '.substfrom': ['.c'],
+                             '.substto': ['.o']}
+                            ]),
+        'substref': ('  $(VAR:VAL) := $(VAL)', 0, (':=', '+=', '=', ':'), 15,
+                     ['  ',
+                      {'type': 'VariableRef',
+                       '.vname': ['VAR:VAL']},
+                      ' ']),
+        'vadsubstref': ('  $(VAR:VAL) = $(VAL)', 15, (), None,
+                        [{'type': 'VariableRef',
+                          '.vname': ['VAL']},
+                         ]),
+        }
+
+    def compareRecursive(self, actual, expected, path):
+        self.assertEqual(len(actual), len(expected),
+                         "compareRecursive: %s" % (path,))
+        for i in xrange(0, len(actual)):
+            ipath = path + [i]
+
+            a, isfunc = actual[i]
+            e = expected[i]
+            if isinstance(e, str):
+                self.assertEqual(a, e, "compareRecursive: %s" % (ipath,))
+            else:
+                self.assertEqual(type(a), getattr(pymake.functions, e['type']),
+                                 "compareRecursive: %s" % (ipath,))
+                for k, v in e.iteritems():
+                    if k == 'type':
+                        pass
+                    elif k[0] == '[':
+                        item = int(k[1:-1])
+                        proppath = ipath + [item]
+                        self.compareRecursive(a[item], v, proppath)
+                    elif k[0] == '.':
+                        item = k[1:]
+                        proppath = ipath + [item]
+                        self.compareRecursive(getattr(a, item), v, proppath)
+                    else:
+                        raise Exception("Unexpected property at %s: %s" % (ipath, k))
+
+    def runSingle(self, s, startat, stopat, stopoffset, expansion):
+        d = pymake.parser.Data.fromstring(s, pymake.parserdata.Location('testdata', 1, 0))
+
+        a, t, offset = pymake.parser.parsemakesyntax(d, startat, stopat, pymake.parser.itermakefilechars)
+        self.compareRecursive(a, expansion, [])
+        self.assertEqual(offset, stopoffset)
+
+multitest(MakeSyntaxTest)
+
+class VariableTest(TestBase):
+    testdata = """
+    VAR = value
+    VARNAME = TESTVAR
+    $(VARNAME) = testvalue
+    $(VARNAME:VAR=VAL) = moretesting
+    IMM := $(VARNAME) # this is a comment
+    MULTIVAR = val1 \\
+  val2
+    VARNAME = newname
+    """
+    expected = {'VAR': 'value',
+                'VARNAME': 'newname',
+                'TESTVAR': 'testvalue',
+                'TESTVAL': 'moretesting',
+                'IMM': 'TESTVAR ',
+                'MULTIVAR': 'val1 val2',
+                'UNDEF': None}
+
+    def runTest(self):
+        stream = StringIO(self.testdata)
+        stmts = pymake.parser.parsestream(stream, 'testdata')
+
+        m = pymake.data.Makefile()
+        stmts.execute(m)
+        for k, v in self.expected.iteritems():
+            flavor, source, val = m.variables.get(k)
+            if val is None:
+                self.assertEqual(val, v, 'variable named %s' % k)
+            else:
+                self.assertEqual(val.resolvestr(m, m.variables), v, 'variable named %s' % k)
+
+class SimpleRuleTest(TestBase):
+    testdata = """
+    VAR = value
+TSPEC = dummy
+all: TSPEC = myrule
+all:: test test2 $(VAR)
+	echo "Hello, $(TSPEC)"
+
+%.o: %.c
+	$(CC) -o $@ $<
+"""
+
+    def runTest(self):
+        stream = StringIO(self.testdata)
+        stmts = pymake.parser.parsestream(stream, 'testdata')
+
+        m = pymake.data.Makefile()
+        stmts.execute(m)
+        self.assertEqual(m.defaulttarget, 'all', "Default target")
+
+        self.assertTrue(m.hastarget('all'), "Has 'all' target")
+        target = m.gettarget('all')
+        rules = target.rules
+        self.assertEqual(len(rules), 1, "Number of rules")
+        prereqs = rules[0].prerequisites
+        self.assertEqual(prereqs, ['test', 'test2', 'value'], "Prerequisites")
+        commands = rules[0].commands
+        self.assertEqual(len(commands), 1, "Number of commands")
+        expanded = commands[0].resolvestr(m, target.variables)
+        self.assertEqual(expanded, 'echo "Hello, myrule"')
+
+        irules = m.implicitrules
+        self.assertEqual(len(irules), 1, "Number of implicit rules")
+
+        irule = irules[0]
+        self.assertEqual(len(irule.targetpatterns), 1, "%.o target pattern count")
+        self.assertEqual(len(irule.prerequisites), 1, "%.o prerequisite count")
+        self.assertEqual(irule.targetpatterns[0].match('foo.o'), 'foo', "%.o stem")
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.DEBUG)
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/patsubst.mk
@@ -0,0 +1,7 @@
+all:
+	test "$(patsubst foo,%.bar,foo)" = "%.bar"
+	test "$(patsubst \%word,replace,word %word other)" = "word replace other"
+	test "$(patsubst %.c,\%%.o,foo.c bar.o baz.cpp)" = "%foo.o bar.o baz.cpp"
+	test "$(patsubst host_%.c,host_%.o,dir/host_foo.c host_bar.c)" = "dir/host_foo.c host_bar.o"
+	test "$(patsubst foo,bar,dir/foo foo baz)" = "dir/foo bar baz"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/recursive-set.mk
@@ -0,0 +1,7 @@
+#T returncode: 2
+
+FOO = $(FOO)
+
+all:
+	echo $(FOO)
+	@echo TEST-FAIL
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/recursive-set2.mk
@@ -0,0 +1,8 @@
+#T returncode: 2
+
+FOO = $(BAR)
+BAR = $(FOO)
+
+all:
+	echo $(FOO)
+	@echo TEST-FAIL
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/runtests.py
@@ -0,0 +1,103 @@
+"""
+Run the test(s) listed on the command line. If a directory is listed, the script will recursively
+walk the directory for files named .mk and run each.
+
+For each test, we run gmake -f test.mk. By default, make must exit with an exit code of 0, and must print 'TEST-PASS'.
+
+Each test is run in an empty directory.
+
+The test file may contain lines at the beginning to alter the default behavior. These are all evaluated as python:
+
+#T commandline: ['extra', 'params', 'here']
+#T returncode: 2
+#T returncode-on: {'win32': 2}
+"""
+
+from subprocess import Popen, PIPE, STDOUT
+from optparse import OptionParser
+import os, re, sys, shutil
+
+thisdir = os.path.dirname(os.path.abspath(__file__))
+
+o = OptionParser()
+o.add_option('-m', '--make',
+             dest="make", default="gmake")
+o.add_option('-d', '--tempdir',
+             dest="tempdir", default="_mktests")
+opts, args = o.parse_args()
+
+if len(args) == 0:
+    args = ['.']
+
+makefiles = []
+for a in args:
+    if os.path.isfile(a):
+        makefiles.append(a)
+    elif os.path.isdir(a):
+        for path, dirnames, filenames in os.walk(a):
+            for f in filenames:
+                if f.endswith('.mk'):
+                    makefiles.append('%s/%s' % (path, f))
+    else:
+        print >>sys.stderr, "Error: Unknown file on command line"
+        sys.exit(1)
+
+tre = re.compile('^#T ([a-z-]+): (.*)$')
+
+for makefile in makefiles:
+    print "Testing: %s" % makefile,
+
+    if os.path.exists(opts.tempdir): shutil.rmtree(opts.tempdir)
+    os.mkdir(opts.tempdir, 0755)
+
+    # For some reason, MAKEFILE_LIST uses native paths in GNU make on Windows
+    # (even in MSYS!) so we pass both TESTPATH and NATIVE_TESTPATH
+    cline = [opts.make, '-C', opts.tempdir, '-f', os.path.abspath(makefile), 'TESTPATH=%s' % thisdir.replace('\\','/'), 'NATIVE_TESTPATH=%s' % thisdir]
+    if sys.platform == 'win32':
+        #XXX: hack to run pymake on windows
+        if opts.make.endswith('.py'):
+            cline = [sys.executable] + cline
+        #XXX: hack so we can specialize the separator character on windows.
+        # we really shouldn't need this, but y'know
+        cline += ['__WIN32__=1']
+        
+    returncode = 0
+
+    mdata = open(makefile)
+    for line in mdata:
+        m = tre.search(line)
+        if m is None:
+            break
+        key, data = m.group(1, 2)
+        data = eval(data)
+        if key == 'commandline':
+            cline.extend(data)
+        elif key == 'returncode':
+            returncode = data
+        elif key == 'returncode-on':
+            if sys.platform in data:
+                returncode = data[sys.platform]
+        else:
+            print >>sys.stderr, "Unexpected #T key: %s" % key
+            sys.exit(1)
+
+    mdata.close()
+
+    p = Popen(cline, stdout=PIPE, stderr=STDOUT)
+    stdout, d = p.communicate()
+    if p.returncode != returncode:
+        print "FAIL (returncode=%i)" % p.returncode
+        print stdout
+    elif stdout.find('TEST-FAIL') != -1:
+        print "FAIL"
+        print stdout
+    elif returncode == 0:
+        if stdout.find('TEST-PASS') != -1:
+            print "PASS"
+        else:
+            print "FAIL (no passing output)"
+            print stdout
+    else:
+        print "EXPECTED-FAIL"
+
+shutil.rmtree(opts.tempdir)
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/serial-dep-resolution.mk
@@ -0,0 +1,5 @@
+all: t1 t2
+	@echo TEST-PASS
+
+t1:
+	touch t1 t2
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/serial-rule-execution.mk
@@ -0,0 +1,5 @@
+all::
+	touch somefile
+
+all:: somefile
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/serial-rule-execution2.mk
@@ -0,0 +1,13 @@
+#T returncode: 2
+
+# The dependencies of the command rule of a single-colon target are resolved before the rules without commands.
+
+all: export
+
+export:
+	sleep 1
+	touch somefile
+
+all: somefile
+	test -f somefile
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/shellfunc.mk
@@ -0,0 +1,6 @@
+all: testfile
+	test "$(shell cat $<)" = "Hello world"
+	@echo TEST-PASS
+
+testfile:
+	printf "Hello\nworld\n" > $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/specified-target.mk
@@ -0,0 +1,7 @@
+#T commandline: ['VAR=all', '$(VAR)']
+
+all:
+	@echo TEST-FAIL: unexpected target 'all'
+
+$$(VAR):
+	@echo TEST-PASS: expected target '$$(VAR)'
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/static-pattern.mk
@@ -0,0 +1,5 @@
+#T returncode: 2
+
+out/host_foo.o: host_%.o: host_%.c out
+	cp $< $@
+	@echo TEST-FAIL
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/static-pattern2.mk
@@ -0,0 +1,10 @@
+all: foo.out
+	test -f $^
+	@echo TEST-PASS
+
+foo.out: %.out: %.in
+	test "$*" = "foo"
+	cp $^ $@
+
+foo.in:
+	touch $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/submake.makefile2
@@ -0,0 +1,19 @@
+# -*- Mode: Makefile -*-
+
+$(info MAKEFLAGS = '$(MAKEFLAGS)')
+$(info MAKE = '$(MAKE)')
+$(info value MAKE = "$(value MAKE)")
+
+all:
+	env
+	test "$(MAKELEVEL)" = "1"
+	echo "value(MAKE)" '$(value MAKE)'
+	echo "value(MAKE_COMMAND)" = '$(value MAKE_COMMAND)'
+	test "$(origin CVAR)" = "command line"
+	test "$(CVAR)" = "c val=spac\ed"
+	test "$(origin EVAR)" = "environment"
+	test "$(EVAR)" = "eval"
+	test "$(OVAL)" = "cline"
+	test "$(OVAL2)" = "cline2"
+	test "$(ALLVAR)" = "allspecific"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/submake.mk
@@ -0,0 +1,16 @@
+#T commandline: ['CVAR=c val=spac\\ed', 'OVAL=cline', 'OVAL2=cline2']
+
+export EVAR = eval
+override OVAL = makefile
+
+# exporting an override variable doesn't mean it's an override variable
+override OVAL2 = makefile2
+export OVAL2
+
+export ALLVAR
+ALLVAR = general
+all: ALLVAR = allspecific
+
+all:
+	test "$(MAKELEVEL)" = "0"
+	$(MAKE) -f $(TESTPATH)/submake.makefile2
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/tab-intro.mk
@@ -0,0 +1,16 @@
+# Initial tab characters should be treated well.
+
+	THIS = a value
+
+	ifdef THIS
+	VAR = conditional value
+	endif
+
+all:
+	test "$(THIS)" = "another value"
+	test "$(VAR)" = "conditional value"
+	@echo TEST-PASS
+
+THAT = makefile syntax
+
+	THIS = another value
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/target-specific.mk
@@ -0,0 +1,30 @@
+TESTVAR = anonval
+
+all: target.suffix target.suffix2 dummy host_test.py my.test1 my.test2
+	@echo TEST-PASS
+
+target.suffix: TESTVAR = testval
+
+%.suffix:
+	test "$(TESTVAR)" = "testval"
+
+%.suffix2: TESTVAR = testval2
+
+%.suffix2:
+	test "$(TESTVAR)" = "testval2"
+
+%my: TESTVAR = dummyval
+
+dummy:
+	test "$(TESTVAR)" = "dummyval"
+
+%.py: TESTVAR = pyval
+host_%.py: TESTVAR = hostval
+
+host_test.py:
+	test "$(TESTVAR)" = "hostval"
+
+%.test1 %.test2: TESTVAR = %val
+
+my.test1 my.test2:
+	test "$(TESTVAR)" = "%val"
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/unterminated-dollar.mk
@@ -0,0 +1,6 @@
+VAR = value$
+VAR2 = other
+
+all:
+	test "$(VAR)" = "value"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/var-change-flavor.mk
@@ -0,0 +1,12 @@
+VAR = value1
+VAR := value2
+
+VAR2 := val1
+VAR2 = val2
+
+default:
+	test "$(flavor VAR)" = "simple"
+	test "$(VAR)" = "value2"
+	test "$(flavor VAR2)" = "recursive"
+	test "$(VAR2)" = "val2"
+	@echo "TEST-PASS"
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/var-commandline.mk
@@ -0,0 +1,8 @@
+#T commandline: ['TESTVAR=$(MAKEVAL)', 'TESTVAR2:=$(MAKEVAL)']
+
+MAKEVAL=testvalue
+
+all:
+	test "$(TESTVAR)" = "testvalue"
+	test "$(TESTVAR2)" = ""
+	@echo "TEST-PASS"
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/var-overrides.mk
@@ -0,0 +1,21 @@
+#T commandline: ['CLINEVAR=clineval', 'CLINEVAR2=clineval2']
+
+# this doesn't actually test overrides yet, because they aren't implemented in pymake,
+# but testing origins in general is important
+
+MVAR = mval
+CLINEVAR = deadbeef
+
+override CLINEVAR2 = mval2
+
+all:
+	test "$(origin NOVAR)" = "undefined"
+	test "$(CLINEVAR)" = "clineval"
+	test "$(origin CLINEVAR)" = "command line"
+	test "$(MVAR)" = "mval"
+	test "$(origin MVAR)" = "file"
+	test "$(@)" = "all"
+	test "$(origin @)" = "automatic"
+	test "$(origin CLINEVAR2)" = "override"
+	test "$(CLINEVAR2)" = "mval2"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/var-ref.mk
@@ -0,0 +1,19 @@
+VAR = value
+VAR2 == value
+
+VAR5 = $(NULL) $(NULL)
+VARC = value # comment
+
+$(VAR3)
+  $(VAR4)  
+$(VAR5)
+
+VAR6$(VAR5) = val6
+
+all:
+	test "$( VAR)" = ""
+	test "$(VAR2)" = "= value"
+	test "${VAR2}" = "= value"
+	test "$(VAR6 )" = "val6"
+	test "$(VARC)" = "value "
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/var-set.mk
@@ -0,0 +1,55 @@
+#T commandline: ['OBASIC=oval']
+
+BASIC = val
+
+TEST = $(TEST)
+
+TEST2 = $(TES
+TEST2 += T)
+
+TES T = val
+
+RECVAR = foo
+RECVAR += var baz 
+
+IMMVAR := bloo
+IMMVAR += $(RECVAR)
+
+BASIC ?= notval
+
+all: BASIC = valall
+all: RECVAR += $(BASIC)
+all: IMMVAR += $(BASIC)
+all: UNSET += more
+all: OBASIC += allmore
+
+CHECKLIT = $(NULL) check
+all: CHECKLIT += appendliteral
+
+RECVAR = blimey
+
+TESTEMPTY = \
+	$(NULL)
+
+all: other
+	test "$(TEST2)" = "val"
+	test '$(value TEST2)' = '$$(TES T)'
+	test "$(RECVAR)" = "blimey valall"
+	test "$(IMMVAR)" = "bloo foo var baz  valall"
+	test "$(UNSET)" = "more"
+	test "$(OBASIC)" = "oval"
+	test "$(CHECKLIT)" = " check appendliteral"
+	test "$(TESTEMPTY)" = ""
+	@echo TEST-PASS
+
+OVAR = oval
+OVAR ?= onotval
+
+other: OVAR ?= ooval
+other: LATERVAR ?= lateroverride
+
+LATERVAR = olater
+
+other:
+	test "$(OVAR)" = "oval"
+	test "$(LATERVAR)" = "lateroverride"
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/var-substitutions.mk
@@ -0,0 +1,49 @@
+SIMPLEVAR = aabb.cc
+SIMPLEPERCENT = test_value%extra
+
+SIMPLE3SUBSTNAME = SIMPLEVAR:.dd
+$(SIMPLE3SUBSTNAME) = weirdval
+
+PERCENT = dummy
+
+SIMPLESUBST = $(SIMPLEVAR:.cc=.dd)
+SIMPLE2SUBST = $(SIMPLEVAR:.cc)
+SIMPLE3SUBST = $(SIMPLEVAR:.dd)
+SIMPLE4SUBST = $(SIMPLEVAR:.cc=.dd=.ee)
+SIMPLE5SUBST = $(SIMPLEVAR:.cc=%.dd)
+PERCENTSUBST = $(SIMPLEVAR:%.cc=%.ee)
+PERCENT2SUBST = $(SIMPLEVAR:aa%.cc=ff%.f)
+PERCENT3SUBST = $(SIMPLEVAR:aa%.dd=gg%.gg)
+PERCENT4SUBST = $(SIMPLEVAR:aa%.cc=gg)
+PERCENT5SUBST = $(SIMPLEVAR:aa)
+PERCENT6SUBST = $(SIMPLEVAR:%.cc=%.dd=%.ee)
+PERCENT7SUBST = $(SIMPLEVAR:$(PERCENT).cc=%.dd)
+PERCENT8SUBST = $(SIMPLEVAR:%.cc=$(PERCENT).dd)
+PERCENT9SUBST = $(SIMPLEVAR:$(PERCENT).cc=$(PERCENT).dd)
+PERCENT10SUBST = $(SIMPLEVAR:%%.bb.cc=zz.bb.cc)
+PERCENT11SUBST = $(SIMPLEPERCENT:test%value%extra=other%value%extra)
+
+SPACEDVAR = $(NULL)  ex1.c ex2.c $(NULL)
+SPACEDSUBST = $(SPACEDVAR:.c=.o)
+
+all:
+	test "$(SIMPLESUBST)" = "aabb.dd"
+	test "$(SIMPLE2SUBST)" = ""
+	test "$(SIMPLE3SUBST)" = "weirdval"
+	test "$(SIMPLE4SUBST)" = "aabb.dd=.ee"
+	test "$(SIMPLE5SUBST)" = "aabb%.dd"
+	test "$(PERCENTSUBST)" = "aabb.ee"
+	test "$(PERCENT2SUBST)" = "ffbb.f"
+	test "$(PERCENT3SUBST)" = "aabb.cc"
+	test "$(PERCENT4SUBST)" = "gg"
+	test "$(PERCENT5SUBST)" = ""
+	test "$(PERCENT6SUBST)" = "aabb.dd=%.ee"
+	test "$(PERCENT7SUBST)" = "aabb.dd"
+	test "$(PERCENT8SUBST)" = "aabb.dd"
+	test "$(PERCENT9SUBST)" = "aabb.dd"
+	test "$(PERCENT10SUBST)" = "aabb.cc"
+	test "$(PERCENT11SUBST)" = "other_value%extra"
+	test "$(SPACEDSUBST)" = "ex1.o ex2.o"
+	@echo TEST-PASS
+
+PERCENT = %
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/vpath-directive-dynamic.mk
@@ -0,0 +1,12 @@
+$(shell \
+mkdir subd1; \
+touch subd1/test.in; \
+)
+
+VVAR = %.in subd1
+
+vpath $(VVAR)
+
+all: test.in
+	test "$<" = "subd1/test.in"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/vpath-directive.mk
@@ -0,0 +1,34 @@
+ifdef __WIN32__
+VPSEP = ;
+else
+VPSEP = :
+endif
+
+$(shell \
+mkdir subd1 subd2 subd3; \
+printf "reallybaddata" >subd1/foo.in; \
+printf "gooddata" >subd2/foo.in; \
+printf "baddata" >subd3/foo.in; \
+touch subd1/foo.in2 subd2/foo.in2 subd3/foo.in2; \
+)
+
+vpath %.in subd
+
+vpath
+vpath %.in subd2$(VPSEP)subd3
+
+vpath %.in2 subd0
+vpath f%.in2 subd1
+vpath %.in2 $(VPSEP)subd2
+
+%.out: %.in
+	test "$<" = "subd2/foo.in"
+	cp $< $@
+
+%.out2: %.in2
+	test "$<" = "subd1/foo.in2"
+	cp $< $@
+
+all: foo.out foo.out2
+	test "$$(cat foo.out)" = "gooddata"
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/vpath.mk
@@ -0,0 +1,18 @@
+VPATH = foo bar
+
+$(shell \
+mkdir foo; touch foo/tfile1; \
+mkdir bar; touch bar/tfile2 bar/tfile3 bar/test.objtest; \
+sleep 2; \
+touch bar/test.source; \
+)
+
+all: tfile1 tfile2 tfile3 test.objtest test.source
+	test "$^" = "foo/tfile1 bar/tfile2 tfile3 test.objtest bar/test.source"
+	@echo TEST-PASS
+
+tfile3: test.objtest
+
+%.objtest: %.source
+	test "$<" = bar/test.source
+	test "$@" = test.objtest
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/wildcards.mk
@@ -0,0 +1,22 @@
+$(shell \
+mkdir foo; \
+touch a.c b.c c.out foo/d.c; \
+sleep 2; \
+touch c.in; \
+)
+
+VPATH = foo
+
+all: c.out prog
+	cat $<
+	test "$$(cat $<)" = "remadec.out"
+	@echo TEST-PASS
+
+*.out: %.out: %.in
+	test "$@" = c.out
+	test "$<" = c.in
+	printf "remade$@" >$@
+
+prog: *.c
+	test "$^" = "a.c b.c"
+	touch $@