Implement -C without actually issuing an os.chdir. This is preparation for same-process recursion.
authorBenjamin Smedberg <benjamin@smedbergs.us>
Wed, 18 Feb 2009 09:43:55 -0500
changeset 129 bc238aefc4d9405820d8f429534f2c86f2cf66dd
parent 128 450813b7b94a3b8f964efe88d80d9b136ffbf81b
child 130 67dc20b5210fd5dd636ca306af51f81d280ae5d1
push id72
push userbsmedberg@mozilla.com
push dateWed, 18 Feb 2009 14:44:04 +0000
Implement -C without actually issuing an os.chdir. This is preparation for same-process recursion.
make.py
pymake/data.py
pymake/functions.py
pymake/globrelative.py
pymake/parser.py
tests/file-functions.mk
--- a/make.py
+++ b/make.py
@@ -107,20 +107,16 @@ if options.debuglog:
 if options.jobcount:
     log.info("pymake doesn't implement -j yet. ignoring")
     shortflags.append('j%i' % options.jobcount)
 
 makeflags = ''.join(shortflags) + ' ' + ' '.join(longflags)
 
 logging.basicConfig(level=loglevel, **logkwargs)
 
-if options.directory:
-    log.info("Switching to directory: %s" % options.directory)
-    os.chdir(options.directory)
-    
 if options.printdir:
     print "make.py[%i]: Entering directory '%s'" % (makelevel, os.getcwd())
     sys.stdout.flush()
 
 if len(options.makefiles) == 0:
     if os.path.exists('Makefile'):
         options.makefiles.append('Makefile')
     else:
@@ -128,17 +124,17 @@ if len(options.makefiles) == 0:
         sys.exit(2)
 
 try:
     def parse():
         i = 0
 
         while True:
             m = Makefile(restarts=i, make='%s %s' % (sys.executable, sys.argv[0]),
-                         makeflags=makeflags, makelevel=makelevel)
+                         makeflags=makeflags, makelevel=makelevel, workdir=options.directory)
 
             starttime = time.time()
             targets = parsecommandlineargs(m, arguments)
             for f in options.makefiles:
                 m.include(f)
 
             log.info("Parsing[%i] took %f seconds" % (i, time.time() - starttime,))
 
--- a/pymake/data.py
+++ b/pymake/data.py
@@ -616,33 +616,35 @@ class Target(object):
                     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)
-                            mtime = getmtime(libpath)
+                            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)
                        for dir in makefile.getvpath(self.target)]
 
         for t in search:
-            mtime = getmtime(t)
+            fspath = os.path.join(makefile.workdir, t)
+            mtime = getmtime(fspath)
             if mtime is not None:
                 self.vpathtarget = t
                 self.mtime = mtime
                 return
 
         self.vpathtarget = self.target
         self.mtime = None
         
@@ -837,17 +839,17 @@ class Rule(object):
         for c in self.commands:
             cstring = c.resolve(makefile, v)
             for cline in splitcommand(cstring):
                 cline, isHidden, isRecursive, ignoreErrors = findmodifiers(cline)
                 if not len(cline) or cline.isspace():
                     continue
                 if not isHidden:
                     print "%s $ %s" % (c.loc, cline)
-                r = subprocess.call(cline, shell=True, env=env)
+                r = subprocess.call(cline, shell=True, env=env, cwd=makefile.workdir)
                 if r != 0 and not ignoreErrors:
                     raise DataError("command '%s' failed, return code was %s" % (cline, r), c.loc)
 
 class PatternRuleInstance(object):
     """
     This represents a pattern rule instance for a particular target. It has the same API as Rule, but
     different internals, forwarding most information on to the PatternRule.
     """
@@ -939,17 +941,17 @@ class PatternRule(object):
         for c in self.commands:
             cstring = c.resolve(makefile, v)
             for cline in splitcommand(cstring):
                 cline, isHidden, isRecursive, ignoreErrors = findmodifiers(cline)
                 if not len(cline) or cline.isspace():
                     continue
                 if not isHidden:
                     print "%s $ %s" % (c.loc, cline)
-                r = subprocess.call(cline, shell=True, env=env)
+                r = subprocess.call(cline, shell=True, env=env, cwd=makefile.workdir)
                 if r != 0 and not ignoreErrors:
                     raise DataError("command '%s' failed, return code was %s" % (cline, r), c.loc)
 
 class Makefile(object):
     def __init__(self, workdir=None, restarts=0, make=None, makeflags=None, makelevel=0):
         self.defaulttarget = None
 
         self.variables = Variables()
@@ -1065,18 +1067,20 @@ class Makefile(object):
                 for p in r.prerequisites:
                     self.gettarget(p).explicit = True
 
     def include(self, path, required=True, loc=None):
         """
         Include the makefile at `path`.
         """
         self.included.append(path)
