Update pymake to tip of my repo, NPODB.
authorBenjamin Smedberg <benjamin@smedbergs.us>
Thu, 26 Mar 2009 16:24:51 -0400
changeset 26608 1197710c68e7b92e4a047f3401c236f5a3544389
parent 26607 6b3cc966ef5268b399bb0b75ef32e953a6e57004
child 26609 136b80e1082b0f3d8a9ef5ccb42ae5bfb3689c9b
push id6143
push userbsmedberg@mozilla.com
push dateThu, 26 Mar 2009 20:25:14 +0000
treeherdermozilla-central@1197710c68e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone1.9.2a1pre
Update pymake to tip of my repo, NPODB.
build/pymake/.hg_archival.txt
build/pymake/make.py
build/pymake/mkparse.py
build/pymake/pymake/command.py
build/pymake/pymake/data.py
build/pymake/pymake/functions.py
build/pymake/pymake/parser.py
build/pymake/pymake/parserdata.py
build/pymake/pymake/process.py
build/pymake/tests/cmdgoals.mk
build/pymake/tests/continuations-in-functions.mk
build/pymake/tests/doublecolon-priordeps.mk
build/pymake/tests/notparallel.mk
build/pymake/tests/parallel-dep-resolution2.mk
build/pymake/tests/parallel-rule-execution.mk
build/pymake/tests/parallel-simple.mk
build/pymake/tests/parsertests.py
build/pymake/tests/phony.mk
build/pymake/tests/runtests.py
build/pymake/tests/serial-doublecolon-execution.mk
build/pymake/tests/serial-toparallel.mk
build/pymake/tests/shellfunc.mk
build/pymake/tests/unexport.mk
build/pymake/tests/unexport.submk
--- a/build/pymake/.hg_archival.txt
+++ b/build/pymake/.hg_archival.txt
@@ -1,2 +1,2 @@
 repo: f5ab154deef2ffa97f1b2139589ae4a1962090a4
-node: b48239b1191839491b6e37c03fcdfcd62024c745
+node: c07f9fe9fdaa85888314efa7001cd4fd768904c4
--- a/build/pymake/make.py
+++ b/build/pymake/make.py
@@ -4,11 +4,11 @@
 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.command.main(sys.argv[1:], os.environ, os.getcwd(), cb=sys.exit)
 pymake.process.ParallelContext.spin()
 assert False, "Not reached"
--- a/build/pymake/mkparse.py
+++ b/build/pymake/mkparse.py
@@ -1,8 +1,11 @@
+#!/usr/bin/env python
+
 import sys
 import pymake.parser
 
 for f in sys.argv[1:]:
     print "Parsing %s" % f
     fd = open(f)
     stmts = pymake.parser.parsestream(fd, f)
+    fd.close()
     print stmts
--- a/build/pymake/pymake/command.py
+++ b/build/pymake/pymake/command.py
@@ -66,17 +66,17 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO TH
 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):
+def main(args, env, cwd, 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'))
@@ -129,27 +129,17 @@ def main(args, env, cwd, context, cb):
             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
+        context = process.getcontext(options.jobcount)
 
         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')
@@ -162,19 +152,16 @@ def main(args, env, cwd, context, cb):
 
         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)
@@ -183,17 +170,18 @@ def main(args, env, cwd, context, cb):
                                                                                   
 
         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)
+                                         context=context, env=env,
+                                         targets=targets)
 
                 try:
                     overrides.execute(makefile)
                     for f in options.makefiles:
                         makefile.include(f)
                     makefile.finishparsing()
                     makefile.remakemakefiles(process.makedeferrable(remakecb, restarts=restarts + 1, makefile=makefile))
 
--- a/build/pymake/pymake/data.py
+++ b/build/pymake/pymake/data.py
@@ -57,23 +57,23 @@ 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
+    __slots__ = ('loc', 's',)
     simple = True
     
-    def __init__(self, s):
+    def __init__(self, s, loc):
         assert isinstance(s, str)
         self.s = s
+        self.loc = loc
 
     def lstrip(self):
         self.s = self.s.lstrip()
 
     def rstrip(self):
         self.s = self.s.rstrip()
 
     def isempty(self):
@@ -84,44 +84,47 @@ class StringExpansion(object):
 
     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 = Expansion(self.loc)
         e.appendstr(self.s)
         return e
 
     def __len__(self):
         return 1
 
     def __getitem__(self, i):
         assert i == 0
         return self.s, False
 
+    def __str__(self):
+        return "Exp<%s>(%r)" % (self.loc, self.s)
+
 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 fromstring(s, path):
