Repository: pypa/setuptools
Pull request #930 (2017-01-20 02:22:10)

Add timestamp-based dependency system to build_clib
`build_clib` is sorely missing one thing in distutils - checking outdated dependencies when compiling. This makes it so that, when building a static c library, **all** objects are always **all** compiled.

The function to create the static library itself *does* implement something of this sort, but since all objects have been recompiled it has no practical effect (everything still runs **every** time).

This is a small patch designed to fix that, with a couple of qol improvements along the way. Two new keys were added to the `build_info` dictionary:

-  obj_deps - Allows specifying extra dependencies for each source so they can be recognized as being newer than the object being compiled. Example:

```
setup(
...
libraries=[(
    'examplelib', 
    {'sources': 'example.c', 
     'obj_deps': {'': 'global.h',  # use an empty string to define global (for all sources) dependencies
                  'example.c': 'example.h'}
    }
)]
...
)
```

- cflags - This just allows the user to provide an extra set of flags to the compiler.

That is pretty much it, a simple (but important) quality of life improvement for those of us who need to compile static libs with python.

If the implementation is fine I can get started on documentation, just not sure where to put it.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 0683a619e..b402cf6d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,15 +5,12 @@ python:
   - 3.3
   - 3.4
   - 3.5
-  - 3.6
+  - "3.6-dev"
   - nightly
   - pypy
-matrix:
-  include:
-  - python: 3.6
-    env: LC_ALL=C LC_CTYPE=C
-  - python: 2.7
-    env: LC_ALL=C LC_CTYPE=C
+env:
+ - ""
+ - LC_ALL=C LC_CTYPE=C
 script:
  - pip install tox
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 339a0e6dc..8c214db3a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,21 +1,3 @@
-v33.1.1
--------
-
-* #921: Correct issue where certifi fallback not being
-  reached on Windows.
-
-v33.1.0
--------
-
-Installation via pip, as indicated in the `Python Packaging
-User's Guide <https://packaging.python.org/installing/>`_,
-is the officially-supported mechanism for installing
-Setuptools, and this recommendation is now explicit in the
-much more concise README.
-
-Other edits and tweaks were made to the documentation. The
-codebase is unchanged.
-
 v33.0.0
 -------
 
diff --git a/README.rst b/README.rst
index 2f55bce10..accaac3c3 100755
--- a/README.rst
+++ b/README.rst
@@ -1,23 +1,241 @@
+===============================
+Installing and Using Setuptools
+===============================
+
+.. contents:: **Table of Contents**
+
+
 .. image:: https://readthedocs.org/projects/setuptools/badge/?version=latest
     :target: https://setuptools.readthedocs.io
 
