Repository: conda/conda-build

Issue:
Repo specifies LF line endings, but some source files have CRLF line endings
As of 610f5922e5625d34f699f2098b503a749ea97a91 by @msarahan, the .gitattributes and .editorconfig file for this git repo specify LF (Unix-style) line endings, but there are a number of source files in the repo that still have CRLF (Windows-style) line endings. This means commits to these files have their line endings converted to LF on commit, resulting in noisy patches (see, e.g. https://github.com/conda/conda-build/pull/3477).

https://github.com/conda/conda-build/blob/cf48343e2e9f9d05aa86105e9c024c0ed8160d96/.gitattributes#L2

https://github.com/conda/conda-build/blob/610f5922e5625d34f699f2098b503a749ea97a91/.editorconfig#L6-L9

Find all files containing `CR` (^-M) characters:

```
$ git config core.autocrlf false
$ git clone git@github.com:conda/conda-build.git
$ cd conda-build
$ grep -IUrl  $'\r'
news/TEMPLATE
.gitignore
tests/test-recipes/metadata/_script_win_creates_exe_garbled/test-script
README.rst
docs/source/resources/define-metadata.rst
docs/source/concepts/index.rst
docs/source/concepts/generating-index.rst
docs/source/concepts/package-anatomy.rst
docs/source/concepts/recipe.rst
ci/appveyor/setup_x64.bat
.travis.yml
rever.xsh
.github/pull_request_template.md
conda_build/utils.py
conda_build/index.py
```

I am not a Git wizard, but I think the solution is to convert all these files to CRLF; I'll submit a PR for this.

<!--
Thanks for opening an issue! A few things to keep in mind:

- This issue tracker is for issues with or enhancements for conda-build.

For conda issues, you should open an issue at https://github.com/conda/conda/issues
For Anaconda issues, you should open an issue at https://github.com/ContinuumIO/anaconda-issues/issues
For an issue with a particular conda-forge package, you should open an issue on the 
corresponding feedstock: https://github.com/conda-forge
-->




Discussion:
I will not change that file in 'tests', which may be data files explicitly to test handling of line endings/control characters.

As of Git 2.16, there is now a 'renormalize' flag to git-add, which is the recommended way to update a repository after introducing EOL normalization (as happened in 610f592)
https://stackoverflow.com/questions/7156694/git-how-to-renormalize-line-endings-in-all-files-in-all-revisions

Interestingly, Git (at least as of version 2.21) no longer tries to normalize that test file, which makes things simpler). 


Resolving patch:
diff --git a/conda_build/index.py b/conda_build/index.py
--- a/conda_build/index.py
+++ b/conda_build/index.py
@@ -1,49 +1,49 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2018 Anaconda, Inc
-# SPDX-License-Identifier: Proprietary
-from __future__ import absolute_import, division, print_function, unicode_literals
-
-import bz2
-from collections import OrderedDict, defaultdict
-from datetime import datetime
-
-import json
-from numbers import Number
-import os
-from os.path import abspath, basename, getmtime, getsize, isdir, isfile, join, lexists, splitext, dirname
-from shutil import copy2, move
-import subprocess
-import tarfile
-from tempfile import gettempdir
-import time
-from uuid import uuid4
-
-# Lots of conda internals here.  Should refactor to use exports.
-from conda.common.compat import ensure_binary
-# from conda.resolve import dashlist
-
-import pytz
-from jinja2 import Environment, PackageLoader
-from tqdm import tqdm
-import yaml
-from yaml.constructor import ConstructorError
-from yaml.parser import ParserError
-from yaml.scanner import ScannerError
-
-import contextlib
-import fnmatch
-from functools import partial
-import logging
-import libarchive
+# -*- coding: utf-8 -*-
+# Copyright (C) 2018 Anaconda, Inc
+# SPDX-License-Identifier: Proprietary
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import bz2
+from collections import OrderedDict, defaultdict
+from datetime import datetime
+
+import json
+from numbers import Number
+import os
+from os.path import abspath, basename, getmtime, getsize, isdir, isfile, join, lexists, splitext, dirname
+from shutil import copy2, move
+import subprocess
+import tarfile
+from tempfile import gettempdir
+import time
+from uuid import uuid4
+
+# Lots of conda internals here.  Should refactor to use exports.
+from conda.common.compat import ensure_binary
+# from conda.resolve import dashlist
+
+import pytz
+from jinja2 import Environment, PackageLoader
+from tqdm import tqdm
+import yaml
+from yaml.constructor import ConstructorError
+from yaml.parser import ParserError
+from yaml.scanner import ScannerError
+
+import contextlib
+import fnmatch
+from functools import partial
+import logging
+import libarchive
 import conda_package_handling.api
-
-
-from . import conda_interface, utils
-from .conda_interface import MatchSpec, VersionOrder, human_bytes, context
-from .conda_interface import CondaError, CondaHTTPError, get_index, url_path
-from .conda_interface import download, TemporaryDirectory
+
+
+from . import conda_interface, utils
+from .conda_interface import MatchSpec, VersionOrder, human_bytes, context
+from .conda_interface import CondaError, CondaHTTPError, get_index, url_path
+from .conda_interface import download, TemporaryDirectory
 from .utils import glob, get_logger, FileNotFoundError, PermissionError
-
+
 # try:
 #     from conda.base.constants import CONDA_TARBALL_EXTENSIONS
 # except Exception:
@@ -52,630 +52,630 @@
 
 # TODO: better to define this in conda; doing it here because we're implementing it in conda-build first
 CONDA_TARBALL_EXTENSIONS = ('.conda', '.tar.bz2')
-
-try:
-    from json.decoder import JSONDecodeError
-except ImportError:
-    JSONDecodeError = ValueError
-
-
-log = get_logger(__name__)
-
-try:
-    from conda.common.io import ThreadLimitedThreadPoolExecutor, as_completed
-except ImportError:
-    from concurrent.futures import ThreadPoolExecutor, _base, as_completed
-    from concurrent.futures.thread import _WorkItem
-
-    class ThreadLimitedThreadPoolExecutor(ThreadPoolExecutor):
-
-        def __init__(self, max_workers=10):
-            super(ThreadLimitedThreadPoolExecutor, self).__init__(max_workers)
-
-        def submit(self, fn, *args, **kwargs):
-            """
-            This is an exact reimplementation of the `submit()` method on the parent class, except
-            with an added `try/except` around `self._adjust_thread_count()`.  So long as there is at
-            least one living thread, this thread pool will not throw an exception if threads cannot
-            be expanded to `max_workers`.
-
-            In the implementation, we use "protected" attributes from concurrent.futures (`_base`
-            and `_WorkItem`). Consider vendoring the whole concurrent.futures library
-            as an alternative to these protected imports.
-
-            https://github.com/agronholm/pythonfutures/blob/3.2.0/concurrent/futures/thread.py#L121-L131  # NOQA
-            https://github.com/python/cpython/blob/v3.6.4/Lib/concurrent/futures/thread.py#L114-L124
-            """
-            with self._shutdown_lock:
-                if self._shutdown:
-                    raise RuntimeError('cannot schedule new futures after shutdown')
-
-                f = _base.Future()
-                w = _WorkItem(f, fn, args, kwargs)
-
-                self._work_queue.put(w)
-                try:
-                    self._adjust_thread_count()
-                except RuntimeError:
-                    # RuntimeError: can't start new thread
-                    # See https://github.com/conda/conda/issues/6624
-                    if len(self._threads) > 0:
-                        # It's ok to not be able to start new threads if we already have at least
-                        # one thread alive.
-                        pass
-                    else:
-                        raise
-                return f
-
-try:
-    from conda.base.constants import NAMESPACES_MAP, NAMESPACE_PACKAGE_NAMES
-except ImportError:
-    NAMESPACES_MAP = {  # base package name, namespace
-        "python": "python",
-        "r": "r",
-        "r-base": "r",
-        "mro-base": "r",
-        "mro-base_impl": "r",
-        "erlang": "erlang",
-        "java": "java",
-        "openjdk": "java",
-        "julia": "julia",
-        "latex": "latex",
-        "lua": "lua",
-        "nodejs": "js",
-        "perl": "perl",
-        "php": "php",
-        "ruby": "ruby",
-        "m2-base": "m2",
-        "msys2-conda-epoch": "m2w64",
-    }
-    NAMESPACE_PACKAGE_NAMES = frozenset(NAMESPACES_MAP)
-    NAMESPACES = frozenset(NAMESPACES_MAP.values())
-
-local_index_timestamp = 0
-cached_index = None
-local_subdir = ""
-cached_channels = []
-channel_data = {}
-
-
+
+try:
+    from json.decoder import JSONDecodeError
+except ImportError:
+    JSONDecodeError = ValueError
+
+
+log = get_logger(__name__)
+
+try:
+    from conda.common.io import ThreadLimitedThreadPoolExecutor, as_completed
+except ImportError:
+    from concurrent.futures import ThreadPoolExecutor, _base, as_completed
+    from concurrent.futures.thread import _WorkItem
+
+    class ThreadLimitedThreadPoolExecutor(ThreadPoolExecutor):
+
+        def __init__(self, max_workers=10):
+            super(ThreadLimitedThreadPoolExecutor, self).__init__(max_workers)
+
+        def submit(self, fn, *args, **kwargs):
+            """
+            This is an exact reimplementation of the `submit()` method on the parent class, except
+            with an added `try/except` around `self._adjust_thread_count()`.  So long as there is at
+            least one living thread, this thread pool will not throw an exception if threads cannot
+            be expanded to `max_workers`.
+
+            In the implementation, we use "protected" attributes from concurrent.futures (`_base`
+            and `_WorkItem`). Consider vendoring the whole concurrent.futures library
+            as an alternative to these protected imports.
+
+            https://github.com/agronholm/pythonfutures/blob/3.2.0/concurrent/futures/thread.py#L121-L131  # NOQA
+            https://github.com/python/cpython/blob/v3.6.4/Lib/concurrent/futures/thread.py#L114-L124
+            """
+            with self._shutdown_lock:
+                if self._shutdown:
+                    raise RuntimeError('cannot schedule new futures after shutdown')
+
+                f = _base.Future()
+                w = _WorkItem(f, fn, args, kwargs)
+
+                self._work_queue.put(w)
+                try:
+                    self._adjust_thread_count()
+                except RuntimeError:
+                    # RuntimeError: can't start new thread
+                    # See https://github.com/conda/conda/issues/6624
+                    if len(self._threads) > 0:
+                        # It's ok to not be able to start new threads if we already have at least
+                        # one thread alive.
+                        pass
+                    else:
+                        raise
+                return f
+
+try:
+    from conda.base.constants import NAMESPACES_MAP, NAMESPACE_PACKAGE_NAMES
+except ImportError:
+    NAMESPACES_MAP = {  # base package name, namespace
+        "python": "python",
+        "r": "r",
+        "r-base": "r",
+        "mro-base": "r",
+        "mro-base_impl": "r",
+        "erlang": "erlang",
+        "java": "java",
+        "openjdk": "java",
+        "julia": "julia",
+        "latex": "latex",
+        "lua": "lua",
+        "nodejs": "js",
+        "perl": "perl",
+        "php": "php",
+        "ruby": "ruby",
+        "m2-base": "m2",
+        "msys2-conda-epoch": "m2w64",
+    }
+    NAMESPACE_PACKAGE_NAMES = frozenset(NAMESPACES_MAP)
+    NAMESPACES = frozenset(NAMESPACES_MAP.values())
+
+local_index_timestamp = 0
+cached_index = None
+local_subdir = ""
+cached_channels = []
+channel_data = {}
+
+
 # TODO: support for libarchive seems to have broken ability to use multiple threads here.
 #    The new conda format is so much faster that it more than makes up for it.  However, it
 #    would be nice to fix this at some point.
 MAX_THREADS_DEFAULT = 1  # os.cpu_count() if (hasattr(os, "cpu_count") and os.cpu_count() > 1) else 1