+        return StringExpansion(s, parserdata.Location(path, 1, 0))
 
     def clone(self):
         e = Expansion()
         e.extend(self)
         return e
 
     def appendstr(self, s):
         assert isinstance(s, str)
@@ -173,17 +176,17 @@ class Expansion(list):
                 return
 
             del self[-1]
 
     def finish(self):
         if self.hasfunc:
             return self
 
-        return StringExpansion(''.join([i for i, isfunc in self]))
+        return StringExpansion(''.join([i for i, isfunc in self]), self.loc)
 
     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
@@ -278,17 +281,17 @@ class Variables(object):
                     return pflavor, psource, pvalue
                     
             if not expand:
                 return flavor, source, valuestr
 
             if flavor == self.FLAVOR_RECURSIVE:
                 val = valueexp
             else:
-                val = Expansion.fromstring(valuestr)
+                val = Expansion.fromstring(valuestr, "Expansion of variable '%s'" % (name,))
 
             return flavor, source, val
 
         if self.parent is not None:
             return self.parent.get(name, expand)
 
         return (None, None, None)
 
@@ -471,16 +474,138 @@ class Pattern(object):
             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 RemakeRuleContext(object):
+    __slots__ = ('rule', 'deps', 'depsremaining', 'error', 'didanything', 'running')
+
+    def __init__(self, rule, deps):
+        self.rule = rule
+        self.deps = deps
+        self.running = False
+        self.depsremaining = len(deps) + 1
+
+    def resolvedeps(self, target, makefile, targetstack, rulestack, serial, cb):
+        if serial:
+            self._resolvedepsserial(target, makefile, targetstack, rulestack, cb)
+        else:
+            self._resolvedepsparallel(target, makefile, targetstack, rulestack, cb)
+
+    def _resolvedepsserial(self, target, makefile, targetstack, rulestack, cb):
+        resolvelist = list(self.deps)
+        self.didanything = False
+
+        def depfinished(error, didanything):
+            if error is not None:
+                cb(error=error, didanything=None)
+                return
+
+            if didanything:
+                self.didanything = True
+            
+            if len(resolvelist):
+                makefile.context.defer(resolvelist.pop(0).make, makefile, targetstack, rulestack, depfinished)
+            else:
+                cb(error=None, didanything=self.didanything)
+
+        depfinished(None, False)
+
+    def _resolvedepsparallel(self, target, makefile, targetstack, rulestack, cb):
+        self.depsremaining -= 1
+        if self.depsremaining == 0:
+            cb(error=None, didanything=False)
+            return
+
+        self.error = None
+        self.didanything = False
+
+        def startdep(d):
+            if self.error is not None:
+                depfinished(None, False)
+            else:
+                d.make(makefile, targetstack, rulestack, depfinished)
+
+        def depfinished(error, didanything):
+            if error is not None:
+                if self.error is None:
+                    self.error = error
+            elif didanything:
+                self.didanything = True
+
+            self.depsremaining -= 1
+            
+            if self.depsremaining == 0:
+                cb(error=self.error, didanything=self.didanything)
+
+        for d in self.deps:
+            makefile.context.defer(startdep, d)
+
+    def runcommands(self, target, makefile, avoidremakeloop, indent, cb):
+        assert not self.running
+        self.running = True
+        if self.rule is None or not len(self.rule.commands):
+            if target.mtime is None:
+                target._beingremade()
+            else:
+                for d in self.deps:
+                    if mtimeislater(d.mtime, target.mtime):
+                        target._beingremade()
+                        break
+            cb(error=None)
+            return
+
+        def commandcb(error):
+            if error is not None:
+                cb(error=error)
+                return
+
+            if len(commands):
+                commands.pop(0)(commandcb)
+            else:
+                cb(error=None)
+
+        remake = False
+        if target.mtime is None:
+            remake = True
+            _log.info("%sRemaking %s using rule at %s: target doesn't exist or is a forced target", indent, target.target, self.rule.loc)
+
+        if not remake:
+            if self.rule.doublecolon:
+                if len(self.deps) == 0:
+                    if avoidremakeloop:
+                        _log.info("%sNot remaking %s using rule at %s because it would introduce an infinite loop.", indent, target.target, self.rule.loc)
+                    else:
+                        _log.info("%sRemaking %s using rule at %s because there are no prerequisites listed for a double-colon rule.", indent, target.target, self.rule.loc)
+                        remake = True
+
+        if not remake:
+            for d in self.deps:
+                if mtimeislater(d.mtime, target.mtime):
+                    _log.info("%sRemaking %s using rule at %s because %s is newer.", indent, target.target, self.rule.loc, d.target)
+                    remake = True
+                    break
+
+        if remake:
+            target._beingremade()
+            target._didanything = True
+            try:
+                commands = [c for c in self.rule.getcommands(target, makefile)]
+            except util.MakeError, e:
+                cb(error=e)
+                return
+
+            commandcb(None)
+        else:
+            cb(error=None)
+
 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
