Extend compiler() method with optional depends argument.

This change is not backwards compatible.  If a compiler subclass
exists outside the distutils package, it may get called with the
unexpected keyword arg.  It's easy to extend that compiler by having
it ignore the argument, and not much harder to do the right thing.  If
this ends up being burdensome, we can change it before 2.3 final to
work harder at compatibility.

Also add _setup_compile() and _get_cc_args() helper functions that
factor out much of the boilerplate for each concrete compiler class.
diff --git a/Lib/distutils/ccompiler.py b/Lib/distutils/ccompiler.py
index 4c8b881..50246b8 100644
--- a/Lib/distutils/ccompiler.py
+++ b/Lib/distutils/ccompiler.py
@@ -160,7 +160,6 @@
             setattr(self, key, value)
 
 
-
     def _find_macro (self, name):
         i = 0
         for defn in self.macros:
@@ -321,6 +320,100 @@
     # -- Private utility methods --------------------------------------
     # (here for the convenience of subclasses)
 
+    # Helper method to prep compiler in subclass compile() methods
+
+    def _setup_compile(self, outdir, macros, incdirs, sources, depends,
+                       extra):
+        """Process arguments and decide which source files to compile.
+
+        Merges _fix_compile_args() and _prep_compile().
+        """
+        if outdir is None:
+            outdir = self.output_dir
+        elif type(outdir) is not StringType:
+            raise TypeError, "'output_dir' must be a string or None"
+
+        if macros is None:
+            macros = self.macros
+        elif type(macros) is ListType:
+            macros = macros + (self.macros or [])
+        else:
+            raise TypeError, "'macros' (if supplied) must be a list of tuples"
+
+        if incdirs is None:
+            incdirs = self.include_dirs
+        elif type(incdirs) in (ListType, TupleType):
+            incdirs = list(incdirs) + (self.include_dirs or [])
+        else:
+            raise TypeError, \
+                  "'include_dirs' (if supplied) must be a list of strings"
+
+        if extra is None:
+            extra = []
+
+        # Get the list of expected output (object) files
+        objects = self.object_filenames(sources, 1, outdir)
+        assert len(objects) == len(sources)
+
+        # XXX should redo this code to eliminate skip_source entirely.
+        # XXX instead create build and issue skip messages inline
+
+        if self.force:
+            skip_source = {}            # rebuild everything
+            for source in sources:
+                skip_source[source] = 0
+        elif depends is None:
+            # If depends is None, figure out which source files we
+            # have to recompile according to a simplistic check. We
+            # just compare the source and object file, no deep
+            # dependency checking involving header files.
+            skip_source = {}            # rebuild everything
+            for source in sources:      # no wait, rebuild nothing
+                skip_source[source] = 1
+
+            n_sources, n_objects = newer_pairwise(sources, objects)
+            for source in n_sources:    # no really, only rebuild what's
+                skip_source[source] = 0 # out-of-date
+        else:
+            # If depends is a list of files, then do a different
+            # simplistic check.  Assume that each object depends on
+            # its source and all files in the depends list.
+            skip_source = {}
+            # L contains all the depends plus a spot at the end for a
+            # particular source file
+            L = depends[:] + [None]
+            for i in range(len(objects)):
+                source = sources[i]
+                L[-1] = source
+                if newer_group(L, objects[i]):
+                    skip_source[source] = 0
+                else:
+                    skip_source[source] = 1
+
+        pp_opts = gen_preprocess_options(macros, incdirs)
+
+        build = {}
+        for i in range(len(sources)):
+            src = sources[i]
+            obj = objects[i]
+            ext = os.path.splitext(src)[1]
+            self.mkpath(os.path.dirname(obj))
+            if skip_source[src]:
+                log.debug("skipping %s (%s up-to-date)", src, obj)
+            else:
+                build[obj] = src, ext
+
+        return macros, objects, extra, pp_opts, build
+
+    def _get_cc_args(self, pp_opts, debug, before):
+        # works for unixccompiler, emxccompiler, cygwinccompiler
+        cc_args = pp_opts + ['-c']
+        if debug:
+            cc_args[:0] = ['-g']
+        if before:
+            cc_args[:0] = before
+        return cc_args
+
     def _fix_compile_args (self, output_dir, macros, include_dirs):
         """Typecheck and fix-up some of the arguments to the 'compile()'
         method, and return fixed-up values.  Specifically: if 'output_dir'
@@ -341,8 +434,7 @@
         elif type (macros) is ListType:
             macros = macros + (self.macros or [])
         else:
-            raise TypeError, \
-                  "'macros' (if supplied) must be a list of tuples"
+            raise TypeError, "'macros' (if supplied) must be a list of tuples"
 
         if include_dirs is None:
             include_dirs = self.include_dirs
@@ -352,40 +444,57 @@
             raise TypeError, \
                   "'include_dirs' (if supplied) must be a list of strings"
 
-        return (output_dir, macros, include_dirs)
+        return output_dir, macros, include_dirs
 
     # _fix_compile_args ()
 
 
-    def _prep_compile (self, sources, output_dir):
-        """Determine the list of object files corresponding to 'sources',
-        and figure out which ones really need to be recompiled.  Return a
-        list of all object files and a dictionary telling which source
-        files can be skipped.
+    def _prep_compile(self, sources, output_dir, depends=None):
+        """Decide which souce files must be recompiled.
+
+        Determine the list of object files corresponding to 'sources',
+        and figure out which ones really need to be recompiled.
+        Return a list of all object files and a dictionary telling
+        which source files can be skipped.
         """
         # Get the list of expected output (object) files