-See the `Installation Instructions
-<https://packaging.python.org/installing/>`_ in the Python Packaging
-User's Guide for instructions on installing, upgrading, and uninstalling
-Setuptools.
+-------------------------
+Installation Instructions
+-------------------------
+
+The recommended way to bootstrap setuptools on any system is to download
+`ez_setup.py`_ and run it using the target Python environment. Different
+operating systems have different recommended techniques to accomplish this
+basic routine, so below are some examples to get you started.
+
+Setuptools requires Python 2.6 or later. To install setuptools
+on Python 2.4 or Python 2.5, use the `bootstrap script for Setuptools 1.x
+<https://raw.githubusercontent.com/pypa/setuptools/bootstrap-py24/ez_setup.py>`_.
+
+The link provided to ez_setup.py is a bookmark to bootstrap script for the
+latest known stable release.
+
+.. _ez_setup.py: https://bootstrap.pypa.io/ez_setup.py
+
+Windows (Powershell 3 or later)
+===============================
+
+For best results, uninstall previous versions FIRST (see `Uninstalling`_).
+
+Using Windows 8 (which includes PowerShell 3) or earlier versions of Windows
+with PowerShell 3 installed, it's possible to install with one simple
+Powershell command. Start up Powershell and paste this command::
+
+    > (Invoke-WebRequest https://bootstrap.pypa.io/ez_setup.py).Content | python -
+
+.. image:: https://badges.gitter.im/pypa/setuptools.svg
+   :alt: Join the chat at https://gitter.im/pypa/setuptools
+   :target: https://gitter.im/pypa/setuptools?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+
+You must start the Powershell with Administrative privileges or you may choose
+to install a user-local installation::
+
+    > (Invoke-WebRequest https://bootstrap.pypa.io/ez_setup.py).Content | python - --user
+
+If you have Python 3.3 or later, you can use the ``py`` command to install to
+different Python versions. For example, to install to Python 3.3 if you have
+Python 2.7 installed::
+
+    > (Invoke-WebRequest https://bootstrap.pypa.io/ez_setup.py).Content | py -3 -
+
+The recommended way to install setuptools on Windows is to download
+`ez_setup.py`_ and run it. The script will download the appropriate
+distribution file and install it for you.
+
+Once installation is complete, you will find an ``easy_install`` program in
+your Python ``Scripts`` subdirectory. For simple invocation and best results,
+add this directory to your ``PATH`` environment variable, if it is not already
+present. If you did a user-local install, the ``Scripts`` subdirectory is
+``$env:APPDATA\Python\Scripts``.
+
+
+Windows (simplified)
+====================
+
+For Windows without PowerShell 3 or for installation without a command-line,
+download `ez_setup.py`_ using your preferred web browser or other technique
+and "run" that file.
+
+
+Unix (wget)
+===========
+
+Most Linux distributions come with wget.
+
+Download `ez_setup.py`_ and run it using the target Python version. The script
+will download the appropriate version and install it for you::
+
+    > wget https://bootstrap.pypa.io/ez_setup.py -O - | python
+
+Note that you will may need to invoke the command with superuser privileges to
+install to the system Python::
+
+    > wget https://bootstrap.pypa.io/ez_setup.py -O - | sudo python
+
+Alternatively, Setuptools may be installed to a user-local path::
+
+    > wget https://bootstrap.pypa.io/ez_setup.py -O - | python - --user
+
+Note that on some older systems (noted on Debian 6 and CentOS 5 installations),
+`wget` may refuse to download `ez_setup.py`, complaining that the certificate common name `*.c.ssl.fastly.net`
+does not match the host name `bootstrap.pypa.io`. In addition, the `ez_setup.py` script may then encounter similar problems using
+`wget` internally to download `setuptools-x.y.zip`, complaining that the certificate common name of `www.python.org` does not match the
+host name `pypi.python.org`. Those are known issues, related to a bug in the older versions of `wget`
+(see `Issue 59 <https://bitbucket.org/pypa/pypi/issue/59#comment-5881915>`_). If you happen to encounter them,
+install Setuptools as follows::
+
+    > wget --no-check-certificate https://bootstrap.pypa.io/ez_setup.py
+    > python ez_setup.py --insecure
+
+
+Unix including Mac OS X (curl)
+==============================
 
-The project is `maintained at GitHub <https://github.com/pypa/setuptools>`_.
+If your system has curl installed, follow the ``wget`` instructions but
+replace ``wget`` with ``curl`` and ``-O`` with ``-o``. For example::
 
-Questions and comments should be directed to the `distutils-sig
-mailing list <http://mail.python.org/pipermail/distutils-sig/>`_.
-Bug reports and especially tested patches may be
-submitted directly to the `bug tracker
-<https://github.com/pypa/setuptools/issues>`_.
+    > curl https://bootstrap.pypa.io/ez_setup.py -o - | python
 
 
+Advanced Installation
+=====================
+
+For more advanced installation options, such as installing to custom
+locations or prefixes, download and extract the source
+tarball from `Setuptools on PyPI <https://pypi.python.org/pypi/setuptools>`_
+and run setup.py with any supported distutils and Setuptools options.
+For example::
+
+    setuptools-x.x$ python setup.py install --prefix=/opt/setuptools
+
+Use ``--help`` to get a full options list, but we recommend consulting
+the `EasyInstall manual`_ for detailed instructions, especially `the section
+on custom installation locations`_.
+
+.. _EasyInstall manual: https://setuptools.readthedocs.io/en/latest/easy_install.html
+.. _the section on custom installation locations: https://setuptools.readthedocs.io/en/latest/easy_install.html#custom-installation-locations
+
+
+Downloads
+=========
+
+All setuptools downloads can be found at `the project's home page in the Python
+Package Index`_. Scroll to the very bottom of the page to find the links.
+
+.. _the project's home page in the Python Package Index: https://pypi.python.org/pypi/setuptools
+
+In addition to the PyPI downloads, the development version of ``setuptools``
+is available from the `GitHub repo`_, and in-development versions of the
+`0.6 branch`_ are available as well.
+
+.. _GitHub repo: https://github.com/pypa/setuptools/archive/master.tar.gz#egg=setuptools-dev
+.. _0.6 branch: http://svn.python.org/projects/sandbox/branches/setuptools-0.6/#egg=setuptools-dev06
+
+Uninstalling
+============
+
+On Windows, if Setuptools was installed using an ``.exe`` or ``.msi``
+installer, simply use the uninstall feature of "Add/Remove Programs" in the
+Control Panel.
+
+Otherwise, to uninstall Setuptools or Distribute, regardless of the Python
+version, delete all ``setuptools*`` and ``distribute*`` files and
+directories from your system's ``site-packages`` directory
+(and any other ``sys.path`` directories) FIRST.
+
+If you are upgrading or otherwise plan to re-install Setuptools or Distribute,
+nothing further needs to be done. If you want to completely remove Setuptools,
+you may also want to remove the 'easy_install' and 'easy_install-x.x' scripts
+and associated executables installed to the Python scripts directory.
+
+--------------------------------
+Using Setuptools and EasyInstall
+--------------------------------
+
+Here are some of the available manuals, tutorials, and other resources for
+learning about Setuptools, Python Eggs, and EasyInstall:
+
+* `The EasyInstall user's guide and reference manual`_
+* `The setuptools Developer's Guide`_
+* `The pkg_resources API reference`_
+* `The Internal Structure of Python Eggs`_
+
+Questions, comments, and bug reports should be directed to the `distutils-sig
+mailing list`_. If you have written (or know of) any tutorials, documentation,
+plug-ins, or other resources for setuptools users, please let us know about
+them there, so this reference list can be updated. If you have working,
+*tested* patches to correct problems or add features, you may submit them to
+the `setuptools bug tracker`_.
+
+.. _setuptools bug tracker: https://github.com/pypa/setuptools/issues
+.. _The Internal Structure of Python Eggs: https://setuptools.readthedocs.io/en/latest/formats.html
+.. _The setuptools Developer's Guide: https://setuptools.readthedocs.io/en/latest/developer-guide.html
+.. _The pkg_resources API reference: https://setuptools.readthedocs.io/en/latest/pkg_resources.html
+.. _The EasyInstall user's guide and reference manual: https://setuptools.readthedocs.io/en/latest/easy_install.html
+.. _distutils-sig mailing list: http://mail.python.org/pipermail/distutils-sig/
+
+
+-------
+Credits
+-------
+
+* The original design for the ``.egg`` format and the ``pkg_resources`` API was
+  co-created by Phillip Eby and Bob Ippolito. Bob also implemented the first
+  version of ``pkg_resources``, and supplied the OS X operating system version
+  compatibility algorithm.
+
+* Ian Bicking implemented many early "creature comfort" features of
+  easy_install, including support for downloading via Sourceforge and
+  Subversion repositories. Ian's comments on the Web-SIG about WSGI
+  application deployment also inspired the concept of "entry points" in eggs,
+  and he has given talks at PyCon and elsewhere to inform and educate the
+  community about eggs and setuptools.
+
+* Jim Fulton contributed time and effort to build automated tests of various
+  aspects of ``easy_install``, and supplied the doctests for the command-line
+  ``.exe`` wrappers on Windows.
+
+* Phillip J. Eby is the seminal author of setuptools, and
+  first proposed the idea of an importable binary distribution format for
+  Python application plug-ins.
+
+* Significant parts of the implementation of setuptools were funded by the Open
+  Source Applications Foundation, to provide a plug-in infrastructure for the
+  Chandler PIM application. In addition, many OSAF staffers (such as Mike
+  "Code Bear" Taylor) contributed their time and stress as guinea pigs for the
+  use of eggs and setuptools, even before eggs were "cool".  (Thanks, guys!)
+
+* Tarek Ziadé is the principal author of the Distribute fork, which
+  re-invigorated the community on the project, encouraged renewed innovation,
+  and addressed many defects.
+
+* Since the merge with Distribute, Jason R. Coombs is the
+  maintainer of setuptools. The project is maintained in coordination with
+  the Python Packaging Authority (PyPA) and the larger Python community.
+
+.. _files:
+
+
+---------------
 Code of Conduct
 ---------------
 
 Everyone interacting in the setuptools project's codebases, issue trackers,
 chat rooms, and mailing lists is expected to follow the
-`PyPA Code of Conduct <https://www.pypa.io/en/latest/code-of-conduct/>`_.
+`PyPA Code of Conduct`_.
+
+.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
diff --git a/docs/history.txt b/docs/history.txt
index 8fd1dc653..8e217503b 100644
--- a/docs/history.txt
+++ b/docs/history.txt
@@ -6,41 +6,3 @@ History
 *******
 
 .. include:: ../CHANGES (links).rst
-
-Credits
-*******
-
-* The original design for the ``.egg`` format and the ``pkg_resources`` API was
-  co-created by Phillip Eby and Bob Ippolito. Bob also implemented the first
-  version of ``pkg_resources``, and supplied the OS X operating system version
-  compatibility algorithm.
-
-* Ian Bicking implemented many early "creature comfort" features of
-  easy_install, including support for downloading via Sourceforge and
-  Subversion repositories. Ian's comments on the Web-SIG about WSGI
-  application deployment also inspired the concept of "entry points" in eggs,
-  and he has given talks at PyCon and elsewhere to inform and educate the
-  community about eggs and setuptools.
-
-* Jim Fulton contributed time and effort to build automated tests of various
-  aspects of ``easy_install``, and supplied the doctests for the command-line
-  ``.exe`` wrappers on Windows.
-
-* Phillip J. Eby is the seminal author of setuptools, and
-  first proposed the idea of an importable binary distribution format for
-  Python application plug-ins.
-
-* Significant parts of the implementation of setuptools were funded by the Open
-  Source Applications Foundation, to provide a plug-in infrastructure for the
-  Chandler PIM application. In addition, many OSAF staffers (such as Mike
-  "Code Bear" Taylor) contributed their time and stress as guinea pigs for the
-  use of eggs and setuptools, even before eggs were "cool".  (Thanks, guys!)
-
-* Tarek Ziadé is the principal author of the Distribute fork, which
-  re-invigorated the community on the project, encouraged renewed innovation,
-  and addressed many defects.
-
-* Since the merge with Distribute, Jason R. Coombs is the
-  maintainer of setuptools. The project is maintained in coordination with
-  the Python Packaging Authority (PyPA) and the larger Python community.
-
diff --git a/setup.cfg b/setup.cfg
index 1d45057be..74d7e085c 100755
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 33.1.1
+current_version = 33.0.0
 commit = True
 tag = True
 
diff --git a/setup.py b/setup.py
index 060c26c2f..8e125fc18 100755
--- a/setup.py
+++ b/setup.py
@@ -85,7 +85,7 @@ def pypi_link(pkg_filename):
 
 setup_params = dict(
     name="setuptools",
-    version="33.1.1",
+    version="33.0.0",
     description="Easily download, build, install, upgrade, and uninstall "
         "Python packages",
     author="Python Packaging Authority",
@@ -145,14 +145,12 @@ setup_params = dict(
         Intended Audience :: Developers
         License :: OSI Approved :: MIT License
         Operating System :: OS Independent
-        Programming Language :: Python :: 2
         Programming Language :: Python :: 2.6
         Programming Language :: Python :: 2.7
         Programming Language :: Python :: 3
         Programming Language :: Python :: 3.3
         Programming Language :: Python :: 3.4
         Programming Language :: Python :: 3.5
-        Programming Language :: Python :: 3.6
         Topic :: Software Development :: Libraries :: Python Modules
         Topic :: System :: Archiving :: Packaging
         Topic :: System :: Systems Administration
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 04f767408..54577cede 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -7,7 +7,7 @@ import distutils.filelist
 from distutils.util import convert_path
 from fnmatch import fnmatchcase
 
-from setuptools.extern.six.moves import filter, map
+from setuptools.extern.six.moves import filter, filterfalse, map
 
 import setuptools.version
 from setuptools.extension import Extension
diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py
index efbe9411c..c96d33c23 100644
--- a/setuptools/command/__init__.py
+++ b/setuptools/command/__init__.py
@@ -2,7 +2,7 @@ __all__ = [
     'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop',
     'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',
     'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts',
-    'register', 'bdist_wininst', 'upload_docs', 'upload',
+    'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib',
 ]
 
 from distutils.command.bdist import bdist
diff --git a/setuptools/command/build_clib.py b/setuptools/command/build_clib.py
new file mode 100644
index 000000000..09caff6ff
--- /dev/null
+++ b/setuptools/command/build_clib.py
@@ -0,0 +1,98 @@
+import distutils.command.build_clib as orig
+from distutils.errors import DistutilsSetupError
+from distutils import log
+from setuptools.dep_util import newer_pairwise_group
+
+
+class build_clib(orig.build_clib):
+    """
+    Override the default build_clib behaviour to do the following:
+
+    1. Implement a rudimentary timestamp-based dependency system
+       so 'compile()' doesn't run every time.
+    2. Add more keys to the 'build_info' dictionary:
+        * obj_deps - specify dependencies for each object compiled.
+                     this should be a dictionary mapping a key
+                     with the source filename to a list of
+                     dependencies. Use an empty string for global
+                     dependencies.
+        * cflags   - specify a list of additional flags to pass to
+                     the compiler.
+    """
+
+    def build_libraries(self, libraries):
+        for (lib_name, build_info) in libraries:
+            sources = build_info.get('sources')
+            if sources is None or not isinstance(sources, (list, tuple)):
+                raise DistutilsSetupError(
+                       "in 'libraries' option (library '%s'), "
+                       "'sources' must be present and must be "
+                       "a list of source filenames" % lib_name)
+            sources = list(sources)
+
+            log.info("building '%s' library", lib_name)
+
+            # 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)
+
+            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(
+                    expected_objects,
+                    lib_name,
+                    output_dir=self.build_clib,
+                    debug=self.debug
+                    )
diff --git a/setuptools/dep_util.py b/setuptools/dep_util.py
new file mode 100644
index 000000000..2931c13ec
--- /dev/null
+++ b/setuptools/dep_util.py
@@ -0,0 +1,23 @@
+from distutils.dep_util import 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
diff --git a/setuptools/namespaces.py b/setuptools/namespaces.py
index bd17d9e7c..0a889f224 100755
--- a/setuptools/namespaces.py
+++ b/setuptools/namespaces.py
@@ -1,4 +1,5 @@
 import os
+import sys
 from distutils import log
 import itertools
 
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 45fac01a4..d80d43bc7 100755
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -20,7 +20,7 @@ from setuptools.extern.six.moves import urllib, http_client, configparser, map
 import setuptools
 from pkg_resources import (
     CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST,
-    Environment, find_distributions, safe_name, safe_version,
+    require, Environment, find_distributions, safe_name, safe_version,
     to_filename, Requirement, DEVELOP_DIST,
 )
 from setuptools import ssl_support
@@ -48,7 +48,7 @@ __all__ = [
 _SOCKET_TIMEOUT = 15
 
 _tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}"
-user_agent = _tmpl.format(py_major=sys.version[:3], setuptools=setuptools)
+user_agent = _tmpl.format(py_major=sys.version[:3], **globals())
 
 
 def parse_requirement_arg(spec):
diff --git a/setuptools/py33compat.py b/setuptools/py33compat.py
index af64d5d1e..2588d680c 100644
--- a/setuptools/py33compat.py
+++ b/setuptools/py33compat.py
@@ -1,4 +1,5 @@
 import dis
+import code
 import array
 import collections
 
diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py
index 72b18ef26..82f8870ab 100644
--- a/setuptools/ssl_support.py
+++ b/setuptools/ssl_support.py
@@ -2,10 +2,10 @@ import os
 import socket
 import atexit
 import re
-import functools
 
-from setuptools.extern.six.moves import urllib, http_client, map, filter
+from setuptools.extern.six.moves import urllib, http_client, map
 
+import pkg_resources
 from pkg_resources import ResolutionError, ExtractionError
 
 try:
@@ -204,52 +204,47 @@ def opener_for(ca_bundle=None):
     ).open
 
 
-# from jaraco.functools
-def once(func):
-    @functools.wraps(func)
-    def wrapper(*args, **kwargs):
-        if not hasattr(func, 'always_returns'):
-            func.always_returns = func(*args, **kwargs)
-        return func.always_returns
-    return wrapper
+_wincerts = None
 
 
-@once
 def get_win_certfile():
+    global _wincerts
+    if _wincerts is not None:
+        return _wincerts.name
+
     try:
-        import wincertstore
+        from wincertstore import CertFile
     except ImportError:
         return None
 
-    class CertFile(wincertstore.CertFile):
-        def __init__(self):
-            super(CertFile, self).__init__()
+    class MyCertFile(CertFile):
+        def __init__(self, stores=(), certs=()):
+            CertFile.__init__(self)
+            for store in stores:
+                self.addstore(store)
+            self.addcerts(certs)
             atexit.register(self.close)
 
         def close(self):
             try:
-                super(CertFile, self).close()
+                super(MyCertFile, self).close()
             except OSError:
                 pass
 
-    _wincerts = CertFile()
-    _wincerts.addstore('CA')
-    _wincerts.addstore('ROOT')
+    _wincerts = MyCertFile(stores=['CA', 'ROOT'])
     return _wincerts.name
 
 
 def find_ca_bundle():
     """Return an existing CA bundle path, or None"""
-    extant_cert_paths = filter(os.path.isfile, cert_paths)
-    return (
-        get_win_certfile()
-        or next(extant_cert_paths, None)
-        or _certifi_where()
-    )
-
-
-def _certifi_where():
+    if os.name == 'nt':
+        return get_win_certfile()
+    else:
+        for cert_path in cert_paths:
+            if os.path.isfile(cert_path):
+                return cert_path
     try:
-        return __import__('certifi').where()
+        import certifi
+        return certifi.where()
     except (ImportError, ResolutionError, ExtractionError):
-        pass
+        return None


Test patch:
diff --git a/setuptools/tests/test_build_clib.py b/setuptools/tests/test_build_clib.py
new file mode 100644
index 000000000..7e3d1de9d
--- /dev/null
+++ b/setuptools/tests/test_build_clib.py
@@ -0,0 +1,59 @@
+import pytest
+import os
+import shutil
+
+from unittest import mock
+from distutils.errors import DistutilsSetupError
+from setuptools.command.build_clib import build_clib
+from setuptools.dist import Distribution
+
+
+class TestBuildCLib:
+    @mock.patch(
+            'setuptools.command.build_clib.newer_pairwise_group'
+            )
+    def test_build_libraries(self, mock_newer):
+        dist = Distribution()
+        cmd = build_clib(dist)
+
+        # this will be a long section, just making sure all
+        # exceptions are properly raised
+        libs = [('example', {'sources': 'broken.c'})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.build_libraries(libs)
+
+        obj_deps = 'some_string'
+        libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.build_libraries(libs)
+
+        obj_deps = {'': ''}
+        libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.build_libraries(libs)
+
+        obj_deps = {'source.c': ''}
+        libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+        with pytest.raises(DistutilsSetupError):
+            cmd.build_libraries(libs)
+
+        # with that out of the way, let's see if the crude dependency
+        # system works
+        cmd.compiler = 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)
+        assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0]
+        assert not cmd.compiler.compile.called
+        assert 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)
+        assert cmd.compiler.compile.call_count == 1
+        assert cmd.compiler.create_static_lib.call_count == 1
diff --git a/setuptools/tests/test_dep_util.py b/setuptools/tests/test_dep_util.py
new file mode 100644
index 000000000..e5027c102
--- /dev/null
+++ b/setuptools/tests/test_dep_util.py
@@ -0,0 +1,30 @@
+from setuptools.dep_util import newer_pairwise_group
+import os
+import pytest
+
+
+@pytest.fixture
+def groups_target(tmpdir):
+    """Sets up some older sources, a target and newer sources.
+    Returns a 3-tuple in this order.
+    """
+    creation_order = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h']
+    mtime = 0
+
+    for i in range(len(creation_order)):
+        creation_order[i] = os.path.join(str(tmpdir), creation_order[i])
+        with open(creation_order[i], 'w'):
+            pass
+
+        # make sure modification times are sequential
+        os.utime(creation_order[i], (mtime, mtime))
+        mtime += 1
+
+    return creation_order[:2], creation_order[2], creation_order[3:]
+
+
+def test_newer_pairwise_group(groups_target):
+    older = newer_pairwise_group([groups_target[0]], [groups_target[1]])
+    newer = newer_pairwise_group([groups_target[2]], [groups_target[1]])
+    assert older == ([], [])
+    assert newer == ([groups_target[2]], [groups_target[1]])