@@ -509,17 +634,17 @@ class Target(object):
 
         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)
+        return makefile.gettarget('.PHONY').hasdependency(self.target)
 
     def hasdependency(self, t):
         for rule in self.rules:
             if t in rule.prerequisites:
                 return True
 
         return False
 
@@ -770,182 +895,124 @@ class Target(object):
 
         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]
+            rulelist = [RemakeRuleContext(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)]
+            rulelist = [RemakeRuleContext(commandrule, alldeps)]
 
         targetstack = targetstack + [self.target]
 
         if serial:
-            rulefinished()
+            def resolvecb(error, didanything):
+                if error is not None:
+                    self._makeerror = error
+                    self._notifydone(makefile)
+                    return
+
+                if didanything:
+                    self._didanything = True
+                rulelist.pop(0).runcommands(self, makefile, avoidremakeloop, indent, commandscb)
+
+            def commandscb(error):
+                if error is not None:
+                    self._makeerror = error
+                    self._notifydone(makefile)
+                    return
+
+                if not len(rulelist):
+                    self._notifydone(makefile)
+                    return
+
+                rulelist[0].resolvedeps(self, makefile, targetstack, rulestack, serial, resolvecb)
+
+            commandscb(None)
         else:
-            makeclosure.rulesremaining = len(rulelist)
-            for r, deps in rulelist:
-                makefile.context.defer(startrule, r, deps)
+            def doresolve(r):
+                if self._makeerror is not None:
+                    resolvecb(None, False)
+                else:
+                    r.resolvedeps(self, makefile, targetstack, rulestack, serial, resolvecb)
+
+            def resolvecb(error, didanything):
+                if error is not None:
+                    if self._makeerror is None:
+                        self._makeerror = error
+                elif didanything:
+                    self._didanything = didanything
+
+                if self._makeerror is not None:
+                    r = rulelist.pop()
+                    assert not r.running
+                    if not len(rulelist):
+                        self._notifydone(makefile)
+                    return
+
+                rtop = rulelist[0]
+                if rtop.running or rtop.depsremaining != 0:
+                    return
+
+                rtop.runcommands(self, makefile, avoidremakeloop, indent, commandscb)
+
+            def commandscb(error):
+                if error is not None:
+                    if self._makeerror is None:
+                        self._makeerror = error
+
+                r = rulelist.pop(0)
+                assert r.running
+
+                if not len(rulelist):
+                    self._notifydone(makefile)
+                    return
+
+                if self._makeerror is not None:
+                    return
+
+                rtop = rulelist[0]
+                if rtop.running or rtop.depsremaining != 0:
+                    return
+
+                rtop.runcommands(self, makefile, avoidremakeloop, indent, commandscb)
+
+            for r in rulelist:
+                makefile.context.defer(doresolve, r)
+
 
 def dirpart(p):
     d, s, f = util.strrpartition(p, '/')
     if d == '':
         return '.'
 
     return d
 
@@ -1146,28 +1213,28 @@ class PatternRule(object):
         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):
+    def __init__(self, workdir=None, env=None, restarts=0, make=None, makeflags=None, makelevel=0, context=None, targets=()):
         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.exportedvars = {}
         self.overrides = []
         self._targets = {}
         self._patternvariables = [] # of (pattern, variables)
         self.implicitrules = []
         self.parsingfinished = False
 
         self._patternvpaths = [] # of (pattern, [dir, ...])
 
@@ -1191,16 +1258,19 @@ class Makefile(object):
         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))
 
+        self.variables.set('MAKECMDGOALS', Variables.FLAVOR_SIMPLE,
+                           Variables.SOURCE_AUTOMATIC, ' '.join(targets))
+
         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.
@@ -1270,16 +1340,20 @@ class Makefile(object):
 
         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
 
+        np = self.gettarget('.NOTPARALLEL')
+        if len(np.rules):
+            self.context = process.getcontext(1)
+
     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)
@@ -1360,23 +1434,26 @@ class Makefile(object):
 
             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 = ''