-        objects = self.object_filenames (sources,
-                                         strip_dir=1,
-                                         output_dir=output_dir)
+        objects = self.object_filenames(sources, strip_dir=1,
+                                        output_dir=output_dir)
+        assert len(objects) == len(sources)
 
         if self.force:
             skip_source = {}            # rebuild everything
             for source in sources:
                 skip_source[source] = 0
-        else:
-            # Figure out which source files we have to recompile according
-            # to a simplistic check -- we just compare the source and
-            # object file, no deep dependency checking involving header
-            # files.
+        elif depends is None:
+            # If depends is None, figure out which source files we
+            # have to recompile according to a simplistic check. We
+            # just compare the source and object file, no deep
+            # dependency checking involving header files.
             skip_source = {}            # rebuild everything
             for source in sources:      # no wait, rebuild nothing
                 skip_source[source] = 1
 
-            (n_sources, n_objects) = newer_pairwise (sources, objects)
+            n_sources, n_objects = newer_pairwise(sources, objects)
             for source in n_sources:    # no really, only rebuild what's
                 skip_source[source] = 0 # out-of-date
+        else:
+            # If depends is a list of files, then do a different
+            # simplistic check.  Assume that each object depends on
+            # its source and all files in the depends list.
+            skip_source = {}
+            # L contains all the depends plus a spot at the end for a
+            # particular source file
+            L = depends[:] + [None]
+            for i in range(len(objects)):
+                source = sources[i]
+                L[-1] = source
+                if newer_group(L, objects[i]):
+                    skip_source[source] = 0
+                else:
+                    skip_source[source] = 1
 
-        return (objects, skip_source)
+        return objects, skip_source
 
     # _prep_compile ()
 
@@ -484,22 +593,19 @@
         """
         pass
 
-    def compile (self,
-                 sources,
-                 output_dir=None,
-                 macros=None,
-                 include_dirs=None,
-                 debug=0,
-                 extra_preargs=None,
-                 extra_postargs=None):
-        """Compile one or more source files.  'sources' must be a list of
-        filenames, most likely C/C++ files, but in reality anything that
-        can be handled by a particular compiler and compiler class
-        (eg. MSVCCompiler can handle resource files in 'sources').  Return
-        a list of object filenames, one per source filename in 'sources'.
-        Depending on the implementation, not all source files will
-        necessarily be compiled, but all corresponding object filenames
-        will be returned.
+    def compile(self, sources, output_dir=None, macros=None,
+                include_dirs=None, debug=0, extra_preargs=None,
+                extra_postargs=None, depends=None):
+        """Compile one or more source files.
+
+        'sources' must be a list of filenames, most likely C/C++
+        files, but in reality anything that can be handled by a
+        particular compiler and compiler class (eg. MSVCCompiler can
+        handle resource files in 'sources').  Return a list of object
+        filenames, one per source filename in 'sources'.  Depending on
+        the implementation, not all source files will necessarily be
+        compiled, but all corresponding object filenames will be
+        returned.
 
         If 'output_dir' is given, object files will be put under it, while
         retaining their original path component.  That is, "foo/bar.c"
@@ -530,6 +636,12 @@
         for those occasions when the abstract compiler framework doesn't
         cut the mustard.
 
+        'depends', if given, is a list of filenames that all targets
+        depend on.  If a source file is older than any file in
+        depends, then the source file will be recompiled.  This
+        supports dependency tracking, but only at a coarse
+        granularity.
+
         Raises CompileError on failure.
         """
         pass
