diff -r 09d4d47ad210 Doc/distutils/apiref.rst --- a/Doc/distutils/apiref.rst Tue Jan 31 13:53:39 2017 +0100 +++ b/Doc/distutils/apiref.rst Wed Feb 01 01:11:52 2017 +0000 @@ -947,6 +947,14 @@ the commands). +.. function:: newer_pairwise_group(sources_groups, targets) + + Walk both arguments in parallel, testing if each source group is newer + than its corresponding target. Returns a pair of lists (*sources_groups*, + *targets*) where sources_groups is newer than target, according to the + semantics of :func:`newer_group`. + + :mod:`distutils.dir_util` --- Directory tree operations ======================================================= diff -r 09d4d47ad210 Lib/distutils/command/build_clib.py --- a/Lib/distutils/command/build_clib.py Tue Jan 31 13:53:39 2017 +0100 +++ b/Lib/distutils/command/build_clib.py Wed Feb 01 01:11:52 2017 +0000 @@ -16,6 +16,7 @@ import os from distutils.core import Command +from distutils.dep_util import newer_pairwise_group from distutils.errors import * from distutils.sysconfig import customize_compiler from distutils import log @@ -190,20 +191,64 @@ log.info("building '%s' library", lib_name) - # First, compile the source code to object files in the library - # directory. (This should probably change to putting object - # files in a temporary build directory.) - macros = build_info.get('macros') - include_dirs = build_info.get('include_dirs') - objects = self.compiler.compile(sources, - output_dir=self.build_temp, - macros=macros, - include_dirs=include_dirs, - debug=self.debug) + # Make sure everything is the correct type. + # obj_deps should be a dictionary of keys as sources + # and a list/tuple of files that are its dependencies. + obj_deps = build_info.get('obj_deps', dict()) + if not isinstance(obj_deps, dict): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) + dependencies = [] + + # Get the global dependencies that are specified by the '' key. + # These will go into every source's dependency list. + global_deps = obj_deps.get('', list()) + if not isinstance(global_deps, (list, tuple)): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) + + # Build the list to be used by newer_pairwise_group + # each source will be auto-added to its dependencies. + for source in sources: + src_deps = [source] + src_deps.extend(global_deps) + extra_deps = obj_deps.get(source, list()) + if not isinstance(extra_deps, (list, tuple)): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) + src_deps.extend(extra_deps) + dependencies.append(src_deps) + + # this is the list of '*.o' files we find after compiling + # if this differs from the actual objects created there's an issue + # with the compiler + expected_objects = self.compiler.object_filenames( + sources, + output_dir=self.build_temp) + + if newer_pairwise_group(dependencies, expected_objects) != ([], []): + # First, compile the source code to object files in the library + # directory. (This should probably change to putting object + # files in a temporary build directory.) + macros = build_info.get('macros') + include_dirs = build_info.get('include_dirs') + cflags = build_info.get('cflags') + objects = self.compiler.compile(sources, + output_dir=self.build_temp, + macros=macros, + include_dirs=include_dirs, + extra_postargs=cflags, + debug=self.debug) # Now "link" the object files together into a static library. # (On Unix at least, this isn't really linking -- it just # builds an archive. Whatever.) - self.compiler.create_static_lib(objects, lib_name, + self.compiler.create_static_lib(expected_objects, lib_name, output_dir=self.build_clib, debug=self.debug) diff -r 09d4d47ad210 Lib/distutils/dep_util.py --- a/Lib/distutils/dep_util.py Tue Jan 31 13:53:39 2017 +0100 +++ b/Lib/distutils/dep_util.py Wed Feb 01 01:11:52 2017 +0000 @@ -90,3 +90,28 @@ return 0 # newer_group () + + +# yes, this is was almost entirely copy-pasted from +# 'newer_pairwise()', this is just another convenience +# function. +def newer_pairwise_group(sources_groups, targets): + """Walk both arguments in parallel, testing if each source group is newer + than its corresponding target. Returns a pair of lists (sources_groups, + targets) where sources is newer than target, according to the semantics + of 'newer_group()'. + """ + if len(sources_groups) != len(targets): + raise ValueError("'sources_group' and 'targets' must be the same length") + + # build a pair of lists (sources_groups, targets) where source is newer + n_sources = [] + n_targets = [] + for i in range(len(sources_groups)): + if newer_group(sources_groups[i], targets[i]): + n_sources.append(sources_groups[i]) + n_targets.append(targets[i]) + + return n_sources, n_targets + +# newer_pairwise_group () diff -r 09d4d47ad210 Lib/distutils/tests/test_build_clib.py --- a/Lib/distutils/tests/test_build_clib.py Tue Jan 31 13:53:39 2017 +0100 +++ b/Lib/distutils/tests/test_build_clib.py Wed Feb 01 01:11:52 2017 +0000 @@ -66,20 +66,54 @@ ('name2', {'sources': ['c', 'd']})] self.assertEqual(cmd.get_source_files(), ['a', 'b', 'c', 'd']) - def test_build_libraries(self): + @unittest.mock.patch('distutils.command.build_clib.newer_pairwise_group') + def test_build_libraries(self, mock_newer): pkg_dir, dist = self.create_dist() cmd = build_clib(dist) - class FakeCompiler: - def compile(*args, **kw): - pass - create_static_lib = compile - cmd.compiler = FakeCompiler() + # this will be a long section, just making sure all + # exceptions are properly raised + libs = [('example', {'sources': 'broken.c'})] + with self.assertRaises(DistutilsSetupError): + cmd.build_libraries(libs) - # build_libraries is also doing a bit of typo checking - lib = [('name', {'sources': 'notvalid'})] - self.assertRaises(DistutilsSetupError, cmd.build_libraries, lib) + obj_deps = 'some_string' + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with self.assertRaises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = {'': ''} + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with self.assertRaises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = {'source.c': ''} + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with self.assertRaises(DistutilsSetupError): + cmd.build_libraries(libs) + + # with that out of the way, let's see if the crude dependency + # system works + cmd.compiler = unittest.mock.MagicMock(spec=cmd.compiler) + mock_newer.return_value = ([],[]) + + obj_deps = {'': ('global.h',), 'example.c': ('example.h',)} + libs = [('example', {'sources': ['example.c'] ,'obj_deps': obj_deps})] + + cmd.build_libraries(libs) + self.assertIn([['example.c', 'global.h', 'example.h']], + mock_newer.call_args[0]) + self.assertFalse(cmd.compiler.compile.called) + self.assertEqual(cmd.compiler.create_static_lib.call_count, 1) + + # reset the call numbers so we can test again + cmd.compiler.reset_mock() + + mock_newer.return_value = '' # anything as long as it's not ([],[]) + cmd.build_libraries(libs) + self.assertEqual(cmd.compiler.compile.call_count, 1) + self.assertEqual(cmd.compiler.create_static_lib.call_count, 1) lib = [('name', {'sources': list()})] cmd.build_libraries(lib) diff -r 09d4d47ad210 Lib/distutils/tests/test_dep_util.py --- a/Lib/distutils/tests/test_dep_util.py Tue Jan 31 13:53:39 2017 +0100 +++ b/Lib/distutils/tests/test_dep_util.py Wed Feb 01 01:11:52 2017 +0000 @@ -2,7 +2,8 @@ import unittest import os -from distutils.dep_util import newer, newer_pairwise, newer_group +from distutils.dep_util import (newer, newer_pairwise, + newer_group, newer_pairwise_group) from distutils.errors import DistutilsFileError from distutils.tests import support from test.support import run_unittest @@ -72,6 +73,40 @@ self.assertTrue(newer_group([one, two, old_file], three, missing='newer')) + def test_newer_pairwise_group(self): + tmpdir = self.mkdtemp() + sources = os.path.join(tmpdir, 'sources') + targets = os.path.join(tmpdir, 'targets') + os.mkdir(sources) + os.mkdir(targets) + older_one = os.path.join(sources, 'older_one') + older_two = os.path.join(sources, 'older_two') + target = os.path.join(targets, 'target_one') + newer_one = os.path.join(sources, 'newer_one') + newer_two = os.path.join(sources, 'newer_two') + + # write the files and make sure mtimes are sequential + self.write_file(older_one) + os.utime(older_one, (0, 0)) + self.write_file(older_two) + os.utime(older_two, (1, 1)) + self.write_file(target) + os.utime(target, (2, 2)) + self.write_file(newer_one) + os.utime(newer_one, (3, 3)) + self.write_file(newer_two) + os.utime(newer_two, (4, 4)) + + # return empty lists if target is up-to-date + self.assertEqual( + newer_pairwise_group([(older_one, older_two)], [target]), + ([],[])) + # return the groups if not + self.assertEqual( + newer_pairwise_group([(newer_one, newer_two)], [target]), + ([(newer_one, newer_two)],[target])) + + def test_suite(): return unittest.makeSuite(DepUtilTestCase)