-LOCK_TIMEOUT_SECS = 3 * 3600
-LOCKFILE_NAME = ".lock"
-DEFAULT_SUBDIRS = (
-    "linux-64",
-    "linux-32",
-    "linux-ppc64le",
-    "linux-armv6l",
-    "linux-armv7l",
-    "linux-aarch64",
-    "win-64",
-    "win-32",
-    "osx-64",
-    "zos-z",
-    "noarch",
-)
-
-# TODO: this is to make sure that the index doesn't leak tokens.  It breaks use of private channels, though.
-# os.environ['CONDA_ADD_ANACONDA_TOKEN'] = "false"
-
-
-try:
-    from cytoolz.itertoolz import concat, concatv, groupby
-except ImportError:  # pragma: no cover
-    from conda._vendor.toolz.itertoolz import concat, concatv, groupby  # NOQA
-
-
-def _download_channeldata(channel_url):
-    with TemporaryDirectory() as td:
-        tf = os.path.join(td, "channeldata.json")
-        download(channel_url, tf)
-        try:
-            with open(tf) as f:
-                data = json.load(f)
-        except JSONDecodeError:
-            data = {}
-    return data
-
-
-def get_build_index(subdir, bldpkgs_dir, output_folder=None, clear_cache=False,
-                    omit_defaults=False, channel_urls=None, debug=False, verbose=True,
-                    **kwargs):
-    global local_index_timestamp
-    global local_subdir
-    global cached_index
-    global cached_channels
-    global channel_data
-    mtime = 0
-
-    channel_urls = list(utils.ensure_list(channel_urls))
-
-    if not output_folder:
-        output_folder = dirname(bldpkgs_dir)
-
-    # check file modification time - this is the age of our index.
-    index_file = os.path.join(output_folder, subdir, 'repodata.json')
-    if os.path.isfile(index_file):
-        mtime = os.path.getmtime(index_file)
-
-    if (clear_cache or
-            not os.path.isfile(index_file) or
-            local_subdir != subdir or
-            mtime > local_index_timestamp or
-            cached_channels != channel_urls):
-
-        # priority: (local as either croot or output_folder IF NOT EXPLICITLY IN CHANNEL ARGS),
-        #     then channels passed as args (if local in this, it remains in same order),
-        #     then channels from condarc.
-        urls = list(channel_urls)
-
-        loggers = utils.LoggingContext.default_loggers + [__name__]
-        if debug:
-            log_context = partial(utils.LoggingContext, logging.DEBUG, loggers=loggers)
-        elif verbose:
-            log_context = partial(utils.LoggingContext, logging.WARN, loggers=loggers)
-        else:
-            log_context = partial(utils.LoggingContext, logging.CRITICAL + 1, loggers=loggers)
-            capture = utils.capture
-        with log_context():
-            # this is where we add the "local" channel.  It's a little smarter than conda, because
-            #     conda does not know about our output_folder when it is not the default setting.
-            if os.path.isdir(output_folder):
-                local_path = url_path(output_folder)
-                # replace local with the appropriate real channel.  Order is maintained.
-                urls = [url if url != 'local' else local_path for url in urls]
-                if local_path not in urls:
-                    urls.insert(0, local_path)
-            _ensure_valid_channel(output_folder, subdir)
-            update_index(output_folder, verbose=debug)
-
-            # silence output from conda about fetching index files
-            capture = contextlib.contextmanager(lambda: (yield))
-
-            with capture():
-                # replace noarch with native subdir - this ends up building an index with both the
-                #      native content and the noarch content.
-                if subdir == 'noarch':
-                    subdir = conda_interface.subdir
-                try:
-                    cached_index = get_index(channel_urls=urls,
-                                    prepend=not omit_defaults,
-                                    use_local=False,
-                                    use_cache=False,
-                                    platform=subdir)
-                # HACK: defaults does not have the many subfolders we support.  Omit it and
-                #          try again.
-                except CondaHTTPError:
-                    if 'defaults' in urls:
-                        urls.remove('defaults')
-                    cached_index = get_index(channel_urls=urls,
-                                             prepend=omit_defaults,
-                                             use_local=False,
-                                             use_cache=False,
-                                             platform=subdir)
-
-            expanded_channels = {rec.channel for rec in cached_index.values()}
-
-            superchannel = {}
-            # we need channeldata.json too, as it is a more reliable source of run_exports data
-            for channel in expanded_channels:
-                if channel.scheme == "file":
-                    location = channel.location
-                    if utils.on_win:
-                        location = location.lstrip("/")
-                    elif (not os.path.isabs(channel.location) and
-                            os.path.exists(os.path.join(os.path.sep, channel.location))):
-                        location = os.path.join(os.path.sep, channel.location)
-                    channeldata_file = os.path.join(location, channel.name, 'channeldata.json')
-                    retry = 0
-                    max_retries = 10
-                    while retry < max_retries:
-                        try:
-                            with open(channeldata_file, "r+") as f:
-                                channel_data[channel.name] = json.load(f)
-                            break
-                        except (IOError, JSONDecodeError):
-                            time.sleep(0.2)
-                            retry += 1
-                else:
-                    # download channeldata.json for url
-                    if not context.offline:
-                        try:
-                            channel_data[channel.name] = _download_channeldata(channel.base_url + '/channeldata.json')
-                        except CondaHTTPError:
-                            continue
-                # collapse defaults metachannel back into one superchannel, merging channeldata
-                if channel.base_url in context.default_channels and channel_data.get(channel.name):
-                    packages = superchannel.get('packages', {})
-                    packages.update(channel_data[channel.name])
-                    superchannel['packages'] = packages
-            channel_data['defaults'] = superchannel
-        local_index_timestamp = os.path.getmtime(index_file)
-        local_subdir = subdir
-        cached_channels = channel_urls
-    return cached_index, local_index_timestamp, channel_data
-
-
-def _ensure_valid_channel(local_folder, subdir):
-    for folder in {subdir, 'noarch'}:
-        path = os.path.join(local_folder, folder)
-        if not os.path.isdir(path):
-            os.makedirs(path)
-
-
-def update_index(dir_path, check_md5=False, channel_name=None, patch_generator=None, threads=MAX_THREADS_DEFAULT,
+LOCK_TIMEOUT_SECS = 3 * 3600
+LOCKFILE_NAME = ".lock"
+DEFAULT_SUBDIRS = (
+    "linux-64",
+    "linux-32",
+    "linux-ppc64le",
+    "linux-armv6l",
+    "linux-armv7l",
+    "linux-aarch64",
+    "win-64",
+    "win-32",
+    "osx-64",
+    "zos-z",
+    "noarch",
+)
+
+# TODO: this is to make sure that the index doesn't leak tokens.  It breaks use of private channels, though.
+# os.environ['CONDA_ADD_ANACONDA_TOKEN'] = "false"
+
+
+try:
+    from cytoolz.itertoolz import concat, concatv, groupby
+except ImportError:  # pragma: no cover
+    from conda._vendor.toolz.itertoolz import concat, concatv, groupby  # NOQA
+
+
+def _download_channeldata(channel_url):
+    with TemporaryDirectory() as td:
+        tf = os.path.join(td, "channeldata.json")
+        download(channel_url, tf)
+        try:
+            with open(tf) as f:
+                data = json.load(f)
+        except JSONDecodeError:
+            data = {}
+    return data
+
+
+def get_build_index(subdir, bldpkgs_dir, output_folder=None, clear_cache=False,
+                    omit_defaults=False, channel_urls=None, debug=False, verbose=True,
+                    **kwargs):
+    global local_index_timestamp
+    global local_subdir
+    global cached_index
+    global cached_channels
+    global channel_data
+    mtime = 0
+
+    channel_urls = list(utils.ensure_list(channel_urls))
+
+    if not output_folder:
+        output_folder = dirname(bldpkgs_dir)
+
+    # check file modification time - this is the age of our index.
+    index_file = os.path.join(output_folder, subdir, 'repodata.json')
+    if os.path.isfile(index_file):
+        mtime = os.path.getmtime(index_file)
+
+    if (clear_cache or
+            not os.path.isfile(index_file) or
+            local_subdir != subdir or
+            mtime > local_index_timestamp or
+            cached_channels != channel_urls):
+
+        # priority: (local as either croot or output_folder IF NOT EXPLICITLY IN CHANNEL ARGS),
+        #     then channels passed as args (if local in this, it remains in same order),
+        #     then channels from condarc.
+        urls = list(channel_urls)
+
+        loggers = utils.LoggingContext.default_loggers + [__name__]
+        if debug:
+            log_context = partial(utils.LoggingContext, logging.DEBUG, loggers=loggers)
+        elif verbose:
+            log_context = partial(utils.LoggingContext, logging.WARN, loggers=loggers)
+        else:
+            log_context = partial(utils.LoggingContext, logging.CRITICAL + 1, loggers=loggers)
+            capture = utils.capture
+        with log_context():
+            # this is where we add the "local" channel.  It's a little smarter than conda, because
+            #     conda does not know about our output_folder when it is not the default setting.
+            if os.path.isdir(output_folder):
+                local_path = url_path(output_folder)
+                # replace local with the appropriate real channel.  Order is maintained.
+                urls = [url if url != 'local' else local_path for url in urls]
+                if local_path not in urls:
+                    urls.insert(0, local_path)
+            _ensure_valid_channel(output_folder, subdir)
+            update_index(output_folder, verbose=debug)
+
+            # silence output from conda about fetching index files
+            capture = contextlib.contextmanager(lambda: (yield))
+
+            with capture():
+                # replace noarch with native subdir - this ends up building an index with both the
+                #      native content and the noarch content.
+                if subdir == 'noarch':
+                    subdir = conda_interface.subdir
+                try:
+                    cached_index = get_index(channel_urls=urls,
+                                    prepend=not omit_defaults,
+                                    use_local=False,
+                                    use_cache=False,
+                                    platform=subdir)
+                # HACK: defaults does not have the many subfolders we support.  Omit it and
+                #          try again.
+                except CondaHTTPError:
+                    if 'defaults' in urls:
+                        urls.remove('defaults')
+                    cached_index = get_index(channel_urls=urls,
+                                             prepend=omit_defaults,
+                                             use_local=False,
+                                             use_cache=False,
+                                             platform=subdir)
+
+            expanded_channels = {rec.channel for rec in cached_index.values()}
+
+            superchannel = {}
+            # we need channeldata.json too, as it is a more reliable source of run_exports data
+            for channel in expanded_channels:
+                if channel.scheme == "file":
+                    location = channel.location
+                    if utils.on_win:
+                        location = location.lstrip("/")
+                    elif (not os.path.isabs(channel.location) and
+                            os.path.exists(os.path.join(os.path.sep, channel.location))):
+                        location = os.path.join(os.path.sep, channel.location)
+                    channeldata_file = os.path.join(location, channel.name, 'channeldata.json')
+                    retry = 0
+                    max_retries = 10
+                    while retry < max_retries:
+                        try:
+                            with open(channeldata_file, "r+") as f:
+                                channel_data[channel.name] = json.load(f)
+                            break
+                        except (IOError, JSONDecodeError):
+                            time.sleep(0.2)
+                            retry += 1
+                else:
+                    # download channeldata.json for url
+                    if not context.offline:
+                        try:
+                            channel_data[channel.name] = _download_channeldata(channel.base_url + '/channeldata.json')
+                        except CondaHTTPError:
+                            continue
+                # collapse defaults metachannel back into one superchannel, merging channeldata
+                if channel.base_url in context.default_channels and channel_data.get(channel.name):
+                    packages = superchannel.get('packages', {})
+                    packages.update(channel_data[channel.name])
+                    superchannel['packages'] = packages
+            channel_data['defaults'] = superchannel
+        local_index_timestamp = os.path.getmtime(index_file)
+        local_subdir = subdir
+        cached_channels = channel_urls
+    return cached_index, local_index_timestamp, channel_data
+
+
+def _ensure_valid_channel(local_folder, subdir):
+    for folder in {subdir, 'noarch'}:
+        path = os.path.join(local_folder, folder)
+        if not os.path.isdir(path):
+            os.makedirs(path)
+
+
+def update_index(dir_path, check_md5=False, channel_name=None, patch_generator=None, threads=MAX_THREADS_DEFAULT,
                  verbose=False, progress=False, hotfix_source_repo=None, subdirs=None, warn=True,
                  convert_if_not_present=False):
-    """
-    If dir_path contains a directory named 'noarch', the path tree therein is treated
-    as though it's a full channel, with a level of subdirs, each subdir having an update
-    to repodata.json.  The full channel will also have a channeldata.json file.
-
-    If dir_path does not contain a directory named 'noarch', but instead contains at least
-    one '*.tar.bz2' file, the directory is assumed to be a standard subdir, and only repodata.json
-    information will be updated.
-
-    """
-    base_path, dirname = os.path.split(dir_path)
-    if dirname in DEFAULT_SUBDIRS:
-        if warn:
-            log.warn("The update_index function has changed to index all subdirs at once.  You're pointing it at a single subdir.  "
-                    "Please update your code to point it at the channel root, rather than a subdir.")
-        return update_index(base_path, check_md5=check_md5, channel_name=channel_name,
-                            threads=threads, verbose=verbose, progress=progress,
+    """
+    If dir_path contains a directory named 'noarch', the path tree therein is treated
+    as though it's a full channel, with a level of subdirs, each subdir having an update
+    to repodata.json.  The full channel will also have a channeldata.json file.
+
+    If dir_path does not contain a directory named 'noarch', but instead contains at least
+    one '*.tar.bz2' file, the directory is assumed to be a standard subdir, and only repodata.json
+    information will be updated.
+
+    """
+    base_path, dirname = os.path.split(dir_path)
+    if dirname in DEFAULT_SUBDIRS:
+        if warn:
+            log.warn("The update_index function has changed to index all subdirs at once.  You're pointing it at a single subdir.  "
+                    "Please update your code to point it at the channel root, rather than a subdir.")
+        return update_index(base_path, check_md5=check_md5, channel_name=channel_name,
+                            threads=threads, verbose=verbose, progress=progress,
                             hotfix_source_repo=hotfix_source_repo,
                             convert_if_not_present=convert_if_not_present)
-    return ChannelIndex(dir_path, channel_name, subdirs=subdirs, threads=threads,
-                        deep_integrity_check=check_md5).index(patch_generator=patch_generator, verbose=verbose,
-                                                              progress=progress,
+    return ChannelIndex(dir_path, channel_name, subdirs=subdirs, threads=threads,
+                        deep_integrity_check=check_md5).index(patch_generator=patch_generator, verbose=verbose,
+                                                              progress=progress,
                                                               hotfix_source_repo=hotfix_source_repo,
                                                               convert_if_not_present=convert_if_not_present)
-
-
-def _determine_namespace(info):
-    if info.get('namespace'):
-        namespace = info['namespace']
-    else:
-        depends_names = set()
-        for spec in info.get('depends', []):
-            try:
-                depends_names.add(MatchSpec(spec).name)
-            except CondaError:
-                pass
-        spaces = depends_names & NAMESPACE_PACKAGE_NAMES
-        if len(spaces) == 1:
-            namespace = NAMESPACES_MAP[spaces.pop()]
-        else:
-            namespace = "global"
-        info['namespace'] = namespace
-
-    if not info.get('namespace_in_name') and '-' in info['name']:
-        namespace_prefix, reduced_name = info['name'].split('-', 1)
-        if namespace_prefix == namespace:
-            info['name_in_channel'] = info['name']
-            info['name'] = reduced_name
-
-    return namespace, info.get('name_in_channel', info['name']), info['name']
-
-
-def _make_seconds(timestamp):
-    timestamp = int(timestamp)
-    if timestamp > 253402300799:  # 9999-12-31
-        timestamp //= 1000  # convert milliseconds to seconds; see conda/conda-build#1988
-    return timestamp
-
-
-# ==========================================================================
-
-
-REPODATA_VERSION = 1
-CHANNELDATA_VERSION = 1
-REPODATA_JSON_FN = 'repodata.json'
-CHANNELDATA_FIELDS = (
-    "description",
-    "dev_url",
-    "doc_url",
-    "doc_source_url",
-    "home",
-    "license",
-    "reference_package",
-    "source_url",
-    "source_git_url",
-    "source_git_tag",
-    "source_git_rev",
-    "summary",
-    "version",
-    "subdirs",
-    "icon_url",
-    "icon_hash",  # "md5:abc123:12"
-    "run_exports",
-    "binary_prefix",
-    "text_prefix",
-    "activate.d",
-    "deactivate.d",
-    "pre_link",
-    "post_link",
-    "pre_unlink",
-    "tags",
-    "identifiers",
-    "keywords",
-    "recipe_origin",
-    "commits",
-)
-
-
-def _clear_newline_chars(record, field_name):
-    if field_name in record:
-        try:
-            record[field_name] = record[field_name].strip().replace('\n', ' ')
-        except AttributeError:
-            # sometimes description gets added as a list instead of just a string
-            record[field_name] = record[field_name][0].strip().replace('\n', ' ')
-
-
-def _apply_instructions(subdir, repodata, instructions):
-    repodata.setdefault("removed", [])
-    utils.merge_or_update_dict(repodata.get('packages', {}), instructions.get('packages', {}), merge=False,
-                               add_missing_keys=False)
-
-    for fn in instructions.get('revoke', ()):
-        repodata['packages'][fn]['revoked'] = True
-        repodata['packages'][fn]['depends'].append('package_has_been_revoked')
-
-    for fn in instructions.get('remove', ()):
-        popped = repodata['packages'].pop(fn, None)
-        if popped:
-            repodata["removed"].append(fn)
-    repodata["removed"].sort()
-
-    return repodata
-
-
-def _get_jinja2_environment():
-    def _filter_strftime(dt, dt_format):
-        if isinstance(dt, Number):
-            if dt > 253402300799:  # 9999-12-31
-                dt //= 1000  # convert milliseconds to seconds; see #1988
-            dt = datetime.utcfromtimestamp(dt).replace(tzinfo=pytz.timezone("UTC"))
-        return dt.strftime(dt_format)
-
-    def _filter_add_href(text, link, **kwargs):
-        if link:
-            kwargs_list = ['href="{0}"'.format(link)]
-            kwargs_list.append('alt="{0}"'.format(text))
-            kwargs_list += ['{0}="{1}"'.format(k, v) for k, v in kwargs.items()]
-            return '<a {0}>{1}</a>'.format(' '.join(kwargs_list), text)
-        else:
-            return text
-
-    environment = Environment(
-        loader=PackageLoader('conda_build', 'templates'),
-    )
-    environment.filters['human_bytes'] = human_bytes
-    environment.filters['strftime'] = _filter_strftime
-    environment.filters['add_href'] = _filter_add_href
-    environment.trim_blocks = True
-    environment.lstrip_blocks = True
-
-    return environment
-
-
-def _maybe_write(path, content, write_newline_end=False, content_is_binary=False):
-    temp_path = join(gettempdir(), str(uuid4()))
-
-    if not content_is_binary:
-        content = ensure_binary(content)
-    with open(temp_path, 'wb') as fh:
-        fh.write(content)
-        if write_newline_end:
-            fh.write(b'\n')
-    if isfile(path):
-        if utils.md5_file(temp_path) == utils.md5_file(path):
-            # No need to change mtimes. The contents already match.
-            os.unlink(temp_path)
-            return False
-    # log.info("writing %s", path)
-    try:
-        move(temp_path, path)
-    except PermissionError:
-        utils.copy_into(temp_path, path)
-        os.unlink(temp_path)
-    return True
-
-
-def _gather_channeldata_reference_packages(all_repodata_packages):
-    groups = groupby('name', all_repodata_packages)
-    reference_packages = []
-    for group in groups.values():
-        try:
-            version_groups = groupby('version', group)
-            latest_version = sorted(version_groups, key=VersionOrder)[-1]
-            build_number_groups = groupby('build_number', version_groups[latest_version])
-            latest_build_number = sorted(build_number_groups)[-1]
-            ref_pkg = sorted(build_number_groups[latest_build_number],
-                                key=lambda x: x['subdir'])[-1]
-            ref_pkg["subdirs"] = sorted(set(rec['subdir'] for rec in group))
-            ref_pkg["reference_package"] = "%s/%s" % (ref_pkg["subdir"], ref_pkg["fn"])
-            reference_packages.append(ref_pkg)
-        except KeyError as e:
-            log.warn("group {} failed to gather channeldata.  Error was {}.  Skipping this one...".format(group, e))
-    return reference_packages
-
-
-def _collect_namemap(subdirs, patched_repodata, patch_instructions):
-    external_dependencies = {
-        name_in_channel: namekey
-        for pi in patch_instructions.values()
-        for name_in_channel, namekey in pi.get('external_dependencies', {}).items()
-    }
-    namemap = {}  # name_in_channel, cuid_namekey
-    namemap.update(external_dependencies)
-
-    ambiguous_namekeys = defaultdict(set)  # name, namekey
-    for subdir in subdirs:
-        repodata = patched_repodata[subdir]
-        for info in repodata['packages'].values():
-            namespace, name_in_channel, name = _determine_namespace(info)
-            namekey = namespace + ":" + name
-            # TODO: this name_in_channel thing should be dropped if the package has an explicitly assigned namespace
-            if name_in_channel in namemap:
-                # This assertion is important. It guarantees that we don't have packages bridging namespaces.
-                # assert namekey == namemap[name_in_channel], (subdir, namekey, namemap[name_in_channel])
-                if namekey != namemap[name_in_channel]:
-                    ambiguous_namekeys[name_in_channel].add(namemap[name_in_channel])
-                    ambiguous_namekeys[name_in_channel].add(namekey)
-                    # ambiguous_namekeys.append((subdir, name_in_channel, namekey, namemap[name_in_channel], fn))
-            else:
-                namemap[name_in_channel] = namekey
-    return namemap, ambiguous_namekeys
-
-
-def _warn_on_ambiguous_namekeys(ambiguous_namekeys, subdirs, patched_repodata):
-    """
-    The following packages ambiguously straddle namespaces and require metadata correction:
-        package_name:
-        namespace1:
-            - subdir/fn1.tar.bz2
-            - subdir/fn2.tar.bz2
-        namespace2:
-            - subdir/fn3.tar.bz2
-            - subdir/fn4.tar.bz2
-
-    The associated packages are being removed from the index.
-    """
-    if ambiguous_namekeys:
-        abc = defaultdict(lambda: defaultdict(list))
-        for subdir in subdirs:
-            repodata = patched_repodata[subdir]
-            for fn, info in repodata['packages'].items():
-                if info["name"] in ambiguous_namekeys:
-                    abc[info["name"]][info["namespace"]].append(subdir + "/" + fn)
-
-        builder = ["WARNING: The following packages ambiguously straddle namespaces and require metadata correction:"]
-        for package_name in sorted(abc):
-            builder.append("  %s:" % package_name)
-            for namespace in sorted(abc[package_name]):
-                builder.append("    %s:" % namespace)
-                for subdir_fn in sorted(abc[package_name][namespace]):
-                    builder.append("      - %s" % subdir_fn)
-                    subdir, fn = subdir_fn.split("/")
-                    popped = patched_repodata[subdir]["packages"].pop(fn, None)
-                    if popped:
-                        patched_repodata[subdir]["removed"].append(fn)
-        # we remove them from the v2 repodata, not b1
-        # builder.append("The associated packages are being removed from the index.")
-        builder.append('')
-        log.warn("\n".join(builder))
-
-
-def _add_namespace_to_spec(fn, info, dep_str, namemap, missing_dependencies, subdir):
-    if not conda_interface.conda_47:
-        return dep_str
-
-    spec = MatchSpec(dep_str)
-    if hasattr(spec, 'namespace') and spec.namespace:
-        # this spec is fine
-        return dep_str
-    else:
-        # look up namekey
-        # spec.name refers to name_in_channel; need to convert to namekey, but the
-        #   correct namekey might not even be in the channel
-        if spec.name not in namemap:
-            missing_dependencies[spec.name].append(subdir + "/" + fn)
-            return dep_str
-        namekey = namemap[spec.name]
-        namespace, name = namekey.split(":", 1)
-        try:
-            spec = MatchSpec(spec, namespace=namespace, name=name)
-        except CondaError:
-            spec = MatchSpec(spec, name=name)
-        return spec.conda_build_form()
-
-
-def _make_build_string(build, build_number):
-    build_number_as_string = str(build_number)
-    if build.endswith(build_number_as_string):
-        build = build[:-len(build_number_as_string)]
-        build = build.rstrip("_")
-    build_string = build
-    return build_string
-
-
-def _warn_on_missing_dependencies(missing_dependencies, patched_repodata):
-    """
-    The following dependencies do not exist in the channel and are not declared
-    as external dependencies:
-
-    dependency1:
-        - subdir/fn1.tar.bz2
-        - subdir/fn2.tar.bz2
-    dependency2:
-        - subdir/fn3.tar.bz2
-        - subdir/fn4.tar.bz2
-
-    The associated packages are being removed from the index.
-    """
-
-    if missing_dependencies:
-        builder = [
-            "WARNING: The following dependencies do not exist in the channel",
-            "    and are not declared as external dependencies:"
-        ]
-        for dep_name in sorted(missing_dependencies):
-            builder.append("  %s" % dep_name)
-            for subdir_fn in sorted(missing_dependencies[dep_name]):
-                builder.append("    - %s" % subdir_fn)
-                subdir, fn = subdir_fn.split("/")
-                popped = patched_repodata[subdir]["packages"].pop(fn, None)
-                if popped:
-                    patched_repodata[subdir]["removed"].append(fn)
-
-        builder.append("The associated packages are being removed from the index.")
-        builder.append('')
-        log.warn("\n".join(builder))
-
-
-def _augment_repodata(subdirs, patched_repodata, patch_instructions):
-    augmented_repodata = {}
-
-    # TODO: handle packages that need to be renamed
-
-    # Step 1. Collect all package names and associated namespaces.
-    #         Attach namespace to every package.
-    namemap, ambiguous_namekeys = _collect_namemap(subdirs, patched_repodata, patch_instructions)
-    _warn_on_ambiguous_namekeys(ambiguous_namekeys, subdirs, patched_repodata)
-    missing_dependencies = defaultdict(list)
-
-    # Step 2. Add depends2 and constrains2, and other fields
-    for subdir in subdirs:
-        repodata = patched_repodata[subdir]
-        for fn, info in repodata['packages'].items():
-            info['record_version'] = 1
-            if 'constrains' in info:
-                constrains_names = set(dep.split()[0] for dep in info["constrains"])
-                try:
-                    info['constrains2'] = [_add_namespace_to_spec(fn, info, dep, namemap, missing_dependencies, subdir)
-                                        for dep in info['constrains']]
-                    info['depends2'] = [_add_namespace_to_spec(fn, info, dep, namemap, missing_dependencies, subdir)
-                                        for dep in info['depends'] if dep.split()[0] not in constrains_names]
-                except CondaError as e:
-                    log.warn("Encountered a file ({}) that conda does not like.  Error was: {}.  Skipping this one...".format(fn, e))
-            else:
-                try:
-                    info['depends2'] = [_add_namespace_to_spec(fn, info, dep, namemap, missing_dependencies, subdir)
-                                        for dep in info['depends']]
-                except CondaError as e:
-                    log.warn("Encountered a file ({}) that conda does not like.  Error was: {}.  Skipping this one...".format(fn, e))
-            # info['build_string'] =_make_build_string(info["build"], info["build_number"])
-        repodata["removed"] = patch_instructions[subdir].get("remove", [])
-        augmented_repodata[subdir] = repodata
-    _warn_on_missing_dependencies(missing_dependencies, patched_repodata)
-    return augmented_repodata
-
-
+
+
+def _determine_namespace(info):
+    if info.get('namespace'):
+        namespace = info['namespace']
+    else:
+        depends_names = set()
+        for spec in info.get('depends', []):
+            try:
+                depends_names.add(MatchSpec(spec).name)
+            except CondaError:
+                pass
+        spaces = depends_names & NAMESPACE_PACKAGE_NAMES
+        if len(spaces) == 1:
+            namespace = NAMESPACES_MAP[spaces.pop()]
+        else:
+            namespace = "global"
+        info['namespace'] = namespace
+
+    if not info.get('namespace_in_name') and '-' in info['name']:
+        namespace_prefix, reduced_name = info['name'].split('-', 1)
+        if namespace_prefix == namespace:
+            info['name_in_channel'] = info['name']
+            info['name'] = reduced_name
+
+    return namespace, info.get('name_in_channel', info['name']), info['name']
+
+
+def _make_seconds(timestamp):
+    timestamp = int(timestamp)
+    if timestamp > 253402300799:  # 9999-12-31
+        timestamp //= 1000  # convert milliseconds to seconds; see conda/conda-build#1988
+    return timestamp
+
+
+# ==========================================================================
+
+
+REPODATA_VERSION = 1
+CHANNELDATA_VERSION = 1
+REPODATA_JSON_FN = 'repodata.json'
+CHANNELDATA_FIELDS = (
+    "description",
+    "dev_url",
+    "doc_url",
+    "doc_source_url",
+    "home",
+    "license",
+    "reference_package",
+    "source_url",
+    "source_git_url",
+    "source_git_tag",
+    "source_git_rev",
+    "summary",
+    "version",
+    "subdirs",
+    "icon_url",
+    "icon_hash",  # "md5:abc123:12"
+    "run_exports",
+    "binary_prefix",
+    "text_prefix",
+    "activate.d",
+    "deactivate.d",
+    "pre_link",
+    "post_link",
+    "pre_unlink",
+    "tags",
+    "identifiers",
+    "keywords",
+    "recipe_origin",
+    "commits",
+)
+
+
+def _clear_newline_chars(record, field_name):
+    if field_name in record:
+        try:
+            record[field_name] = record[field_name].strip().replace('\n', ' ')
+        except AttributeError:
+            # sometimes description gets added as a list instead of just a string
+            record[field_name] = record[field_name][0].strip().replace('\n', ' ')
+
+
+def _apply_instructions(subdir, repodata, instructions):
+    repodata.setdefault("removed", [])
+    utils.merge_or_update_dict(repodata.get('packages', {}), instructions.get('packages', {}), merge=False,
+                               add_missing_keys=False)
+
+    for fn in instructions.get('revoke', ()):
+        repodata['packages'][fn]['revoked'] = True
+        repodata['packages'][fn]['depends'].append('package_has_been_revoked')
+
+    for fn in instructions.get('remove', ()):
+        popped = repodata['packages'].pop(fn, None)
+        if popped:
+            repodata["removed"].append(fn)
+    repodata["removed"].sort()
+
+    return repodata
+
+
+def _get_jinja2_environment():
+    def _filter_strftime(dt, dt_format):
+        if isinstance(dt, Number):
+            if dt > 253402300799:  # 9999-12-31
+                dt //= 1000  # convert milliseconds to seconds; see #1988
+            dt = datetime.utcfromtimestamp(dt).replace(tzinfo=pytz.timezone("UTC"))
+        return dt.strftime(dt_format)
+
+    def _filter_add_href(text, link, **kwargs):
+        if link:
+            kwargs_list = ['href="{0}"'.format(link)]
+            kwargs_list.append('alt="{0}"'.format(text))
+            kwargs_list += ['{0}="{1}"'.format(k, v) for k, v in kwargs.items()]
+            return '<a {0}>{1}</a>'.format(' '.join(kwargs_list), text)
+        else:
+            return text
+
+    environment = Environment(
+        loader=PackageLoader('conda_build', 'templates'),
+    )
+    environment.filters['human_bytes'] = human_bytes
+    environment.filters['strftime'] = _filter_strftime
+    environment.filters['add_href'] = _filter_add_href
+    environment.trim_blocks = True
+    environment.lstrip_blocks = True
+
+    return environment
+
+
+def _maybe_write(path, content, write_newline_end=False, content_is_binary=False):
+    temp_path = join(gettempdir(), str(uuid4()))
+
+    if not content_is_binary:
+        content = ensure_binary(content)
+    with open(temp_path, 'wb') as fh:
+        fh.write(content)
+        if write_newline_end:
+            fh.write(b'\n')
+    if isfile(path):
+        if utils.md5_file(temp_path) == utils.md5_file(path):
+            # No need to change mtimes. The contents already match.
+            os.unlink(temp_path)
+            return False
+    # log.info("writing %s", path)
+    try:
+        move(temp_path, path)
+    except PermissionError:
+        utils.copy_into(temp_path, path)
+        os.unlink(temp_path)
+    return True
+
+
+def _gather_channeldata_reference_packages(all_repodata_packages):
+    groups = groupby('name', all_repodata_packages)
+    reference_packages = []
+    for group in groups.values():
+        try:
+            version_groups = groupby('version', group)
+            latest_version = sorted(version_groups, key=VersionOrder)[-1]
+            build_number_groups = groupby('build_number', version_groups[latest_version])
+            latest_build_number = sorted(build_number_groups)[-1]
+            ref_pkg = sorted(build_number_groups[latest_build_number],
+                                key=lambda x: x['subdir'])[-1]
+            ref_pkg["subdirs"] = sorted(set(rec['subdir'] for rec in group))
+            ref_pkg["reference_package"] = "%s/%s" % (ref_pkg["subdir"], ref_pkg["fn"])
+            reference_packages.append(ref_pkg)
+        except KeyError as e:
+            log.warn("group {} failed to gather channeldata.  Error was {}.  Skipping this one...".format(group, e))
+    return reference_packages
+
+
+def _collect_namemap(subdirs, patched_repodata, patch_instructions):
+    external_dependencies = {
+        name_in_channel: namekey
+        for pi in patch_instructions.values()
+        for name_in_channel, namekey in pi.get('external_dependencies', {}).items()
+    }
+    namemap = {}  # name_in_channel, cuid_namekey
+    namemap.update(external_dependencies)
+
+    ambiguous_namekeys = defaultdict(set)  # name, namekey
+    for subdir in subdirs:
+        repodata = patched_repodata[subdir]
+        for info in repodata['packages'].values():
+            namespace, name_in_channel, name = _determine_namespace(info)
+            namekey = namespace + ":" + name
+            # TODO: this name_in_channel thing should be dropped if the package has an explicitly assigned namespace
+            if name_in_channel in namemap:
+                # This assertion is important. It guarantees that we don't have packages bridging namespaces.
+                # assert namekey == namemap[name_in_channel], (subdir, namekey, namemap[name_in_channel])
+                if namekey != namemap[name_in_channel]:
+                    ambiguous_namekeys[name_in_channel].add(namemap[name_in_channel])
+                    ambiguous_namekeys[name_in_channel].add(namekey)
+                    # ambiguous_namekeys.append((subdir, name_in_channel, namekey, namemap[name_in_channel], fn))
+            else:
+                namemap[name_in_channel] = namekey
+    return namemap, ambiguous_namekeys
+
+
+def _warn_on_ambiguous_namekeys(ambiguous_namekeys, subdirs, patched_repodata):
+    """
+    The following packages ambiguously straddle namespaces and require metadata correction:
+        package_name:
+        namespace1:
+            - subdir/fn1.tar.bz2
+            - subdir/fn2.tar.bz2
+        namespace2:
+            - subdir/fn3.tar.bz2
+            - subdir/fn4.tar.bz2
+
+    The associated packages are being removed from the index.
+    """
+    if ambiguous_namekeys:
+        abc = defaultdict(lambda: defaultdict(list))
+        for subdir in subdirs:
+            repodata = patched_repodata[subdir]
+            for fn, info in repodata['packages'].items():
+                if info["name"] in ambiguous_namekeys:
+                    abc[info["name"]][info["namespace"]].append(subdir + "/" + fn)
+
+        builder = ["WARNING: The following packages ambiguously straddle namespaces and require metadata correction:"]
+        for package_name in sorted(abc):
+            builder.append("  %s:" % package_name)
+            for namespace in sorted(abc[package_name]):
+                builder.append("    %s:" % namespace)
+                for subdir_fn in sorted(abc[package_name][namespace]):
+                    builder.append("      - %s" % subdir_fn)
+                    subdir, fn = subdir_fn.split("/")
+                    popped = patched_repodata[subdir]["packages"].pop(fn, None)
+                    if popped:
+                        patched_repodata[subdir]["removed"].append(fn)
+        # we remove them from the v2 repodata, not b1
+        # builder.append("The associated packages are being removed from the index.")
+        builder.append('')
+        log.warn("\n".join(builder))
+
+
+def _add_namespace_to_spec(fn, info, dep_str, namemap, missing_dependencies, subdir):
+    if not conda_interface.conda_47:
+        return dep_str
+
+    spec = MatchSpec(dep_str)
+    if hasattr(spec, 'namespace') and spec.namespace:
+        # this spec is fine
+        return dep_str
+    else:
+        # look up namekey
+        # spec.name refers to name_in_channel; need to convert to namekey, but the
+        #   correct namekey might not even be in the channel
+        if spec.name not in namemap:
+            missing_dependencies[spec.name].append(subdir + "/" + fn)
+            return dep_str
+        namekey = namemap[spec.name]
+        namespace, name = namekey.split(":", 1)
+        try:
+            spec = MatchSpec(spec, namespace=namespace, name=name)
+        except CondaError:
+            spec = MatchSpec(spec, name=name)
+        return spec.conda_build_form()
+
+
+def _make_build_string(build, build_number):
+    build_number_as_string = str(build_number)
+    if build.endswith(build_number_as_string):
+        build = build[:-len(build_number_as_string)]
+        build = build.rstrip("_")
+    build_string = build
+    return build_string
+
+
+def _warn_on_missing_dependencies(missing_dependencies, patched_repodata):
+    """
+    The following dependencies do not exist in the channel and are not declared
+    as external dependencies:
+
+    dependency1:
+        - subdir/fn1.tar.bz2
+        - subdir/fn2.tar.bz2
+    dependency2:
+        - subdir/fn3.tar.bz2
+        - subdir/fn4.tar.bz2
+
+    The associated packages are being removed from the index.
+    """
+
+    if missing_dependencies:
+        builder = [
+            "WARNING: The following dependencies do not exist in the channel",
+            "    and are not declared as external dependencies:"
+        ]
+        for dep_name in sorted(missing_dependencies):
+            builder.append("  %s" % dep_name)
+            for subdir_fn in sorted(missing_dependencies[dep_name]):
+                builder.append("    - %s" % subdir_fn)
+                subdir, fn = subdir_fn.split("/")
+                popped = patched_repodata[subdir]["packages"].pop(fn, None)
+                if popped:
+                    patched_repodata[subdir]["removed"].append(fn)
+
+        builder.append("The associated packages are being removed from the index.")
+        builder.append('')
+        log.warn("\n".join(builder))
+
+
+def _augment_repodata(subdirs, patched_repodata, patch_instructions):
+    augmented_repodata = {}
+
+    # TODO: handle packages that need to be renamed
+
+    # Step 1. Collect all package names and associated namespaces.
+    #         Attach namespace to every package.
+    namemap, ambiguous_namekeys = _collect_namemap(subdirs, patched_repodata, patch_instructions)
+    _warn_on_ambiguous_namekeys(ambiguous_namekeys, subdirs, patched_repodata)
+    missing_dependencies = defaultdict(list)
+
+    # Step 2. Add depends2 and constrains2, and other fields
+    for subdir in subdirs:
+        repodata = patched_repodata[subdir]
+        for fn, info in repodata['packages'].items():
+            info['record_version'] = 1
+            if 'constrains' in info:
+                constrains_names = set(dep.split()[0] for dep in info["constrains"])
+                try:
+                    info['constrains2'] = [_add_namespace_to_spec(fn, info, dep, namemap, missing_dependencies, subdir)
+                                        for dep in info['constrains']]
+                    info['depends2'] = [_add_namespace_to_spec(fn, info, dep, namemap, missing_dependencies, subdir)
+                                        for dep in info['depends'] if dep.split()[0] not in constrains_names]
+                except CondaError as e:
+                    log.warn("Encountered a file ({}) that conda does not like.  Error was: {}.  Skipping this one...".format(fn, e))
+            else:
+                try:
+                    info['depends2'] = [_add_namespace_to_spec(fn, info, dep, namemap, missing_dependencies, subdir)
+                                        for dep in info['depends']]
+                except CondaError as e:
+                    log.warn("Encountered a file ({}) that conda does not like.  Error was: {}.  Skipping this one...".format(fn, e))
+            # info['build_string'] =_make_build_string(info["build"], info["build_number"])
+        repodata["removed"] = patch_instructions[subdir].get("remove", [])
+        augmented_repodata[subdir] = repodata
+    _warn_on_missing_dependencies(missing_dependencies, patched_repodata)
+    return augmented_repodata
+
+
 def _cache_post_install_details(paths_cache_path, post_install_cache_path):
     post_install_details_json = {'binary_prefix': False, 'text_prefix': False,
                                  'activate.d': False, 'deactivate.d': False,
@@ -684,32 +684,32 @@ def _cache_post_install_details(paths_cache_path, post_install_cache_path):
         with open(paths_cache_path) as f:
             paths = json.load(f).get('paths', [])
 
-    # get embedded prefix data from paths.json
+    # get embedded prefix data from paths.json
         for f in paths:
-            if f.get('prefix_placeholder'):
-                if f.get('file_mode') == 'binary':
-                    post_install_details_json['binary_prefix'] = True
-                elif f.get('file_mode') == 'text':
-                    post_install_details_json['text_prefix'] = True
+            if f.get('prefix_placeholder'):
+                if f.get('file_mode') == 'binary':
+                    post_install_details_json['binary_prefix'] = True
+                elif f.get('file_mode') == 'text':
+                    post_install_details_json['text_prefix'] = True
                 # check for any activate.d/deactivate.d scripts
                 for k in ('activate.d', 'deactivate.d'):
                     if not post_install_details_json.get(k) and f['_path'].startswith('etc/conda/%s' % k):
                         post_install_details_json[k] = True
-            # check for any link scripts
+            # check for any link scripts
             for pat in ('pre-link', 'post-link', 'pre-unlink'):
                 if not post_install_details_json.get(pat) and fnmatch.fnmatch(f['_path'], '*/.*-%s.*' % pat):
                     post_install_details_json[pat.replace("-", "_")] = True
-
-    with open(post_install_cache_path, 'w') as fh:
-        json.dump(post_install_details_json, fh)
-
-
+
+    with open(post_install_cache_path, 'w') as fh:
+        json.dump(post_install_details_json, fh)
+
+
 def _cache_recipe(tmpdir, recipe_cache_path):
-    recipe_path_search_order = (
-                'info/recipe/meta.yaml.rendered',
-                'info/recipe/meta.yaml',
-                'info/meta.yaml',
-            )
+    recipe_path_search_order = (
+                'info/recipe/meta.yaml.rendered',
+                'info/recipe/meta.yaml',
+                'info/meta.yaml',
+            )
     for path in recipe_path_search_order:
         recipe_path = os.path.join(tmpdir, path)
         if os.path.lexists(recipe_path):
@@ -717,126 +717,126 @@ def _cache_recipe(tmpdir, recipe_cache_path):
         recipe_path = None
 
     recipe_json = {}
-    if recipe_path:
+    if recipe_path:
         with open(recipe_path) as f:
-            try:
+            try:
                 recipe_json = yaml.safe_load(f)
-            except (ConstructorError, ParserError, ScannerError):
+            except (ConstructorError, ParserError, ScannerError):
                 pass
-    try:
-        recipe_json_str = json.dumps(recipe_json, skipkeys=True)
-    except TypeError:
-        recipe_json.get('requirements', {}).pop('build')
-        recipe_json_str = json.dumps(recipe_json, skipkeys=True)
-    with open(recipe_cache_path, 'w') as fh:
-        fh.write(recipe_json_str)
-    return recipe_json
-
-
+    try:
+        recipe_json_str = json.dumps(recipe_json, skipkeys=True)
+    except TypeError:
+        recipe_json.get('requirements', {}).pop('build')
+        recipe_json_str = json.dumps(recipe_json, skipkeys=True)
+    with open(recipe_cache_path, 'w') as fh:
+        fh.write(recipe_json_str)
+    return recipe_json
+
+
 def _cache_run_exports(tmpdir, run_exports_cache_path):
-    run_exports = {}
-    try:
+    run_exports = {}
+    try:
         with open(os.path.join(tmpdir, 'info', 'run_exports.json')) as f:
-            run_exports = json.load(f)
-    except (IOError, FileNotFoundError):
-        try:
+            run_exports = json.load(f)
+    except (IOError, FileNotFoundError):
+        try:
             with open(os.path.join(tmpdir, 'info', 'run_exports.yaml')) as f:
-                run_exports = yaml.safe_load(f)
-        except (IOError, FileNotFoundError):
+                run_exports = yaml.safe_load(f)
+        except (IOError, FileNotFoundError):
             log.debug("%s has no run_exports file (this is OK)" % tmpdir)
-    with open(run_exports_cache_path, 'w') as fh:
-        json.dump(run_exports, fh)
-
-
+    with open(run_exports_cache_path, 'w') as fh:
+        json.dump(run_exports, fh)
+
+
 def _cache_icon(tmpdir, recipe_json, icon_cache_path):
-    # If a conda package contains an icon, also extract and cache that in an .icon/
-    # directory.  The icon file name is the name of the package, plus the extension
-    # of the icon file as indicated by the meta.yaml `app/icon` key.
-    # apparently right now conda-build renames all icons to 'icon.png'
-    # What happens if it's an ico file, or a svg file, instead of a png? Not sure!
+    # If a conda package contains an icon, also extract and cache that in an .icon/
+    # directory.  The icon file name is the name of the package, plus the extension
+    # of the icon file as indicated by the meta.yaml `app/icon` key.
+    # apparently right now conda-build renames all icons to 'icon.png'
+    # What happens if it's an ico file, or a svg file, instead of a png? Not sure!
     app_icon_path = recipe_json.get('app', {}).get('icon')
     if app_icon_path:
         icon_path = os.path.join(tmpdir, 'info', 'recipe', app_icon_path)
         if not os.path.lexists(icon_path):
             icon_path = os.path.join(tmpdir, 'info', 'icon.png')
         if os.path.lexists(icon_path):
-            icon_cache_path += splitext(app_icon_path)[-1]
+            icon_cache_path += splitext(app_icon_path)[-1]
             move(icon_path, icon_cache_path)
-
-
-def _make_subdir_index_html(channel_name, subdir, repodata_packages, extra_paths):
-    environment = _get_jinja2_environment()
-    template = environment.get_template('subdir-index.html.j2')
-    rendered_html = template.render(
-        title="%s/%s" % (channel_name or '', subdir),
-        packages=repodata_packages,
-        current_time=datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")),
-        extra_paths=extra_paths,
-    )
-    return rendered_html
-
-
-def _make_channeldata_index_html(channel_name, channeldata):
-    environment = _get_jinja2_environment()
-    template = environment.get_template('channeldata-index.html.j2')
-    rendered_html = template.render(
-        title=channel_name,
-        packages=channeldata['packages'],
-        subdirs=channeldata['subdirs'],
-        current_time=datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")),
-    )
-    return rendered_html
-
-
-def _get_source_repo_git_info(path):
-    is_repo = subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"], cwd=path)
-    if is_repo.strip().decode('utf-8') == "true":
-        output = subprocess.check_output(['git', 'log',
-                                        "--pretty=format:'%h|%ad|%an|%s'",
-                                        "--date=unix"], cwd=path)
-        commits = []
-        for line in output.decode("utf-8").strip().splitlines():
-            _hash, _time, _author, _desc = line.split("|")
-            commits.append({"hash": _hash, "timestamp": int(_time),
-                            "author": _author, "description": _desc})
-    return commits
-
-
-@contextlib.contextmanager
-def _tmp_chdir(dest):
-    curdir = os.getcwd()
-    try:
-        os.chdir(dest)
-        yield
-    finally:
-        os.chdir(curdir)
-
-
-def _collect_commits(package_order, hotfix_source_repo, cutoff_time):
-    commit_info = {}
-
-    for package_name, info in package_order.items():
-        commits = info.get('commits', [])
-        for commit in commits:
-            commit['timestamp'] = int(commit['timestamp'])
-        commits = [commit for commit in commits if commit['timestamp'] > cutoff_time]
-        commits.sort(key=lambda x: x['timestamp'], reverse=True)
-        if commits:
-            commit_info["%s (%s)" % (package_name, info['version'])] = {"recipe_origin": info.get('recipe_origin'),
-                                                                        "commits": commits}
-    if hotfix_source_repo:
-        commit_info['index hotfixes'] = {"recipe_origin": hotfix_source_repo,
-                                        "commits": sorted([commit for commit in _get_source_repo_git_info(hotfix_source_repo)
-                                                if commit["timestamp"] > cutoff_time],
-                                            key=lambda x: x["timestamp"], reverse=True)}
-    sorted_commit_info = OrderedDict()
-
-    order = sorted(commit_info, key=lambda k: commit_info[k]['commits'][0]['timestamp'], reverse=True)
-    for k in order:
-        sorted_commit_info[k] = commit_info[k]
-    return sorted_commit_info
-
-
+
+
+def _make_subdir_index_html(channel_name, subdir, repodata_packages, extra_paths):
+    environment = _get_jinja2_environment()
+    template = environment.get_template('subdir-index.html.j2')
+    rendered_html = template.render(
+        title="%s/%s" % (channel_name or '', subdir),
+        packages=repodata_packages,
+        current_time=datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")),
+        extra_paths=extra_paths,
+    )
+    return rendered_html
+
+
+def _make_channeldata_index_html(channel_name, channeldata):
+    environment = _get_jinja2_environment()
+    template = environment.get_template('channeldata-index.html.j2')
+    rendered_html = template.render(
+        title=channel_name,
+        packages=channeldata['packages'],
+        subdirs=channeldata['subdirs'],
+        current_time=datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")),
+    )
+    return rendered_html
+
+
+def _get_source_repo_git_info(path):
+    is_repo = subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"], cwd=path)
+    if is_repo.strip().decode('utf-8') == "true":
+        output = subprocess.check_output(['git', 'log',
+                                        "--pretty=format:'%h|%ad|%an|%s'",
+                                        "--date=unix"], cwd=path)
+        commits = []
+        for line in output.decode("utf-8").strip().splitlines():
+            _hash, _time, _author, _desc = line.split("|")
+            commits.append({"hash": _hash, "timestamp": int(_time),
+                            "author": _author, "description": _desc})
+    return commits
+
+
+@contextlib.contextmanager
+def _tmp_chdir(dest):
+    curdir = os.getcwd()
+    try:
+        os.chdir(dest)
+        yield
+    finally:
+        os.chdir(curdir)
+
+
+def _collect_commits(package_order, hotfix_source_repo, cutoff_time):
+    commit_info = {}
+
+    for package_name, info in package_order.items():
+        commits = info.get('commits', [])
+        for commit in commits:
+            commit['timestamp'] = int(commit['timestamp'])
+        commits = [commit for commit in commits if commit['timestamp'] > cutoff_time]
+        commits.sort(key=lambda x: x['timestamp'], reverse=True)
+        if commits:
+            commit_info["%s (%s)" % (package_name, info['version'])] = {"recipe_origin": info.get('recipe_origin'),
+                                                                        "commits": commits}
+    if hotfix_source_repo:
+        commit_info['index hotfixes'] = {"recipe_origin": hotfix_source_repo,
+                                        "commits": sorted([commit for commit in _get_source_repo_git_info(hotfix_source_repo)
+                                                if commit["timestamp"] > cutoff_time],
+                                            key=lambda x: x["timestamp"], reverse=True)}
+    sorted_commit_info = OrderedDict()
+
+    order = sorted(commit_info, key=lambda k: commit_info[k]['commits'][0]['timestamp'], reverse=True)
+    for k in order:
+        sorted_commit_info[k] = commit_info[k]
+    return sorted_commit_info
+
+
 def get_converse_format_path(fn, subdir_path, convert_if_not_present=True):
     """Gets the new .conda file that matches an old .tar.bz2.  If it doesn't exist, run cph to convert it."""
     if fn.endswith("tar.bz2"):
@@ -863,86 +863,86 @@ def _cache_info_file(tmpdir, info_fn, cache_path):
         move(info_path, cache_path)
 
 
-class ChannelIndex(object):
-
-    def __init__(self, channel_root, channel_name, subdirs=None, threads=MAX_THREADS_DEFAULT,
-                 deep_integrity_check=False):
-        self.channel_root = abspath(channel_root)
-        self.channel_name = channel_name or basename(channel_root.rstrip('/'))
-        self._subdirs = subdirs
-        self.thread_executor = ThreadLimitedThreadPoolExecutor(threads)
-        self.deep_integrity_check = deep_integrity_check
-
+class ChannelIndex(object):
+
+    def __init__(self, channel_root, channel_name, subdirs=None, threads=MAX_THREADS_DEFAULT,
+                 deep_integrity_check=False):
+        self.channel_root = abspath(channel_root)
+        self.channel_name = channel_name or basename(channel_root.rstrip('/'))
+        self._subdirs = subdirs
+        self.thread_executor = ThreadLimitedThreadPoolExecutor(threads)
+        self.deep_integrity_check = deep_integrity_check
+
     def index(self, patch_generator, hotfix_source_repo=None, verbose=False, progress=False,
               convert_if_not_present=False):
-        if verbose:
-            level = logging.DEBUG
-        else:
-            level = logging.ERROR
-
-        with utils.LoggingContext(level, loggers=[__name__]):
-            if not self._subdirs:
-                detected_subdirs = set(subdir for subdir in os.listdir(self.channel_root)
-                                    if subdir in DEFAULT_SUBDIRS and isdir(join(self.channel_root, subdir)))
-                log.debug("found subdirs %s" % detected_subdirs)
-                self.subdirs = subdirs = sorted(detected_subdirs | {'noarch'})
-            else:
-                self.subdirs = subdirs = sorted(set(self._subdirs) | {'noarch'})
-
-            # Step 1. Lock local channel.
-            with utils.try_acquire_locks([utils.get_lock(self.channel_root)], timeout=900):
-                # Step 2. Collect repodata from packages.
-                repodata_from_packages = {}
-                with tqdm(total=len(subdirs), disable=(verbose or not progress)) as t:
-                    for subdir in subdirs:
-                        t.set_description("Subdir: %s" % subdir)
-                        t.update()
-                        _ensure_valid_channel(self.channel_root, subdir)
+        if verbose:
+            level = logging.DEBUG
+        else:
+            level = logging.ERROR
+
+        with utils.LoggingContext(level, loggers=[__name__]):
+            if not self._subdirs:
+                detected_subdirs = set(subdir for subdir in os.listdir(self.channel_root)
+                                    if subdir in DEFAULT_SUBDIRS and isdir(join(self.channel_root, subdir)))
+                log.debug("found subdirs %s" % detected_subdirs)
+                self.subdirs = subdirs = sorted(detected_subdirs | {'noarch'})
+            else:
+                self.subdirs = subdirs = sorted(set(self._subdirs) | {'noarch'})
+
+            # Step 1. Lock local channel.
+            with utils.try_acquire_locks([utils.get_lock(self.channel_root)], timeout=900):
+                # Step 2. Collect repodata from packages.
+                repodata_from_packages = {}
+                with tqdm(total=len(subdirs), disable=(verbose or not progress)) as t:
+                    for subdir in subdirs:
+                        t.set_description("Subdir: %s" % subdir)
+                        t.update()
+                        _ensure_valid_channel(self.channel_root, subdir)
                         repodata_from_packages[subdir] = self.index_subdir(
                             subdir, verbose=verbose, progress=progress,
                             convert_if_not_present=convert_if_not_present)
-
-                # Step 3. Apply patch instructions.
-                patched_repodata = {}
-                patch_instructions = {}
-                for subdir in subdirs:
-                    patched_repodata[subdir], patch_instructions[subdir] = self._patch_repodata(
-                        subdir, repodata_from_packages[subdir], patch_generator)
-
-                # Step 4. Save patched and augmented repodata.
-                for subdir in subdirs:
-                    # If the contents of repodata have changed, write a new repodata.json file.
-                    # Also create associated index.html.
-                    self._write_repodata(subdir, patched_repodata[subdir])
-
-                # Step 5. Augment repodata with additional information.
-                augmented_repodata = _augment_repodata(subdirs, patched_repodata, patch_instructions)
-
-                # Step 6. Create and save repodata2.json
-                repodata2 = {}
-                for subdir in subdirs:
-                    repodata2[subdir] = self._create_repodata2(subdir, augmented_repodata[subdir])
-                    changed = self._write_repodata2(subdir, repodata2[subdir])
-                    if changed:
-                        self._write_subdir_index_html(subdir, repodata2[subdir])
-
-                # Step 7. Create and write channeldata.
-                all_repodata_packages = tuple(concat(repodata["packages"] for repodata in repodata2.values()))
-                reference_packages = _gather_channeldata_reference_packages(all_repodata_packages)
-                channel_data, package_mtimes = self._build_channeldata(subdirs, reference_packages)
-                self._write_channeldata_index_html(channel_data)
-                self._write_channeldata_rss(channel_data, package_mtimes, hotfix_source_repo)
-                self._write_channeldata(channel_data)
-
+
+                # Step 3. Apply patch instructions.
+                patched_repodata = {}
+                patch_instructions = {}
+                for subdir in subdirs:
+                    patched_repodata[subdir], patch_instructions[subdir] = self._patch_repodata(
+                        subdir, repodata_from_packages[subdir], patch_generator)
+
+                # Step 4. Save patched and augmented repodata.
+                for subdir in subdirs:
+                    # If the contents of repodata have changed, write a new repodata.json file.
+                    # Also create associated index.html.
+                    self._write_repodata(subdir, patched_repodata[subdir])
+
+                # Step 5. Augment repodata with additional information.
+                augmented_repodata = _augment_repodata(subdirs, patched_repodata, patch_instructions)
+
+                # Step 6. Create and save repodata2.json
+                repodata2 = {}
+                for subdir in subdirs:
+                    repodata2[subdir] = self._create_repodata2(subdir, augmented_repodata[subdir])
+                    changed = self._write_repodata2(subdir, repodata2[subdir])
+                    if changed:
+                        self._write_subdir_index_html(subdir, repodata2[subdir])
+
+                # Step 7. Create and write channeldata.
+                all_repodata_packages = tuple(concat(repodata["packages"] for repodata in repodata2.values()))
+                reference_packages = _gather_channeldata_reference_packages(all_repodata_packages)
+                channel_data, package_mtimes = self._build_channeldata(subdirs, reference_packages)
+                self._write_channeldata_index_html(channel_data)
+                self._write_channeldata_rss(channel_data, package_mtimes, hotfix_source_repo)
+                self._write_channeldata(channel_data)
+
     def index_subdir(self, subdir, verbose=False, progress=False, convert_if_not_present=False):
-        subdir_path = join(self.channel_root, subdir)
-        self._ensure_dirs(subdir)
-        repodata_json_path = join(subdir_path, REPODATA_JSON_FN)
-
-        if verbose:
-            log.info("Building repodata for %s" % subdir_path)
-
-        # gather conda package filenames in subdir
+        subdir_path = join(self.channel_root, subdir)
+        self._ensure_dirs(subdir)
+        repodata_json_path = join(subdir_path, REPODATA_JSON_FN)
+
+        if verbose:
+            log.info("Building repodata for %s" % subdir_path)
+
+        # gather conda package filenames in subdir
         # we'll process these first, because reading their metadata is much faster
         new_fns_in_subdir = set(fn for fn in os.listdir(subdir_path)
                                 if fn.endswith('.conda'))
@@ -954,168 +954,168 @@ def index_subdir(self, subdir, verbose=False, progress=False, convert_if_not_pre
         log.debug("found %d old conda packages in %s" % (len(old_fns_in_subdir), subdir))
 
         # file_pairs = {fn.replace('.conda', ''): [fn, ] }
-
-        # load current/old repodata
-        try:
-            with open(repodata_json_path) as fh:
-                old_repodata = json.load(fh) or {}
-        except (EnvironmentError, JSONDecodeError):
-            # log.info("no repodata found at %s", repodata_json_path)
-            old_repodata = {}
-        old_repodata_packages = old_repodata.get("packages", {})
-        old_repodata_fns = set(old_repodata_packages)
-
-        # Load stat cache. The stat cache has the form
-        #   {
-        #     'package_name.tar.bz2': {
-        #       'mtime': 123456,
-        #       'md5': 'abd123',
-        #     },
-        #   }
+
+        # load current/old repodata
+        try:
+            with open(repodata_json_path) as fh:
+                old_repodata = json.load(fh) or {}
+        except (EnvironmentError, JSONDecodeError):
+            # log.info("no repodata found at %s", repodata_json_path)
+            old_repodata = {}
+        old_repodata_packages = old_repodata.get("packages", {})
+        old_repodata_fns = set(old_repodata_packages)
+
+        # Load stat cache. The stat cache has the form
+        #   {
+        #     'package_name.tar.bz2': {
+        #       'mtime': 123456,
+        #       'md5': 'abd123',
+        #     },
+        #   }
         stat_cache_path = join(subdir_path, '.cache', 'stat.json')
-        try:
-            with open(stat_cache_path) as fh:
-                stat_cache = json.load(fh) or {}
+        try:
+            with open(stat_cache_path) as fh:
+                stat_cache = json.load(fh) or {}
         except:
             stat_cache = {}
 
-        stat_cache_original = stat_cache.copy()
-
-        try:
-            # calculate all the paths and figure out what we're going to do with them
-            # add_set: filenames that aren't in the current/old repodata, but exist in the subdir
+        stat_cache_original = stat_cache.copy()
+
+        try:
+            # calculate all the paths and figure out what we're going to do with them
+            # add_set: filenames that aren't in the current/old repodata, but exist in the subdir
             add_set = old_fns_in_subdir - old_repodata_fns
             remove_set = old_repodata_fns - old_fns_in_subdir
             ignore_set = set(old_repodata.get('removed', []))
             add_set -= ignore_set
 
-            # update_set: Filenames that are in both old repodata and new repodata,
-            #     and whose contents have changed based on file size or mtime. We're
-            #     not using md5 here because it takes too long. If needing to do full md5 checks,
-            #     use the --deep-integrity-check flag / self.deep_integrity_check option.
+            # update_set: Filenames that are in both old repodata and new repodata,
+            #     and whose contents have changed based on file size or mtime. We're
+            #     not using md5 here because it takes too long. If needing to do full md5 checks,
+            #     use the --deep-integrity-check flag / self.deep_integrity_check option.
             update_set = self._calculate_update_set(
                 subdir, old_fns_in_subdir - ignore_set, old_repodata_fns, stat_cache,
                 verbose=verbose, progress=progress
             )
-            # unchanged_set: packages in old repodata whose information can carry straight
-            #     across to new repodata
+            # unchanged_set: packages in old repodata whose information can carry straight
+            #     across to new repodata
             unchanged_set = sorted(old_repodata_fns - update_set - remove_set - ignore_set)
-
-            # clean up removed files
+
+            # clean up removed files
             removed_set = (old_repodata_fns - old_fns_in_subdir)
-            for fn in removed_set:
-                if fn in stat_cache:
-                    del stat_cache[fn]
-
-            new_repodata_packages = {}
-            for fn in sorted(unchanged_set):
-                try:
+            for fn in removed_set:
+                if fn in stat_cache:
+                    del stat_cache[fn]
+
+            new_repodata_packages = {}
+            for fn in sorted(unchanged_set):
+                try:
                     new_repodata_packages[fn] = self._load_index_from_cache(subdir, fn, stat_cache,
                                                                             convert_if_not_present)
-                except IOError:
-                    update_set.add(fn)
-
-            # files that are no longer in the folder should trigger an update for removal
+                except IOError:
+                    update_set.add(fn)
+
+            # files that are no longer in the folder should trigger an update for removal
             # removed_set = old_repodata_fns - old_fns_in_subdir))
-
-            # Invalidate cached files for update_set.
-            # Extract and cache update_set and add_set, then add to new_repodata_packages.
-            # This is also where we update the contents of the stat_cache for successfully
-            #   extracted packages.
-            hash_extract_set = sorted(set(concatv(add_set, update_set)))
-            # log.info("hashing and extracting %d packages", len(hash_extract_set))
-            futures = tuple(self.thread_executor.submit(
+
+            # Invalidate cached files for update_set.
+            # Extract and cache update_set and add_set, then add to new_repodata_packages.
+            # This is also where we update the contents of the stat_cache for successfully
+            #   extracted packages.
+            hash_extract_set = sorted(set(concatv(add_set, update_set)))
+            # log.info("hashing and extracting %d packages", len(hash_extract_set))
+            futures = tuple(self.thread_executor.submit(
                 self._extract_to_cache, subdir, fn, convert_if_not_present
-            ) for fn in hash_extract_set)
-            with tqdm(desc="hash & extract packages for %s" % subdir,
-                      total=len(futures), disable=(verbose or not progress)) as t:
-                for future in as_completed(futures):
-                    fn, mtime, size, index_json = future.result()
-                    # fn can be None if the file was corrupt or no longer there
+            ) for fn in hash_extract_set)
+            with tqdm(desc="hash & extract packages for %s" % subdir,
+                      total=len(futures), disable=(verbose or not progress)) as t:
+                for future in as_completed(futures):
+                    fn, mtime, size, index_json = future.result()
+                    # fn can be None if the file was corrupt or no longer there
                     if fn and mtime:
-                        # the progress bar shows package names, but we don't know what their name is before they complete.
-                        t.set_description("Hash & extract: %s" % fn)
-                        t.update()
+                        # the progress bar shows package names, but we don't know what their name is before they complete.
+                        t.set_description("Hash & extract: %s" % fn)
+                        t.update()
                         stat_cache_fn = get_converse_format_path(fn, subdir_path, False) or fn
                         stat_cache[stat_cache_fn] = {'mtime': int(mtime), 'size': size}
                         if fn.endswith(".conda"):
                             raise RuntimeError(".conda files should not appear as keys in index")
-                        new_repodata_packages[fn] = index_json
-            new_repodata = {
-                'packages': new_repodata_packages,
-                'info': {
-                    'subdir': subdir,
-                },
-                'repodata_version': REPODATA_VERSION,
+                        new_repodata_packages[fn] = index_json
+            new_repodata = {
+                'packages': new_repodata_packages,
+                'info': {
+                    'subdir': subdir,
+                },
+                'repodata_version': REPODATA_VERSION,
                 'removed': sorted(list(ignore_set))
-            }
-        finally:
-            if stat_cache != stat_cache_original:
-                # log.info("writing stat cache to %s", stat_cache_path)
-                with open(stat_cache_path, 'w') as fh:
-                    json.dump(stat_cache, fh)
-        return new_repodata
-
-    def _ensure_dirs(self, subdir):
-        # Create all cache directories in the subdir.
-        ensure = lambda path: isdir(path) or os.makedirs(path)
-        cache_path = join(self.channel_root, subdir, '.cache')
-        ensure(cache_path)
-        ensure(join(cache_path, 'index'))
-        ensure(join(cache_path, 'about'))
-        ensure(join(cache_path, 'paths'))
-        ensure(join(cache_path, 'recipe'))
-        ensure(join(cache_path, 'run_exports'))
-        ensure(join(cache_path, 'post_install'))
-        ensure(join(cache_path, 'icon'))
-        ensure(join(self.channel_root, 'icons'))
-        ensure(join(cache_path, 'recipe_log'))
-
-    def _calculate_update_set(self, subdir, fns_in_subdir, old_repodata_fns, stat_cache, verbose=False, progress=False):
-        # Determine the packages that already exist in repodata, but need to be updated.
-        # We're not using md5 here because it takes too long.
-        candidate_fns = fns_in_subdir & old_repodata_fns
-        subdir_path = join(self.channel_root, subdir)
+            }
+        finally:
+            if stat_cache != stat_cache_original:
+                # log.info("writing stat cache to %s", stat_cache_path)
+                with open(stat_cache_path, 'w') as fh:
+                    json.dump(stat_cache, fh)
+        return new_repodata
+
+    def _ensure_dirs(self, subdir):
+        # Create all cache directories in the subdir.
+        ensure = lambda path: isdir(path) or os.makedirs(path)
+        cache_path = join(self.channel_root, subdir, '.cache')
+        ensure(cache_path)
+        ensure(join(cache_path, 'index'))
+        ensure(join(cache_path, 'about'))
+        ensure(join(cache_path, 'paths'))
+        ensure(join(cache_path, 'recipe'))
+        ensure(join(cache_path, 'run_exports'))
+        ensure(join(cache_path, 'post_install'))
+        ensure(join(cache_path, 'icon'))
+        ensure(join(self.channel_root, 'icons'))
+        ensure(join(cache_path, 'recipe_log'))
+
+    def _calculate_update_set(self, subdir, fns_in_subdir, old_repodata_fns, stat_cache, verbose=False, progress=False):
+        # Determine the packages that already exist in repodata, but need to be updated.
+        # We're not using md5 here because it takes too long.
+        candidate_fns = fns_in_subdir & old_repodata_fns
+        subdir_path = join(self.channel_root, subdir)
         candidate_fns = {get_converse_format_path(fn, subdir_path, False) or fn for fn in candidate_fns}
 
         stat_results = tuple((fn, os.lstat(
             join(subdir_path, fn))) for fn in candidate_fns)
 
-        update_set = set(fn for fn, stat_result in tqdm(stat_results, desc="Finding updated files",
-                                                        disable=(verbose or not progress))
+        update_set = set(fn for fn, stat_result in tqdm(stat_results, desc="Finding updated files",
+                                                        disable=(verbose or not progress))
                          if int(stat_result.st_mtime) != stat_cache.get(fn, {}).get('mtime') or
-                         stat_result.st_size != stat_cache.get(fn, {}).get('size'))
-        return update_set
-
+                         stat_result.st_size != stat_cache.get(fn, {}).get('size'))
+        return update_set
+
     def _extract_to_cache(self, subdir, fn, convert_if_not_present=False):
-        # This method WILL reread the tarball. Probably need another one to exit early if
-        # there are cases where it's fine not to reread.  Like if we just rebuild repodata
-        # from the cached files, but don't use the existing repodata.json as a starting point.
-        subdir_path = join(self.channel_root, subdir)
+        # This method WILL reread the tarball. Probably need another one to exit early if
+        # there are cases where it's fine not to reread.  Like if we just rebuild repodata
+        # from the cached files, but don't use the existing repodata.json as a starting point.
+        subdir_path = join(self.channel_root, subdir)
         if fn.endswith(".conda"):
             tar_path = join(subdir_path, get_converse_format_path(
                 fn, subdir_path, convert_if_not_present=True))
             new_format_path = fn
         else:
-            tar_path = join(subdir_path, fn)
+            tar_path = join(subdir_path, fn)
             new_format_path = get_converse_format_path(
                 tar_path, subdir_path, convert_if_not_present=convert_if_not_present)
         # try to use any existing .conda format here, because extracting it will be much, much faster
-        # default value indicates either corrupt or removed file.  For corrupt, there
-        #      is an error message shown.
-        retval = fn, None, None, None
-
+        # default value indicates either corrupt or removed file.  For corrupt, there
+        #      is an error message shown.
+        retval = fn, None, None, None
+
         if os.path.isfile(new_format_path or tar_path):
-            index_cache_path = join(subdir_path, '.cache', 'index', fn + '.json')
-            about_cache_path = join(subdir_path, '.cache', 'about', fn + '.json')
-            paths_cache_path = join(subdir_path, '.cache', 'paths', fn + '.json')
-            recipe_cache_path = join(subdir_path, '.cache', 'recipe', fn + '.json')
-            run_exports_cache_path = join(subdir_path, '.cache', 'run_exports', fn + '.json')
-            post_install_cache_path = join(subdir_path, '.cache', 'post_install', fn + '.json')
-            icon_cache_path = join(subdir_path, '.cache', 'icon', fn)
-
+            index_cache_path = join(subdir_path, '.cache', 'index', fn + '.json')
+            about_cache_path = join(subdir_path, '.cache', 'about', fn + '.json')
+            paths_cache_path = join(subdir_path, '.cache', 'paths', fn + '.json')
+            recipe_cache_path = join(subdir_path, '.cache', 'recipe', fn + '.json')
+            run_exports_cache_path = join(subdir_path, '.cache', 'run_exports', fn + '.json')
+            post_install_cache_path = join(subdir_path, '.cache', 'post_install', fn + '.json')
+            icon_cache_path = join(subdir_path, '.cache', 'icon', fn)
+
             log.debug("hashing, extracting, and caching %s" % new_format_path or tar_path)
-            try:
+            try:
                 with TemporaryDirectory() as tmpdir:
                     conda_package_handling.api.extract(new_format_path or tar_path, dest_dir=tmpdir, components="info")
                     index_file = os.path.join(tmpdir, 'info', 'index.json')
@@ -1133,14 +1133,14 @@ def _extract_to_cache(self, subdir, fn, convert_if_not_present=False):
                     recipe_json = _cache_recipe(tmpdir, recipe_cache_path)
                     _cache_icon(tmpdir, recipe_json, icon_cache_path)
 
-                # calculate extra stuff to add to index.json cache, size, md5, sha256
+                # calculate extra stuff to add to index.json cache, size, md5, sha256
                     #    This is done for both the old and possibly the new file format.
                     #    The old one is the one that shows up in repodata.json.  The new
                     #    one makes up the stat cache.
 
                 stat_result = os.lstat(tar_path)
                 size = stat_result.st_size
-                mtime = stat_result.st_mtime
+                mtime = stat_result.st_mtime
                 index_json.update(conda_package_handling.api.get_pkg_details(tar_path))
 
                 if new_format_path:
@@ -1149,37 +1149,37 @@ def _extract_to_cache(self, subdir, fn, convert_if_not_present=False):
                     mtime = stat_result.st_mtime
                     index_json.update({"conda_" + k: v for k, v in
                                        conda_package_handling.api.get_pkg_details(new_format_path).items()})
-
-                # decide what fields to filter out, like has_prefix
-                filter_fields = {
-                    'arch',
-                    'has_prefix',
-                    'mtime',
-                    'platform',
-                    'ucs',
-                    'requires_features',
-                    'binstar',
-                    'target-triplet',
-                    'machine',
-                    'operatingsystem',
-                }
-                for field_name in filter_fields & set(index_json):
-                    del index_json[field_name]
-
-                with open(index_cache_path, 'w') as fh:
-                    json.dump(index_json, fh)
-                retval = fn, mtime, size, index_json
+
+                # decide what fields to filter out, like has_prefix
+                filter_fields = {
+                    'arch',
+                    'has_prefix',
+                    'mtime',
+                    'platform',
+                    'ucs',
+                    'requires_features',
+                    'binstar',
+                    'target-triplet',
+                    'machine',
+                    'operatingsystem',
+                }
+                for field_name in filter_fields & set(index_json):
+                    del index_json[field_name]
+
+                with open(index_cache_path, 'w') as fh:
+                    json.dump(index_json, fh)
+                retval = fn, mtime, size, index_json
             except (libarchive.exception.ArchiveError, tarfile.ReadError, KeyError, EOFError) as e:
-                log.error("Package %s/%s appears to be corrupt.  Please remove it and re-download it" % (subdir, fn))
+                log.error("Package %s/%s appears to be corrupt.  Please remove it and re-download it" % (subdir, fn))
                 log.error(e.message)
-        return retval
-
+        return retval
+
     def _load_index_from_cache(self, subdir, fn, stat_cache, convert_if_not_present=False):
-        index_cache_path = join(self.channel_root, subdir, '.cache', 'index', fn + '.json')
-        log.debug("loading index cache %s" % index_cache_path)
+        index_cache_path = join(self.channel_root, subdir, '.cache', 'index', fn + '.json')
+        log.debug("loading index cache %s" % index_cache_path)
 
-        with open(index_cache_path) as fh:
-            index_json = json.load(fh)
+        with open(index_cache_path) as fh:
+            index_json = json.load(fh)
         subdir_path = os.path.join(self.channel_root, subdir)
 
         new_format_path = get_converse_format_path(fn, subdir_path,
@@ -1189,302 +1189,302 @@ def _load_index_from_cache(self, subdir, fn, stat_cache, convert_if_not_present=
             new_format_path = join(subdir_path, new_format_path)
             index_json.update({"conda_" + k: v for k, v in
                                conda_package_handling.api.get_pkg_details(new_format_path).items()})
-        return index_json
-
-    def _load_all_from_cache(self, subdir, fn):
-        subdir_path = join(self.channel_root, subdir)
-
-        try:
-            mtime = getmtime(join(subdir_path, fn))
-        except FileNotFoundError:
-            return {}
-        # In contrast to self._load_index_from_cache(), this method reads up pretty much
-        # all of the cached metadata, except for paths. It all gets dumped into a single map.
-        index_cache_path = join(subdir_path, '.cache', 'index', fn + '.json')
-        about_cache_path = join(subdir_path, '.cache', 'about', fn + '.json')
-        recipe_cache_path = join(subdir_path, '.cache', 'recipe', fn + '.json')
-        run_exports_cache_path = join(subdir_path, '.cache', 'run_exports', fn + '.json')
-        post_install_cache_path = join(subdir_path, '.cache', 'post_install', fn + '.json')
-        icon_cache_path_glob = join(subdir_path, '.cache', 'icon', fn + ".*")
-        recipe_log_path = join(subdir_path, '.cache', 'recipe_log', fn + '.json')
-
-        data = {}
-        for path in (recipe_cache_path, about_cache_path, index_cache_path, post_install_cache_path, recipe_log_path):
-            try:
-                if os.path.getsize(path) != 0:
-                    with open(path) as fh:
-                        data.update(json.load(fh))
-            except (OSError, EOFError, IOError):
-                pass
-
-        icon_cache_paths = glob(icon_cache_path_glob)
-        if icon_cache_paths:
-            icon_cache_path = sorted(icon_cache_paths)[-1]
-            icon_ext = icon_cache_path.rsplit('.', 1)[-1]
-            channel_icon_fn = "%s.%s" % (data['name'], icon_ext)
-            icon_url = "icons/" + channel_icon_fn
-            icon_channel_path = join(self.channel_root, 'icons', channel_icon_fn)
-            icon_md5 = utils.md5_file(icon_cache_path)
-            icon_hash = "md5:%s:%s" % (icon_md5, getsize(icon_cache_path))
-            data.update(icon_hash=icon_hash, icon_url=icon_url)
-            if lexists(icon_channel_path) and utils.md5_file(icon_channel_path) != icon_md5:
-                os.unlink(icon_channel_path)
-            if not lexists(icon_channel_path):
-                # log.info("writing icon from %s to %s", icon_cache_path, icon_channel_path)
-                copy2(icon_cache_path, icon_channel_path)
-
-        # have to stat again, because we don't have access to the stat cache here
-        data['mtime'] = mtime
-
-        source = data.get("source", {})
-        try:
-            data.update({"source_" + k: v for k, v in source.items()})
-        except AttributeError:
-            # sometimes source is a  list instead of a dict
-            pass
-        _clear_newline_chars(data, 'description')
-        _clear_newline_chars(data, 'summary')
-        try:
-            with open(run_exports_cache_path) as fh:
-                data["run_exports"] = json.load(fh)
-        except (OSError, EOFError):
-            data["run_exports"] = {}
-        return data
-
-    def _write_repodata(self, subdir, repodata):
-        repodata_json_path = join(self.channel_root, subdir, REPODATA_JSON_FN)
-        new_repodata_binary = json.dumps(repodata, indent=2, sort_keys=True,
-                                  separators=(',', ': ')).encode("utf-8")
-        write_result = _maybe_write(repodata_json_path, new_repodata_binary, write_newline_end=True)
-        if write_result:
-            repodata_bz2_path = repodata_json_path + ".bz2"
-            bz2_content = bz2.compress(new_repodata_binary)
-            _maybe_write(repodata_bz2_path, bz2_content, content_is_binary=True)
-        return write_result
-
-    def _write_subdir_index_html(self, subdir, repodata):
-        repodata_packages = repodata["packages"]
-        subdir_path = join(self.channel_root, subdir)
-
-        def _add_extra_path(extra_paths, path):
-            if isfile(join(self.channel_root, path)):
-                extra_paths[basename(path)] = {
-                    'size': getsize(path),
-                    'timestamp': int(getmtime(path)),
-                    'md5': utils.md5_file(path),
-                }
-
-        extra_paths = OrderedDict()
-        _add_extra_path(extra_paths, join(subdir_path, REPODATA_JSON_FN))
-        _add_extra_path(extra_paths, join(subdir_path, REPODATA_JSON_FN + '.bz2'))
-        _add_extra_path(extra_paths, join(subdir_path, "repodata2.json"))
-        _add_extra_path(extra_paths, join(subdir_path, "patch_instructions.json"))
-        rendered_html = _make_subdir_index_html(
-            self.channel_name, subdir, repodata_packages, extra_paths
-        )
-        index_path = join(subdir_path, 'index.html')
-        return _maybe_write(index_path, rendered_html)
-
-    def _write_channeldata_rss(self, channeldata, package_mtimes, hotfix_source_repo):
-        cutoff_time = time.time() - 14 * 24 * 3600
-
-        current = {name: channeldata['packages'][name] for name, mtime in package_mtimes.items()
-                   if mtime > cutoff_time}
-
-        # our RSS feed is fed by up to 2 things:
-        #    - package recipe_log.json files
-        #    - commit log from the repo where we get our patch instructions from.  This is a config option and cli flag.
-        commit_info = _collect_commits(current, hotfix_source_repo, cutoff_time)
-
-        environment = _get_jinja2_environment()
-        template = environment.get_template('rss.xml.j2')
-        rendered_xml = template.render(
-            channel_name=self.channel_name,
-            channel_url="https://anaconda.org",  # TODO: figure this out
-            current_time=datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")),
-
-            commit_info=commit_info,
-            trim_blocks=True
-        )
-
-        rss_path = join(self.channel_root, 'rss.xml')
-        _maybe_write(rss_path, rendered_xml)
-        return rendered_xml
-
-    def _write_channeldata_index_html(self, channeldata):
-        rendered_html = _make_channeldata_index_html(
-            self.channel_name, channeldata
-        )
-        index_path = join(self.channel_root, 'index.html')
-        _maybe_write(index_path, rendered_html)
-
-    def _build_channeldata(self, subdirs, reference_packages):
-        _CHANNELDATA_FIELDS = CHANNELDATA_FIELDS
-        package_data = {}
-        package_mtimes = {}
-
-        futures = tuple(self.thread_executor.submit(
-            self._load_all_from_cache, rec["subdir"], rec["fn"]
-        ) for rec in reference_packages)
-        for rec, future in zip(reference_packages, futures):
-            data = future.result()
-            if data:
-                data.update(rec)
-                name = data['name']
-                package_data[name] = {k: v for k, v in data.items() if k in _CHANNELDATA_FIELDS}
-                package_mtimes[name] = data['mtime']
-
-        channeldata = {
-            'channeldata_version': CHANNELDATA_VERSION,
-            'subdirs': subdirs,
-            'packages': package_data,
-        }
-        return channeldata, package_mtimes
-
-    def _write_channeldata(self, channeldata):
-        # trim out commits, as they can take up a ton of space.  They're really only for the RSS feed.
-        for _pkg, pkg_dict in channeldata.get('packages', {}).items():
-            if "commits" in pkg_dict:
-                del pkg_dict['commits']
-        channeldata_path = join(self.channel_root, 'channeldata.json')
-        content = json.dumps(channeldata, indent=2, sort_keys=True, separators=(',', ': '))
-        _maybe_write(channeldata_path, content, True)
-
-    def _load_patch_instructions_tarball(self, subdir, patch_generator):
-        patch_instructions_file = utils.package_has_file(patch_generator,
-                                                         os.path.join(subdir, "patch_instructions.json"))
-        instructions = {}
-        if patch_instructions_file:
-            instructions = json.loads(patch_instructions_file)
-        return instructions
-
-    def _create_patch_instructions(self, subdir, repodata, patch_generator=None):
-        gen_patch_path = patch_generator or join(self.channel_root, 'gen_patch.py')
-        if isfile(gen_patch_path):
-            log.debug("using patch generator %s for %s" % (gen_patch_path, subdir))
-
-            # https://stackoverflow.com/a/41595552/2127762
-            try:
-                from importlib.util import spec_from_file_location, module_from_spec
-                spec = spec_from_file_location('a_b', gen_patch_path)
-                mod = module_from_spec(spec)
-
-                spec.loader.exec_module(mod)
-            # older pythons
-            except ImportError:
-                import imp
-                mod = imp.load_source('a_b', gen_patch_path)
-
-            instructions = mod._patch_repodata(repodata, subdir)
-
-            if instructions.get('patch_instructions_version', 0) > 1:
-                raise RuntimeError("Incompatible patch instructions version")
-
-            return instructions
-        else:
-            if patch_generator:
-                raise ValueError("Specified metadata patch file '{}' does not exist.  Please try an absolute "
-                                 "path, or examine your relative path carefully with respect to your cwd."
-                                 .format(patch_generator))
-            return {}
-
-    def _write_patch_instructions(self, subdir, instructions):
-        new_patch = json.dumps(instructions, indent=2, sort_keys=True, separators=(',', ': '))
-        patch_instructions_path = join(self.channel_root, subdir, 'patch_instructions.json')
-        _maybe_write(patch_instructions_path, new_patch, True)
-
-    def _load_instructions(self, subdir):
-        patch_instructions_path = join(self.channel_root, subdir, 'patch_instructions.json')
-        if isfile(patch_instructions_path):
-            log.debug("using patch instructions %s" % patch_instructions_path)
-            with open(patch_instructions_path) as fh:
-                instructions = json.load(fh)
-                if instructions.get('patch_instructions_version', 0) > 1:
-                    raise RuntimeError("Incompatible patch instructions version")
-                return instructions
-        return {}
-
-    def _patch_repodata(self, subdir, repodata, patch_generator=None):
-        if patch_generator and patch_generator.endswith("bz2"):
-            instructions = self._load_patch_instructions_tarball(subdir, patch_generator)
-        else:
-            instructions = self._create_patch_instructions(subdir, repodata, patch_generator)
-        if instructions:
-            self._write_patch_instructions(subdir, instructions)
-        else:
-            instructions = self._load_instructions(subdir)
-        if instructions.get('patch_instructions_version', 0) > 1:
-            raise RuntimeError("Incompatible patch instructions version")
-
-        return _apply_instructions(subdir, repodata, instructions), instructions
-
-    def _create_repodata2(self, subdir, augmented_repodata):
-        repodata2 = augmented_repodata  # I guess we're mutating in place for now
-        repodata2["repodata_version"] = 2
-        revoked_set = set()
-
-        channel_name = self.channel_name
-
-        for fn, info in repodata2["packages"].items():
-            info["record_version"] = 2
-            if 'depends2' not in info:
-                continue
-            info["requires"] = info["depends2"]
-            del info["depends"]
-            del info["depends2"]
-            if "constrains2" in info:
-                info["constrains"] = info["constrains2"]
-                del info["constrains2"]
-
-            info["fn"] = fn  # rename fn to filename?
-            # add "location", relative to subdir
-
-            info["channel_name"] = channel_name
-            if "timestamp" in info:
-                info["timestamp"] = _make_seconds(info["timestamp"])
-
-            # noarch -> package_type: noarch_generic, noarch_python
-            if "noarch" in info:
-                if info["noarch"] == "python":
-                    info["package_type"] = "noarch_python"
-                del info["noarch"]
-
-            # dump features
-            info.pop("features", None)  # 😱
-
-            # convert track_features to list
-            if "track_features" in info:
-                info["track_features"] = info["track_features"].split(" ")
-
-            # enforce md5 and sha256
-            assert "md5" in info
-            # assert "sha256" in info  # TODO: re-enable
-            assert "size" in info, (subdir, fn, info)
-
-            # drop arch and platform, enforce subdir
-            info.pop("arch", None)
-            info.pop("platform", None)
-            info["subdir"] = subdir
-
-            if info.get('revoked'):
-                revoked_set.add(fn)
-
-        sort_key = lambda x: (
-            x["namespace"] == "global" and "0" or x["namespace"],
-            x["name"],
-            VersionOrder(x["version"]),
-            x["build_number"],
-            # x["build_string"],
-            x["build"],
-        )
-
-        package_groups = groupby(lambda x: x.get('revoked', False), augmented_repodata["packages"].values())
-        repodata2["packages"] = sorted(package_groups.get(False, ()), key=sort_key)
-        repodata2["revoked"] = sorted(package_groups.get(True, ()), key=sort_key)
-
-        return repodata2
-
-    def _write_repodata2(self, subdir, repodata2):
-        repodata_json_path = join(self.channel_root, subdir, "repodata2.json")
-        new_repodata = json.dumps(repodata2, indent=2, sort_keys=True, separators=(',', ': '))
-        return _maybe_write(repodata_json_path, new_repodata, True)
+        return index_json
+
+    def _load_all_from_cache(self, subdir, fn):
+        subdir_path = join(self.channel_root, subdir)
+
+        try:
+            mtime = getmtime(join(subdir_path, fn))
+        except FileNotFoundError:
+            return {}
+        # In contrast to self._load_index_from_cache(), this method reads up pretty much
+        # all of the cached metadata, except for paths. It all gets dumped into a single map.
+        index_cache_path = join(subdir_path, '.cache', 'index', fn + '.json')
+        about_cache_path = join(subdir_path, '.cache', 'about', fn + '.json')
+        recipe_cache_path = join(subdir_path, '.cache', 'recipe', fn + '.json')
+        run_exports_cache_path = join(subdir_path, '.cache', 'run_exports', fn + '.json')
+        post_install_cache_path = join(subdir_path, '.cache', 'post_install', fn + '.json')
+        icon_cache_path_glob = join(subdir_path, '.cache', 'icon', fn + ".*")
+        recipe_log_path = join(subdir_path, '.cache', 'recipe_log', fn + '.json')
+
+        data = {}
+        for path in (recipe_cache_path, about_cache_path, index_cache_path, post_install_cache_path, recipe_log_path):
+            try:
+                if os.path.getsize(path) != 0:
+                    with open(path) as fh:
+                        data.update(json.load(fh))
+            except (OSError, EOFError, IOError):
+                pass
+
+        icon_cache_paths = glob(icon_cache_path_glob)
+        if icon_cache_paths:
+            icon_cache_path = sorted(icon_cache_paths)[-1]
+            icon_ext = icon_cache_path.rsplit('.', 1)[-1]
+            channel_icon_fn = "%s.%s" % (data['name'], icon_ext)
+            icon_url = "icons/" + channel_icon_fn
+            icon_channel_path = join(self.channel_root, 'icons', channel_icon_fn)
+            icon_md5 = utils.md5_file(icon_cache_path)
+            icon_hash = "md5:%s:%s" % (icon_md5, getsize(icon_cache_path))
+            data.update(icon_hash=icon_hash, icon_url=icon_url)
+            if lexists(icon_channel_path) and utils.md5_file(icon_channel_path) != icon_md5:
+                os.unlink(icon_channel_path)
+            if not lexists(icon_channel_path):
+                # log.info("writing icon from %s to %s", icon_cache_path, icon_channel_path)
+                copy2(icon_cache_path, icon_channel_path)
+
+        # have to stat again, because we don't have access to the stat cache here
+        data['mtime'] = mtime
+
+        source = data.get("source", {})
+        try:
+            data.update({"source_" + k: v for k, v in source.items()})
+        except AttributeError:
+            # sometimes source is a  list instead of a dict
+            pass
+        _clear_newline_chars(data, 'description')
+        _clear_newline_chars(data, 'summary')
+        try:
+            with open(run_exports_cache_path) as fh:
+                data["run_exports"] = json.load(fh)
+        except (OSError, EOFError):
+            data["run_exports"] = {}
+        return data
+
+    def _write_repodata(self, subdir, repodata):
+        repodata_json_path = join(self.channel_root, subdir, REPODATA_JSON_FN)
+        new_repodata_binary = json.dumps(repodata, indent=2, sort_keys=True,
+                                  separators=(',', ': ')).encode("utf-8")
+        write_result = _maybe_write(repodata_json_path, new_repodata_binary, write_newline_end=True)
+        if write_result:
+            repodata_bz2_path = repodata_json_path + ".bz2"
+            bz2_content = bz2.compress(new_repodata_binary)
+            _maybe_write(repodata_bz2_path, bz2_content, content_is_binary=True)
+        return write_result
+
+    def _write_subdir_index_html(self, subdir, repodata):
+        repodata_packages = repodata["packages"]
+        subdir_path = join(self.channel_root, subdir)
+
+        def _add_extra_path(extra_paths, path):
+            if isfile(join(self.channel_root, path)):
+                extra_paths[basename(path)] = {
+                    'size': getsize(path),
+                    'timestamp': int(getmtime(path)),
+                    'md5': utils.md5_file(path),
+                }
+
+        extra_paths = OrderedDict()
+        _add_extra_path(extra_paths, join(subdir_path, REPODATA_JSON_FN))
+        _add_extra_path(extra_paths, join(subdir_path, REPODATA_JSON_FN + '.bz2'))
+        _add_extra_path(extra_paths, join(subdir_path, "repodata2.json"))
+        _add_extra_path(extra_paths, join(subdir_path, "patch_instructions.json"))
+        rendered_html = _make_subdir_index_html(
+            self.channel_name, subdir, repodata_packages, extra_paths
+        )
+        index_path = join(subdir_path, 'index.html')
+        return _maybe_write(index_path, rendered_html)
+
+    def _write_channeldata_rss(self, channeldata, package_mtimes, hotfix_source_repo):
+        cutoff_time = time.time() - 14 * 24 * 3600
+
+        current = {name: channeldata['packages'][name] for name, mtime in package_mtimes.items()
+                   if mtime > cutoff_time}
+
+        # our RSS feed is fed by up to 2 things:
+        #    - package recipe_log.json files
+        #    - commit log from the repo where we get our patch instructions from.  This is a config option and cli flag.
+        commit_info = _collect_commits(current, hotfix_source_repo, cutoff_time)
+
+        environment = _get_jinja2_environment()
+        template = environment.get_template('rss.xml.j2')
+        rendered_xml = template.render(
+            channel_name=self.channel_name,
+            channel_url="https://anaconda.org",  # TODO: figure this out
+            current_time=datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")),
+
+            commit_info=commit_info,
+            trim_blocks=True
+        )
+
+        rss_path = join(self.channel_root, 'rss.xml')
+        _maybe_write(rss_path, rendered_xml)
+        return rendered_xml
+
+    def _write_channeldata_index_html(self, channeldata):
+        rendered_html = _make_channeldata_index_html(
+            self.channel_name, channeldata
+        )
+        index_path = join(self.channel_root, 'index.html')
+        _maybe_write(index_path, rendered_html)
+
+    def _build_channeldata(self, subdirs, reference_packages):
+        _CHANNELDATA_FIELDS = CHANNELDATA_FIELDS
+        package_data = {}
+        package_mtimes = {}
+
+        futures = tuple(self.thread_executor.submit(
+            self._load_all_from_cache, rec["subdir"], rec["fn"]
+        ) for rec in reference_packages)
+        for rec, future in zip(reference_packages, futures):
+            data = future.result()
+            if data:
+                data.update(rec)
+                name = data['name']
+                package_data[name] = {k: v for k, v in data.items() if k in _CHANNELDATA_FIELDS}
+                package_mtimes[name] = data['mtime']
+
+        channeldata = {
+            'channeldata_version': CHANNELDATA_VERSION,
+            'subdirs': subdirs,
+            'packages': package_data,
+        }
+        return channeldata, package_mtimes
+
+    def _write_channeldata(self, channeldata):
+        # trim out commits, as they can take up a ton of space.  They're really only for the RSS feed.
+        for _pkg, pkg_dict in channeldata.get('packages', {}).items():
+            if "commits" in pkg_dict:
+                del pkg_dict['commits']
+        channeldata_path = join(self.channel_root, 'channeldata.json')
+        content = json.dumps(channeldata, indent=2, sort_keys=True, separators=(',', ': '))
+        _maybe_write(channeldata_path, content, True)
+
+    def _load_patch_instructions_tarball(self, subdir, patch_generator):
+        patch_instructions_file = utils.package_has_file(patch_generator,
+                                                         os.path.join(subdir, "patch_instructions.json"))
+        instructions = {}
+        if patch_instructions_file:
+            instructions = json.loads(patch_instructions_file)
+        return instructions
+
+    def _create_patch_instructions(self, subdir, repodata, patch_generator=None):
+        gen_patch_path = patch_generator or join(self.channel_root, 'gen_patch.py')
+        if isfile(gen_patch_path):
+            log.debug("using patch generator %s for %s" % (gen_patch_path, subdir))
+
+            # https://stackoverflow.com/a/41595552/2127762
+            try:
+                from importlib.util import spec_from_file_location, module_from_spec
+                spec = spec_from_file_location('a_b', gen_patch_path)
+                mod = module_from_spec(spec)
+
+                spec.loader.exec_module(mod)
+            # older pythons
+            except ImportError:
+                import imp
+                mod = imp.load_source('a_b', gen_patch_path)
+
+            instructions = mod._patch_repodata(repodata, subdir)
+
+            if instructions.get('patch_instructions_version', 0) > 1:
+                raise RuntimeError("Incompatible patch instructions version")
+
+            return instructions
+        else:
+            if patch_generator:
+                raise ValueError("Specified metadata patch file '{}' does not exist.  Please try an absolute "
+                                 "path, or examine your relative path carefully with respect to your cwd."
+                                 .format(patch_generator))
+            return {}
+
+    def _write_patch_instructions(self, subdir, instructions):
+        new_patch = json.dumps(instructions, indent=2, sort_keys=True, separators=(',', ': '))
+        patch_instructions_path = join(self.channel_root, subdir, 'patch_instructions.json')
+        _maybe_write(patch_instructions_path, new_patch, True)
+
+    def _load_instructions(self, subdir):
+        patch_instructions_path = join(self.channel_root, subdir, 'patch_instructions.json')
+        if isfile(patch_instructions_path):
+            log.debug("using patch instructions %s" % patch_instructions_path)
+            with open(patch_instructions_path) as fh:
+                instructions = json.load(fh)
+                if instructions.get('patch_instructions_version', 0) > 1:
+                    raise RuntimeError("Incompatible patch instructions version")
+                return instructions
+        return {}
+
+    def _patch_repodata(self, subdir, repodata, patch_generator=None):
+        if patch_generator and patch_generator.endswith("bz2"):
+            instructions = self._load_patch_instructions_tarball(subdir, patch_generator)
+        else:
+            instructions = self._create_patch_instructions(subdir, repodata, patch_generator)
+        if instructions:
+            self._write_patch_instructions(subdir, instructions)
+        else:
+            instructions = self._load_instructions(subdir)
+        if instructions.get('patch_instructions_version', 0) > 1:
+            raise RuntimeError("Incompatible patch instructions version")
+
+        return _apply_instructions(subdir, repodata, instructions), instructions
+
+    def _create_repodata2(self, subdir, augmented_repodata):
+        repodata2 = augmented_repodata  # I guess we're mutating in place for now
+        repodata2["repodata_version"] = 2
+        revoked_set = set()
+
+        channel_name = self.channel_name
+
+        for fn, info in repodata2["packages"].items():
+            info["record_version"] = 2
+            if 'depends2' not in info:
+                continue
+            info["requires"] = info["depends2"]
+            del info["depends"]
+            del info["depends2"]
+            if "constrains2" in info:
+                info["constrains"] = info["constrains2"]
+                del info["constrains2"]
+
+            info["fn"] = fn  # rename fn to filename?
+            # add "location", relative to subdir
+
+            info["channel_name"] = channel_name
+            if "timestamp" in info:
+                info["timestamp"] = _make_seconds(info["timestamp"])
+
+            # noarch -> package_type: noarch_generic, noarch_python
+            if "noarch" in info:
+                if info["noarch"] == "python":
+                    info["package_type"] = "noarch_python"
+                del info["noarch"]
+
+            # dump features
+            info.pop("features", None)  # 😱
+
+            # convert track_features to list
+            if "track_features" in info:
+                info["track_features"] = info["track_features"].split(" ")
+
+            # enforce md5 and sha256
+            assert "md5" in info
+            # assert "sha256" in info  # TODO: re-enable
+            assert "size" in info, (subdir, fn, info)
+
+            # drop arch and platform, enforce subdir
+            info.pop("arch", None)
+            info.pop("platform", None)
+            info["subdir"] = subdir
+
+            if info.get('revoked'):
+                revoked_set.add(fn)
+
+        sort_key = lambda x: (
+            x["namespace"] == "global" and "0" or x["namespace"],
+            x["name"],
+            VersionOrder(x["version"]),
+            x["build_number"],
+            # x["build_string"],
+            x["build"],
+        )
+
+        package_groups = groupby(lambda x: x.get('revoked', False), augmented_repodata["packages"].values())
+        repodata2["packages"] = sorted(package_groups.get(False, ()), key=sort_key)
+        repodata2["revoked"] = sorted(package_groups.get(True, ()), key=sort_key)
+
+        return repodata2
+
+    def _write_repodata2(self, subdir, repodata2):
+        repodata_json_path = join(self.channel_root, subdir, "repodata2.json")
+        new_repodata = json.dumps(repodata2, indent=2, sort_keys=True, separators=(',', ': '))
+        return _maybe_write(repodata_json_path, new_repodata, True)
diff --git a/conda_build/utils.py b/conda_build/utils.py
--- a/conda_build/utils.py
+++ b/conda_build/utils.py
@@ -1,1770 +1,1770 @@
-from __future__ import absolute_import, division, print_function
-
-from collections import OrderedDict, defaultdict
-import contextlib
-import fnmatch
-import hashlib
-import json
-from locale import getpreferredencoding
-import libarchive
-import logging
-import logging.config
-import mmap
-import operator
-import os
-from os.path import (dirname, getmtime, getsize, isdir, join, isfile, abspath, islink,
-                     expanduser, expandvars)
-import re
-import stat
-import subprocess
-import sys
-import shutil
-import tarfile
-import tempfile
-from threading import Thread
-import time
-import yaml
-
-import filelock
-
-try:
-    from conda.base.constants import CONDA_TARBALL_EXTENSIONS
-except Exception:
-    from conda.base.constants import CONDA_TARBALL_EXTENSION
-    CONDA_TARBALL_EXTENSIONS = (CONDA_TARBALL_EXTENSION,)
-
-from .conda_interface import hashsum_file, md5_file, unix_path_to_win, win_path_to_unix
-from .conda_interface import PY3, iteritems
-from .conda_interface import root_dir, pkgs_dirs
-from .conda_interface import string_types
-from .conda_interface import memoized
-from .conda_interface import StringIO
-from .conda_interface import VersionOrder, MatchSpec
-from .conda_interface import cc_conda_build
-from .conda_interface import conda_43, conda_46, Dist
-from .conda_interface import context
-# NOQA because it is not used in this file.
-from conda_build.conda_interface import rm_rf as _rm_rf # NOQA
-from conda_build.exceptions import BuildLockError
-from conda_build.os_utils import external
-
-if PY3:
-    from glob import glob as glob_glob
-
-    # stdlib glob is less feature-rich but considerably faster than glob2
-    def glob(pathname, recursive=True):
-        return glob_glob(pathname, recursive=recursive)
-
-    import urllib.parse as urlparse
-    import urllib.request as urllib
-    # NOQA because it is not used in this file.
-    from contextlib import ExitStack  # NOQA
-    PermissionError = PermissionError  # NOQA
-    FileNotFoundError = FileNotFoundError
-else:
-    from glob2 import glob as glob2_glob
-
-    def glob(pathname, recursive=True):
-        return glob2_glob(pathname, recursive=recursive)
-
-    import urlparse
-    import urllib
-    # NOQA because it is not used in this file.
-    from contextlib2 import ExitStack  # NOQA
-    PermissionError = OSError
-    FileNotFoundError = OSError
-
-on_win = (sys.platform == 'win32')
-
-codec = getpreferredencoding() or 'utf-8'
-on_win = sys.platform == "win32"
-root_script_dir = os.path.join(root_dir, 'Scripts' if on_win else 'bin')
-mmap_MAP_PRIVATE = 0 if on_win else mmap.MAP_PRIVATE
-mmap_PROT_READ = 0 if on_win else mmap.PROT_READ
-mmap_PROT_WRITE = 0 if on_win else mmap.PROT_WRITE
-
-
-PY_TMPL = """
-# -*- coding: utf-8 -*-
-import re
-import sys
-
-from %(module)s import %(import_name)s
-
-if __name__ == '__main__':
-    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
-    sys.exit(%(func)s())
-"""
-
-try:
-    from os import scandir, walk  # NOQA
-except ImportError:
-    from scandir import walk
-
-
-@memoized
-def stat_file(path):
-    return os.stat(path)
-
-
-def directory_size_slow(path):
-    total_size = 0
-    seen = set()
-
-    for root, _, files in walk(path):
-        for f in files:
-            try:
-                stat = stat_file(os.path.join(root, f))
-            except OSError:
-                continue
-
-            if stat.st_ino in seen:
-                continue
-
-            seen.add(stat.st_ino)
-
-            total_size += stat.st_size
-    return total_size
-
-
-def directory_size(path):
-    '''
-    '''
-    try:
-        if on_win:
-            command = 'dir /s "{}"'  # Windows path can have spaces
-            out = subprocess.check_output(command.format(path), shell=True)
-        else:
-            command = "du -s {}"
-            out = subprocess.check_output(command.format(path).split(), stderr=subprocess.PIPE)
-
-        if hasattr(out, 'decode'):
-            try:
-                out = out.decode(errors='ignore')
-            # This isn't important anyway so give up. Don't try search on bytes.
-            except (UnicodeDecodeError, IndexError):
-                if on_win:
-                    return 0
-                else:
-                    pass
-        if on_win:
-            # Windows can give long output, we need only 2nd to last line
-            out = out.strip().rsplit('\r\n', 2)[-2]
-            pattern = "\s([\d\W]+).+"  # Language and punctuation neutral
-            out = re.search(pattern, out.strip()).group(1).strip()
-            out = out.replace(',', '').replace('.', '').replace(' ', '')
-        else:
-            out = out.split()[0]
-    except subprocess.CalledProcessError:
-        out = directory_size_slow(path)
-
-    try:
-        return int(out)  # size in bytes
-    except ValueError:
-        return 0
-
-
-class DummyPsutilProcess(object):
-    def children(self, *args, **kwargs):
-        return []
-
-
-def _setup_rewrite_pipe(env):
-    """Rewrite values of env variables back to $ENV in stdout
-
-    Takes output on the pipe and finds any env value
-    and rewrites it as the env key
-
-    Useful for replacing "~/conda/conda-bld/pkg_<date>/_h_place..." with "$PREFIX"
-
-    Returns an FD to be passed to Popen(stdout=...)
-    """
-    # replacements is the env dict reversed,
-    # ordered by the length of the value so that longer replacements
-    # always occur first in case of common prefixes
-    replacements = OrderedDict()
-    for k, v in sorted(env.items(), key=lambda kv: len(kv[1]), reverse=True):
-        replacements[v] = k
-
-    r_fd, w_fd = os.pipe()
-    r = os.fdopen(r_fd, 'rt')
-    if sys.platform == 'win32':
-        replacement_t = '%{}%'
-    else:
-        replacement_t = '${}'
-
-    def rewriter():
-        while True:
-            try:
-                line = r.readline()
-                if not line:
-                    # reading done
-                    r.close()
-                    os.close(w_fd)
-                    return
-                for s, key in replacements.items():
-                    line = line.replace(s, replacement_t.format(key))
-                sys.stdout.write(line)
-            except UnicodeDecodeError:
-                try:
-                    txt = os.read(r, 10000)
-                    sys.stdout.write(txt or '')
-                except TypeError:
-                    pass
-
-    t = Thread(target=rewriter)
-    t.daemon = True
-    t.start()
-
-    return w_fd
-
-
-class PopenWrapper(object):
-    # Small wrapper around subprocess.Popen to allow memory usage monitoring
-    # copied from ProtoCI, https://github.com/ContinuumIO/ProtoCI/blob/59159bc2c9f991fbfa5e398b6bb066d7417583ec/protoci/build2.py#L20  # NOQA
-
-    def __init__(self, *args, **kwargs):
-        self.elapsed = None
-        self.rss = 0
-        self.vms = 0
-        self.returncode = None
-        self.disk = 0
-        self.processes = 1
-
-        self.out, self.err = self._execute(*args, **kwargs)
-
-    def _execute(self, *args, **kwargs):
-        try:
-            import psutil
-            psutil_exceptions = psutil.NoSuchProcess, psutil.AccessDenied, psutil.NoSuchProcess
-        except ImportError as e:
-            psutil = None
-            psutil_exceptions = (OSError, ValueError)
-            log = get_logger(__name__)
-            log.warn("psutil import failed.  Error was {}".format(e))
-            log.warn("only disk usage and time statistics will be available.  Install psutil to "
-                     "get CPU time and memory usage statistics.")
-
-        # The polling interval (in seconds)
-        time_int = kwargs.pop('time_int', 2)
-
-        disk_usage_dir = kwargs.get('cwd', sys.prefix)
-
-        # Create a process of this (the parent) process
-        parent = psutil.Process(os.getpid()) if psutil else DummyPsutilProcess()
-
-        cpu_usage = defaultdict(dict)
-
-        # Using the convenience Popen class provided by psutil
-        start_time = time.time()
-        _popen = psutil.Popen(*args, **kwargs) if psutil else subprocess.Popen(*args, **kwargs)
-        try:
-            while self.returncode is None:
-                # We need to get all of the children of our process since our
-                # process spawns other processes.  Collect all of the child
-                # processes
-
-                rss = 0
-                vms = 0
-                processes = 0
-                # We use the parent process to get mem usage of all spawned processes
-                for child in parent.children(recursive=True):
-                    child_cpu_usage = cpu_usage.get(child.pid, {})
-                    try:
-                        mem = child.memory_info()
-                        rss += mem.rss
-                        vms += mem.rss
-                        # listing child times are only available on linux, so we don't use them.
-                        #    we are instead looping over children and getting each individually.
-                        #    https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_times
-                        cpu_stats = child.cpu_times()
-                        child_cpu_usage['sys'] = cpu_stats.system
-                        child_cpu_usage['user'] = cpu_stats.user
-                        cpu_usage[child.pid] = child_cpu_usage
-                    except psutil_exceptions:
-                        # process already died.  Just ignore it.
-                        continue
-                    processes += 1
-
-                # Sum the memory usage of all the children together (2D columnwise sum)
-                self.rss = max(rss, self.rss)
-                self.vms = max(vms, self.vms)
-                self.cpu_sys = sum(child['sys'] for child in cpu_usage.values())
-                self.cpu_user = sum(child['user'] for child in cpu_usage.values())
-                self.processes = max(processes, self.processes)
-
-                # Get disk usage
-                self.disk = max(directory_size(disk_usage_dir), self.disk)
-
-                time.sleep(time_int)
-                self.elapsed = time.time() - start_time
-                self.returncode = _popen.poll()
-
-        except KeyboardInterrupt:
-            _popen.kill()
-            raise
-
-        self.disk = max(directory_size(disk_usage_dir), self.disk)
-        self.elapsed = time.time() - start_time
-        return _popen.stdout, _popen.stderr
-
-    def __repr__(self):
-        return str({'elapsed': self.elapsed,
-                    'rss': self.rss,
-                    'vms': self.vms,
-                    'disk': self.disk,
-                    'processes': self.processes,
-                    'cpu_user': self.cpu_user,
-                    'cpu_sys': self.cpu_sys,
-                    'returncode': self.returncode})
-
-
-def _func_defaulting_env_to_os_environ(func, *popenargs, **kwargs):
-    if 'env' not in kwargs:
-        kwargs = kwargs.copy()
-        env_copy = os.environ.copy()
-        kwargs.update({'env': env_copy})
-    kwargs['env'] = {str(key): str(value) for key, value in kwargs['env'].items()}
-    _args = []
-    if 'stdin' not in kwargs:
-        kwargs['stdin'] = subprocess.PIPE
-    for arg in popenargs:
-        # arguments to subprocess need to be bytestrings
-        if sys.version_info.major < 3 and hasattr(arg, 'encode'):
-            arg = arg.encode(codec)
-        elif sys.version_info.major >= 3 and hasattr(arg, 'decode'):
-            arg = arg.decode(codec)
-        _args.append(str(arg))
-
-    stats = kwargs.get('stats')
-    if 'stats' in kwargs:
-        del kwargs['stats']
-
-    rewrite_stdout_env = kwargs.pop('rewrite_stdout_env', None)
-    if rewrite_stdout_env:
-        kwargs['stdout'] = _setup_rewrite_pipe(rewrite_stdout_env)
-
-    out = None
-    if stats is not None:
-        proc = PopenWrapper(_args, **kwargs)
-        if func == 'output':
-            out = proc.out.read()
-
-        if proc.returncode != 0:
-            raise subprocess.CalledProcessError(proc.returncode, _args)
-
-        stats.update({'elapsed': proc.elapsed,
-                    'disk': proc.disk,
-                    'processes': proc.processes,
-                    'cpu_user': proc.cpu_user,
-                    'cpu_sys': proc.cpu_sys,
-                    'rss': proc.rss,
-                    'vms': proc.vms})
-    else:
-        if func == 'call':
-            subprocess.check_call(_args, **kwargs)
-        else:
-            if 'stdout' in kwargs:
-                del kwargs['stdout']
-            out = subprocess.check_output(_args, **kwargs)
-    return out
-
-
-def check_call_env(popenargs, **kwargs):
-    return _func_defaulting_env_to_os_environ('call', *popenargs, **kwargs)
-
-
-def check_output_env(popenargs, **kwargs):
-    return _func_defaulting_env_to_os_environ('output', stdout=subprocess.PIPE,
-                                              *popenargs, **kwargs).rstrip()
-
-
-def bytes2human(n):
-    # http://code.activestate.com/recipes/578019
-    # >>> bytes2human(10000)
-    # '9.8K'
-    # >>> bytes2human(100001221)
-    # '95.4M'
-    symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
-    prefix = {}
-    for i, s in enumerate(symbols):
-        prefix[s] = 1 << (i + 1) * 10
-    for s in reversed(symbols):
-        if n >= prefix[s]:
-            value = float(n) / prefix[s]
-            return '%.1f%s' % (value, s)
-    return "%sB" % n
-
-
-def get_recipe_abspath(recipe):
-    """resolve recipe dir as absolute path.  If recipe is a tarball rather than a folder,
-    extract it and return the extracted directory.
-
-    Returns the absolute path, and a boolean flag that is true if a tarball has been extracted
-    and needs cleanup.
-    """
-    # Don't use byte literals for paths in Python 2
-    if not PY3:
-        recipe = recipe.decode(getpreferredencoding() or 'utf-8')
-    if isfile(recipe):
-        if recipe.lower().endswith(decompressible_exts) or recipe.lower().endswith(CONDA_TARBALL_EXTENSIONS):
-            recipe_dir = tempfile.mkdtemp()
-            tar_xf(recipe, recipe_dir)
-            # At some stage the old build system started to tar up recipes.
-            recipe_tarfile = os.path.join(recipe_dir, 'info', 'recipe.tar')
-            if isfile(recipe_tarfile):
-                tar_xf(recipe_tarfile, os.path.join(recipe_dir, 'info'))
-            need_cleanup = True
-        else:
-            print("Ignoring non-recipe: %s" % recipe)
-            return (None, None)
-    else:
-        recipe_dir = abspath(os.path.join(os.getcwd(), recipe))
-        need_cleanup = False
-    if not os.path.exists(recipe_dir):
-        raise ValueError("Package or recipe at path {0} does not exist".format(recipe_dir))
-    return recipe_dir, need_cleanup
-
-
-@contextlib.contextmanager
-def try_acquire_locks(locks, timeout):
-    """Try to acquire all locks.
-
-    If any lock can't be immediately acquired, free all locks.
-    If the timeout is reached withou acquiring all locks, free all locks and raise.
-
-    http://stackoverflow.com/questions/9814008/multiple-mutex-locking-strategies-and-why-libraries-dont-use-address-comparison
-    """
-    t = time.time()
-    while (time.time() - t < timeout):
-        # Continuously try to acquire all locks.
-        # By passing a short timeout to each individual lock, we give other
-        # processes that might be trying to acquire the same locks (and may
-        # already hold some of them) a chance to the remaining locks - and
-        # hopefully subsequently release them.
-        try:
-            for lock in locks:
-                lock.acquire(timeout=0.1)
-        except filelock.Timeout:
-            # If we failed to acquire a lock, it is important to release all
-            # locks we may have already acquired, to avoid wedging multiple
-            # processes that try to acquire the same set of locks.
-            # That is, we want to avoid a situation where processes 1 and 2 try
-            # to acquire locks A and B, and proc 1 holds lock A while proc 2
-            # holds lock B.
-            for lock in locks:
-                lock.release()
-        else:
-            break
-    else:
-        # If we reach this point, we weren't able to acquire all locks within
-        # the specified timeout. We shouldn't be holding any locks anymore at
-        # this point, so we just raise an exception.
-        raise BuildLockError('Failed to acquire all locks')
-
-    try:
-        yield
-    finally:
-        for lock in locks:
-            lock.release()
-
-
-# with each of these, we are copying less metadata.  This seems to be necessary
-#   to cope with some shared filesystems with some virtual machine setups.
-#  See https://github.com/conda/conda-build/issues/1426
-def _copy_with_shell_fallback(src, dst):
-    is_copied = False
-    for func in (shutil.copy2, shutil.copy, shutil.copyfile):
-        try:
-            func(src, dst)
-            is_copied = True
-            break
-        except (IOError, OSError, PermissionError):
-            continue
-    if not is_copied:
-        try:
-            subprocess.check_call('cp -a {} {}'.format(src, dst), shell=True,
-                                  stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-        except subprocess.CalledProcessError as e:
-            if not os.path.isfile(dst):
-                raise OSError("Failed to copy {} to {}.  Error was: {}".format(src, dst, e))
-
-
-def get_prefix_replacement_paths(src, dst):
-    ssplit = src.split(os.path.sep)
-    dsplit = dst.split(os.path.sep)
-    while ssplit and ssplit[-1] == dsplit[-1]:
-        del ssplit[-1]
-        del dsplit[-1]
-    return os.path.join(*ssplit), os.path.join(*dsplit)
-
-
-def copy_into(src, dst, timeout=900, symlinks=False, lock=None, locking=True, clobber=False):
-    """Copy all the files and directories in src to the directory dst"""
-    log = get_logger(__name__)
-    if symlinks and islink(src):
-        try:
-            os.makedirs(os.path.dirname(dst))
-        except OSError:
-            pass
-        if os.path.lexists(dst):
-            os.remove(dst)
-        src_base, dst_base = get_prefix_replacement_paths(src, dst)
-        src_target = os.readlink(src)
-        src_replaced = src_target.replace(src_base, dst_base)
-        os.symlink(src_replaced, dst)
-        try:
-            st = os.lstat(src)
-            mode = stat.S_IMODE(st.st_mode)
-            os.lchmod(dst, mode)
-        except:
-            pass  # lchmod not available
-    elif isdir(src):
-        merge_tree(src, dst, symlinks, timeout=timeout, lock=lock, locking=locking, clobber=clobber)
-
-    else:
-        if isdir(dst):
-            dst_fn = os.path.join(dst, os.path.basename(src))
-        else:
-            dst_fn = dst
-
-        if os.path.isabs(src):
-            src_folder = os.path.dirname(src)
-        else:
-            if os.path.sep in dst_fn:
-                src_folder = os.path.dirname(dst_fn)
-                if not os.path.isdir(src_folder):
-                    os.makedirs(src_folder)
-            else:
-                src_folder = os.getcwd()
-
-        if os.path.islink(src) and not os.path.exists(os.path.realpath(src)):
-            log.warn('path %s is a broken symlink - ignoring copy', src)
-            return
-
-        if not lock and locking:
-            lock = get_lock(src_folder, timeout=timeout)
-        locks = [lock] if locking else []
-        with try_acquire_locks(locks, timeout):
-            # if intermediate folders not not exist create them
-            dst_folder = os.path.dirname(dst)
-            if dst_folder and not os.path.exists(dst_folder):
-                try:
-                    os.makedirs(dst_folder)
-                except OSError:
-                    pass
-            try:
-                _copy_with_shell_fallback(src, dst_fn)
-            except shutil.Error:
-                log.debug("skipping %s - already exists in %s",
-                            os.path.basename(src), dst)
-
-
-# http://stackoverflow.com/a/22331852/1170370
-def copytree(src, dst, symlinks=False, ignore=None, dry_run=False):
-    if not os.path.exists(dst):
-        os.makedirs(dst)
-        shutil.copystat(src, dst)
-    lst = os.listdir(src)
-    if ignore:
-        excl = ignore(src, lst)
-        lst = [x for x in lst if x not in excl]
-
-    # do not copy lock files
-    if '.conda_lock' in lst:
-        lst.remove('.conda_lock')
-
-    dst_lst = [os.path.join(dst, item) for item in lst]
-
-    if not dry_run:
-        for idx, item in enumerate(lst):
-            s = os.path.join(src, item)
-            d = dst_lst[idx]
-            if symlinks and os.path.islink(s):
-                if os.path.lexists(d):
-                    os.remove(d)
-                os.symlink(os.readlink(s), d)
-                try:
-                    st = os.lstat(s)
-                    mode = stat.S_IMODE(st.st_mode)
-                    os.lchmod(d, mode)
-                except:
-                    pass  # lchmod not available
-            elif os.path.isdir(s):
-                copytree(s, d, symlinks, ignore)
-            else:
-                _copy_with_shell_fallback(s, d)
-
-    return dst_lst
-
-
-def merge_tree(src, dst, symlinks=False, timeout=900, lock=None, locking=True, clobber=False):
-    """
-    Merge src into dst recursively by copying all files from src into dst.
-    Return a list of all files copied.
-
-    Like copytree(src, dst), but raises an error if merging the two trees
-    would overwrite any files.
-    """
-    dst = os.path.normpath(os.path.normcase(dst))
-    src = os.path.normpath(os.path.normcase(src))
-    assert not dst.startswith(src), ("Can't merge/copy source into subdirectory of itself.  "
-                                     "Please create separate spaces for these things.\n"
-                                     "  src: {0}\n"
-                                     "  dst: {1}".format(src, dst))
-
-    new_files = copytree(src, dst, symlinks=symlinks, dry_run=True)
-    existing = [f for f in new_files if isfile(f)]
-
-    if existing and not clobber:
-        raise IOError("Can't merge {0} into {1}: file exists: "
-                      "{2}".format(src, dst, existing[0]))
-
-    locks = []
-    if locking:
-        if not lock:
-            lock = get_lock(src, timeout=timeout)
-        locks = [lock]
-    with try_acquire_locks(locks, timeout):
-        copytree(src, dst, symlinks=symlinks)
-
-
-# purpose here is that we want *one* lock per location on disk.  It can be locked or unlocked
-#    at any time, but the lock within this process should all be tied to the same tracking
-#    mechanism.
-_lock_folders = (os.path.join(root_dir, 'locks'),
-                 os.path.expanduser(os.path.join('~', '.conda_build_locks')))
-
-
-def get_lock(folder, timeout=900):
-    fl = None
-    try:
-        location = os.path.abspath(os.path.normpath(folder))
-    except OSError:
-        location = folder
-    b_location = location
-    if hasattr(b_location, 'encode'):
-        b_location = b_location.encode()
-
-    # Hash the entire filename to avoid collisions.
-    lock_filename = hashlib.sha256(b_location).hexdigest()
-
-    if hasattr(lock_filename, 'decode'):
-        lock_filename = lock_filename.decode()
-    for locks_dir in _lock_folders:
-        try:
-            if not os.path.isdir(locks_dir):
-                os.makedirs(locks_dir)
-            lock_file = os.path.join(locks_dir, lock_filename)
-            with open(lock_file, 'w') as f:
-                f.write("")
-            fl = filelock.FileLock(lock_file, timeout)
-            break
-        except (OSError, IOError):
-            continue
-    else:
-        raise RuntimeError("Could not write locks folder to either system location ({0})"
-                           "or user location ({1}).  Aborting.".format(*_lock_folders))
-    return fl
-
-
-def get_conda_operation_locks(locking=True, bldpkgs_dirs=None, timeout=900):
-    locks = []
-    bldpkgs_dirs = ensure_list(bldpkgs_dirs)
-    # locks enabled by default
-    if locking:
-        _pkgs_dirs = pkgs_dirs[:1]
-        locked_folders = _pkgs_dirs + list(bldpkgs_dirs)
-        for folder in locked_folders:
-            if not os.path.isdir(folder):
-                os.makedirs(folder)
-            lock = get_lock(folder, timeout=timeout)
-            locks.append(lock)
-        # lock used to generally indicate a conda operation occurring
-        locks.append(get_lock('conda-operation', timeout=timeout))
-    return locks
-
-
-def relative(f, d='lib'):
-    assert not f.startswith('/'), f
-    assert not d.startswith('/'), d
-    d = d.strip('/').split('/')
-    if d == ['.']:
-        d = []
-    f = dirname(f).split('/')
-    if f == ['']:
-        f = []
-    while d and f and d[0] == f[0]:
-        d.pop(0)
-        f.pop(0)
-    return '/'.join(((['..'] * len(f)) if f else ['.']) + d)
-
-
-# This is the lowest common denominator of the formats supported by our libarchive/python-libarchive-c
-# packages across all platforms
-decompressible_exts = ('.7z', '.tar', '.tar.bz2', '.tar.gz', '.tar.lzma', '.tar.xz',
-                       '.tar.z', '.tgz', '.whl', '.zip', '.rpm', '.deb')
-
-
-def _tar_xf_fallback(tarball, dir_path, mode='r:*'):
-    if tarball.lower().endswith('.tar.z'):
-        uncompress = external.find_executable('uncompress')
-        if not uncompress:
-            uncompress = external.find_executable('gunzip')
-        if not uncompress:
-            sys.exit("""\
-uncompress (or gunzip) is required to unarchive .z source files.
-""")
-        check_call_env([uncompress, '-f', tarball])
-        tarball = tarball[:-2]
-    if not PY3 and tarball.endswith('.tar.xz'):
-        unxz = external.find_executable('unxz')
-        if not unxz:
-            sys.exit("""\
-unxz is required to unarchive .xz source files.
-""")
-
-        check_call_env([unxz, '-f', '-k', tarball])
-        tarball = tarball[:-3]
-    t = tarfile.open(tarball, mode)
-    members = t.getmembers()
-    for i, member in enumerate(members, 0):
-        if os.path.isabs(member.name):
-            member.name = os.path.relpath(member.name, '/')
-        if not os.path.realpath(member.name).startswith(os.getcwd()):
-            member.name = member.name.replace("../", "")
-        if not os.path.realpath(member.name).startswith(os.getcwd()):
-            sys.exit("tarball contains unsafe path: " + member.name)
-        members[i] = member
-
-    if not PY3:
-        t.extractall(path=dir_path.encode(codec))
-    else:
-        t.extractall(path=dir_path)
-    t.close()
-
-
-def tar_xf_file(tarball, entries):
-    from conda_build.utils import ensure_list
-    entries = ensure_list(entries)
-    if not os.path.isabs(tarball):
-        tarball = os.path.join(os.getcwd(), tarball)
-    result = None
-    n_found = 0
-    with libarchive.file_reader(tarball) as archive:
-        for entry in archive:
-            if entry.name in entries:
-                n_found += 1
-                for block in entry.get_blocks():
-                    if result is None:
-                        result = bytes(block)
-                    else:
-                        result += block
-                break
-    if n_found != len(entries):
-        raise KeyError()
-    return result
-
-
-def tar_xf_getnames(tarball):
-    if not os.path.isabs(tarball):
-        tarball = os.path.join(os.getcwd(), tarball)
-    result = []
-    with libarchive.file_reader(tarball) as archive:
-        for entry in archive:
-            result.append(entry.name)
-    return result
-
-
-def tar_xf(tarball, dir_path):
-    flags = libarchive.extract.EXTRACT_TIME | \
-            libarchive.extract.EXTRACT_PERM | \
-            libarchive.extract.EXTRACT_SECURE_NODOTDOT | \
-            libarchive.extract.EXTRACT_SECURE_SYMLINKS | \
-            libarchive.extract.EXTRACT_SECURE_NOABSOLUTEPATHS
-    if not os.path.isabs(tarball):
-        tarball = os.path.join(os.getcwd(), tarball)
-    try:
-        with tmp_chdir(dir_path):
-            libarchive.extract_file(tarball, flags)
-    except libarchive.exception.ArchiveError:
-        # try again, maybe we are on Windows and the archive contains symlinks
-        # https://github.com/conda/conda-build/issues/3351
-        # https://github.com/libarchive/libarchive/pull/1030
-        if tarball.endswith(('.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tar.z', '.tar.xz')):
-            _tar_xf_fallback(tarball, dir_path)
-        else:
-            raise
-
-
-def file_info(path):
-    return {'size': getsize(path),
-            'md5': md5_file(path),
-            'sha256': hashsum_file(path, 'sha256'),
-            'mtime': getmtime(path)}
-
-# Taken from toolz
-
-
-def groupby(key, seq):
-    """ Group a collection by a key function
-    >>> names = ['Alice', 'Bob', 'Charlie', 'Dan', 'Edith', 'Frank']
-    >>> groupby(len, names)  # doctest: +SKIP
-    {3: ['Bob', 'Dan'], 5: ['Alice', 'Edith', 'Frank'], 7: ['Charlie']}
-    >>> iseven = lambda x: x % 2 == 0
-    >>> groupby(iseven, [1, 2, 3, 4, 5, 6, 7, 8])  # doctest: +SKIP
-    {False: [1, 3, 5, 7], True: [2, 4, 6, 8]}
-    Non-callable keys imply grouping on a member.
-    >>> groupby('gender', [{'name': 'Alice', 'gender': 'F'},
-    ...                    {'name': 'Bob', 'gender': 'M'},
-    ...                    {'name': 'Charlie', 'gender': 'M'}]) # doctest:+SKIP
-    {'F': [{'gender': 'F', 'name': 'Alice'}],
-     'M': [{'gender': 'M', 'name': 'Bob'},
-           {'gender': 'M', 'name': 'Charlie'}]}
-    See Also:
-        countby
-    """
-    if not callable(key):
-        key = getter(key)
-    d = defaultdict(lambda: [].append)
-    for item in seq:
-        d[key(item)](item)
-    rv = {}
-    for k, v in iteritems(d):
-        rv[k] = v.__self__
-    return rv
-
-
-def getter(index):
-    if isinstance(index, list):
-        if len(index) == 1:
-            index = index[0]
-            return lambda x: (x[index],)
-        elif index:
-            return operator.itemgetter(*index)
-        else:
-            return lambda x: ()
-    else:
-        return operator.itemgetter(index)
-
-
-def comma_join(items):
-    """
-    Like ', '.join(items) but with and
-
-    Examples:
-
-    >>> comma_join(['a'])
-    'a'
-    >>> comma_join(['a', 'b'])
-    'a and b'
-    >>> comma_join(['a', 'b', 'c])
-    'a, b, and c'
-    """
-    return ' and '.join(items) if len(items) <= 2 else ', '.join(items[:-1]) + ', and ' + items[-1]
-
-
-def safe_print_unicode(*args, **kwargs):
-    """
-    prints unicode strings to stdout using configurable `errors` handler for
-    encoding errors
-
-    :param args: unicode strings to print to stdout
-    :param sep: separator (defaults to ' ')
-    :param end: ending character (defaults to '\n')
-    :param errors: error handler for encoding errors (defaults to 'replace')
-    """
-    sep = kwargs.pop('sep', u' ')
-    end = kwargs.pop('end', u'\n')
-    errors = kwargs.pop('errors', 'replace')
-    if PY3:
-        func = sys.stdout.buffer.write
-    else:
-        func = sys.stdout.write
-    line = sep.join(args) + end
-    encoding = sys.stdout.encoding or 'utf8'
-    func(line.encode(encoding, errors))
-
-
-def rec_glob(path, patterns):
-    result = []
-    for d_f in walk(path):
-        # ignore the .git folder
-        # if '.git' in d_f[0]:
-        #     continue
-        m = []
-        for pattern in patterns:
-            m.extend(fnmatch.filter(d_f[2], pattern))
-        if m:
-            result.extend([os.path.join(d_f[0], f) for f in m])
-    return result
-
-
-def convert_unix_path_to_win(path):
-    if external.find_executable('cygpath'):
-        cmd = "cygpath -w {0}".format(path)
-        if PY3:
-            path = subprocess.getoutput(cmd)
-        else:
-            path = subprocess.check_output(cmd.split()).rstrip().rstrip("\\")
-
-    else:
-        path = unix_path_to_win(path)
-    return path
-
-
-def convert_win_path_to_unix(path):
-    if external.find_executable('cygpath'):
-        cmd = "cygpath -u {0}".format(path)
-        if PY3:
-            path = subprocess.getoutput(cmd)
-        else:
-            path = subprocess.check_output(cmd.split()).rstrip().rstrip("\\")
-
-    else:
-        path = win_path_to_unix(path)
-    return path
-
-
-# Used for translating local paths into url (file://) paths
-#   http://stackoverflow.com/a/14298190/1170370
-def path2url(path):
-    return urlparse.urljoin('file:', urllib.pathname2url(path))
-
-
-def get_stdlib_dir(prefix, py_ver):
-    if sys.platform == 'win32':
-        lib_dir = os.path.join(prefix, 'Lib')
-    else:
-        lib_dir = os.path.join(prefix, 'lib')
-        python_folder = glob(os.path.join(lib_dir, 'python?.*'))
-        if python_folder:
-            lib_dir = os.path.join(lib_dir, python_folder[0])
-        else:
-            lib_dir = os.path.join(lib_dir, 'python{}'.format(py_ver))
-    return lib_dir
-
-
-def get_site_packages(prefix, py_ver):
-    return os.path.join(get_stdlib_dir(prefix, py_ver), 'site-packages')
-
-
-def get_build_folders(croot):
-    # remember, glob is not a regex.
-    return glob(os.path.join(croot, "*" + "[0-9]" * 10 + "*"))
-
-
-def prepend_bin_path(env, prefix, prepend_prefix=False):
-    # bin_dirname takes care of bin on *nix, Scripts on win
-    env['PATH'] = join(prefix, bin_dirname) + os.pathsep + env['PATH']
-    if sys.platform == "win32":
-        env['PATH'] = join(prefix, "Library", "mingw-w64", "bin") + os.pathsep + \
-                      join(prefix, "Library", "usr", "bin") + os.pathsep + os.pathsep + \
-                      join(prefix, "Library", "bin") + os.pathsep + \
-                      join(prefix, "Scripts") + os.pathsep + \
-                      env['PATH']
-        prepend_prefix = True  # windows has Python in the prefix.  Use it.
-    if prepend_prefix:
-        env['PATH'] = prefix + os.pathsep + env['PATH']
-    return env
-
-
-# not currently used.  Leaving in because it may be useful for when we do things
-#   like load setup.py data, and we need the modules from some prefix other than
-#   the root prefix, which is what conda-build runs from.
-@contextlib.contextmanager
-def sys_path_prepended(prefix):
-    path_backup = sys.path[:]
-    if on_win:
-        sys.path.insert(1, os.path.join(prefix, 'lib', 'site-packages'))
-    else:
-        lib_dir = os.path.join(prefix, 'lib')
-        python_dir = glob(os.path.join(lib_dir, 'python[0-9\.]*'))
-        if python_dir:
-            python_dir = python_dir[0]
-            sys.path.insert(1, os.path.join(python_dir, 'site-packages'))
-    try:
-        yield
-    finally:
-        sys.path = path_backup
-
-
-@contextlib.contextmanager
-def path_prepended(prefix):
-    old_path = os.environ['PATH']
-    os.environ['PATH'] = prepend_bin_path(os.environ.copy(), prefix, True)['PATH']
-    try:
-        yield
-    finally:
-        os.environ['PATH'] = old_path
-
-
-bin_dirname = 'Scripts' if sys.platform == 'win32' else 'bin'
-
-entry_pat = re.compile('\s*([\w\-\.]+)\s*=\s*([\w.]+):([\w.]+)\s*$')
-
-
-def iter_entry_points(items):
-    for item in items:
-        m = entry_pat.match(item)
-        if m is None:
-            sys.exit("Error cound not match entry point: %r" % item)
-        yield m.groups()
-
-
-def create_entry_point(path, module, func, config):
-    import_name = func.split('.')[0]
-    pyscript = PY_TMPL % {
-        'module': module, 'func': func, 'import_name': import_name}
-    if on_win:
-        with open(path + '-script.py', 'w') as fo:
-            if os.path.isfile(os.path.join(config.host_prefix, 'python_d.exe')):
-                fo.write('#!python_d\n')
-            fo.write(pyscript)
-            copy_into(join(dirname(__file__), 'cli-{}.exe'.format(str(config.host_arch))),
-                    path + '.exe', config.timeout)
-    else:
-        if os.path.islink(path):
-            os.remove(path)
-        with open(path, 'w') as fo:
-            if not config.noarch:
-                fo.write('#!%s\n' % config.host_python)
-            fo.write(pyscript)
-        os.chmod(path, 0o775)
-
-
-def create_entry_points(items, config):
-    if not items:
-        return
-    bin_dir = join(config.host_prefix, bin_dirname)
-    if not isdir(bin_dir):
-        os.mkdir(bin_dir)
-    for cmd, module, func in iter_entry_points(items):
-        create_entry_point(join(bin_dir, cmd), module, func, config)
-
-
-# Return all files in dir, and all its subdirectories, ending in pattern
-def get_ext_files(start_path, pattern):
-    for root, _, files in walk(start_path):
-        for f in files:
-            if f.endswith(pattern):
-                yield os.path.join(root, f)
-
-
-_posix_exes_cache = {}
-
-
-def convert_path_for_cygwin_or_msys2(exe, path):
-    "If exe is a Cygwin or MSYS2 executable then filters it through `cygpath -u`"
-    if sys.platform != 'win32':
-        return path
-    if exe not in _posix_exes_cache:
-        with open(exe, "rb") as exe_file:
-            exe_binary = exe_file.read()
-            msys2_cygwin = re.findall(b'(cygwin1.dll|msys-2.0.dll)', exe_binary)
-            _posix_exes_cache[exe] = True if msys2_cygwin else False
-    if _posix_exes_cache[exe]:
-        try:
-            path = check_output_env(['cygpath', '-u',
-                                     path]).splitlines()[0].decode(getpreferredencoding())
-        except WindowsError:
-            log = get_logger(__name__)
-            log.debug('cygpath executable not found.  Passing native path.  This is OK for msys2.')
-    return path
-
-
-def get_skip_message(m):
-    return ("Skipped: {} from {} defines build/skip for this configuration ({}).".format(
-        m.name(), m.path,
-        {k: m.config.variant[k] for k in m.get_used_vars()}))
-
-
-@memoized
-def package_has_file(package_path, file_path):
-    try:
-        locks = get_conda_operation_locks()
-        with try_acquire_locks(locks, timeout=900):
-            with tarfile.open(package_path) as t:
-                try:
-                    # internal paths are always forward slashed on all platforms
-                    file_path = file_path.replace('\\', '/')
-                    text = t.extractfile(file_path).read()
-                    return text
-                except KeyError:
-                    return False
-                except OSError as e:
-                    raise RuntimeError("Could not extract %s (%s)" % (package_path, e))
-    except (tarfile.ReadError, IOError, EOFError):
-        raise RuntimeError("Could not extract metadata from %s. File probably corrupt.  "
-                           "Please manually remove this file and try again." % package_path)
-
-
-def ensure_list(arg):
-    if (isinstance(arg, string_types) or not hasattr(arg, '__iter__')):
-        if arg is not None:
-            arg = [arg]
-        else:
-            arg = []
-    return arg
-
-
-@contextlib.contextmanager
-def tmp_chdir(dest):
-    curdir = os.getcwd()
-    try:
-        os.chdir(dest)
-        yield
-    finally:
-        os.chdir(curdir)
-
-
-def expand_globs(path_list, root_dir):
-    files = []
-    for path in path_list:
-        if not os.path.isabs(path):
-            path = os.path.join(root_dir, path)
-        if os.path.isfile(path):
-            files.append(path)
-        elif os.path.islink(path):
-            files.append(path)
-        elif os.path.isdir(path):
-            files.extend(os.path.join(root, f) for root, _, fs in walk(path) for f in fs)
-        else:
-            # File compared to the globs use / as separator indenpendently of the os
-            glob_files = glob(path)
-            if not glob_files:
-                log = get_logger(__name__)
-                log.error('Glob {} did not match in root_dir {}'.format(path, root_dir))
-            files.extend(glob_files)
-    prefix_path_re = re.compile('^' + re.escape('%s%s' % (root_dir, os.path.sep)))
-    files = [prefix_path_re.sub('', f, 1) for f in files]
-    return files
-
-
-def find_recipe(path):
-    """recurse through a folder, locating meta.yaml.  Raises error if more than one is found.
-
-    Returns folder containing meta.yaml, to be built.
-
-    If we have a base level meta.yaml and other supplemental ones, use that first"""
-    if os.path.isfile(path) and os.path.basename(path) in ["meta.yaml", "conda.yaml"]:
-        return os.path.dirname(path)
-    results = rec_glob(path, ["meta.yaml", "conda.yaml"])
-    if len(results) > 1:
-        base_recipe = os.path.join(path, "meta.yaml")
-        if base_recipe in results:
-            get_logger(__name__).warn("Multiple meta.yaml files found. "
-                                      "The meta.yaml file in the base directory "
-                                      "will be used.")
-            results = [base_recipe]
-        else:
-            raise IOError("More than one meta.yaml files found in %s" % path)
-    elif not results:
-        raise IOError("No meta.yaml or conda.yaml files found in %s" % path)
-    return results[0]
-
-
-class LoggingContext(object):
-    default_loggers = ['conda', 'binstar', 'install', 'conda.install', 'fetch', 'conda.instructions',
-                       'fetch.progress', 'print', 'progress', 'dotupdate', 'stdoutlog', 'requests',
-                       'conda.core.package_cache', 'conda.plan', 'conda.gateways.disk.delete',
-                       'conda_build', 'conda_build.index']
-
-    def __init__(self, level=logging.WARN, handler=None, close=True, loggers=None):
-        self.level = level
-        self.old_levels = {}
-        self.handler = handler
-        self.close = close
-        self.quiet = context.quiet
-        if not loggers:
-            self.loggers = LoggingContext.default_loggers
-        else:
-            self.loggers = loggers
-
-    def __enter__(self):
-        for logger in self.loggers:
-            if isinstance(logger, string_types):
-                log = logging.getLogger(logger)
-            self.old_levels[logger] = log.level
-            log.setLevel(self.level if ('install' not in logger or
-                                        self.level < logging.INFO) else self.level + 10)
-        if self.handler:
-            self.logger.addHandler(self.handler)
-
-        context.quiet = True
-
-    def __exit__(self, et, ev, tb):
-        for logger, level in self.old_levels.items():
-            logging.getLogger(logger).setLevel(level)
-        if self.handler:
-            self.logger.removeHandler(self.handler)
-        if self.handler and self.close:
-            self.handler.close()
-
-        context.quiet = self.quiet
-
-        # implicit return of None => don't swallow exceptions
-
-
-def get_installed_packages(path):
-    '''
-    Scan all json files in 'path' and return a dictionary with their contents.
-    Files are assumed to be in 'index.json' format.
-    '''
-    installed = dict()
-    for filename in glob(os.path.join(path, 'conda-meta', '*.json')):
-        with open(filename) as file:
-            data = json.load(file)
-            installed[data['name']] = data
-    return installed
-
-
-def _convert_lists_to_sets(_dict):
-    for k, v in _dict.items():
-        if hasattr(v, 'keys'):
-            _dict[k] = HashableDict(_convert_lists_to_sets(v))
-        elif hasattr(v, '__iter__') and not isinstance(v, string_types):
-            try:
-                _dict[k] = sorted(list(set(v)))
-            except TypeError:
-                _dict[k] = sorted(list(set(tuple(_) for _ in v)))
-    return _dict
-
-
-class HashableDict(dict):
-    """use hashable frozen dictionaries for resources and resource types so that they can be in sets
-    """
-    def __init__(self, *args, **kwargs):
-        super(HashableDict, self).__init__(*args, **kwargs)
-        self = _convert_lists_to_sets(self)
-
-    def __hash__(self):
-        return hash(json.dumps(self, sort_keys=True))
-
-
-def represent_hashabledict(dumper, data):
-    value = []
-
-    for item_key, item_value in data.items():
-        node_key = dumper.represent_data(item_key)
-        node_value = dumper.represent_data(item_value)
-
-        value.append((node_key, node_value))
-
-    return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', value)
-
-
-yaml.add_representer(HashableDict, represent_hashabledict)
-
-
-# http://stackoverflow.com/a/10743550/1170370
-@contextlib.contextmanager
-def capture():
-    import sys
-    oldout, olderr = sys.stdout, sys.stderr
-    try:
-        out = [StringIO(), StringIO()]
-        sys.stdout, sys.stderr = out
-        yield out
-    finally:
-        sys.stdout, sys.stderr = oldout, olderr
-        out[0] = out[0].getvalue()
-        out[1] = out[1].getvalue()
-
-
-# copied from conda; added in 4.3, not currently part of exported functionality
-@contextlib.contextmanager
-def env_var(name, value, callback=None):
-    # NOTE: will likely want to call reset_context() when using this function, so pass
-    #       it as callback
-    name, value = str(name), str(value)
-    saved_env_var = os.environ.get(name)
-    try:
-        os.environ[name] = value
-        if callback:
-            callback()
-        yield
-    finally:
-        if saved_env_var:
-            os.environ[name] = saved_env_var
-        else:
-            del os.environ[name]
-        if callback:
-            callback()
-
-
-def trim_empty_keys(dict_):
-    to_remove = set()
-    negative_means_empty = ('final', 'noarch_python', 'zip_keys')
-    for k, v in dict_.items():
-        if hasattr(v, 'keys'):
-            trim_empty_keys(v)
-        # empty lists and empty strings, and None are always empty.
-        if v == list() or v == '' or v is None or v == dict():
-            to_remove.add(k)
-        # other things that evaluate as False may not be "empty" - things can be manually set to
-        #     false, and we need to keep that setting.
-        if not v and k in negative_means_empty:
-            to_remove.add(k)
-    if 'zip_keys' in dict_ and not any(v for v in dict_['zip_keys']):
-        to_remove.add('zip_keys')
-    for k in to_remove:
-        del dict_[k]
-
-
-def _increment(version, alpha_ver):
-    try:
-        if alpha_ver:
-            suffix = 'a'
-        else:
-            suffix = '.0a0'
-        last_version = str(int(version) + 1) + suffix
-    except ValueError:
-        last_version = chr(ord(version) + 1)
-    return last_version
-
-
-def apply_pin_expressions(version, min_pin='x.x.x.x.x.x.x', max_pin='x'):
-    pins = [len(p.split('.')) if p else None for p in (min_pin, max_pin)]
-    parsed_version = VersionOrder(version).version[1:]
-    nesting_position = None
-    flat_list = []
-    for idx, item in enumerate(parsed_version):
-        if isinstance(item, list):
-            nesting_position = idx
-            flat_list.extend(item)
-        else:
-            flat_list.append(item)
-    if max_pin and len(max_pin.split('.')) > len(flat_list):
-        pins[1] = len(flat_list)
-    versions = ['', '']
-    # first idx is lower bound pin; second is upper bound pin.
-    #    pin value is number of places to pin.
-    for p_idx, pin in enumerate(pins):
-        if pin:
-            # flat_list is the blown-out representation of the version
-            for v_idx, v in enumerate(flat_list[:pin]):
-                # upper bound pin
-                if p_idx == 1 and v_idx == pin - 1:
-                    # is the last place an alphabetic character?  OpenSSL, JPEG
-                    alpha_ver = str(flat_list[min(pin, len(flat_list) - 1)]).isalpha()
-                    v = _increment(v, alpha_ver)
-                versions[p_idx] += str(v)
-                if v_idx != nesting_position:
-                    versions[p_idx] += '.'
-            if versions[p_idx][-1] == '.':
-                versions[p_idx] = versions[p_idx][:-1]
-    if versions[0]:
-        versions[0] = '>=' + versions[0]
-    if versions[1]:
-        versions[1] = '<' + versions[1]
-    return ','.join([v for v in versions if v])
-
-
-def filter_files(files_list, prefix, filter_patterns=('(.*[\\\\/])?\.git[\\\\/].*',
-                                                      '(.*[\\\\/])?\.git$',
-                                                      '(.*)?\.DS_Store.*',
-                                                      '.*\.la$',
-                                                      'conda-meta.*',
-                                                      '.*\.conda_trash(?:_\d+)*$')):
-    """Remove things like the .git directory from the list of files to be copied"""
-    for pattern in filter_patterns:
-        r = re.compile(pattern)
-        files_list = set(files_list) - set(filter(r.match, files_list))
-    return [f for f in files_list
-            if not os.path.isdir(os.path.join(prefix, f)) or
-            os.path.islink(os.path.join(prefix, f))]
-
-
-def filter_info_files(files_list, prefix):
-    return filter_files(files_list, prefix, filter_patterns=(
-                    'info[\\\\/]index.json',
-                    'info[\\\\/]files',
-                    'info[\\\\/]paths.json',
-                    'info[\\\\/]about.json',
-                    'info[\\\\/]has_prefix',
-                    'info[\\\\/]hash_input_files',   # legacy, not used anymore
-                    'info[\\\\/]hash_input.json',
-                    'info[\\\\/]run_exports.yaml',   # legacy
-                    'info[\\\\/]run_exports.json',   # current
-                    'info[\\\\/]git',
-                    'info[\\\\/]recipe[\\\\/].*',
-                    'info[\\\\/]recipe_log.json',
-                    'info[\\\\/]recipe.tar',
-                    'info[\\\\/]test[\\\\/].*',
-                    'info[\\\\/]LICENSE.txt',
-                    'info[\\\\/]requires',
-                    'info[\\\\/]meta',
-                    'info[\\\\/]platform',
-                    'info[\\\\/]no_link',
-                    'info[\\\\/]link.json',
-                    'info[\\\\/]icon.png',
-            ))
-
-
-def rm_rf(path, config=None):
-    if conda_46:
-        return _rm_rf(path)
-    if os.path.isdir(path):
-        try:
-            # subprocessing to delete large folders can be quite a bit faster
-            if on_win:
-                subprocess.check_call('rd /s /q {}'.format(path), shell=True)
-            else:
-                try:
-                    os.makedirs('.empty')
-                except:
-                    pass
-                del_dir_cmd = 'rsync -a --delete .empty {}/'
-                subprocess.check_call(del_dir_cmd.format(path).split())
-                try:
-                    shutil.rmtree('.empty')
-                except:
-                    pass
-        # we don't really care about errors that much. People can and should
-        #     clean out their folders once in a while with "purge"
-        except:
-            pass
-
-    conda_log_level = logging.WARN
-    if config and config.debug:
-        conda_log_level = logging.DEBUG
-    with LoggingContext(conda_log_level):
-        # this clears out the path from conda's cache, which otherwise thinks
-        #    that things are still installed here
-        _rm_rf(path)
-
-
-# https://stackoverflow.com/a/31459386/1170370
-class LessThanFilter(logging.Filter):
-    def __init__(self, exclusive_maximum, name=""):
-        super(LessThanFilter, self).__init__(name)
-        self.max_level = exclusive_maximum
-
-    def filter(self, record):
-        # non-zero return means we log this message
-        return 1 if record.levelno < self.max_level else 0
-
-
-class GreaterThanFilter(logging.Filter):
-    def __init__(self, exclusive_minimum, name=""):
-        super(GreaterThanFilter, self).__init__(name)
-        self.min_level = exclusive_minimum
-
-    def filter(self, record):
-        # non-zero return means we log this message
-        return 1 if record.levelno > self.min_level else 0
-
-
-# unclutter logs - show messages only once
-class DuplicateFilter(logging.Filter):
-    def __init__(self):
-        self.msgs = set()
-
-    def filter(self, record):
-        log = record.msg not in self.msgs
-        self.msgs.add(record.msg)
-        return int(log)
-
-
-dedupe_filter = DuplicateFilter()
-info_debug_stdout_filter = LessThanFilter(logging.WARNING)
-warning_error_stderr_filter = GreaterThanFilter(logging.INFO)
-
-# set filelock's logger to only show warnings by default
-logging.getLogger('filelock').setLevel(logging.WARN)
-
-# quiet some of conda's less useful output
-logging.getLogger('conda.core.linked_data').setLevel(logging.WARN)
-logging.getLogger('conda.gateways.disk.delete').setLevel(logging.WARN)
-logging.getLogger('conda.gateways.disk.test').setLevel(logging.WARN)
-
-
-def reset_deduplicator():
-    """Most of the time, we want the deduplication.  There are some cases (tests especially)
-    where we want to be able to control the duplication."""
-    global dedupe_filter
-    dedupe_filter = DuplicateFilter()
-
-
-def get_logger(name, level=logging.INFO, dedupe=True, add_stdout_stderr_handlers=True):
-    config_file = None
-    if cc_conda_build.get('log_config_file'):
-        config_file = abspath(expanduser(expandvars(cc_conda_build.get('log_config_file'))))
-    # by loading config file here, and then only adding handlers later, people
-    # should be able to override conda-build's logger settings here.
-    if config_file:
-        with open(config_file) as f:
-            config_dict = yaml.safe_load(f)
-        logging.config.dictConfig(config_dict)
-        level = config_dict.get('loggers', {}).get(name, {}).get('level', level)
-    log = logging.getLogger(name)
-    log.setLevel(level)
-    if dedupe:
-        log.addFilter(dedupe_filter)
-
-    # these are defaults.  They can be overridden by configuring a log config yaml file.
-    if not log.handlers and add_stdout_stderr_handlers:
-        stdout_handler = logging.StreamHandler(sys.stdout)
-        stderr_handler = logging.StreamHandler(sys.stderr)
-        stdout_handler.addFilter(info_debug_stdout_filter)
-        stderr_handler.addFilter(warning_error_stderr_filter)
-        stdout_handler.setLevel(level)
-        stderr_handler.setLevel(level)
-        log.addHandler(stdout_handler)
-        log.addHandler(stderr_handler)
-    return log
-
-
-def _equivalent(base_value, value, path):
-    equivalent = value == base_value
-    if isinstance(value, string_types) and isinstance(base_value, string_types):
-        if not os.path.isabs(base_value):
-            base_value = os.path.abspath(os.path.normpath(os.path.join(path, base_value)))
-        if not os.path.isabs(value):
-            value = os.path.abspath(os.path.normpath(os.path.join(path, value)))
-        equivalent |= base_value == value
-    return equivalent
-
-
-def merge_or_update_dict(base, new, path="", merge=True, raise_on_clobber=False, add_missing_keys=True):
-    if base == new:
-        return base
-    log = get_logger(__name__)
-    for key, value in new.items():
-        if key in base or add_missing_keys:
-            base_value = base.get(key, value)
-            if hasattr(value, 'keys'):
-                base_value = merge_or_update_dict(base_value, value, path, merge,
-                                                raise_on_clobber=raise_on_clobber)
-                base[key] = base_value
-            elif hasattr(value, '__iter__') and not isinstance(value, string_types):
-                if merge:
-                    if base_value != value:
-                        try:
-                            base_value.extend(value)
-                        except TypeError:
-                            base_value = value
-                    try:
-                        base[key] = list(base_value)
-                    except TypeError:
-                        base[key] = base_value
-                else:
-                    base[key] = value
-            else:
-                if (base_value and merge and not _equivalent(base_value, value, path) and
-                        raise_on_clobber):
-                    log.debug('clobbering key {} (original value {}) with value {}'.format(key,
-                                                                                base_value, value))
-                if value is None and key in base:
-                    del base[key]
-                else:
-                    base[key] = value
-    return base
-
-
-def merge_dicts_of_lists(dol1, dol2):
-    '''
-    From Alex Martelli: https://stackoverflow.com/a/1495821/3257826
-    '''
-    keys = set(dol1).union(dol2)
-    no = []
-    return dict((k, dol1.get(k, no) + dol2.get(k, no)) for k in keys)
-
-
-def prefix_files(prefix):
-    '''
-    Returns a set of all files in prefix.
-    '''
-    res = set()
-    for root, dirs, files in walk(prefix):
-        for fn in files:
-            res.add(join(root, fn)[len(prefix) + 1:])
-        for dn in dirs:
-            path = join(root, dn)
-            if islink(path):
-                res.add(path[len(prefix) + 1:])
-    res = set(expand_globs(res, prefix))
-    return res
-
-
-def mmap_mmap(fileno, length, tagname=None, flags=0, prot=mmap_PROT_READ | mmap_PROT_WRITE,
-              access=None, offset=0):
-    '''
-    Hides the differences between mmap.mmap on Windows and Unix.
-    Windows has `tagname`.
-    Unix does not, but makes up for it with `flags` and `prot`.
-    On both, the default value for `access` is determined from how the file
-    was opened so must not be passed in at all to get this default behaviour.
-    '''
-    if on_win:
-        if access:
-            return mmap.mmap(fileno, length, tagname=tagname, access=access, offset=offset)
-        else:
-            return mmap.mmap(fileno, length, tagname=tagname)
-    else:
-        if access:
-            return mmap.mmap(fileno, length, flags=flags, prot=prot, access=access, offset=offset)
-        else:
-            return mmap.mmap(fileno, length, flags=flags, prot=prot)
-
-
-def remove_pycache_from_scripts(build_prefix):
-    """Remove pip created pycache directory from bin or Scripts."""
-    if on_win:
-        scripts_path = os.path.join(build_prefix, 'Scripts')
-    else:
-        scripts_path = os.path.join(build_prefix, 'bin')
-
-    if os.path.isdir(scripts_path):
-        for entry in os.listdir(scripts_path):
-            entry_path = os.path.join(scripts_path, entry)
-            if os.path.isdir(entry_path) and entry.strip(os.sep) == '__pycache__':
-                shutil.rmtree(entry_path)
-
-            elif os.path.isfile(entry_path) and entry_path.endswith('.pyc'):
-                os.remove(entry_path)
-
-
-def sort_list_in_nested_structure(dictionary, omissions=''):
-    """Recurse through a nested dictionary and sort any lists that are found.
-
-    If the list that is found contains anything but strings, it is skipped
-    as we can't compare lists containing different types. The omissions argument
-    allows for certain sections of the dictionary to be omitted from sorting.
-    """
-    for field, value in dictionary.items():
-        if isinstance(value, dict):
-            for key in value.keys():
-                section = dictionary[field][key]
-                if isinstance(section, dict):
-                    sort_list_in_nested_structure(section)
-                elif (isinstance(section, list) and
-                    '{}/{}' .format(field, key) not in omissions and
-                        all(isinstance(item, str) for item in section)):
-                    section.sort()
-
-        # there's a possibility for nested lists containing dictionaries
-        # in this case we recurse until we find a list to sort
-        elif isinstance(value, list):
-            for element in value:
-                if isinstance(element, dict):
-                    sort_list_in_nested_structure(element)
-            try:
-                value.sort()
-            except TypeError:
-                pass
-
-
-# group one: package name
-# group two: version (allows _, +, . in version)
-# group three: build string - mostly not used here.  Match primarily matters
-#        to specify when not to add .*
-
-# if you are seeing mysterious unsatisfiable errors, with the package you're building being the
-#    unsatisfiable part, then you probably need to update this regex.
-
-spec_needing_star_re = re.compile(r"([\w\d\.\-\_]+)\s+((?<![><=])[\w\d\.\-\_]+?(?!\*))(\s+[\w\d\.\_]+)?$")  # NOQA
-spec_ver_needing_star_re = re.compile("^([0-9a-zA-Z\.]+)$")
-
-
-def ensure_valid_spec(spec, warn=False):
-    if isinstance(spec, MatchSpec):
-        if (hasattr(spec, 'version') and spec.version and (not spec.get('build', '')) and
-                spec_ver_needing_star_re.match(str(spec.version))):
-            if str(spec.name) not in ('python', 'numpy') or str(spec.version) != 'x.x':
-                spec = MatchSpec("{} {}".format(str(spec.name), str(spec.version) + '.*'))
-    else:
-        match = spec_needing_star_re.match(spec)
-        # ignore exact pins (would be a 3rd group)
-        if match and not match.group(3):
-            if match.group(1) in ('python', 'numpy') and match.group(2) == 'x.x':
-                spec = spec_needing_star_re.sub(r"\1 \2", spec)
-            else:
-                if "*" not in spec:
-                    if match.group(1) not in ('python', 'vc') and warn:
-                        log = get_logger(__name__)
-                        log.warn("Adding .* to spec '{}' to ensure satisfiability.  Please "
-                                 "consider putting {{{{ var_name }}}}.* or some relational "
-                                 "operator (>/</>=/<=) on this spec in meta.yaml, or if req is "
-                                 "also a build req, using {{{{ pin_compatible() }}}} jinja2 "
-                                 "function instead.  See "
-                "https://conda.io/docs/user-guide/tasks/build-packages/variants.html#pinning-at-the-variant-level"  # NOQA
-                        .format(spec))
-                    spec = spec_needing_star_re.sub(r"\1 \2.*", spec)
-    return spec
-
-
-def insert_variant_versions(requirements_dict, variant, env):
-    build_deps = (ensure_list(requirements_dict.get('build')) +
-                  ensure_list(requirements_dict.get('host')))
-    reqs = ensure_list(requirements_dict.get(env))
-    for key, val in variant.items():
-        regex = re.compile(r'^(%s)(?:\s*$)' % key.replace('_', '[-_]'))
-        matches = [regex.match(pkg) for pkg in reqs]
-        if any(matches):
-            for i, x in enumerate(matches):
-                if x and (env in ('build', 'host') or x.group(1) in build_deps):
-                    del reqs[i]
-                    if not isinstance(val, string_types):
-                        val = val[0]
-                    reqs.insert(i, ensure_valid_spec(' '.join((x.group(1), val))))
-
-    xx_re = re.compile("([0-9a-zA-Z\.\-\_]+)\s+x\.x")
-
-    matches = [xx_re.match(pkg) for pkg in reqs]
-    if any(matches):
-        for i, x in enumerate(matches):
-            if x:
-                del reqs[i]
-                reqs.insert(i, ensure_valid_spec(' '.join((x.group(1), variant.get(x.group(1))))))
-    if reqs:
-        requirements_dict[env] = reqs
-
-
-def match_peer_job(target_matchspec, other_m, this_m=None):
-    """target_matchspec comes from the recipe.  target_variant is the variant from the recipe whose
-    deps we are matching.  m is the peer job, which must satisfy conda and also have matching keys
-    for any keys that are shared between target_variant and m.config.variant"""
-    match_dict = {'name': other_m.name(),
-                'version': other_m.version(),
-                'build': '', }
-    if conda_43:
-        match_dict = Dist(name=match_dict['name'],
-                          dist_name='-'.join((match_dict['name'],
-                                              match_dict['version'],
-                                              match_dict['build'])),
-                          version=match_dict['version'],
-                          build_string=match_dict['build'],
-                          build_number=int(other_m.build_number() or 0),
-                          channel=None)
-    matchspec_matches = target_matchspec.match(match_dict)
-
-    variant_matches = True
-    if this_m:
-        other_m_used_vars = other_m.get_used_loop_vars()
-        for v in this_m.get_used_loop_vars():
-            if v in other_m_used_vars:
-                variant_matches &= this_m.config.variant[v] == other_m.config.variant[v]
-    return matchspec_matches and variant_matches
-
-
-def expand_reqs(reqs_entry):
-    if not hasattr(reqs_entry, 'keys'):
-        original = ensure_list(reqs_entry)[:]
-        reqs_entry = {'host': ensure_list(original),
-                      'run': ensure_list(original)} if original else {}
-    else:
-        for sec in reqs_entry:
-            reqs_entry[sec] = ensure_list(reqs_entry[sec])
-    return reqs_entry
-
-
-def sha256_checksum(filename, buffersize=65536):
-    if not isfile(filename):
-        return None
-    sha256 = hashlib.sha256()
-    with open(filename, 'rb') as f:
-        for block in iter(lambda: f.read(buffersize), b''):
-            sha256.update(block)
-    return sha256.hexdigest()
+from __future__ import absolute_import, division, print_function
+
+from collections import OrderedDict, defaultdict
+import contextlib
+import fnmatch
+import hashlib
+import json
+from locale import getpreferredencoding
+import libarchive
+import logging
+import logging.config
+import mmap
+import operator
+import os
+from os.path import (dirname, getmtime, getsize, isdir, join, isfile, abspath, islink,
+                     expanduser, expandvars)
+import re
+import stat
+import subprocess
+import sys
+import shutil
+import tarfile
+import tempfile
+from threading import Thread
+import time
+import yaml
+
+import filelock
+
+try:
+    from conda.base.constants import CONDA_TARBALL_EXTENSIONS
+except Exception:
+    from conda.base.constants import CONDA_TARBALL_EXTENSION
+    CONDA_TARBALL_EXTENSIONS = (CONDA_TARBALL_EXTENSION,)
+
+from .conda_interface import hashsum_file, md5_file, unix_path_to_win, win_path_to_unix
+from .conda_interface import PY3, iteritems
+from .conda_interface import root_dir, pkgs_dirs
+from .conda_interface import string_types
+from .conda_interface import memoized
+from .conda_interface import StringIO
+from .conda_interface import VersionOrder, MatchSpec
+from .conda_interface import cc_conda_build
+from .conda_interface import conda_43, conda_46, Dist
+from .conda_interface import context
+# NOQA because it is not used in this file.
+from conda_build.conda_interface import rm_rf as _rm_rf # NOQA
+from conda_build.exceptions import BuildLockError
+from conda_build.os_utils import external
+
+if PY3:
+    from glob import glob as glob_glob
+
+    # stdlib glob is less feature-rich but considerably faster than glob2
+    def glob(pathname, recursive=True):
+        return glob_glob(pathname, recursive=recursive)
+
+    import urllib.parse as urlparse
+    import urllib.request as urllib
+    # NOQA because it is not used in this file.
+    from contextlib import ExitStack  # NOQA
+    PermissionError = PermissionError  # NOQA
+    FileNotFoundError = FileNotFoundError
+else:
+    from glob2 import glob as glob2_glob
+
+    def glob(pathname, recursive=True):
+        return glob2_glob(pathname, recursive=recursive)
+
+    import urlparse
+    import urllib
+    # NOQA because it is not used in this file.
+    from contextlib2 import ExitStack  # NOQA
+    PermissionError = OSError
+    FileNotFoundError = OSError
+
+on_win = (sys.platform == 'win32')
+
+codec = getpreferredencoding() or 'utf-8'
+on_win = sys.platform == "win32"
+root_script_dir = os.path.join(root_dir, 'Scripts' if on_win else 'bin')
+mmap_MAP_PRIVATE = 0 if on_win else mmap.MAP_PRIVATE
+mmap_PROT_READ = 0 if on_win else mmap.PROT_READ
+mmap_PROT_WRITE = 0 if on_win else mmap.PROT_WRITE
+
+
+PY_TMPL = """
+# -*- coding: utf-8 -*-
+import re
+import sys
+
+from %(module)s import %(import_name)s
+
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+    sys.exit(%(func)s())
+"""
+
+try:
+    from os import scandir, walk  # NOQA
+except ImportError:
+    from scandir import walk
+
+
+@memoized
+def stat_file(path):
+    return os.stat(path)
+
+
+def directory_size_slow(path):
+    total_size = 0
+    seen = set()
+
+    for root, _, files in walk(path):
+        for f in files:
+            try:
+                stat = stat_file(os.path.join(root, f))
+            except OSError:
+                continue
+
+            if stat.st_ino in seen:
+                continue
+
+            seen.add(stat.st_ino)
+
+            total_size += stat.st_size
+    return total_size
+
+
+def directory_size(path):
+    '''
+    '''
+    try:
+        if on_win:
+            command = 'dir /s "{}"'  # Windows path can have spaces
+            out = subprocess.check_output(command.format(path), shell=True)
+        else:
+            command = "du -s {}"
+            out = subprocess.check_output(command.format(path).split(), stderr=subprocess.PIPE)
+
+        if hasattr(out, 'decode'):
+            try:
+                out = out.decode(errors='ignore')
+            # This isn't important anyway so give up. Don't try search on bytes.
+            except (UnicodeDecodeError, IndexError):
+                if on_win:
+                    return 0
+                else:
+                    pass
+        if on_win:
+            # Windows can give long output, we need only 2nd to last line
+            out = out.strip().rsplit('\r\n', 2)[-2]
+            pattern = "\s([\d\W]+).+"  # Language and punctuation neutral
+            out = re.search(pattern, out.strip()).group(1).strip()
+            out = out.replace(',', '').replace('.', '').replace(' ', '')
+        else:
+            out = out.split()[0]
+    except subprocess.CalledProcessError:
+        out = directory_size_slow(path)
+
+    try:
+        return int(out)  # size in bytes
+    except ValueError:
+        return 0
+
+
+class DummyPsutilProcess(object):
+    def children(self, *args, **kwargs):
+        return []
+
+
+def _setup_rewrite_pipe(env):
+    """Rewrite values of env variables back to $ENV in stdout
+
+    Takes output on the pipe and finds any env value
+    and rewrites it as the env key
+
+    Useful for replacing "~/conda/conda-bld/pkg_<date>/_h_place..." with "$PREFIX"
+
+    Returns an FD to be passed to Popen(stdout=...)
+    """
+    # replacements is the env dict reversed,
+    # ordered by the length of the value so that longer replacements
+    # always occur first in case of common prefixes
+    replacements = OrderedDict()
+    for k, v in sorted(env.items(), key=lambda kv: len(kv[1]), reverse=True):
+        replacements[v] = k
+
+    r_fd, w_fd = os.pipe()
+    r = os.fdopen(r_fd, 'rt')
+    if sys.platform == 'win32':
+        replacement_t = '%{}%'
+    else:
+        replacement_t = '${}'
+
+    def rewriter():
+        while True:
+            try:
+                line = r.readline()
+                if not line:
+                    # reading done
+                    r.close()
+                    os.close(w_fd)
+                    return
+                for s, key in replacements.items():
+                    line = line.replace(s, replacement_t.format(key))
+                sys.stdout.write(line)
+            except UnicodeDecodeError:
+                try:
+                    txt = os.read(r, 10000)
+                    sys.stdout.write(txt or '')
+                except TypeError:
+                    pass
+
+    t = Thread(target=rewriter)
+    t.daemon = True
+    t.start()
+
+    return w_fd
+
+
+class PopenWrapper(object):
+    # Small wrapper around subprocess.Popen to allow memory usage monitoring
+    # copied from ProtoCI, https://github.com/ContinuumIO/ProtoCI/blob/59159bc2c9f991fbfa5e398b6bb066d7417583ec/protoci/build2.py#L20  # NOQA
+
+    def __init__(self, *args, **kwargs):
+        self.elapsed = None
+        self.rss = 0
+        self.vms = 0
+        self.returncode = None
+        self.disk = 0
+        self.processes = 1
+
+        self.out, self.err = self._execute(*args, **kwargs)
+
+    def _execute(self, *args, **kwargs):
+        try:
+            import psutil
+            psutil_exceptions = psutil.NoSuchProcess, psutil.AccessDenied, psutil.NoSuchProcess
+        except ImportError as e:
+            psutil = None
+            psutil_exceptions = (OSError, ValueError)
+            log = get_logger(__name__)
+            log.warn("psutil import failed.  Error was {}".format(e))
+            log.warn("only disk usage and time statistics will be available.  Install psutil to "
+                     "get CPU time and memory usage statistics.")
+
+        # The polling interval (in seconds)
+        time_int = kwargs.pop('time_int', 2)
+
+        disk_usage_dir = kwargs.get('cwd', sys.prefix)
+
+        # Create a process of this (the parent) process
+        parent = psutil.Process(os.getpid()) if psutil else DummyPsutilProcess()
+
+        cpu_usage = defaultdict(dict)
+
+        # Using the convenience Popen class provided by psutil
+        start_time = time.time()
+        _popen = psutil.Popen(*args, **kwargs) if psutil else subprocess.Popen(*args, **kwargs)
+        try:
+            while self.returncode is None:
+                # We need to get all of the children of our process since our
+                # process spawns other processes.  Collect all of the child
+                # processes
+
+                rss = 0
+                vms = 0
+                processes = 0
+                # We use the parent process to get mem usage of all spawned processes
+                for child in parent.children(recursive=True):
+                    child_cpu_usage = cpu_usage.get(child.pid, {})
+                    try:
+                        mem = child.memory_info()
+                        rss += mem.rss
+                        vms += mem.rss
+                        # listing child times are only available on linux, so we don't use them.
+                        #    we are instead looping over children and getting each individually.
+                        #    https://psutil.readthedocs.io/en/latest/#psutil.Process.cpu_times
+                        cpu_stats = child.cpu_times()
+                        child_cpu_usage['sys'] = cpu_stats.system
+                        child_cpu_usage['user'] = cpu_stats.user
+                        cpu_usage[child.pid] = child_cpu_usage
+                    except psutil_exceptions:
+                        # process already died.  Just ignore it.
+                        continue
+                    processes += 1
+
+                # Sum the memory usage of all the children together (2D columnwise sum)
+                self.rss = max(rss, self.rss)
+                self.vms = max(vms, self.vms)
+                self.cpu_sys = sum(child['sys'] for child in cpu_usage.values())
+                self.cpu_user = sum(child['user'] for child in cpu_usage.values())
+                self.processes = max(processes, self.processes)
+
+                # Get disk usage
+                self.disk = max(directory_size(disk_usage_dir), self.disk)
+
+                time.sleep(time_int)
+                self.elapsed = time.time() - start_time
+                self.returncode = _popen.poll()
+
+        except KeyboardInterrupt:
+            _popen.kill()
+            raise
+
+        self.disk = max(directory_size(disk_usage_dir), self.disk)
+        self.elapsed = time.time() - start_time
+        return _popen.stdout, _popen.stderr
+
+    def __repr__(self):
+        return str({'elapsed': self.elapsed,
+                    'rss': self.rss,
+                    'vms': self.vms,
+                    'disk': self.disk,
+                    'processes': self.processes,
+                    'cpu_user': self.cpu_user,
+                    'cpu_sys': self.cpu_sys,
+                    'returncode': self.returncode})
+
+
+def _func_defaulting_env_to_os_environ(func, *popenargs, **kwargs):
+    if 'env' not in kwargs:
+        kwargs = kwargs.copy()
+        env_copy = os.environ.copy()
+        kwargs.update({'env': env_copy})
+    kwargs['env'] = {str(key): str(value) for key, value in kwargs['env'].items()}
+    _args = []
+    if 'stdin' not in kwargs:
+        kwargs['stdin'] = subprocess.PIPE
+    for arg in popenargs:
+        # arguments to subprocess need to be bytestrings
+        if sys.version_info.major < 3 and hasattr(arg, 'encode'):
+            arg = arg.encode(codec)
+        elif sys.version_info.major >= 3 and hasattr(arg, 'decode'):
+            arg = arg.decode(codec)
+        _args.append(str(arg))
+
+    stats = kwargs.get('stats')
+    if 'stats' in kwargs:
+        del kwargs['stats']
+
+    rewrite_stdout_env = kwargs.pop('rewrite_stdout_env', None)
+    if rewrite_stdout_env:
+        kwargs['stdout'] = _setup_rewrite_pipe(rewrite_stdout_env)
+
+    out = None
+    if stats is not None:
+        proc = PopenWrapper(_args, **kwargs)
+        if func == 'output':
+            out = proc.out.read()
+
+        if proc.returncode != 0:
+            raise subprocess.CalledProcessError(proc.returncode, _args)
+
+        stats.update({'elapsed': proc.elapsed,
+                    'disk': proc.disk,
+                    'processes': proc.processes,
+                    'cpu_user': proc.cpu_user,
+                    'cpu_sys': proc.cpu_sys,
+                    'rss': proc.rss,
+                    'vms': proc.vms})
+    else:
+        if func == 'call':
+            subprocess.check_call(_args, **kwargs)
+        else:
+            if 'stdout' in kwargs:
+                del kwargs['stdout']
+            out = subprocess.check_output(_args, **kwargs)
+    return out
+
+
+def check_call_env(popenargs, **kwargs):
+    return _func_defaulting_env_to_os_environ('call', *popenargs, **kwargs)
+
+
+def check_output_env(popenargs, **kwargs):
+    return _func_defaulting_env_to_os_environ('output', stdout=subprocess.PIPE,
+                                              *popenargs, **kwargs).rstrip()
+
+
+def bytes2human(n):
+    # http://code.activestate.com/recipes/578019
+    # >>> bytes2human(10000)
+    # '9.8K'
+    # >>> bytes2human(100001221)
+    # '95.4M'
+    symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
+    prefix = {}
+    for i, s in enumerate(symbols):
+        prefix[s] = 1 << (i + 1) * 10
+    for s in reversed(symbols):
+        if n >= prefix[s]:
+            value = float(n) / prefix[s]
+            return '%.1f%s' % (value, s)
+    return "%sB" % n
+
+
+def get_recipe_abspath(recipe):
+    """resolve recipe dir as absolute path.  If recipe is a tarball rather than a folder,
+    extract it and return the extracted directory.
+
+    Returns the absolute path, and a boolean flag that is true if a tarball has been extracted
+    and needs cleanup.
+    """
+    # Don't use byte literals for paths in Python 2
+    if not PY3:
+        recipe = recipe.decode(getpreferredencoding() or 'utf-8')
+    if isfile(recipe):
+        if recipe.lower().endswith(decompressible_exts) or recipe.lower().endswith(CONDA_TARBALL_EXTENSIONS):
+            recipe_dir = tempfile.mkdtemp()
+            tar_xf(recipe, recipe_dir)
+            # At some stage the old build system started to tar up recipes.
+            recipe_tarfile = os.path.join(recipe_dir, 'info', 'recipe.tar')
+            if isfile(recipe_tarfile):
+                tar_xf(recipe_tarfile, os.path.join(recipe_dir, 'info'))
+            need_cleanup = True
+        else:
+            print("Ignoring non-recipe: %s" % recipe)
+            return (None, None)
+    else:
+        recipe_dir = abspath(os.path.join(os.getcwd(), recipe))
+        need_cleanup = False
+    if not os.path.exists(recipe_dir):
+        raise ValueError("Package or recipe at path {0} does not exist".format(recipe_dir))
+    return recipe_dir, need_cleanup
+
+
+@contextlib.contextmanager
+def try_acquire_locks(locks, timeout):
+    """Try to acquire all locks.
+
+    If any lock can't be immediately acquired, free all locks.
+    If the timeout is reached withou acquiring all locks, free all locks and raise.
+
+    http://stackoverflow.com/questions/9814008/multiple-mutex-locking-strategies-and-why-libraries-dont-use-address-comparison
+    """
+    t = time.time()
+    while (time.time() - t < timeout):
+        # Continuously try to acquire all locks.
+        # By passing a short timeout to each individual lock, we give other
+        # processes that might be trying to acquire the same locks (and may
+        # already hold some of them) a chance to the remaining locks - and
+        # hopefully subsequently release them.
+        try:
+            for lock in locks:
+                lock.acquire(timeout=0.1)
+        except filelock.Timeout:
+            # If we failed to acquire a lock, it is important to release all
+            # locks we may have already acquired, to avoid wedging multiple
+            # processes that try to acquire the same set of locks.
+            # That is, we want to avoid a situation where processes 1 and 2 try
+            # to acquire locks A and B, and proc 1 holds lock A while proc 2
+            # holds lock B.
+            for lock in locks:
+                lock.release()
+        else:
+            break
+    else:
+        # If we reach this point, we weren't able to acquire all locks within
+        # the specified timeout. We shouldn't be holding any locks anymore at
+        # this point, so we just raise an exception.
+        raise BuildLockError('Failed to acquire all locks')
+
+    try:
+        yield
+    finally:
+        for lock in locks:
+            lock.release()
+
+
+# with each of these, we are copying less metadata.  This seems to be necessary
+#   to cope with some shared filesystems with some virtual machine setups.
+#  See https://github.com/conda/conda-build/issues/1426
+def _copy_with_shell_fallback(src, dst):
+    is_copied = False
+    for func in (shutil.copy2, shutil.copy, shutil.copyfile):
+        try:
+            func(src, dst)
+            is_copied = True
+            break
+        except (IOError, OSError, PermissionError):
+            continue
+    if not is_copied:
+        try:
+            subprocess.check_call('cp -a {} {}'.format(src, dst), shell=True,
+                                  stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+        except subprocess.CalledProcessError as e:
+            if not os.path.isfile(dst):
+                raise OSError("Failed to copy {} to {}.  Error was: {}".format(src, dst, e))
+
+
+def get_prefix_replacement_paths(src, dst):
+    ssplit = src.split(os.path.sep)
+    dsplit = dst.split(os.path.sep)
+    while ssplit and ssplit[-1] == dsplit[-1]:
+        del ssplit[-1]
+        del dsplit[-1]
+    return os.path.join(*ssplit), os.path.join(*dsplit)
+
+
+def copy_into(src, dst, timeout=900, symlinks=False, lock=None, locking=True, clobber=False):
+    """Copy all the files and directories in src to the directory dst"""
+    log = get_logger(__name__)
+    if symlinks and islink(src):
+        try:
+            os.makedirs(os.path.dirname(dst))
+        except OSError:
+            pass
+        if os.path.lexists(dst):
+            os.remove(dst)
+        src_base, dst_base = get_prefix_replacement_paths(src, dst)
+        src_target = os.readlink(src)
+        src_replaced = src_target.replace(src_base, dst_base)
+        os.symlink(src_replaced, dst)
+        try:
+            st = os.lstat(src)
+            mode = stat.S_IMODE(st.st_mode)
+            os.lchmod(dst, mode)
+        except:
+            pass  # lchmod not available
+    elif isdir(src):
+        merge_tree(src, dst, symlinks, timeout=timeout, lock=lock, locking=locking, clobber=clobber)
+
+    else:
+        if isdir(dst):
+            dst_fn = os.path.join(dst, os.path.basename(src))
+        else:
+            dst_fn = dst
+
+        if os.path.isabs(src):
+            src_folder = os.path.dirname(src)
+        else:
+            if os.path.sep in dst_fn:
+                src_folder = os.path.dirname(dst_fn)
+                if not os.path.isdir(src_folder):
+                    os.makedirs(src_folder)
+            else:
+                src_folder = os.getcwd()
+
+        if os.path.islink(src) and not os.path.exists(os.path.realpath(src)):
+            log.warn('path %s is a broken symlink - ignoring copy', src)
+            return
+
+        if not lock and locking:
+            lock = get_lock(src_folder, timeout=timeout)
+        locks = [lock] if locking else []
+        with try_acquire_locks(locks, timeout):
+            # if intermediate folders not not exist create them
+            dst_folder = os.path.dirname(dst)
+            if dst_folder and not os.path.exists(dst_folder):
+                try:
+                    os.makedirs(dst_folder)
+                except OSError:
+                    pass
+            try:
+                _copy_with_shell_fallback(src, dst_fn)
+            except shutil.Error:
+                log.debug("skipping %s - already exists in %s",
+                            os.path.basename(src), dst)
+
+
+# http://stackoverflow.com/a/22331852/1170370
+def copytree(src, dst, symlinks=False, ignore=None, dry_run=False):
+    if not os.path.exists(dst):
+        os.makedirs(dst)
+        shutil.copystat(src, dst)
+    lst = os.listdir(src)
+    if ignore:
+        excl = ignore(src, lst)
+        lst = [x for x in lst if x not in excl]
+
+    # do not copy lock files
+    if '.conda_lock' in lst:
+        lst.remove('.conda_lock')
+
+    dst_lst = [os.path.join(dst, item) for item in lst]
+
+    if not dry_run:
+        for idx, item in enumerate(lst):
+            s = os.path.join(src, item)
+            d = dst_lst[idx]
+            if symlinks and os.path.islink(s):
+                if os.path.lexists(d):
+                    os.remove(d)
+                os.symlink(os.readlink(s), d)
+                try:
+                    st = os.lstat(s)
+                    mode = stat.S_IMODE(st.st_mode)
+                    os.lchmod(d, mode)
+                except:
+                    pass  # lchmod not available
+            elif os.path.isdir(s):
+                copytree(s, d, symlinks, ignore)
+            else:
+                _copy_with_shell_fallback(s, d)
+
+    return dst_lst
+
+
+def merge_tree(src, dst, symlinks=False, timeout=900, lock=None, locking=True, clobber=False):
+    """
+    Merge src into dst recursively by copying all files from src into dst.
+    Return a list of all files copied.
+
+    Like copytree(src, dst), but raises an error if merging the two trees
+    would overwrite any files.
+    """
+    dst = os.path.normpath(os.path.normcase(dst))
+    src = os.path.normpath(os.path.normcase(src))
+    assert not dst.startswith(src), ("Can't merge/copy source into subdirectory of itself.  "
+                                     "Please create separate spaces for these things.\n"
+                                     "  src: {0}\n"
+                                     "  dst: {1}".format(src, dst))
+
+    new_files = copytree(src, dst, symlinks=symlinks, dry_run=True)
+    existing = [f for f in new_files if isfile(f)]
+
+    if existing and not clobber:
+        raise IOError("Can't merge {0} into {1}: file exists: "
+                      "{2}".format(src, dst, existing[0]))
+
+    locks = []
+    if locking:
+        if not lock:
+            lock = get_lock(src, timeout=timeout)
+        locks = [lock]
+    with try_acquire_locks(locks, timeout):
+        copytree(src, dst, symlinks=symlinks)
+
+
+# purpose here is that we want *one* lock per location on disk.  It can be locked or unlocked
+#    at any time, but the lock within this process should all be tied to the same tracking
+#    mechanism.
+_lock_folders = (os.path.join(root_dir, 'locks'),
+                 os.path.expanduser(os.path.join('~', '.conda_build_locks')))
+
+
+def get_lock(folder, timeout=900):
+    fl = None
+    try:
+        location = os.path.abspath(os.path.normpath(folder))
+    except OSError:
+        location = folder
+    b_location = location
+    if hasattr(b_location, 'encode'):
+        b_location = b_location.encode()
+
+    # Hash the entire filename to avoid collisions.
+    lock_filename = hashlib.sha256(b_location).hexdigest()
+
+    if hasattr(lock_filename, 'decode'):
+        lock_filename = lock_filename.decode()
+    for locks_dir in _lock_folders:
+        try:
+            if not os.path.isdir(locks_dir):
+                os.makedirs(locks_dir)
+            lock_file = os.path.join(locks_dir, lock_filename)
+            with open(lock_file, 'w') as f:
+                f.write("")
+            fl = filelock.FileLock(lock_file, timeout)
+            break
+        except (OSError, IOError):
+            continue
+    else:
+        raise RuntimeError("Could not write locks folder to either system location ({0})"
+                           "or user location ({1}).  Aborting.".format(*_lock_folders))
+    return fl
+
+
+def get_conda_operation_locks(locking=True, bldpkgs_dirs=None, timeout=900):
+    locks = []
+    bldpkgs_dirs = ensure_list(bldpkgs_dirs)
+    # locks enabled by default
+    if locking:
+        _pkgs_dirs = pkgs_dirs[:1]
+        locked_folders = _pkgs_dirs + list(bldpkgs_dirs)
+        for folder in locked_folders:
+            if not os.path.isdir(folder):
+                os.makedirs(folder)
+            lock = get_lock(folder, timeout=timeout)
+            locks.append(lock)
+        # lock used to generally indicate a conda operation occurring
+        locks.append(get_lock('conda-operation', timeout=timeout))
+    return locks
+
+
+def relative(f, d='lib'):
+    assert not f.startswith('/'), f
+    assert not d.startswith('/'), d
+    d = d.strip('/').split('/')
+    if d == ['.']:
+        d = []
+    f = dirname(f).split('/')
+    if f == ['']:
+        f = []
+    while d and f and d[0] == f[0]:
+        d.pop(0)
+        f.pop(0)
+    return '/'.join(((['..'] * len(f)) if f else ['.']) + d)
+
+
+# This is the lowest common denominator of the formats supported by our libarchive/python-libarchive-c
+# packages across all platforms
+decompressible_exts = ('.7z', '.tar', '.tar.bz2', '.tar.gz', '.tar.lzma', '.tar.xz',
+                       '.tar.z', '.tgz', '.whl', '.zip', '.rpm', '.deb')
+
+
+def _tar_xf_fallback(tarball, dir_path, mode='r:*'):
+    if tarball.lower().endswith('.tar.z'):
+        uncompress = external.find_executable('uncompress')
+        if not uncompress:
+            uncompress = external.find_executable('gunzip')
+        if not uncompress:
+            sys.exit("""\
+uncompress (or gunzip) is required to unarchive .z source files.
+""")
+        check_call_env([uncompress, '-f', tarball])
+        tarball = tarball[:-2]
+    if not PY3 and tarball.endswith('.tar.xz'):
+        unxz = external.find_executable('unxz')
+        if not unxz:
+            sys.exit("""\
+unxz is required to unarchive .xz source files.
+""")
+
+        check_call_env([unxz, '-f', '-k', tarball])
+        tarball = tarball[:-3]
+    t = tarfile.open(tarball, mode)
+    members = t.getmembers()
+    for i, member in enumerate(members, 0):
+        if os.path.isabs(member.name):
+            member.name = os.path.relpath(member.name, '/')
+        if not os.path.realpath(member.name).startswith(os.getcwd()):
+            member.name = member.name.replace("../", "")
+        if not os.path.realpath(member.name).startswith(os.getcwd()):
+            sys.exit("tarball contains unsafe path: " + member.name)
+        members[i] = member
+
+    if not PY3:
+        t.extractall(path=dir_path.encode(codec))
+    else:
+        t.extractall(path=dir_path)
+    t.close()
+
+
+def tar_xf_file(tarball, entries):
+    from conda_build.utils import ensure_list
+    entries = ensure_list(entries)
+    if not os.path.isabs(tarball):
+        tarball = os.path.join(os.getcwd(), tarball)
+    result = None
+    n_found = 0
+    with libarchive.file_reader(tarball) as archive:
+        for entry in archive:
+            if entry.name in entries:
+                n_found += 1
+                for block in entry.get_blocks():
+                    if result is None:
+                        result = bytes(block)
+                    else:
+                        result += block
+                break
+    if n_found != len(entries):
+        raise KeyError()
+    return result
+
+
+def tar_xf_getnames(tarball):
+    if not os.path.isabs(tarball):
+        tarball = os.path.join(os.getcwd(), tarball)
+    result = []
+    with libarchive.file_reader(tarball) as archive:
+        for entry in archive:
+            result.append(entry.name)
+    return result
+
+
+def tar_xf(tarball, dir_path):
+    flags = libarchive.extract.EXTRACT_TIME | \
+            libarchive.extract.EXTRACT_PERM | \
+            libarchive.extract.EXTRACT_SECURE_NODOTDOT | \
+            libarchive.extract.EXTRACT_SECURE_SYMLINKS | \
+            libarchive.extract.EXTRACT_SECURE_NOABSOLUTEPATHS
+    if not os.path.isabs(tarball):
+        tarball = os.path.join(os.getcwd(), tarball)
+    try:
+        with tmp_chdir(dir_path):
+            libarchive.extract_file(tarball, flags)
+    except libarchive.exception.ArchiveError:
+        # try again, maybe we are on Windows and the archive contains symlinks
+        # https://github.com/conda/conda-build/issues/3351
+        # https://github.com/libarchive/libarchive/pull/1030
+        if tarball.endswith(('.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tar.z', '.tar.xz')):
+            _tar_xf_fallback(tarball, dir_path)
+        else:
+            raise
+
+
+def file_info(path):
+    return {'size': getsize(path),
+            'md5': md5_file(path),
+            'sha256': hashsum_file(path, 'sha256'),
+            'mtime': getmtime(path)}
+
+# Taken from toolz
+
+
+def groupby(key, seq):
+    """ Group a collection by a key function
+    >>> names = ['Alice', 'Bob', 'Charlie', 'Dan', 'Edith', 'Frank']
+    >>> groupby(len, names)  # doctest: +SKIP
+    {3: ['Bob', 'Dan'], 5: ['Alice', 'Edith', 'Frank'], 7: ['Charlie']}
+    >>> iseven = lambda x: x % 2 == 0
+    >>> groupby(iseven, [1, 2, 3, 4, 5, 6, 7, 8])  # doctest: +SKIP
+    {False: [1, 3, 5, 7], True: [2, 4, 6, 8]}
+    Non-callable keys imply grouping on a member.
+    >>> groupby('gender', [{'name': 'Alice', 'gender': 'F'},
+    ...                    {'name': 'Bob', 'gender': 'M'},
+    ...                    {'name': 'Charlie', 'gender': 'M'}]) # doctest:+SKIP
+    {'F': [{'gender': 'F', 'name': 'Alice'}],
+     'M': [{'gender': 'M', 'name': 'Bob'},
+           {'gender': 'M', 'name': 'Charlie'}]}
+    See Also:
+        countby
+    """
+    if not callable(key):
+        key = getter(key)
+    d = defaultdict(lambda: [].append)
+    for item in seq:
+        d[key(item)](item)
+    rv = {}
+    for k, v in iteritems(d):
+        rv[k] = v.__self__
+    return rv
+
+
+def getter(index):
+    if isinstance(index, list):
+        if len(index) == 1:
+            index = index[0]
+            return lambda x: (x[index],)
+        elif index:
+            return operator.itemgetter(*index)
+        else:
+            return lambda x: ()
+    else:
+        return operator.itemgetter(index)
+
+
+def comma_join(items):
+    """
+    Like ', '.join(items) but with and
+
+    Examples:
+
+    >>> comma_join(['a'])
+    'a'
+    >>> comma_join(['a', 'b'])
+    'a and b'
+    >>> comma_join(['a', 'b', 'c])
+    'a, b, and c'
+    """
+    return ' and '.join(items) if len(items) <= 2 else ', '.join(items[:-1]) + ', and ' + items[-1]
+
+
+def safe_print_unicode(*args, **kwargs):
+    """
+    prints unicode strings to stdout using configurable `errors` handler for
+    encoding errors
+
+    :param args: unicode strings to print to stdout
+    :param sep: separator (defaults to ' ')
+    :param end: ending character (defaults to '\n')
+    :param errors: error handler for encoding errors (defaults to 'replace')
+    """
+    sep = kwargs.pop('sep', u' ')
+    end = kwargs.pop('end', u'\n')
+    errors = kwargs.pop('errors', 'replace')
+    if PY3:
+        func = sys.stdout.buffer.write
+    else:
+        func = sys.stdout.write
+    line = sep.join(args) + end
+    encoding = sys.stdout.encoding or 'utf8'
+    func(line.encode(encoding, errors))
+
+
+def rec_glob(path, patterns):
+    result = []
+    for d_f in walk(path):
+        # ignore the .git folder
+        # if '.git' in d_f[0]:
+        #     continue
+        m = []
+        for pattern in patterns:
+            m.extend(fnmatch.filter(d_f[2], pattern))
+        if m:
+            result.extend([os.path.join(d_f[0], f) for f in m])
+    return result
+
+
+def convert_unix_path_to_win(path):
+    if external.find_executable('cygpath'):
+        cmd = "cygpath -w {0}".format(path)
+        if PY3:
+            path = subprocess.getoutput(cmd)
+        else:
+            path = subprocess.check_output(cmd.split()).rstrip().rstrip("\\")
+
+    else:
+        path = unix_path_to_win(path)
+    return path
+
+
+def convert_win_path_to_unix(path):
+    if external.find_executable('cygpath'):
+        cmd = "cygpath -u {0}".format(path)
+        if PY3:
+            path = subprocess.getoutput(cmd)
+        else:
+            path = subprocess.check_output(cmd.split()).rstrip().rstrip("\\")
+
+    else:
+        path = win_path_to_unix(path)
+    return path
+
+
+# Used for translating local paths into url (file://) paths
+#   http://stackoverflow.com/a/14298190/1170370
+def path2url(path):
+    return urlparse.urljoin('file:', urllib.pathname2url(path))
+
+
+def get_stdlib_dir(prefix, py_ver):
+    if sys.platform == 'win32':
+        lib_dir = os.path.join(prefix, 'Lib')
+    else:
+        lib_dir = os.path.join(prefix, 'lib')
+        python_folder = glob(os.path.join(lib_dir, 'python?.*'))
+        if python_folder:
+            lib_dir = os.path.join(lib_dir, python_folder[0])
+        else:
+            lib_dir = os.path.join(lib_dir, 'python{}'.format(py_ver))
+    return lib_dir
+
+
+def get_site_packages(prefix, py_ver):
+    return os.path.join(get_stdlib_dir(prefix, py_ver), 'site-packages')
+
+
+def get_build_folders(croot):
+    # remember, glob is not a regex.
+    return glob(os.path.join(croot, "*" + "[0-9]" * 10 + "*"))
+
+
+def prepend_bin_path(env, prefix, prepend_prefix=False):
+    # bin_dirname takes care of bin on *nix, Scripts on win
+    env['PATH'] = join(prefix, bin_dirname) + os.pathsep + env['PATH']
+    if sys.platform == "win32":
+        env['PATH'] = join(prefix, "Library", "mingw-w64", "bin") + os.pathsep + \
+                      join(prefix, "Library", "usr", "bin") + os.pathsep + os.pathsep + \
+                      join(prefix, "Library", "bin") + os.pathsep + \
+                      join(prefix, "Scripts") + os.pathsep + \
+                      env['PATH']
+        prepend_prefix = True  # windows has Python in the prefix.  Use it.
+    if prepend_prefix:
+        env['PATH'] = prefix + os.pathsep + env['PATH']
+    return env
+
+
+# not currently used.  Leaving in because it may be useful for when we do things
+#   like load setup.py data, and we need the modules from some prefix other than
+#   the root prefix, which is what conda-build runs from.
+@contextlib.contextmanager
+def sys_path_prepended(prefix):
+    path_backup = sys.path[:]
+    if on_win:
+        sys.path.insert(1, os.path.join(prefix, 'lib', 'site-packages'))
+    else:
+        lib_dir = os.path.join(prefix, 'lib')
+        python_dir = glob(os.path.join(lib_dir, 'python[0-9\.]*'))
+        if python_dir:
+            python_dir = python_dir[0]
+            sys.path.insert(1, os.path.join(python_dir, 'site-packages'))
+    try:
+        yield
+    finally:
+        sys.path = path_backup
+
+
+@contextlib.contextmanager
+def path_prepended(prefix):
+    old_path = os.environ['PATH']
+    os.environ['PATH'] = prepend_bin_path(os.environ.copy(), prefix, True)['PATH']
+    try:
+        yield
+    finally:
+        os.environ['PATH'] = old_path
+
+
+bin_dirname = 'Scripts' if sys.platform == 'win32' else 'bin'
+
+entry_pat = re.compile('\s*([\w\-\.]+)\s*=\s*([\w.]+):([\w.]+)\s*$')
+
+
+def iter_entry_points(items):
+    for item in items:
+        m = entry_pat.match(item)
+        if m is None:
+            sys.exit("Error cound not match entry point: %r" % item)
+        yield m.groups()
+
+
+def create_entry_point(path, module, func, config):
+    import_name = func.split('.')[0]
+    pyscript = PY_TMPL % {
+        'module': module, 'func': func, 'import_name': import_name}
+    if on_win:
+        with open(path + '-script.py', 'w') as fo:
+            if os.path.isfile(os.path.join(config.host_prefix, 'python_d.exe')):
+                fo.write('#!python_d\n')
+            fo.write(pyscript)
+            copy_into(join(dirname(__file__), 'cli-{}.exe'.format(str(config.host_arch))),
+                    path + '.exe', config.timeout)
+    else:
+        if os.path.islink(path):
+            os.remove(path)
+        with open(path, 'w') as fo:
+            if not config.noarch:
+                fo.write('#!%s\n' % config.host_python)
+            fo.write(pyscript)
+        os.chmod(path, 0o775)
+
+
+def create_entry_points(items, config):
+    if not items:
+        return
+    bin_dir = join(config.host_prefix, bin_dirname)
+    if not isdir(bin_dir):
+        os.mkdir(bin_dir)
+    for cmd, module, func in iter_entry_points(items):
+        create_entry_point(join(bin_dir, cmd), module, func, config)
+
+
+# Return all files in dir, and all its subdirectories, ending in pattern
+def get_ext_files(start_path, pattern):
+    for root, _, files in walk(start_path):
+        for f in files:
+            if f.endswith(pattern):
+                yield os.path.join(root, f)
+
+
+_posix_exes_cache = {}
+
+
+def convert_path_for_cygwin_or_msys2(exe, path):
+    "If exe is a Cygwin or MSYS2 executable then filters it through `cygpath -u`"
+    if sys.platform != 'win32':
+        return path
+    if exe not in _posix_exes_cache:
+        with open(exe, "rb") as exe_file:
+            exe_binary = exe_file.read()
+            msys2_cygwin = re.findall(b'(cygwin1.dll|msys-2.0.dll)', exe_binary)
+            _posix_exes_cache[exe] = True if msys2_cygwin else False
+    if _posix_exes_cache[exe]:
+        try:
+            path = check_output_env(['cygpath', '-u',
+                                     path]).splitlines()[0].decode(getpreferredencoding())
+        except WindowsError:
+            log = get_logger(__name__)
+            log.debug('cygpath executable not found.  Passing native path.  This is OK for msys2.')
+    return path
+
+
+def get_skip_message(m):
+    return ("Skipped: {} from {} defines build/skip for this configuration ({}).".format(
+        m.name(), m.path,
+        {k: m.config.variant[k] for k in m.get_used_vars()}))
+
+
+@memoized
+def package_has_file(package_path, file_path):
+    try:
+        locks = get_conda_operation_locks()
+        with try_acquire_locks(locks, timeout=900):
+            with tarfile.open(package_path) as t:
+                try:
+                    # internal paths are always forward slashed on all platforms
+                    file_path = file_path.replace('\\', '/')
+                    text = t.extractfile(file_path).read()
+                    return text
+                except KeyError:
+                    return False
+                except OSError as e:
+                    raise RuntimeError("Could not extract %s (%s)" % (package_path, e))
+    except (tarfile.ReadError, IOError, EOFError):
+        raise RuntimeError("Could not extract metadata from %s. File probably corrupt.  "
+                           "Please manually remove this file and try again." % package_path)
+
+
+def ensure_list(arg):
+    if (isinstance(arg, string_types) or not hasattr(arg, '__iter__')):
+        if arg is not None:
+            arg = [arg]
+        else:
+            arg = []
+    return arg
+
+
+@contextlib.contextmanager
+def tmp_chdir(dest):
+    curdir = os.getcwd()
+    try:
+        os.chdir(dest)
+        yield
+    finally:
+        os.chdir(curdir)
+
+
+def expand_globs(path_list, root_dir):
+    files = []
+    for path in path_list:
+        if not os.path.isabs(path):
+            path = os.path.join(root_dir, path)
+        if os.path.isfile(path):
+            files.append(path)
+        elif os.path.islink(path):
+            files.append(path)
+        elif os.path.isdir(path):
+            files.extend(os.path.join(root, f) for root, _, fs in walk(path) for f in fs)
+        else:
+            # File compared to the globs use / as separator indenpendently of the os
+            glob_files = glob(path)
+            if not glob_files:
+                log = get_logger(__name__)
+                log.error('Glob {} did not match in root_dir {}'.format(path, root_dir))
+            files.extend(glob_files)
+    prefix_path_re = re.compile('^' + re.escape('%s%s' % (root_dir, os.path.sep)))
+    files = [prefix_path_re.sub('', f, 1) for f in files]
+    return files
+
+
+def find_recipe(path):
+    """recurse through a folder, locating meta.yaml.  Raises error if more than one is found.
+
+    Returns folder containing meta.yaml, to be built.
+
+    If we have a base level meta.yaml and other supplemental ones, use that first"""
+    if os.path.isfile(path) and os.path.basename(path) in ["meta.yaml", "conda.yaml"]:
+        return os.path.dirname(path)
+    results = rec_glob(path, ["meta.yaml", "conda.yaml"])
+    if len(results) > 1:
+        base_recipe = os.path.join(path, "meta.yaml")
+        if base_recipe in results:
+            get_logger(__name__).warn("Multiple meta.yaml files found. "
+                                      "The meta.yaml file in the base directory "
+                                      "will be used.")
+            results = [base_recipe]
+        else:
+            raise IOError("More than one meta.yaml files found in %s" % path)
+    elif not results:
+        raise IOError("No meta.yaml or conda.yaml files found in %s" % path)
+    return results[0]
+
+
+class LoggingContext(object):
+    default_loggers = ['conda', 'binstar', 'install', 'conda.install', 'fetch', 'conda.instructions',
+                       'fetch.progress', 'print', 'progress', 'dotupdate', 'stdoutlog', 'requests',
+                       'conda.core.package_cache', 'conda.plan', 'conda.gateways.disk.delete',
+                       'conda_build', 'conda_build.index']
+
+    def __init__(self, level=logging.WARN, handler=None, close=True, loggers=None):
+        self.level = level
+        self.old_levels = {}
+        self.handler = handler
+        self.close = close
+        self.quiet = context.quiet
+        if not loggers:
+            self.loggers = LoggingContext.default_loggers
+        else:
+            self.loggers = loggers
+
+    def __enter__(self):
+        for logger in self.loggers:
+            if isinstance(logger, string_types):
+                log = logging.getLogger(logger)
+            self.old_levels[logger] = log.level
+            log.setLevel(self.level if ('install' not in logger or
+                                        self.level < logging.INFO) else self.level + 10)
+        if self.handler:
+            self.logger.addHandler(self.handler)
+
+        context.quiet = True
+
+    def __exit__(self, et, ev, tb):
+        for logger, level in self.old_levels.items():
+            logging.getLogger(logger).setLevel(level)
+        if self.handler:
+            self.logger.removeHandler(self.handler)
+        if self.handler and self.close:
+            self.handler.close()
+
+        context.quiet = self.quiet
+
+        # implicit return of None => don't swallow exceptions
+
+
+def get_installed_packages(path):
+    '''
+    Scan all json files in 'path' and return a dictionary with their contents.
+    Files are assumed to be in 'index.json' format.
+    '''
+    installed = dict()
+    for filename in glob(os.path.join(path, 'conda-meta', '*.json')):
+        with open(filename) as file:
+            data = json.load(file)
+            installed[data['name']] = data
+    return installed
+
+
+def _convert_lists_to_sets(_dict):
+    for k, v in _dict.items():
+        if hasattr(v, 'keys'):
+            _dict[k] = HashableDict(_convert_lists_to_sets(v))
+        elif hasattr(v, '__iter__') and not isinstance(v, string_types):
+            try:
+                _dict[k] = sorted(list(set(v)))
+            except TypeError:
+                _dict[k] = sorted(list(set(tuple(_) for _ in v)))
+    return _dict
+
+
+class HashableDict(dict):
+    """use hashable frozen dictionaries for resources and resource types so that they can be in sets
+    """
+    def __init__(self, *args, **kwargs):
+        super(HashableDict, self).__init__(*args, **kwargs)
+        self = _convert_lists_to_sets(self)
+
+    def __hash__(self):
+        return hash(json.dumps(self, sort_keys=True))
+
+
+def represent_hashabledict(dumper, data):
+    value = []
+
+    for item_key, item_value in data.items():
+        node_key = dumper.represent_data(item_key)
+        node_value = dumper.represent_data(item_value)
+
+        value.append((node_key, node_value))
+
+    return yaml.nodes.MappingNode(u'tag:yaml.org,2002:map', value)
+
+
+yaml.add_representer(HashableDict, represent_hashabledict)
+
+
+# http://stackoverflow.com/a/10743550/1170370
+@contextlib.contextmanager
+def capture():
+    import sys
+    oldout, olderr = sys.stdout, sys.stderr
+    try:
+        out = [StringIO(), StringIO()]
+        sys.stdout, sys.stderr = out
+        yield out
+    finally:
+        sys.stdout, sys.stderr = oldout, olderr
+        out[0] = out[0].getvalue()
+        out[1] = out[1].getvalue()
+
+
+# copied from conda; added in 4.3, not currently part of exported functionality
+@contextlib.contextmanager
+def env_var(name, value, callback=None):
+    # NOTE: will likely want to call reset_context() when using this function, so pass
+    #       it as callback
+    name, value = str(name), str(value)
+    saved_env_var = os.environ.get(name)
+    try:
+        os.environ[name] = value
+        if callback:
+            callback()
+        yield
+    finally:
+        if saved_env_var:
+            os.environ[name] = saved_env_var
+        else:
+            del os.environ[name]
+        if callback:
+            callback()
+
+
+def trim_empty_keys(dict_):
+    to_remove = set()
+    negative_means_empty = ('final', 'noarch_python', 'zip_keys')
+    for k, v in dict_.items():
+        if hasattr(v, 'keys'):
+            trim_empty_keys(v)
+        # empty lists and empty strings, and None are always empty.
+        if v == list() or v == '' or v is None or v == dict():
+            to_remove.add(k)
+        # other things that evaluate as False may not be "empty" - things can be manually set to
+        #     false, and we need to keep that setting.
+        if not v and k in negative_means_empty:
+            to_remove.add(k)
+    if 'zip_keys' in dict_ and not any(v for v in dict_['zip_keys']):
+        to_remove.add('zip_keys')
+    for k in to_remove:
+        del dict_[k]
+
+
+def _increment(version, alpha_ver):
+    try:
+        if alpha_ver:
+            suffix = 'a'
+        else:
+            suffix = '.0a0'
+        last_version = str(int(version) + 1) + suffix
+    except ValueError:
+        last_version = chr(ord(version) + 1)
+    return last_version
+
+
+def apply_pin_expressions(version, min_pin='x.x.x.x.x.x.x', max_pin='x'):
+    pins = [len(p.split('.')) if p else None for p in (min_pin, max_pin)]
+    parsed_version = VersionOrder(version).version[1:]
+    nesting_position = None
+    flat_list = []
+    for idx, item in enumerate(parsed_version):
+        if isinstance(item, list):
+            nesting_position = idx
+            flat_list.extend(item)
+        else:
+            flat_list.append(item)
+    if max_pin and len(max_pin.split('.')) > len(flat_list):
+        pins[1] = len(flat_list)
+    versions = ['', '']
+    # first idx is lower bound pin; second is upper bound pin.
+    #    pin value is number of places to pin.
+    for p_idx, pin in enumerate(pins):
+        if pin:
+            # flat_list is the blown-out representation of the version
+            for v_idx, v in enumerate(flat_list[:pin]):
+                # upper bound pin
+                if p_idx == 1 and v_idx == pin - 1:
+                    # is the last place an alphabetic character?  OpenSSL, JPEG
+                    alpha_ver = str(flat_list[min(pin, len(flat_list) - 1)]).isalpha()
+                    v = _increment(v, alpha_ver)
+                versions[p_idx] += str(v)
+                if v_idx != nesting_position:
+                    versions[p_idx] += '.'
+            if versions[p_idx][-1] == '.':
+                versions[p_idx] = versions[p_idx][:-1]
+    if versions[0]:
+        versions[0] = '>=' + versions[0]
+    if versions[1]:
+        versions[1] = '<' + versions[1]
+    return ','.join([v for v in versions if v])
+
+
+def filter_files(files_list, prefix, filter_patterns=('(.*[\\\\/])?\.git[\\\\/].*',
+                                                      '(.*[\\\\/])?\.git$',
+                                                      '(.*)?\.DS_Store.*',
+                                                      '.*\.la$',
+                                                      'conda-meta.*',
+                                                      '.*\.conda_trash(?:_\d+)*$')):
+    """Remove things like the .git directory from the list of files to be copied"""
+    for pattern in filter_patterns:
+        r = re.compile(pattern)
+        files_list = set(files_list) - set(filter(r.match, files_list))
+    return [f for f in files_list
+            if not os.path.isdir(os.path.join(prefix, f)) or
+            os.path.islink(os.path.join(prefix, f))]
+
+
+def filter_info_files(files_list, prefix):
+    return filter_files(files_list, prefix, filter_patterns=(
+                    'info[\\\\/]index.json',
+                    'info[\\\\/]files',
+                    'info[\\\\/]paths.json',
+                    'info[\\\\/]about.json',
+                    'info[\\\\/]has_prefix',
+                    'info[\\\\/]hash_input_files',   # legacy, not used anymore
+                    'info[\\\\/]hash_input.json',
+                    'info[\\\\/]run_exports.yaml',   # legacy
+                    'info[\\\\/]run_exports.json',   # current
+                    'info[\\\\/]git',
+                    'info[\\\\/]recipe[\\\\/].*',
+                    'info[\\\\/]recipe_log.json',
+                    'info[\\\\/]recipe.tar',
+                    'info[\\\\/]test[\\\\/].*',
+                    'info[\\\\/]LICENSE.txt',
+                    'info[\\\\/]requires',
+                    'info[\\\\/]meta',
+                    'info[\\\\/]platform',
+                    'info[\\\\/]no_link',
+                    'info[\\\\/]link.json',
+                    'info[\\\\/]icon.png',
+            ))
+
+
+def rm_rf(path, config=None):
+    if conda_46:
+        return _rm_rf(path)
+    if os.path.isdir(path):
+        try:
+            # subprocessing to delete large folders can be quite a bit faster
+            if on_win:
+                subprocess.check_call('rd /s /q {}'.format(path), shell=True)
+            else:
+                try:
+                    os.makedirs('.empty')
+                except:
+                    pass
+                del_dir_cmd = 'rsync -a --delete .empty {}/'
+                subprocess.check_call(del_dir_cmd.format(path).split())
+                try:
+                    shutil.rmtree('.empty')
+                except:
+                    pass
+        # we don't really care about errors that much. People can and should
+        #     clean out their folders once in a while with "purge"
+        except:
+            pass
+
+    conda_log_level = logging.WARN
+    if config and config.debug:
+        conda_log_level = logging.DEBUG
+    with LoggingContext(conda_log_level):
+        # this clears out the path from conda's cache, which otherwise thinks
+        #    that things are still installed here
+        _rm_rf(path)
+
+
+# https://stackoverflow.com/a/31459386/1170370
+class LessThanFilter(logging.Filter):
+    def __init__(self, exclusive_maximum, name=""):
+        super(LessThanFilter, self).__init__(name)
+        self.max_level = exclusive_maximum
+
+    def filter(self, record):
+        # non-zero return means we log this message
+        return 1 if record.levelno < self.max_level else 0
+
+
+class GreaterThanFilter(logging.Filter):
+    def __init__(self, exclusive_minimum, name=""):
+        super(GreaterThanFilter, self).__init__(name)
+        self.min_level = exclusive_minimum
+
+    def filter(self, record):
+        # non-zero return means we log this message
+        return 1 if record.levelno > self.min_level else 0
+
+
+# unclutter logs - show messages only once
+class DuplicateFilter(logging.Filter):
+    def __init__(self):
+        self.msgs = set()
+
+    def filter(self, record):
+        log = record.msg not in self.msgs
+        self.msgs.add(record.msg)
+        return int(log)
+
+
+dedupe_filter = DuplicateFilter()
+info_debug_stdout_filter = LessThanFilter(logging.WARNING)
+warning_error_stderr_filter = GreaterThanFilter(logging.INFO)
+
+# set filelock's logger to only show warnings by default
+logging.getLogger('filelock').setLevel(logging.WARN)
+
+# quiet some of conda's less useful output
+logging.getLogger('conda.core.linked_data').setLevel(logging.WARN)
+logging.getLogger('conda.gateways.disk.delete').setLevel(logging.WARN)
+logging.getLogger('conda.gateways.disk.test').setLevel(logging.WARN)
+
+
+def reset_deduplicator():
+    """Most of the time, we want the deduplication.  There are some cases (tests especially)
+    where we want to be able to control the duplication."""
+    global dedupe_filter
+    dedupe_filter = DuplicateFilter()
+
+
+def get_logger(name, level=logging.INFO, dedupe=True, add_stdout_stderr_handlers=True):
+    config_file = None
+    if cc_conda_build.get('log_config_file'):
+        config_file = abspath(expanduser(expandvars(cc_conda_build.get('log_config_file'))))
+    # by loading config file here, and then only adding handlers later, people
+    # should be able to override conda-build's logger settings here.
+    if config_file:
+        with open(config_file) as f:
+            config_dict = yaml.safe_load(f)
+        logging.config.dictConfig(config_dict)
+        level = config_dict.get('loggers', {}).get(name, {}).get('level', level)
+    log = logging.getLogger(name)
+    log.setLevel(level)
+    if dedupe:
+        log.addFilter(dedupe_filter)
+
+    # these are defaults.  They can be overridden by configuring a log config yaml file.
+    if not log.handlers and add_stdout_stderr_handlers:
+        stdout_handler = logging.StreamHandler(sys.stdout)
+        stderr_handler = logging.StreamHandler(sys.stderr)
+        stdout_handler.addFilter(info_debug_stdout_filter)
+        stderr_handler.addFilter(warning_error_stderr_filter)
+        stdout_handler.setLevel(level)
+        stderr_handler.setLevel(level)
+        log.addHandler(stdout_handler)
+        log.addHandler(stderr_handler)
+    return log
+
+
+def _equivalent(base_value, value, path):
+    equivalent = value == base_value
+    if isinstance(value, string_types) and isinstance(base_value, string_types):
+        if not os.path.isabs(base_value):
+            base_value = os.path.abspath(os.path.normpath(os.path.join(path, base_value)))
+        if not os.path.isabs(value):
+            value = os.path.abspath(os.path.normpath(os.path.join(path, value)))
+        equivalent |= base_value == value
+    return equivalent
+
+
+def merge_or_update_dict(base, new, path="", merge=True, raise_on_clobber=False, add_missing_keys=True):
+    if base == new:
+        return base
+    log = get_logger(__name__)
+    for key, value in new.items():
+        if key in base or add_missing_keys:
+            base_value = base.get(key, value)
+            if hasattr(value, 'keys'):
+                base_value = merge_or_update_dict(base_value, value, path, merge,
+                                                raise_on_clobber=raise_on_clobber)
+                base[key] = base_value
+            elif hasattr(value, '__iter__') and not isinstance(value, string_types):
+                if merge:
+                    if base_value != value:
+                        try:
+                            base_value.extend(value)
+                        except TypeError:
+                            base_value = value
+                    try:
+                        base[key] = list(base_value)
+                    except TypeError:
+                        base[key] = base_value
+                else:
+                    base[key] = value
+            else:
+                if (base_value and merge and not _equivalent(base_value, value, path) and
+                        raise_on_clobber):
+                    log.debug('clobbering key {} (original value {}) with value {}'.format(key,
+                                                                                base_value, value))
+                if value is None and key in base:
+                    del base[key]
+                else:
+                    base[key] = value
+    return base
+
+
+def merge_dicts_of_lists(dol1, dol2):
+    '''
+    From Alex Martelli: https://stackoverflow.com/a/1495821/3257826
+    '''
+    keys = set(dol1).union(dol2)
+    no = []
+    return dict((k, dol1.get(k, no) + dol2.get(k, no)) for k in keys)
+
+
+def prefix_files(prefix):
+    '''
+    Returns a set of all files in prefix.
+    '''
+    res = set()
+    for root, dirs, files in walk(prefix):
+        for fn in files:
+            res.add(join(root, fn)[len(prefix) + 1:])
+        for dn in dirs:
+            path = join(root, dn)
+            if islink(path):
+                res.add(path[len(prefix) + 1:])
+    res = set(expand_globs(res, prefix))
+    return res
+
+
+def mmap_mmap(fileno, length, tagname=None, flags=0, prot=mmap_PROT_READ | mmap_PROT_WRITE,
+              access=None, offset=0):
+    '''
+    Hides the differences between mmap.mmap on Windows and Unix.
+    Windows has `tagname`.
+    Unix does not, but makes up for it with `flags` and `prot`.
+    On both, the default value for `access` is determined from how the file
+    was opened so must not be passed in at all to get this default behaviour.
+    '''
+    if on_win:
+        if access:
+            return mmap.mmap(fileno, length, tagname=tagname, access=access, offset=offset)
+        else:
+            return mmap.mmap(fileno, length, tagname=tagname)
+    else:
+        if access:
+            return mmap.mmap(fileno, length, flags=flags, prot=prot, access=access, offset=offset)
+        else:
+            return mmap.mmap(fileno, length, flags=flags, prot=prot)
+
+
+def remove_pycache_from_scripts(build_prefix):
+    """Remove pip created pycache directory from bin or Scripts."""
+    if on_win:
+        scripts_path = os.path.join(build_prefix, 'Scripts')
+    else:
+        scripts_path = os.path.join(build_prefix, 'bin')
+
+    if os.path.isdir(scripts_path):
+        for entry in os.listdir(scripts_path):
+            entry_path = os.path.join(scripts_path, entry)
+            if os.path.isdir(entry_path) and entry.strip(os.sep) == '__pycache__':
+                shutil.rmtree(entry_path)
+
+            elif os.path.isfile(entry_path) and entry_path.endswith('.pyc'):
+                os.remove(entry_path)
+
+
+def sort_list_in_nested_structure(dictionary, omissions=''):
+    """Recurse through a nested dictionary and sort any lists that are found.
+
+    If the list that is found contains anything but strings, it is skipped
+    as we can't compare lists containing different types. The omissions argument
+    allows for certain sections of the dictionary to be omitted from sorting.
+    """
+    for field, value in dictionary.items():
+        if isinstance(value, dict):
+            for key in value.keys():
+                section = dictionary[field][key]
+                if isinstance(section, dict):
+                    sort_list_in_nested_structure(section)
+                elif (isinstance(section, list) and
+                    '{}/{}' .format(field, key) not in omissions and
+                        all(isinstance(item, str) for item in section)):
+                    section.sort()
+
+        # there's a possibility for nested lists containing dictionaries
+        # in this case we recurse until we find a list to sort
+        elif isinstance(value, list):
+            for element in value:
+                if isinstance(element, dict):
+                    sort_list_in_nested_structure(element)
+            try:
+                value.sort()
+            except TypeError:
+                pass
+
+
+# group one: package name
+# group two: version (allows _, +, . in version)
+# group three: build string - mostly not used here.  Match primarily matters
+#        to specify when not to add .*
+
+# if you are seeing mysterious unsatisfiable errors, with the package you're building being the
+#    unsatisfiable part, then you probably need to update this regex.
+
+spec_needing_star_re = re.compile(r"([\w\d\.\-\_]+)\s+((?<![><=])[\w\d\.\-\_]+?(?!\*))(\s+[\w\d\.\_]+)?$")  # NOQA
+spec_ver_needing_star_re = re.compile("^([0-9a-zA-Z\.]+)$")
+
+
+def ensure_valid_spec(spec, warn=False):
+    if isinstance(spec, MatchSpec):
+        if (hasattr(spec, 'version') and spec.version and (not spec.get('build', '')) and
+                spec_ver_needing_star_re.match(str(spec.version))):
+            if str(spec.name) not in ('python', 'numpy') or str(spec.version) != 'x.x':
+                spec = MatchSpec("{} {}".format(str(spec.name), str(spec.version) + '.*'))
+    else:
+        match = spec_needing_star_re.match(spec)
+        # ignore exact pins (would be a 3rd group)
+        if match and not match.group(3):
+            if match.group(1) in ('python', 'numpy') and match.group(2) == 'x.x':
+                spec = spec_needing_star_re.sub(r"\1 \2", spec)
+            else:
+                if "*" not in spec:
+                    if match.group(1) not in ('python', 'vc') and warn:
+                        log = get_logger(__name__)
+                        log.warn("Adding .* to spec '{}' to ensure satisfiability.  Please "
+                                 "consider putting {{{{ var_name }}}}.* or some relational "
+                                 "operator (>/</>=/<=) on this spec in meta.yaml, or if req is "
+                                 "also a build req, using {{{{ pin_compatible() }}}} jinja2 "
+                                 "function instead.  See "
+                "https://conda.io/docs/user-guide/tasks/build-packages/variants.html#pinning-at-the-variant-level"  # NOQA
+                        .format(spec))
+                    spec = spec_needing_star_re.sub(r"\1 \2.*", spec)
+    return spec
+
+
+def insert_variant_versions(requirements_dict, variant, env):
+    build_deps = (ensure_list(requirements_dict.get('build')) +
+                  ensure_list(requirements_dict.get('host')))
+    reqs = ensure_list(requirements_dict.get(env))
+    for key, val in variant.items():
+        regex = re.compile(r'^(%s)(?:\s*$)' % key.replace('_', '[-_]'))
+        matches = [regex.match(pkg) for pkg in reqs]
+        if any(matches):
+            for i, x in enumerate(matches):
+                if x and (env in ('build', 'host') or x.group(1) in build_deps):
+                    del reqs[i]
+                    if not isinstance(val, string_types):
+                        val = val[0]
+                    reqs.insert(i, ensure_valid_spec(' '.join((x.group(1), val))))
+
+    xx_re = re.compile("([0-9a-zA-Z\.\-\_]+)\s+x\.x")
+
+    matches = [xx_re.match(pkg) for pkg in reqs]
+    if any(matches):
+        for i, x in enumerate(matches):
+            if x:
+                del reqs[i]
+                reqs.insert(i, ensure_valid_spec(' '.join((x.group(1), variant.get(x.group(1))))))
+    if reqs:
+        requirements_dict[env] = reqs
+
+
+def match_peer_job(target_matchspec, other_m, this_m=None):
+    """target_matchspec comes from the recipe.  target_variant is the variant from the recipe whose
+    deps we are matching.  m is the peer job, which must satisfy conda and also have matching keys
+    for any keys that are shared between target_variant and m.config.variant"""
+    match_dict = {'name': other_m.name(),
+                'version': other_m.version(),
+                'build': '', }
+    if conda_43:
+        match_dict = Dist(name=match_dict['name'],
+                          dist_name='-'.join((match_dict['name'],
+                                              match_dict['version'],
+                                              match_dict['build'])),
+                          version=match_dict['version'],
+                          build_string=match_dict['build'],
+                          build_number=int(other_m.build_number() or 0),
+                          channel=None)
+    matchspec_matches = target_matchspec.match(match_dict)
+
+    variant_matches = True
+    if this_m:
+        other_m_used_vars = other_m.get_used_loop_vars()
+        for v in this_m.get_used_loop_vars():
+            if v in other_m_used_vars:
+                variant_matches &= this_m.config.variant[v] == other_m.config.variant[v]
+    return matchspec_matches and variant_matches
+
+
+def expand_reqs(reqs_entry):
+    if not hasattr(reqs_entry, 'keys'):
+        original = ensure_list(reqs_entry)[:]
+        reqs_entry = {'host': ensure_list(original),
+                      'run': ensure_list(original)} if original else {}
+    else:
+        for sec in reqs_entry:
+            reqs_entry[sec] = ensure_list(reqs_entry[sec])
+    return reqs_entry
+
+
+def sha256_checksum(filename, buffersize=65536):
+    if not isfile(filename):
+        return None
+    sha256 = hashlib.sha256()
+    with open(filename, 'rb') as f:
+        for block in iter(lambda: f.read(buffersize), b''):
+            sha256.update(block)
+    return sha256.hexdigest()


Test patch: