Implement the vpath directive.
authorBenjamin Smedberg <benjamin@smedbergs.us>
Tue, 17 Feb 2009 14:06:23 -0500
changeset 122 1995f94b1c2f
parent 121 ace16e634043
child 123 17169ca68e03
push id68
push userbsmedberg@mozilla.com
push date2009-02-17 19:06 +0000
Implement the vpath directive.
README
pymake/data.py
pymake/parser.py
tests/vpath-directive.mk
--- a/README
+++ b/README
@@ -18,18 +18,16 @@ The Mozilla project inspired this tool w
   with converting in-tree makefiles to another build system, such as SCons, waf, ant, ...insert
   your favorite build tool here. Or we could experiment along the lines of makepp, keeping
   our existing makefiles, but change the engine to build a global dependency graph.
 
 KNOWN INCOMPATIBILITIES
 
 * Parallel builds (-j > 1) are not yet supported
 
-* The vpath directive is not yet supported
-
 * Order-only prerequisites are not yet supported
 
 * Secondary expansion is not yet supported.
 
 * Target-specific variables behave differently than in GNU make: in pymake, the target-specific
   variable only applies to the specific target that is mentioned, and does not apply recursively
   to all dependencies which are remade. This is an intentional change: the behavior of GNU make
   is neither deterministic nor intuitive.
--- a/pymake/data.py
+++ b/pymake/data.py
@@ -605,17 +605,18 @@ class Target(object):
             return
 
         if self.target.startswith('-l'):
             stem = self.target[2:]
             f, s, e = makefile.variables.get('.LIBPATTERNS')
             if e is not None:
                 libpatterns = map(Pattern, splitwords(e.resolve(makefile, makefile.variables)))
                 if len(libpatterns):
-                    searchdirs = [''] + makefile.vpath
+                    searchdirs = ['']
+                    searchdirs.extend(makefile.getvpath(self.target))
 
                     for lp in libpatterns:
                         if not lp.ispattern():
                             raise DataError('.LIBPATTERNS contains a non-pattern')
 
                         libname = lp.resolve('', stem)
 
                         for dir in searchdirs:
@@ -628,17 +629,17 @@ class Target(object):
 
                     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.vpath]
+                       for dir in makefile.getvpath(self.target)]
 
         for t in search:
             mtime = getmtime(t)
             if mtime is not None:
                 self.vpathtarget = t
                 self.mtime = mtime
                 return
 
@@ -949,16 +950,18 @@ class Makefile(object):
 
         self.exportedvars = set()
         self.overrides = []
         self._targets = {}
         self._patternvariables = [] # of (pattern, variables)
         self.implicitrules = []
         self.parsingfinished = False
 
+        self._patternvpaths = [] # of (pattern, [dir, ...])
+
         if workdir is None:
             workdir = os.getcwd()
         workdir = os.path.realpath(workdir)
         self.workdir = workdir
         self.variables.set('CURDIR', Variables.FLAVOR_SIMPLE,
                            Variables.SOURCE_AUTOMATIC, workdir)
 
         # the list of included makefiles, whether or not they existed
@@ -1039,19 +1042,19 @@ class Makefile(object):
         self.parsingfinished = True
 
         flavor, source, value = self.variables.get('GPATH')
         if value is not None and value.resolve(self, self.variables, ['GPATH']).strip() != '':
             raise DataError('GPATH was set: pymake does not support GPATH semantics')
 
         flavor, source, value = self.variables.get('VPATH')
         if value is None:
-            self.vpath = []
+            self._vpath = []
         else:
-            self.vpath = filter(lambda e: e != '', re.split('[:\s]+', value.resolve(self, self.variables, ['VPATH'])))
+            self._vpath = filter(lambda e: e != '', re.split('[:\s]+', value.resolve(self, self.variables, ['VPATH'])))
 
         targets = list(self._targets.itervalues())
         for t in targets:
             t.explicit = True
             for r in t.rules:
                 for p in r.prerequisites:
                     self.gettarget(p).explicit = True
 
@@ -1063,16 +1066,41 @@ class Makefile(object):
         if os.path.exists(path):
             fd = open(path)
             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):
+        """
+        Add a directory to the vpath search for the given pattern.
+        """
+        self._patternvpaths.append((pattern, dirs))
+
+    def clearvpath(self, pattern):
+        """
+        Clear vpaths for the given pattern.
+        """
+        self._patternvpaths = [(p, dirs)
+                               for p, dirs in self._patternvpaths
+                               if not p.match(pattern)]
+
+    def clearallvpaths(self):
+        self._patternvpaths = []
+
+    def getvpath(self, target):
+        vp = list(self._vpath)
+        for p, dirs in self._patternvpaths:
+            if p.match(target):
+                vp.extend(dirs)
+
+        return withoutdups(vp)
+
     def remakemakefiles(self):
         reparse = False
 
         for f in self.included:
             t = self.gettarget(f)
             t.explicit = True
             t.resolvevpath(self)
             oldmtime = t.mtime
--- a/pymake/parser.py
+++ b/pymake/parser.py
@@ -655,16 +655,37 @@ def parsestream(fd, filename, makefile):
 
             if kword in ('include', '-include'):
                 incfile, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
                 files = data.splitwords(incfile.resolve(makefile, makefile.variables))
                 for f in files:
                     makefile.include(f, kword == 'include', loc=d.getloc(offset))
                 continue
 
+            if kword == 'vpath':
+                e, t, offset = parsemakesyntax(d, offset, (' ', '\t'), itermakefilechars)
+                patstr = e.resolve(makefile, makefile.variables)
+                pattern = data.Pattern(patstr)
+                if t is None:
+                    makefile.clearallvpaths()
+                else:
+                    e, t, offset = parsemakesyntax(d, offset, (), itermakefilechars)
+                    dirs = e.resolve(makefile, makefile.variables)
+                    dirlist = []
+                    for direl in data.splitwords(dirs):
+                        dirlist.extend((dir
+                                        for dir in direl.split(':')
+                                        if dir != ''))
+
+                    if len(dirlist) == 0:
+                        makefile.clearvpath(pattern)
+                    else:
+                        makefile.addvpath(pattern, dirlist)
+                continue
+
             if kword == 'override':
                 e, token, offset = parsemakesyntax(d, offset, varsettokens, itermakefilechars)
                 e.lstrip()
                 e.rstrip()
 
                 if token is None:
                     raise SyntaxError("Malformed override directive, need =", d.getloc(offset))
 
new file mode 100644
--- /dev/null
+++ b/tests/vpath-directive.mk
@@ -0,0 +1,28 @@
+$(shell \
+mkdir subd1 subd2 subd3; \
+printf "reallybaddata" >subd1/foo.in; \
+printf "gooddata" >subd2/foo.in; \
+printf "baddata" >subd3/foo.in; \
+touch subd1/foo.in2 subd2/foo.in2 subd3/foo.in2; \
+)
+
+vpath %.in subd
+
+vpath
+vpath %.in subd2:subd3
+
+vpath %.in2 subd0
+vpath f%.in2 subd1
+vpath %.in2 :subd2
+
+%.out: %.in
+	test "$<" = "subd2/foo.in"
+	cp $< $@
+
+%.out2: %.in2
+	test "$<" = "subd1/foo.in2"
+	cp $< $@
+
+all: foo.out foo.out2
+	test "$$(cat foo.out)" = "gooddata"
+	@echo TEST-PASS