+        for vname, v in self.exportedvars.iteritems():
+            if v:
+                flavor, source, val = variables.get(vname)
+                if val is None:
+                    strval = ''
+                else:
+                    strval = val.resolvestr(self, variables, [vname])
+                env[vname] = strval
             else:
-                strval = val.resolvestr(self, variables, [vname])
-            env[vname] = strval
+                env.pop(vname, None)
 
         makeflags = ''
 
         flavor, source, val = variables.get('MAKEFLAGS')
         if val is not None:
             flagsval = val.resolvestr(self, variables, ['MAKEFLAGS'])
             if flagsval != '':
                 makeflags = flagsval
--- a/build/pymake/pymake/functions.py
+++ b/build/pymake/pymake/functions.py
@@ -16,16 +16,19 @@ class Function(object):
 
     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
     """
+
+    __slots__ = ('_arguments', 'loc')
+
     def __init__(self, loc):
         self._arguments = []
         self.loc = loc
         assert self.minargs > 0
 
     def __getitem__(self, key):
         return self._arguments[key]
 
@@ -40,16 +43,18 @@ class Function(object):
     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):
+    __slots__ = ('vname', 'loc')
+
     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"
 
@@ -62,16 +67,19 @@ class VariableRef(Function):
         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)"""
+
+    __slots__ = ('loc', 'vname', 'substfrom', 'substto')
+
     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"
@@ -97,108 +105,126 @@ class SubstitutionRef(Function):
         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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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))
@@ -210,34 +236,40 @@ class WordlistFunction(Function):
 
         util.joiniter(fd, words[nfrom - 1:nto])
 
 class WordsFunction(Function):
     name = 'words'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
@@ -258,41 +290,47 @@ class DirFunction(Function):
         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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     @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
 
+    __slots__ = Function.__slots__
+
     @staticmethod
     def basenames(words):
         for w in words:
             dir, file = pathsplit(w, '')
             base, dot, suffix = util.strrpartition(file, '.')
             if dot == '':
                 base = suffix
 
@@ -301,16 +339,18 @@ class BasenameFunction(Function):
     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
 
+    __slots__ = Function.__slots__
+
     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
@@ -321,16 +361,18 @@ class AddPrefixFunction(Function):
 
         fd.write(' '.join([prefix + w for w in self._arguments[1].resolvesplit(makefile, variables, setting)]))
 
 class JoinFunction(Function):
     name = 'join'
     minargs = 2
     maxargs = 2
 
+    __slots__ = Function.__slots__
+
     @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):
@@ -339,47 +381,55 @@ class JoinFunction(Function):
 
         util.joiniter(fd, self.iterjoin(list1, list2))
 
 class WildcardFunction(Function):
     name = 'wildcard'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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)]))
 
+    __slots__ = Function.__slots__
+
 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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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)
 
@@ -388,43 +438,49 @@ class IfFunction(Function):
         elif len(self._arguments) > 2:
             return self._arguments[2].resolve(makefile, variables, fd, setting)
 
 class OrFunction(Function):
     name = 'or'
     minargs = 1
     maxargs = 0
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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):
@@ -436,16 +492,18 @@ class ForEachFunction(Function):
             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
 
+    __slots__ = Function.__slots__
+
     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)):
@@ -463,16 +521,18 @@ class CallFunction(Function):
         # but we'll do it anyway
         e.resolve(makefile, v, fd, setting + [vname])
 
 class ValueFunction(Function):
     name = 'value'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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):
@@ -490,16 +550,18 @@ class EvalFunction(Function):
         stmts = parser.parsestream(text, 'evaluation from %s' % self.loc)
         stmts.execute(makefile)
 
 class OriginFunction(Function):
     name = 'origin'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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'
@@ -515,16 +577,18 @@ class OriginFunction(Function):
 
         fd.write(r)
 
 class FlavorFunction(Function):
     name = 'flavor'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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'
@@ -532,57 +596,65 @@ class FlavorFunction(Function):
             r = 'simple'
         fd.write(r)
 
 class ShellFunction(Function):
     name = 'shell'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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':
+        if stdout.endswith('\n'):
             stdout = stdout[:-1]
         stdout = stdout.replace('\n', ' ')
 
         fd.write(stdout)
 
 class ErrorFunction(Function):
     name = 'error'
     minargs = 1
     maxargs = 1
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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
 
+    __slots__ = Function.__slots__
+
     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,
--- a/build/pymake/pymake/parser.py
+++ b/build/pymake/pymake/parser.py
@@ -37,79 +37,37 @@ 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')
+    __slots__ = ('data', 'startloc')
 