-        if os.path.exists(path):
-            fd = open(path)
+
+        fspath = os.path.join(self.workdir, path)
+        if os.path.exists(fspath):
+            fd = open(fspath)
             self.variables.append('MAKEFILE_LIST', Variables.SOURCE_AUTOMATIC, path, None, self)
             pymake.parser.parsestream(fd, path, 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):
         """
--- a/pymake/functions.py
+++ b/pymake/functions.py
@@ -1,15 +1,16 @@
 """
 Makefile functions.
 """
 
 import pymake
 from pymake import data
-import subprocess, os, glob
+import subprocess, os
+from pymake.globrelative import glob
 from cStringIO import StringIO
 
 log = data.log
 
 class Function(object):
     """
     An object that represents a function call. This class is always subclassed
     with the following methods and attributes:
@@ -357,41 +358,44 @@ class JoinFunction(Function):
 
 class WildcardFunction(Function):
     name = 'wildcard'
     minargs = 1
     maxargs = 1
 
     def resolve(self, makefile, variables, setting):
         # TODO: will need work when we support -C without actually changing the OS cwd
-        pattern = self._arguments[0].resolve(makefile, variables, setting)
+        patterns = data.splitwords(self._arguments[0].resolve(makefile, variables, setting))
 
-        r = glob.glob(pattern)
-        r.sort()
+        r = []
+        for p in patterns:
+            r.extend(glob(makefile.workdir, p))
         return ' '.join(r)
 
 class RealpathFunction(Function):
     name = 'realpath'
     minargs = 1
     maxargs = 1
 
     def resolve(self, makefile, variables, setting):
-        # TODO: will need work when we support -C without actually changing the OS cwd
-        return ' '.join((os.path.realpath(f)
-                         for f in data.splitwords(self._arguments[0].resolve(makefile, variables, setting))))
+        paths = data.splitwords(self._arguments[0].resolve(makefile, variables, setting))
+        fspaths = [os.path.join(makefile.workdir, path) for path in paths]
+        realpaths = [os.path.realpath(path) for path in fspaths]
+        return ' '.join(realpaths)
 
 class AbspathFunction(Function):
     name = 'abspath'
     minargs = 1
     maxargs = 1
 
     def resolve(self, makefile, variables, setting):
-        # TODO: will need work when we support -C without actually changing the OS cwd
-        return ' '.join((os.path.abspath(f)
-                         for f in data.splitwords(self._arguments[0].resolve(makefile, variables, setting))))
+        assert os.path.isabs(makefile.workdir)
+        paths = data.splitwords(self._arguments[0].resolve(makefile, variables, setting))
+        fspaths = [os.path.join(makefile.workdir, path) for path in paths]
+        return ' '.join(fspaths)
 
 class IfFunction(Function):
     name = 'if'
     minargs = 1
     maxargs = 3
 
     def setup(self):
         Function.setup(self)
@@ -564,17 +568,17 @@ class ShellFunction(Function):
     minargs = 1
     maxargs = 1
 
     def resolve(self, makefile, variables, setting):
         cline = self._arguments[0].resolve(makefile, variables, setting)
 
         log.debug("%s: running shell command '%s'" % (self.loc, cline))
 
-        p = subprocess.Popen(cline, shell=True, stdout=subprocess.PIPE)
+        p = subprocess.Popen(cline, shell=True, 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', ' ')
 
         return stdout
new file mode 100644
--- /dev/null
+++ b/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
--- a/pymake/parser.py
+++ b/pymake/parser.py
@@ -13,17 +13,18 @@ Lines with command syntax do not condens
 
 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.
 
 After splitting data into parseable chunks, we use a recursive-descent parser to
 nest parenthesized syntax.
 """
 
-import logging, re, glob
+import logging, re
+from pymake.globrelative import hasglob, glob
 from pymake import data, functions
 from cStringIO import StringIO
 
 tabwidth = 4
 
 log = logging.getLogger('pymake.parser')
 
 class SyntaxError(Exception):
@@ -556,21 +557,20 @@ class Condition(object):
             return
 
         self.active = active
         if active:
             self.everactive = True
 
 def expandwildcards(makefile, tlist):
     for t in tlist:
-        if t.find('*') == -1 and t.find('?') == -1 and t.find('[') == -1:
+        if not hasglob(t):
             yield t
         else:
-            l = glob.glob(t)
-            l.sort()
+            l = glob(makefile.workdir, t)
             for r in l:
                 yield r
 
 conditiontokens = tuple(conditionkeywords.iterkeys())
 directivestokenlist = TokenList.get(conditiontokens + \
     ('else', 'endif', 'define', 'endef', 'override', 'include', '-include', 'vpath', 'export', 'unexport'))
 conditionkeywordstokenlist = TokenList.get(conditiontokens)
 
--- a/tests/file-functions.mk
+++ b/tests/file-functions.mk
@@ -1,20 +1,21 @@
 $(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 test.file test.symlink"
-# commented out because GNU make matches . and .. while python doesn't, and I don't
-# care enough
-#	test "$(sort $(wildcard .*))" = ". .. .testhidden"
+	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 ./test.file ./test.symlink"
+	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