@@ -710,7 +822,6 @@
         """
         raise NotImplementedError
 
-
     # -- Filename generation methods -----------------------------------
 
     # The default implementation of the filename generating methods are
@@ -745,63 +856,46 @@
     #   * exe_extension -
     #     extension for executable files, eg. '' or '.exe'
 
-    def object_filenames (self,
-                          source_filenames,
-                          strip_dir=0,
-                          output_dir=''):
-        if output_dir is None: output_dir = ''
+    def object_filenames(self, source_filenames, strip_dir=0, output_dir=''):
+        assert output_dir is not None
         obj_names = []
         for src_name in source_filenames:
-            (base, ext) = os.path.splitext (src_name)
+            base, ext = os.path.splitext(src_name)
             if ext not in self.src_extensions:
                 raise UnknownFileError, \
-                      "unknown file type '%s' (from '%s')" % \
-                      (ext, src_name)
+                      "unknown file type '%s' (from '%s')" % (ext, src_name)
             if strip_dir:
-                base = os.path.basename (base)
-            obj_names.append (os.path.join (output_dir,
-                                            base + self.obj_extension))
+                base = os.path.basename(base)
+            obj_names.append(os.path.join(output_dir,
+                                          base + self.obj_extension))
         return obj_names
 
-    # object_filenames ()
-
-
-    def shared_object_filename (self,
-                                basename,
-                                strip_dir=0,
-                                output_dir=''):
-        if output_dir is None: output_dir = ''
+    def shared_object_filename(self, basename, strip_dir=0, output_dir=''):
+        assert output_dir is not None
         if strip_dir:
             basename = os.path.basename (basename)
-        return os.path.join (output_dir, basename + self.shared_lib_extension)
+        return os.path.join(output_dir, basename + self.shared_lib_extension)
 
-    def executable_filename (self,
-                                basename,
-                                strip_dir=0,
-                                output_dir=''):
-        if output_dir is None: output_dir = ''
+    def executable_filename(self, basename, strip_dir=0, output_dir=''):
+        assert output_dir is not None
         if strip_dir:
             basename = os.path.basename (basename)
         return os.path.join(output_dir, basename + (self.exe_extension or ''))
 
-    def library_filename (self,
-                          libname,
-                          lib_type='static',     # or 'shared'
-                          strip_dir=0,
-                          output_dir=''):
-
-        if output_dir is None: output_dir = ''
-        if lib_type not in ("static","shared","dylib"):
+    def library_filename(self, libname, lib_type='static',     # or 'shared'
+                         strip_dir=0, output_dir=''):
+        assert output_dir is not None
+        if lib_type not in ("static", "shared", "dylib"):
             raise ValueError, "'lib_type' must be \"static\", \"shared\" or \"dylib\""
-        fmt = getattr (self, lib_type + "_lib_format")
-        ext = getattr (self, lib_type + "_lib_extension")
+        fmt = getattr(self, lib_type + "_lib_format")
+        ext = getattr(self, lib_type + "_lib_extension")
 
-        (dir, base) = os.path.split (libname)
+        dir, base = os.path.split (libname)
         filename = fmt % (base, ext)
         if strip_dir:
             dir = ''
 
-        return os.path.join (output_dir, dir, filename)
+        return os.path.join(output_dir, dir, filename)
 
 
     # -- Utility methods -----------------------------------------------