-    def __init__(self):
-        self.data = ""
-
-        # _offsets and _locs are matched lists
-        self._offsets = []
-        self._locs = []
+    def __init__(self, startloc, data):
+        self.data = data
+        self.startloc = startloc
 
     @staticmethod
-    def fromstring(str, loc):
-        d = Data()
-        d.append(str, loc)
-        return d
+    def fromstring(str, path):
+        return Data(parserdata.Location(path, 1, 0), str)
 
-    def append(self, data, loc):
-        self._offsets.append(len(self.data))
-        self._locs.append(loc)
+    def append(self, data):
         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)
+        return self.startloc + self.data[: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():
@@ -137,25 +95,30 @@ class Data(object):
                 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.
     """
+    __slots__ = Data.__slots__ + ('lineiter',)
+
     def __init__(self, lineiter, path):
-        Data.__init__(self)
-        self.lineiter = lineiter
-        self.path = path
+        try:
+            lineno, line = lineiter.next()
+            Data.__init__(self, parserdata.Location(path, lineno + 1, 0), line)
+            self.lineiter = lineiter
+        except StopIteration:
+            self.data = None
 
     def readline(self):
         try:
             lineno, line = self.lineiter.next()
-            self.append(line, parserdata.Location(self.path, lineno, 0))
+            self.append(line)
             return True
         except StopIteration:
             return False
 
 _makefiletokensescaped = [r'\\\\#', r'\\#', '\\\\\n', '\\\\\\s+\\\\\n', r'\\.', '#', '\n']
 _continuationtokensescaped = ['\\\\\n', r'\\.', '\n']
 
 class TokenList(object):
@@ -221,17 +184,17 @@ def iterdata(d, offset, tokenlist):
         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):
+def itermakefilechars(d, offset, tokenlist, ignorecomments=False):
     """
     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):
@@ -245,16 +208,21 @@ def itermakefilechars(d, offset, tokenli
         end = m.end(0)
 
         if token == '\n':
             assert end == len(d.data)
             yield d.data[offset:start], None, None, None
             return
 
         if token == '#':
+            if ignorecomments:
+                yield d.data[offset:end], None, None, None
+                offset = end
+                continue
+
             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
@@ -374,17 +342,17 @@ def iterdefinechars(d, offset, tokenlist
 
         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)
+            assert end == len(d.data), "end: %r len(d.data): %r" % (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('\\'):
@@ -499,17 +467,19 @@ def parsefile(pathname):
         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)
+    fd = open(pathname, "rU")
+    stmts = parsestream(fd, pathname)
+    fd.close()
     _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.
 
@@ -520,17 +490,17 @@ def parsestream(fd, filename):
     condstack = [parserdata.StatementList()]
 
     fdlines = enumerate(fd)
 
     while True:
         assert len(condstack) > 0
 
         d = DynamicData(fdlines, filename)
-        if not d.readline():
+        if d.data is None:
             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
@@ -577,20 +547,21 @@ def parsestream(fd, filename):
             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()
+                startpos = len(d.data)
+                if not d.readline():
+                    raise SyntaxError("Unterminated define", d.getloc())
 
-                value = _iterflatten(iterdefinechars, d, 0)
+                value = _iterflatten(iterdefinechars, d, startpos)
                 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'))
 
@@ -628,17 +599,19 @@ def parsestream(fd, filename):
                     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))
+                e, token, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+                condstack[-1].append(parserdata.UnexportDirective(e))
+                continue
 
             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():
@@ -803,28 +776,24 @@ def parsemakesyntax(d, startat, stopon, 
                     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()
+                    tokenlist = TokenList.get((':', c, closebrace, '$'))
+                    stacktop = ParseStackFrame(_PARSESTATE_VARNAME, stacktop,
+                                               e, tokenlist,
+                                               openbrace=c, closebrace=closebrace, loc=loc)
             else:
-                e = data.Expansion.fromstring(c)
+                e = data.Expansion.fromstring(c, loc)
                 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,
@@ -889,16 +858,20 @@ def parsemakesyntax(d, startat, stopon, 
 
             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 and iterfunc == itercommandchars:
+            di = itermakefilechars(d, offset, stacktop.tokenlist,
+                                   ignorecomments=True)
+        else:
+            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
--- a/build/pymake/pymake/parserdata.py
+++ b/build/pymake/pymake/parserdata.py
@@ -18,35 +18,46 @@ class Location(object):
 
     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
+        Returns a new location offset by
         the specified string.
         """
-        column = self.column
+
+        if data == '':
+            return self
+        
+        skiplines = data.count('\n')
+        line = self.line + skiplines
+        if skiplines:
+            lastnl = data.rfind('\n')
+            assert lastnl != -1
+            data = data[lastnl + 1:]
+            column = 0
+        else:
+            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)
+        return Location(self.path, 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
@@ -68,17 +79,17 @@ def parsecommandlineargs(args):
 
         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)
+            vnameexp = data.Expansion.fromstring(vname, "Command-line argument")
 
             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
@@ -87,30 +98,36 @@ 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):
+    __slots__ = ('s',)
+
     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,)
+        print >>fd, "%sOverride: %s" % (indent, self.s)
 
 class DummyRule(object):
+    __slots__ = ()
+
     def addcommand(self, r):
         pass
 
 class Rule(Statement):
+    __slots__ = ('targetexp', 'depexp', 'doublecolon')
+
     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
 
@@ -135,19 +152,21 @@ class Rule(Statement):
             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)
+        print >>fd, "%sRule %s: %s" % (indent, self.targetexp, self.depexp)
 
 class StaticPatternRule(Statement):
+    __slots__ = ('targetexp', 'patternexp', 'depexp', 'doublecolon')
+
     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
@@ -176,31 +195,35 @@ class StaticPatternRule(Statement):
             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)
+        print >>fd, "%sStaticPatternRule %s: %s: %s" % (indent, self.targetexp, self.patternexp, self.depexp)
 
 class Command(Statement):
+    __slots__ = ('exp',)
+
     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,)
+        print >>fd, "%sCommand %s" % (indent, self.exp,)
 
 class SetVariable(Statement):
+    __slots__ = ('vnameexp', 'token', 'value', 'valueloc', 'targetexp', 'source')
+
     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
 
@@ -248,73 +271,79 @@ class SetVariable(Statement):
                 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)
+        print >>fd, "%sSetVariable<%s> %s %s\n%s %r" % (indent, self.valueloc, self.vnameexp, self.token, indent, 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
+    __slots__ = ('exp1', 'exp2', 'expected')
 
     def __init__(self, exp1, exp2):
         assert isinstance(exp1, (data.Expansion, data.StringExpansion))
         assert isinstance(exp2, (data.Expansion, data.StringExpansion))
 
+        self.expected = True
         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)
+        return "ifeq (expected=%s) %s %s" % (self.expected, self.exp1, self.exp2)
 
 class IfdefCondition(Condition):
-    expected = True
+    __slots__ = ('exp', 'expected')
 
     def __init__(self, exp):
         assert isinstance(exp, (data.Expansion, data.StringExpansion))
         self.exp = exp
+        self.expected = True
 
     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)
+        return "ifdef (expected=%s) %s" % (self.expected, self.exp)
 
 class ElseCondition(Condition):
+    __slots__ = ()
+
     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.
     """
+    __slots__ = ('loc', '_groups')
+
     def __init__(self, loc, condition):
         self.loc = loc
         self._groups = []
         self.addcondition(loc, condition)
 
     def getloc(self):
         return self._groups[0][0].loc
 
@@ -335,41 +364,44 @@ class ConditionBlock(Statement):
             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"
+        print >>fd, "%sConditionBlock" % (indent,)
 
-        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"
+            print >>fd, "%s Condition %s" % (indent, c)
+            statements.dump(fd, indent2)
+            print >>fd, "%s ~Condition" % (indent,)
+        print >>fd, "%s~ConditionBlock" % (indent,)
 
 class Include(Statement):
+    __slots__ = ('exp', 'required')
+
     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,)
+        print >>fd, "%sInclude %s" % (indent, self.exp)
 
 class VPathDirective(Statement):
+    __slots__ = ('exp',)
+
     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()
@@ -383,62 +415,80 @@ class VPathDirective(Statement):
                 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,)
+        print >>fd, "%sVPath %s" % (indent, self.exp)
 
 class ExportDirective(Statement):
+    __slots__ = ('exp', 'single')
+
     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)
+            makefile.exportedvars[v] = True
 
     def dump(self, fd, indent):
-        print >>fd, indent, "Export (single=%s) %r" % (self.single, self.exp)
+        print >>fd, "%sExport (single=%s) %s" % (indent, self.single, self.exp)
+
+class UnexportDirective(Statement):
+    __slots__ = ('exp',)
+
+    def __init__(self, exp):
+        self.exp = exp
+
+    def execute(self, makefile, context):
+        vlist = list(self.exp.resolvesplit(makefile, makefile.variables))
+        for v in vlist:
+            makefile.exportedvars[v] = False
 
 class EmptyDirective(Statement):
+    __slots__ = ('exp',)
+
     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
+        print >>fd, "%sEmptyDirective: %s" % (indent, self.exp)
 
 class StatementList(list):
+    __slots__ = ()
+
     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 dump(self, fd, indent):
+        for s in self:
+            s.dump(fd, indent)
+
     def __str__(self):
         fd = StringIO()
-        print >>fd, "StatementList"
-        for s in self:
-            s.dump(fd, ' ')
-        print >>fd, "~StatementList"
+        self.dump(fd, '')
         return fd.getvalue()
--- a/build/pymake/pymake/process.py
+++ b/build/pymake/pymake/process.py
@@ -59,22 +59,22 @@ def call(cline, env, cwd, loc, cb, conte
         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)
+        command.main(argv[1:], env, cwd, cb)
         return
 
     if argv[0:2] == [sys.executable.replace('\\', '/'),
                      command.makepypath.replace('\\', '/')]:
-        command.main(argv[2:], env, cwd, context, cb)
+        command.main(argv[2:], env, cwd, 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)
@@ -84,20 +84,16 @@ 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):
@@ -198,15 +194,32 @@ class ParallelContext(object):
                         p, cb = c.running[i]
                         if ParallelContext._comparepid(pid, p):
                             del c.running[i]
                             cb(result)
                             found = True
                             break
 
                     if found: break
+            else:
+                assert any(len(c.pending) for c in ParallelContext._allcontexts)
 
 def makedeferrable(usercb, **userkwargs):
     def cb(*args, **kwargs):
         kwargs.update(userkwargs)
         return usercb(*args, **kwargs)
 
     return cb
+
+_serialContext = None
+_parallelContext = None
+
+def getcontext(jcount):
+    global _serialContext, _parallelContext
+    if jcount == 1:
+        if _serialContext is None:
+            _serialContext = ParallelContext(1)
+        return _serialContext
+    else:
+        if _parallelContext is None:
+            _parallelContext = ParallelContext(jcount)
+        return _parallelContext
+
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/cmdgoals.mk
@@ -0,0 +1,9 @@
+default:
+	test "$(MAKECMDGOALS)" = ""
+	$(MAKE) -f $(TESTPATH)/cmdgoals.mk t1   t2
+	@echo TEST-PASS
+
+t1:
+	test "$(MAKECMDGOALS)" = "t1 t2"
+
+t2:
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/continuations-in-functions.mk
@@ -0,0 +1,7 @@
+all:
+	test 'Hello world.' = '$(if 1,Hello \
+	  world.)'
+	test '(Hello \
+	  world.)' = '(Hello \
+	  world.)'
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/doublecolon-priordeps.mk
@@ -0,0 +1,19 @@
+#T commandline: ['-j3']
+
+# All *prior* dependencies of a doublecolon rule must be satisfied before
+# subsequent commands are run.
+
+all:: target1
+
+all:: target2
+	test -f target1
+	@echo TEST-PASS
+
+target1:
+	touch starting-$@
+	sleep 1
+	touch $@
+
+target2:
+	sleep 0.1
+	test -f starting-target1
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/notparallel.mk
@@ -0,0 +1,8 @@
+#T commandline: ['-j3']
+
+include $(TESTPATH)/serial-rule-execution.mk
+
+all::
+	$(MAKE) -f $(TESTPATH)/parallel-simple.mk
+
+.NOTPARALLEL:
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/parallel-dep-resolution2.mk
@@ -0,0 +1,9 @@
+#T commandline: ['-j3']
+#T returncode: 2
+
+all::
+	sleep 1
+	touch somefile
+
+all:: somefile
+	@echo TEST-PASS
deleted file mode 100644
--- a/build/pymake/tests/parallel-rule-execution.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-#T commandline: ['-j3']
-#T returncode: 2
-
-all::
-	sleep 1
-	touch somefile
-
-all:: somefile
-	@echo TEST-PASS
--- a/build/pymake/tests/parallel-simple.mk
+++ b/build/pymake/tests/parallel-simple.mk
@@ -1,10 +1,12 @@
 #T commandline: ['-j2']
 
+# CAUTION: this makefile is also used by serial-toparallel.mk
+
 define SLOWMAKE
 printf "$@:0:" >>results
 sleep 0.5
 printf "$@:1:" >>results
 sleep 0.5
 printf "$@:2:" >>results
 endef
 
--- a/build/pymake/tests/parsertests.py
+++ b/build/pymake/tests/parsertests.py
@@ -13,33 +13,34 @@ def multitest(cls):
     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))),
-    )
+    testdata = {
+        'oneline':
+            ("He\tllo", "f", 1, 0,
+             ((0, "f", 1, 0), (2, "f", 1, 2), (3, "f", 1, 4))),
+        'twoline':
+            ("line1 \n\tl\tine2", "f", 1, 4,
+             ((0, "f", 1, 4), (5, "f", 1, 9), (6, "f", 1, 10), (7, "f", 2, 0), (8, "f", 2, 4), (10, "f", 2, 8), (13, "f", 2, 11))),
+    }
 
-    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))
+    def runSingle(self, data, filename, line, col, results):
+        d = pymake.parser.Data(pymake.parserdata.Location(filename, line, col),
+                               data)
+        for pos, file, lineno, col in results:
+            loc = d.getloc(pos)
+            self.assertEqual(loc.path, file, "data file offset %i" % pos)
+            self.assertEqual(loc.line, lineno, "data line offset %i" % pos)
+            self.assertEqual(loc.column, col, "data col offset %i" % pos)
+multitest(DataTest)
 
 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),
         }
@@ -128,17 +129,16 @@ endef\n""",
         ),
     }
 
     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):
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/phony.mk
@@ -0,0 +1,10 @@
+$(shell \
+touch dep; \
+sleep 2; \
+touch all; \
+)
+
+all:: dep
+	@echo TEST-PASS
+
+.PHONY: all
--- a/build/pymake/tests/runtests.py
+++ b/build/pymake/tests/runtests.py
@@ -6,16 +6,17 @@ For each test, we run gmake -f test.mk. 
 
 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}
+#T environment: {'VAR': 'VALUE}
 """
 
 from subprocess import Popen, PIPE, STDOUT
 from optparse import OptionParser
 import os, re, sys, shutil
 
 thisdir = os.path.dirname(os.path.abspath(__file__))
 
@@ -58,37 +59,42 @@ for makefile in makefiles:
         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
 
+    env = dict(os.environ)
+
     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]
+        elif key == 'environment':
+            for k, v in data.iteritems():
+                env[k] = v
         else:
             print >>sys.stderr, "Unexpected #T key: %s" % key
             sys.exit(1)
 
     mdata.close()
 
-    p = Popen(cline, stdout=PIPE, stderr=STDOUT)
+    p = Popen(cline, stdout=PIPE, stderr=STDOUT, env=env)
     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:
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/serial-doublecolon-execution.mk
@@ -0,0 +1,18 @@
+#T commandline: ['-j3']
+
+# Commands of double-colon rules are always executed in order.
+
+all: dc
+	cat status
+	test "$$(cat status)" = "all1:all2:"
+	@echo TEST-PASS
+
+dc:: slowt
+	printf "all1:" >> status
+
+dc::
+	sleep 0.2
+	printf "all2:" >> status
+
+slowt:
+	sleep 1
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/serial-toparallel.mk
@@ -0,0 +1,5 @@
+all::
+	$(MAKE) -j2 -f $(TESTPATH)/parallel-simple.mk
+
+all:: results
+	@echo TEST-PASS
--- a/build/pymake/tests/shellfunc.mk
+++ b/build/pymake/tests/shellfunc.mk
@@ -1,6 +1,7 @@
 all: testfile
 	test "$(shell cat $<)" = "Hello world"
+	test "$(shell printf "\n")" = ""
 	@echo TEST-PASS
 
 testfile:
 	printf "Hello\nworld\n" > $@
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/unexport.mk
@@ -0,0 +1,15 @@
+#T environment: {'ENVVAR': 'envval'}
+
+VAR1 = val1
+VAR2 = val2
+VAR3 = val3
+
+unexport VAR3
+export VAR1 VAR2 VAR3
+unexport VAR2 ENVVAR
+unexport
+
+all:
+	test "$(ENVVAR)" = "envval" # unexport.mk
+	$(MAKE) -f $(TESTPATH)/unexport.submk
+	@echo TEST-PASS
new file mode 100644
--- /dev/null
+++ b/build/pymake/tests/unexport.submk
@@ -0,0 +1,15 @@
+# -@- Mode: Makefile -@- 
+
+unexport VAR1
+
+all:
+	env
+	test "$(VAR1)" = "val1"
+	test "$(origin VAR1)" = "environment"
+	test "$(VAR2)" = "" # VAR2
+	test "$(VAR3)" = "val3"
+	test "$(ENVVAR)" = ""
+	$(MAKE) -f $(TESTPATH)/unexport.submk subt
+
+subt:
+	test "$(VAR1)" = ""