Repository: conda/conda-package-handling (language: python)

Issue:
`cph transmute --force` does not overwrite file
### Checklist

- [X] I added a descriptive title
- [X] I searched open reports and couldn't find a duplicate

### What happened?

cph transmute --force fails if the destination file exists

### Conda Info

_No response_

### Conda Config

_No response_

### Conda list

_No response_

### Additional Context

_No response_


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml
new file mode 100644
index 00000000..f62fb07a
--- /dev/null
+++ b/.github/workflows/sphinx.yml
@@ -0,0 +1,52 @@
+name: Sphinx
+on:
+  push:
+    branches:
+      - main
+
+  pull_request:
+    branches:
+      - main
+
+jobs:
+  sphinx:
+
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.x"
+          architecture: "x64"
+          cache: "pip"
+      - name: Build Documentation
+        run: |
+          pip install -e .[docs]
+          make html
+      - name: Upload artifact
+        uses: actions/upload-pages-artifact@v1
+        with:
+          # Upload entire repository
+          path: 'build/html'
+
+  pages:
+
+    runs-on: ubuntu-latest
+    if: github.ref == 'refs/heads/main'
+    needs: [sphinx]
+
+    # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+    permissions:
+      contents: read
+      pages: write
+      id-token: write
+
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+
+    steps:
+      - name: Deploy to GitHub Pages
+        id: deployment
+        uses: actions/deploy-pages@v1
diff --git a/.gitignore b/.gitignore
index 09d4ef9d..16c543a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,3 +108,6 @@ venv.bak/
 
 junit.xml
 rever/
+
+# ignore .vscode
+.vscode
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..78d4f92a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = docs
+BUILDDIR      = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+style:
+	isort --profile=black .
+	black .
+
+usage:
+	python update_usage.py && echo "updated README.md" || echo "unchanged README.md"
\ No newline at end of file
diff --git a/README.md b/README.md
index b0e9e7fb..9fee6f41 100644
--- a/README.md
+++ b/README.md
@@ -2,30 +2,31 @@
 Create and extract conda packages of various formats
 
 ```
-usage: cph [-h] {extract,x,create,c,transmute,t} ...
+usage: cph [-h] [-V] {extract,x,create,c,transmute,t} ...
 
-optional arguments:
+options:
   -h, --help            show this help message and exit
+  -V, --version         Show the conda-package-handling version number and
+                        exit.
 
 subcommands:
   {extract,x,create,c,transmute,t}
     extract (x)         extract package contents
     create (c)          bundle files into a package
     transmute (t)       convert from one package type to another
+
 ```
 
 cph is an abstraction of conda package handling and a tool for extracting,
 creating, and converting between formats.
 
-At the time of writing, the standard conda package format is a .tar.bz2 file.
-That will need to be maintained for quite a long time, thanks to the long tail
-of people using old conda versions. There is a new conda format, described at
-https://docs.google.com/document/d/1HGKsbg_j69rKXPihhpCb1kNQSE8Iy3yOsUU2x68x8uw/edit?usp=sharing.
-This new format is designed to have much faster metadata access and utilize more
-modern compression algorithms, while also facilitating package signing without
-adding sidecar files.
+There are two conda formats. The new conda format, described at [.conda file
+format](https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/packages.html?highlight=format#conda-file-format),
+consists of an outer, uncompressed ZIP-format container, with 2 inner compressed
+.tar files. It is designed to have much faster metadata access and utilize more
+modern compression algorithms. The old conda format is a `.tar.bz2` archive.
 
-For example, one can transmute (convert) the old package format to the new one with cph:
+One can transmute (convert) the old package format to the new one with cph:
 
 ```
 cph transmute mkl-2018.0.3-1.tar.bz2 .conda
@@ -40,11 +41,10 @@ $ time cph extract mkl-2018.0.3-1.conda --dest mkl-b
 cph extract mkl-2018.0.3-1.conda --dest mkl-b  1.41s user 0.65s system 87% cpu 2.365 total
 ```
 
-The new package format is an indexed, uncompressed zip file that contains
-tarfiles that can use any compression supported by libarchive. The info metadata
-about packages is separated into its own tarfile from the rest of the package
-contents. By doing this, we can extract only the metadata, for speeding up
-operations like indexing.
+The new package format is an indexed, uncompressed zip file that contains two
+Zstandard compressed tarfiles. The info metadata about packages is separated
+into its own tarfile from the rest of the package contents. By doing this, we
+can extract only the metadata, for speeding up operations like indexing.
 
 ```
 $ time cph extract mkl-2018.0.3-1.conda --dest mkl-b --info
@@ -66,23 +66,33 @@ a standard conda package needs. The .conda file it creates, however, uses all
 the nice new compression formats, though, and you could use cph on some other
 computer to extract it.
 
+## Development
+
+Install this package and its test dependencies; run tests.
+
+```
+pip install -e ".[test]"
+pytest
+```
+
 ## Releasing
 
- Conda-package-handling releases may be performed via the `rever command <https://regro.github.io/rever-docs/>`_.
-Rever is configured to perform the activities for a typical conda-build release.
-To cut a release, simply run ``rever <X.Y.Z>`` where ``<X.Y.Z>`` is the
-release number that you want bump to. For example, ``rever 1.2.3``.  However,
-it is always good idea to make sure that the you have permissions everywhere
-to actually perform the release.  So it is customary to run ``rever check`` before
-the release, just to make sure.  The standard workflow is thus::
+Conda-package-handling releases may be performed via the `rever command
+<https://regro.github.io/rever-docs/>`_. Rever is configured to perform the
+activities for a typical conda-build release. To cut a release, simply run
+``rever <X.Y.Z>`` where ``<X.Y.Z>`` is the release number that you want bump to.
+For example, ``rever 1.2.3``.  However, it is always good idea to make sure that
+the you have permissions everywhere to actually perform the release.  So it is
+customary to run ``rever check`` before the release, just to make sure.  The
+standard workflow is thus::
 
     rever check
     rever 1.2.3
 
- If for some reason a release fails partway through, or you want to claw back a
-release that you have made, rever allows you to undo activities. If you find yourself
-in this pickle, you can pass the ``--undo`` option a comma-separated list of
-activities you'd like to undo.  For example::
+If for some reason a release fails partway through, or you want to claw back a
+release that you have made, rever allows you to undo activities. If you find
+yourself in this pickle, you can pass the ``--undo`` option a comma-separated
+list of activities you'd like to undo.  For example::
 
      rever --undo tag,changelog,authors 1.2.3
 
diff --git a/conda.recipe/libarchive-patches/0001-Add-lib-to-CMAKE_FIND_LIBRARY_PREFIXES-for-lzma.patch b/conda.recipe/libarchive-patches/0001-Add-lib-to-CMAKE_FIND_LIBRARY_PREFIXES-for-lzma.patch
deleted file mode 100644
index 8dd5f54c..00000000
--- a/conda.recipe/libarchive-patches/0001-Add-lib-to-CMAKE_FIND_LIBRARY_PREFIXES-for-lzma.patch
+++ /dev/null
@@ -1,13 +0,0 @@
---- work/CMakeLists.txt.orig	2017-12-15 16:52:54.709341000 -0600
-+++ work/CMakeLists.txt	2017-12-15 16:54:40.943948000 -0600
-@@ -454,7 +454,10 @@
- # Find LZMA
- #
- IF(ENABLE_LZMA)
-+  SET(OLD_CMAKE_FIND_LIBRARY_PREFIXES "${CMAKE_FIND_LIBRARY_PREFIXES}")
-+  SET(CMAKE_FIND_LIBRARY_PREFIXES ";lib")
-   FIND_PACKAGE(LibLZMA)
-+  SET(CMAKE_FIND_LIBRARY_PREFIXES "${OLD_CMAKE_FIND_LIBRARY_PREFIXES}")
- ELSE()
-   SET(LIBZMA_FOUND FALSE) # Override cached value
- ENDIF()
diff --git a/conda.recipe/libarchive-patches/0002-xar-fix-bad-seek-in-move_reading_point.patch b/conda.recipe/libarchive-patches/0002-xar-fix-bad-seek-in-move_reading_point.patch
deleted file mode 100644
index a16fc009..00000000
--- a/conda.recipe/libarchive-patches/0002-xar-fix-bad-seek-in-move_reading_point.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-From 5a837b80fe94cec2aeee77751a9b14386167c509 Mon Sep 17 00:00:00 2001
-From: Grzegorz Antoniak <ga@anadoxin.org>
-Date: Thu, 15 Feb 2018 18:15:32 +0100
-Subject: [PATCH] xar: fix bad seek in move_reading_point()
-
----
- libarchive/archive_read_support_format_xar.c | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/libarchive/archive_read_support_format_xar.c b/libarchive/archive_read_support_format_xar.c
-index 602fc7722..4c3d50a10 100644
---- a/libarchive/archive_read_support_format_xar.c
-+++ b/libarchive/archive_read_support_format_xar.c
-@@ -967,7 +967,7 @@ move_reading_point(struct archive_read *a, uint64_t offset)
- 				return ((int)step);
- 			xar->offset += step;
- 		} else {
--			int64_t pos = __archive_read_seek(a, offset, SEEK_SET);
-+			int64_t pos = __archive_read_seek(a, xar->h_base + offset, SEEK_SET);
- 			if (pos == ARCHIVE_FAILED) {
- 				archive_set_error(&(a->archive),
- 				    ARCHIVE_ERRNO_MISC,
diff --git a/conda.recipe/libarchive-patches/0004-VC9-compatibility-define-BCRYPT_SUCCESS.patch b/conda.recipe/libarchive-patches/0004-VC9-compatibility-define-BCRYPT_SUCCESS.patch
deleted file mode 100644
index 89d9964b..00000000
--- a/conda.recipe/libarchive-patches/0004-VC9-compatibility-define-BCRYPT_SUCCESS.patch
+++ /dev/null
@@ -1,44 +0,0 @@
---- work.orig/libarchive/archive_cryptor_private.h	2018-04-22 06:33:58.205352500 -0500
-+++ work/libarchive/archive_cryptor_private.h	2018-04-22 07:01:39.353981900 -0500
-@@ -64,12 +64,7 @@
- } archive_crypto_ctx;
- 
- #elif defined(_WIN32) && !defined(__CYGWIN__) && defined(HAVE_BCRYPT_H)
--#include <bcrypt.h>
--
--/* Common in other bcrypt implementations, but missing from VS2008. */
--#ifndef BCRYPT_SUCCESS
--#define BCRYPT_SUCCESS(r) ((NTSTATUS)(r) == STATUS_SUCCESS)
--#endif
-+#include "bcrypt_with_vs2008.h"
- 
- #define AES_MAX_KEY_SIZE 32
- #define AES_BLOCK_SIZE 16
---- work.orig/libarchive/archive_hmac_private.h	2018-04-22 06:33:58.565158400 -0500
-+++ work/libarchive/archive_hmac_private.h	2018-04-22 07:01:16.915110000 -0500
-@@ -64,7 +64,7 @@
- } archive_crypto_ctx;
- 
- #elif defined(_WIN32) && !defined(__CYGWIN__) && defined(HAVE_BCRYPT_H)
--#include <bcrypt.h>
-+#include "bcrypt_with_vs2008.h"
- 
- /* Common in other bcrypt implementations, but missing from VS2008. */
- #ifndef BCRYPT_SUCCESS
---- work.orig/libarchive/bcrypt_with_vs2008.h	2018-04-22 07:33:12.000000000 -0500
-+++ work/libarchive/bcrypt_with_vs2008.h	2018-04-22 07:01:13.212019900 -0500
-@@ -0,0 +1,14 @@
-+#if !defined(BCRYPT_WITH_VS2008)
-+#define BCRYPT_WITH_VS2008
-+#include <bcrypt.h>
-+
-+/* Common in other bcrypt implementations, but missing from VS2008. */
-+#ifndef BCRYPT_SUCCESS
-+#ifndef NT_SUCCESS
-+#define NT_SUCCESS(x) ((x)>=0)
-+#define STATUS_SUCCESS ((NTSTATUS)0)
-+#endif
-+#define BCRYPT_SUCCESS(r) ((NTSTATUS)(r) == STATUS_SUCCESS)
-+#endif
-+
-+#endif /* BCRYPT_WITH_VS2008 */
diff --git a/conda.recipe/libarchive-patches/0005-VC9-compatibility-ignore-some-warnings.patch b/conda.recipe/libarchive-patches/0005-VC9-compatibility-ignore-some-warnings.patch
deleted file mode 100644
index ab51378f..00000000
--- a/conda.recipe/libarchive-patches/0005-VC9-compatibility-ignore-some-warnings.patch
+++ /dev/null
@@ -1,18 +0,0 @@
---- work.orig/libarchive/archive_private.h	2018-04-22 06:33:58.393242100 -0500
-+++ work/libarchive/archive_private.h	2018-04-22 07:13:01.088919200 -0500
-@@ -46,6 +46,15 @@
- #define	__LA_DEAD
- #endif
- 
-+#if defined(_WIN32) && (_MSC_VER<1900)
-+// Disable conversion from size_t to int possible loss of data
-+#pragma warning(disable:4267)
-+#pragma warning(disable:4244)
-+// Disable: "The POSIX name for this item is deprecated."
-+#pragma warning(disable:4996)
-+#endif
-+
-+
- #define	ARCHIVE_WRITE_MAGIC	(0xb0c5c0deU)
- #define	ARCHIVE_READ_MAGIC	(0xdeb0c5U)
- #define	ARCHIVE_WRITE_DISK_MAGIC (0xc001b0c5U)
diff --git a/conda.recipe/libarchive-patches/0006-VC-openssl-define-inline-as-__inline.patch b/conda.recipe/libarchive-patches/0006-VC-openssl-define-inline-as-__inline.patch
deleted file mode 100644
index 7b863b5d..00000000
--- a/conda.recipe/libarchive-patches/0006-VC-openssl-define-inline-as-__inline.patch
+++ /dev/null
@@ -1,41 +0,0 @@
---- work.orig/libarchive/archive_openssl_evp_private.h	2017-07-09 21:38:04.000000000 -0500
-+++ work/libarchive/archive_openssl_evp_private.h	2018-04-22 08:25:29.735929400 -0500
-@@ -31,6 +31,11 @@
- #if OPENSSL_VERSION_NUMBER < 0x10100000L
- #include <stdlib.h> /* malloc, free */
- #include <string.h> /* memset */
-+
-+#if defined(_MSC_VER) && (_MSC_VER < 1900)
-+#define inline __inline
-+#endif
-+
- static inline EVP_MD_CTX *EVP_MD_CTX_new(void)
- {
- 	EVP_MD_CTX *ctx = (EVP_MD_CTX *)calloc(1, sizeof(EVP_MD_CTX));
---- work.orig/libarchive/archive_openssl_hmac_private.h	2017-07-09 21:38:04.000000000 -0500
-+++ work/libarchive/archive_openssl_hmac_private.h	2018-04-22 08:37:10.860250700 -0500
-@@ -31,6 +31,11 @@
- #if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
- #include <stdlib.h> /* malloc, free */
- #include <string.h> /* memset */
-+
-+#if defined(_MSC_VER) && (_MSC_VER < 1900)
-+#define inline __inline
-+#endif
-+
- static inline HMAC_CTX *HMAC_CTX_new(void)
- {
- 	HMAC_CTX *ctx = (HMAC_CTX *)calloc(1, sizeof(HMAC_CTX));
---- work.orig/libarchive/xxhash.c	2018-04-23 10:39:36.548437500 -0500
-+++ work/libarchive/xxhash.c	2018-04-23 10:40:26.751993100 -0500
-@@ -37,6 +37,10 @@
- #include "archive_xxhash.h"
- 
- #ifdef HAVE_LIBLZ4
-+
-+#if defined(_MSC_VER) && (_MSC_VER < 1900)
-+#define inline __inline
-+#endif
- 
- /***************************************
- ** Tuning parameters
diff --git a/conda.recipe/libarchive-patches/0007-CMake-VC9-compatibility-ignore-some-warnings.patch b/conda.recipe/libarchive-patches/0007-CMake-VC9-compatibility-ignore-some-warnings.patch
deleted file mode 100644
index c6eeb822..00000000
--- a/conda.recipe/libarchive-patches/0007-CMake-VC9-compatibility-ignore-some-warnings.patch
+++ /dev/null
@@ -1,34 +0,0 @@
-From 998330d99a8951a6cd22afcce9377645912c7004 Mon Sep 17 00:00:00 2001
-From: Ray Donnelly <mingw.android@gmail.com>
-Date: Mon, 23 Apr 2018 10:23:54 -0500
-Subject: [PATCH 2/2] CMake: VC9 compatibility ignore some warnings
-
----
- CMakeLists.txt | 11 +++++++++++
- 1 file changed, 11 insertions(+)
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
-index 73bf07b..60e391c 100644
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -172,6 +172,17 @@ IF (MSVC)
-   #################################################################
-   # Set compile flags for release build.
-   SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /Oi")
-+  if(MSVC_VERSION LESS 1900)
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4127")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4711")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4820")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4668")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4710")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4255")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4267")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4244")
-+    SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4996")
-+  ENDIF (MSVC_VERSION LESS 1900)
- ENDIF (MSVC)
- 
- # Enable CTest/CDash support
--- 
-2.15.0
-
diff --git a/conda.recipe/libarchive-patches/0008-VC9-compatibility-remove-C99.patch b/conda.recipe/libarchive-patches/0008-VC9-compatibility-remove-C99.patch
deleted file mode 100644
index b14bbda2..00000000
--- a/conda.recipe/libarchive-patches/0008-VC9-compatibility-remove-C99.patch
+++ /dev/null
@@ -1,121 +0,0 @@
---- libarchive-3.3.2/libarchive/archive_read_support_filter_lz4.c.orig	2018-04-23 16:51:54.000000000 +0100
-+++ libarchive-3.3.2/libarchive/archive_read_support_filter_lz4.c	2018-04-23 16:52:00.000000000 +0100
-@@ -336,6 +336,7 @@
- 
- 	while (state->stage == SELECT_STREAM) {
- 		const char *read_buf;
-+		uint32_t number;
- 
- 		/* Read a magic number. */
- 		read_buf = __archive_read_filter_ahead(self->upstream, 4,
-@@ -345,13 +346,14 @@
- 			*p = NULL;
- 			return (0);
- 		}
--		uint32_t number = archive_le32dec(read_buf);
-+		number = archive_le32dec(read_buf);
- 		__archive_read_filter_consume(self->upstream, 4);
- 		if (number == LZ4_MAGICNUMBER)
- 			return lz4_filter_read_default_stream(self, p);
- 		else if (number == LZ4_LEGACY)
- 			return lz4_filter_read_legacy_stream(self, p);
- 		else if ((number & ~0xF) == LZ4_SKIPPABLED) {
-+			uint32_t skip_bytes;
- 			read_buf = __archive_read_filter_ahead(
- 				self->upstream, 4, NULL);
- 			if (read_buf == NULL) {
-@@ -361,7 +363,7 @@
- 				    "Malformed lz4 data");
- 				return (ARCHIVE_FATAL);
- 			}
--			uint32_t skip_bytes = archive_le32dec(read_buf);
-+			skip_bytes = archive_le32dec(read_buf);
- 			__archive_read_filter_consume(self->upstream,
- 				4 + skip_bytes);
- 		} else {
---- work.orig/libarchive/test/test_sparse_basic.c	2018-09-03 22:07:16.000000000 -0500
-+++ work/libarchive/test/test_sparse_basic.c	2018-09-04 07:27:01.946534600 -0500
-@@ -109,6 +109,7 @@
- 	char buff[1024];
- 	HANDLE handle;
- 	DWORD dmy;
-+	size_t offsetSoFar = 0;
- 
- 	memset(buff, ' ', sizeof(buff));
- 
-@@ -119,20 +120,19 @@
- 	assert(DeviceIoControl(handle, FSCTL_SET_SPARSE, NULL, 0,
- 	    NULL, 0, &dmy, NULL) != 0);
- 
--	size_t offsetSoFar = 0;
--
- 	while (s->type != END) {
- 		if (s->type == HOLE) {
- 			LARGE_INTEGER fileOffset, beyondOffset, distanceToMove;
-+			FILE_ZERO_DATA_INFORMATION zeroInformation;
-+			DWORD bytesReturned;
-+
- 			fileOffset.QuadPart = offsetSoFar;
- 			beyondOffset.QuadPart = offsetSoFar + s->size;
- 			distanceToMove.QuadPart = s->size;
- 
--			FILE_ZERO_DATA_INFORMATION zeroInformation;
- 			zeroInformation.FileOffset = fileOffset;
- 			zeroInformation.BeyondFinalZero = beyondOffset;
- 
--			DWORD bytesReturned;
- 			assert(SetFilePointerEx(handle, distanceToMove,
- 				NULL, FILE_CURRENT) != 0);
- 			assert(SetEndOfFile(handle) != 0);
---- work.orig/libarchive/archive_write_disk_windows.c	2018-09-04 07:15:12.902734600 -0500
-+++ work/libarchive/archive_write_disk_windows.c	2018-09-04 07:24:43.543382500 -0500
-@@ -1515,7 +1515,7 @@
- 		return symlink(linkname, a->name) ? errno : 0;
- #else
- 		if (linkname != NULL) {
--			wchar_t *linkfull, *namefull;
-+			wchar_t const *linkfull, *namefull;
- 
- 			// Not sure which is best here, this is an absolute link, always:
- 			// linkfull = __la_win_permissive_name_w(linkname);
---- work.orig/tar/test/test_option_C_mtree.c	2018-09-03 22:07:16.000000000 -0500
-+++ work/tar/test/test_option_C_mtree.c	2018-09-04 07:23:14.413229300 -0500
-@@ -30,10 +30,9 @@
- 
- DEFINE_TEST(test_option_C_mtree)
- {
--	char *p0;
-+	char *p0 = NULL;
- 	size_t s;
- 	int r;
--	p0 = NULL;
- 	char *content = "./foo type=file uname=root gname=root mode=0755\n";
- 	char *filename = "output.tar";
- 
---- work.orig/libarchive/archive_read_support_filter_zstd.c	2018-09-03 22:07:16.000000000 -0500
-+++ work/libarchive/archive_read_support_filter_zstd.c	2018-11-02 08:36:21.350115700 -0500
-@@ -209,7 +209,9 @@
- 
- 	state = (struct private_data *)self->data;
- 
--	out = (ZSTD_outBuffer) { state->out_block, state->out_block_size, 0 };
-+	out.dst = state->out_block;
-+	out.size = state->out_block_size;
-+	out.pos = 0;
- 
- 	/* Try to fill the output buffer. */
- 	while (out.pos < out.size && !state->eof) {
---- work.orig/libarchive/archive_write_add_filter_zstd.c	2018-09-03 22:07:16.000000000 -0500
-+++ work/libarchive/archive_write_add_filter_zstd.c	2018-11-02 08:52:09.469969000 -0500
-@@ -259,7 +259,10 @@
- drive_compressor(struct archive_write_filter *f,
-     struct private_data *data, int finishing, const void *src, size_t length)
- {
--	ZSTD_inBuffer in = (ZSTD_inBuffer) { src, length, 0 };
-+	ZSTD_inBuffer in;
-+	in.src = src;
-+	in.size = length;
-+	in.pos = 0;
- 
- 	for (;;) {
- 		if (data->out.pos == data->out.size) {
diff --git a/conda.recipe/libarchive-patches/0009-CMake-Force-Multi-threaded-DLL-runtime.patch b/conda.recipe/libarchive-patches/0009-CMake-Force-Multi-threaded-DLL-runtime.patch
deleted file mode 100644
index c77c976f..00000000
--- a/conda.recipe/libarchive-patches/0009-CMake-Force-Multi-threaded-DLL-runtime.patch
+++ /dev/null
@@ -1,34 +0,0 @@
-From bfddc49f8119d6718a8a11b130381096ae7c0362 Mon Sep 17 00:00:00 2001
-From: Ray Donnelly <mingw.android@gmail.com>
-Date: Tue, 24 Apr 2018 18:37:28 -0500
-Subject: [PATCH] CMake: Force Multi-threaded DLL runtime
-
----
- CMakeLists.txt | 4 ++++
- 1 file changed, 4 insertions(+)
-
-diff --git a/CMakeLists.txt b/CMakeLists.txt
-index bd4517c..828fc80 100644
---- a/CMakeLists.txt
-+++ b/CMakeLists.txt
-@@ -169,6 +169,8 @@ IF (MSVC)
-   SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /WX")
-   # /Oi option enables built-in functions.
-   SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /Oi")
-+  # /MD option for Multi-threaded DLL (non-debug since you cannot mix them).
-+  SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MD")
-   #################################################################
-   # Set compile flags for release build.
-   SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /Oi")
-@@ -183,6 +185,8 @@ IF (MSVC)
-     SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4244")
-     SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /wd4996")
-   ENDIF (MSVC_VERSION LESS 1900)
-+  # /MD option for Multi-threaded DLL.
-+  SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MD")
- ENDIF (MSVC)
- 
- # Enable CTest/CDash support
--- 
-2.15.0
-
diff --git a/conda.recipe/libarchive-patches/0010-Use-CreateSymbolicLinkW-on-Windows-for-symlinks-WIP.patch b/conda.recipe/libarchive-patches/0010-Use-CreateSymbolicLinkW-on-Windows-for-symlinks-WIP.patch
deleted file mode 100644
index c4700bfb..00000000
--- a/conda.recipe/libarchive-patches/0010-Use-CreateSymbolicLinkW-on-Windows-for-symlinks-WIP.patch
+++ /dev/null
@@ -1,51 +0,0 @@
-From 401ad3c40f5876da0e05511196b27d2b77b61957 Mon Sep 17 00:00:00 2001
-From: Ray Donnelly <mingw.android@gmail.com>
-Date: Wed, 20 Jun 2018 05:40:46 -0700
-Subject: [PATCH 1/2] Use CreateSymbolicLinkW on Windows for symlinks (WIP)
-
-This doesn't handle SYMBOLIC_LINK_FLAG_DIRECTORY yet
----
- libarchive/archive_write_disk_windows.c | 27 ++++++++++++++++++++++++++-
- 1 file changed, 26 insertions(+), 1 deletion(-)
-
-diff --git a/libarchive/archive_write_disk_windows.c b/libarchive/archive_write_disk_windows.c
-index 78eda4ab..b393a1fe 100644
---- a/libarchive/archive_write_disk_windows.c
-+++ b/libarchive/archive_write_disk_windows.c
-@@ -1514,7 +1514,32 @@ create_filesystem_object(struct archive_write_disk *a)
- #if HAVE_SYMLINK
- 		return symlink(linkname, a->name) ? errno : 0;
- #else
--		return (EPERM);
-+		if (linkname != NULL) {
-+			wchar_t *linkfull, *namefull;
-+
-+			// Not sure which is best here, this is an absolute link, always:
-+			// linkfull = __la_win_permissive_name_w(linkname);
-+			linkfull = linkname;
-+			namefull = __la_win_permissive_name_w(a->name);
-+			if (linkfull == NULL || namefull == NULL) {
-+				errno = EINVAL;
-+				r = -1;
-+			}
-+			else {
-+#if !defined(SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)
-+#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
-+#endif
-+				r = CreateSymbolicLinkW(namefull, linkfull, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE);
-+				if (r == 0) {
-+					la_dosmaperr(GetLastError());
-+					r = errno;
-+				}
-+				else
-+					r = 0;
-+			}
-+
-+			return r;
-+		}
- #endif
- 	}
- 
--- 
-2.16.2
-
diff --git a/conda.recipe/libarchive-patches/0011-Debugging-for-this.patch b/conda.recipe/libarchive-patches/0011-Debugging-for-this.patch
deleted file mode 100644
index 8b66d187..00000000
--- a/conda.recipe/libarchive-patches/0011-Debugging-for-this.patch
+++ /dev/null
@@ -1,40 +0,0 @@
-From 39702357f26920b078537762e1877e30fffbf6b9 Mon Sep 17 00:00:00 2001
-From: Ray Donnelly <mingw.android@gmail.com>
-Date: Wed, 20 Jun 2018 05:43:59 -0700
-Subject: [PATCH 2/2] Debugging for this
-
----
- libarchive/archive_write_disk_windows.c | 10 ++++++++--
- 1 file changed, 8 insertions(+), 2 deletions(-)
-
-diff --git a/libarchive/archive_write_disk_windows.c b/libarchive/archive_write_disk_windows.c
-index b393a1fe..9fd8ab0b 100644
---- a/libarchive/archive_write_disk_windows.c
-+++ b/libarchive/archive_write_disk_windows.c
-@@ -283,7 +283,9 @@ file_information(struct archive_write_disk *a, wchar_t *path,
- 		        & FILE_ATTRIBUTE_REPARSE_POINT) &&
- 		(findData.dwReserved0 == IO_REPARSE_TAG_SYMLINK)))
- 		flag |= FILE_FLAG_OPEN_REPARSE_POINT;
--
-+	if (flag & FILE_FLAG_OPEN_REPARSE_POINT) {
-+		wprintf(L"Hello symlink a->name %s\n",a->name);
-+	}
- 	h = CreateFileW(a->name, 0, 0, NULL,
- 	    OPEN_EXISTING, flag, NULL);
- 	if (h == INVALID_HANDLE_VALUE &&
-@@ -1275,7 +1277,11 @@ static int
- restore_entry(struct archive_write_disk *a)
- {
- 	int ret = ARCHIVE_OK, en;
--
-+	if (wcsstr(a->name, L"cluster.tgz"))
-+	{
-+		// Using e.g.: C:\Users\builder\m64\conda-bld\src_cache\R-3.5.0_fd1725535e.tar.gz
-+		printf("Debug this\n");
-+	}
- 	if (a->flags & ARCHIVE_EXTRACT_UNLINK && !S_ISDIR(a->mode)) {
- 		/*
- 		 * TODO: Fix this.  Apparently, there are platforms
--- 
-2.16.2
-
diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml
index e44372fe..209eefdb 100644
--- a/conda.recipe/meta.yaml
+++ b/conda.recipe/meta.yaml
@@ -1,42 +1,58 @@
-# libarchive 3.4.0 breaks some symlink stuff on macos.  Holding back for now.
-{% set libarchive_version = "3.3.3" %}
+{% set name = "conda-package-handling" %}
+{% set version_match = load_file_regex(
+  load_file="src/conda_package_handling/__init__.py",
+  regex_pattern='^__version__ = "(.+)"') %}
+{% set version = version_match[1] %}
 
 package:
-  name: conda-package-handling
-  version: {{ GIT_DESCRIBE_TAG }}.{{ GIT_BUILD_STR }}
+  name: {{ name }}
+  version: {{ version }}
 
 source:
   - path: ..
 
 build:
-  script: python setup.py install --single-version-externally-managed --record=record.txt
+  number: 0
+  script: {{ PYTHON }} -m pip install . --no-deps -vv
   entry_points:
     - cph = conda_package_handling.cli:main
+  # by skipping nooarch: python, tests run under build python
+
 requirements:
-  build:
-    - {{ compiler('c') }}
   host:
     - python
-    - setuptools
-    - cython
-    - msinttypes  # [win and vc<14]
-    - _libarchive_static_for_cph
+    - pip
+    - wheel
   run:
     - python
     - tqdm
+    - conda-package-streaming >=0.5.0
+
 test:
   source_files:
     - tests
   requires:
+    - mock
     - pytest
     - pytest-cov
     - pytest-mock
-    - mock
   imports:
     - conda_package_handling
-    - conda_package_handling.archive_utils_cy
+    - conda_package_handling.api
   commands:
-    - pytest -v --cov=tests/ --cov-report=xml --color=yes tests/
+    - pytest -v --cov=conda_package_handling --color=yes tests/
+
 about:
   home: https://github.com/conda/conda-package-handling
+  dev_url: https://github.com/conda/conda-package-handling
+  doc_url: https://conda.github.io/conda-package-handling/
+  license: BSD-3-Clause
+  license_family: BSD
+  license_file:
+    - LICENSE
   summary: Create and extract conda packages of various formats
+
+extra:
+  recipe-maintainers:
+    - dholth
+    - jezdez
diff --git a/conda.recipe/static_bld.bat b/conda.recipe/static_bld.bat
deleted file mode 100644
index 91a43f34..00000000
--- a/conda.recipe/static_bld.bat
+++ /dev/null
@@ -1,112 +0,0 @@
-:: Needed so we can find stdint.h from msinttypes.
-set LIB=%LIBRARY_LIB%;%LIB%
-set LIBPATH=%LIBRARY_LIB%;%LIBPATH%
-set INCLUDE=%LIBRARY_INC%;%INCLUDE%
-
-:: VS2008 doesn't have stdbool.h so copy in our own
-:: to 'lib' where the other headers are so it gets picked up.
-if "%VS_MAJOR%" == "9" (
-  if "%ARCH%" == "64" (
-::  The Windows 6.0A SDK does not provide the bcrypt.lib for 64-bit:
-::  C:\Program Files\Microsoft SDKs\Windows\v6.0A\Lib\x64
-::  .. yet does for 32-bit, oh well, this may disable password protected zip support.
-::  https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/673cc344-430c-4510-96e8-80b0bb42ae11/can-not-link-bcryptlib-to-an-64bit-build?forum=windowssdk
-    set ENABLE_CNG=NO
-  ) else (
-    set ENABLE_CNG=YES
-::  Have decided to standardise on *not* using bcrypt instead. If we update to the Windows Server 2008 SDK we could revisit this
-    set ENABLE_CNG=NO
-  )
-)
-
-if "%vc%" NEQ "9" goto not_vc9
-:: This does not work yet:
-:: usage: cl [ option... ] filename... [ /link linkoption... ]
-  set USE_C99_WRAP=no
-  copy %LIBRARY_INC%\inttypes.h src\common\inttypes.h
-  copy %LIBRARY_INC%\stdint.h src\common\stdint.h
-  goto endit
-:not_vc9
-  set USE_C99_WRAP=no
-:endit
-
-if exist CMakeCache.txt goto build
-if "%USE_C99_WRAP%" NEQ "yes" goto skip_c99_wrap
-set COMPILER=-DCMAKE_C_COMPILER=c99-to-c89-cmake-nmake-wrap.bat
-set C99_TO_C89_WRAP_DEBUG_LEVEL=1
-set C99_TO_C89_WRAP_SAVE_TEMPS=1
-set C99_TO_C89_WRAP_NO_LINE_DIRECTIVES=1
-set C99_TO_C89_CONV_DEBUG_LEVEL=1
-:skip_c99_wrap
-:: set cflags because NDEBUG is set in Release configuration, which errors out in test suite due to no assert
-
-cmake -G "Ninja" ^
-      -DCMAKE_INSTALL_PREFIX="%LIBRARY_PREFIX%" ^
-      %COMPILER% ^
-      -DCMAKE_BUILD_TYPE=Release ^
-      -DCMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS:BOOL=FALSE ^
-      -DCMAKE_INSTALL_PREFIX=%LIBRARY_PREFIX% ^
-      -DCMAKE_C_FLAGS_RELEASE="%CFLAGS%" ^
-      -DENABLE_ACL=ON ^
-      -DENABLE_BZip2=ON ^
-      -DENABLE_CAT=OFF ^
-      -DENABLE_CNG=%ENABLE_CNG% ^
-      -DENABLE_COVERAGE=OFF ^
-      -DENABLE_CPIO=OFF ^
-      -DENABLE_EXPAT=ON ^
-      -DENABLE_ICONV=OFF ^
-      -DENABLE_INSTALL=ON ^
-      -DENABLE_LIBB2=OFF ^
-      -DENABLE_LIBXML2=OFF ^
-      -DENABLE_LZ4=OFF ^
-      -DENABLE_LZMA=OFF ^
-      -DENABLE_LZO=OFF ^
-      -DENABLE_LibGCC=OFF ^
-      -DENABLE_NETTLE=OFF ^
-      -DENABLE_OPENSSL=OFF ^
-      -DENABLE_SAFESEH=AUTO ^
-      -DENABLE_TAR=ON ^
-      -DENABLE_TAR_SHARED=OFF ^
-      -DENABLE_XATTR=ON ^
-      -DENABLE_ZLIB=ON ^
-      -DENABLE_ZSTD=ON ^
-      -DBZIP2_LIBRARY_RELEASE=%PREFIX%/Library/lib/bzip2_static.lib ^
-      -DZLIB_LIBRARY_RELEASE=%PREFIX%/Library/lib/zlibstatic.lib ^
-      -DZSTD_LIBRARY=%PREFIX%/Library/lib/libzstd_static.lib ^
-      .
-
-:build
-
-:: Build.
-:: jom -j%CPU_COUNT% VERBOSE=1
-ninja -j%CPU_COUNT% -v
-if errorlevel 1 exit /b 1
-ninja install
-if errorlevel 1 exit /b 1
-
-:: Test.
-:: Failures:
-:: The following tests FAILED:
-::         365 - libarchive_test_read_truncated_filter_bzip2 (Timeout) => runs msys2's bzip2.exe
-::         372 - libarchive_test_sparse_basic (Failed)
-::         373 - libarchive_test_fully_sparse_files (Failed)
-::         386 - libarchive_test_warn_missing_hardlink_target (Failed)
-:: ctest -C Release
-:: if errorlevel 1 exit 1
-
-:: Test extracting a 7z. This failed due to not using the multi-threaded DLL runtime, fixed by 0009-CMake-Force-Multi-threaded-DLL-runtime.patch
-::powershell -command "& { (New-Object Net.WebClient).DownloadFile('http://download.qt.io/development_releases/prebuilt/llvmpipe/windows/opengl32sw-64-mesa_12_0_rc2.7z', 'opengl32sw-64-mesa_12_0_rc2.7z') }"
-::if errorlevel 1 exit 1
-::%LIBRARY_BIN%\bsdtar -xf opengl32sw-64-mesa_12_0_rc2.7z
-::if errorlevel 1 exit 1
-
-:: remove man pages
-rd /s /q %PREFIX%\Library\share\man
-
-:: remove the dynamic libraries
-del %PREFIX%\Library\bin\archive.DLL
-del %PREFIX%\Library\lib\archive.lib
-
-pushd %PREFIX%\Library\lib
-lib.exe /OUT:archive_and_deps.lib archive_static.lib libzstd_static.lib bzip2_static.lib zlibstatic.lib
-popd
\ No newline at end of file
diff --git a/conda.recipe/static_build.sh b/conda.recipe/static_build.sh
deleted file mode 100644
index 2050ce26..00000000
--- a/conda.recipe/static_build.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/bash
-
-# Build libarchive as a static library with support for the features needed for
-# conda-package-handling (bzip2, zlib, zstd).
-# This is not a general purpose libarchive.
-autoreconf -vfi
-mkdir build-${HOST} && pushd build-${HOST}
-${SRC_DIR}/configure --prefix=${PREFIX}     \
-                     --enable-bsdtar=static \
-                     --enable-static        \
-                     --with-bz2lib          \
-                     --with-iconv           \
-                     --with-zlib            \
-                     --with-zstd            \
-                     --disable-bsdcat       \
-                     --disable-bsdcpio      \
-                     --disable-shared       \
-                     --without-cng          \
-                     --without-expat        \
-                     --without-lz4          \
-                     --without-lzma         \
-                     --without-lzo2         \
-                     --without-nettle       \
-                     --without-openssl      \
-                     --without-xml2
-make -j${CPU_COUNT} ${VERBOSE_AT}
-make install
-popd
-
-# remove the man pages
-rm -rf ${PREFIX}/share/man
-
-# create libarchive_and_deps.a
-if [[ ${HOST} =~ .*darwin.* ]]; then
-    mkdir -p tmp_reform
-    pushd tmp_reform
-    # extract the object files into seperate directories since some of the
-    # archives have members of the same name (e.g.  compress.o). See:
-    # https://stackoverflow.com/a/23557928
-    for name in libz libbz2 libzstd libiconv libarchive
-    do
-        ${AR} -x ${PREFIX}/lib/${name}.a
-        mkdir -p ${name}_objs
-        mv *.o ${name}_objs
-    done
-    ${AR} crv libarchive_and_deps.a */*.o
-    ranlib libarchive_and_deps.a
-    cp libarchive_and_deps.a ${PREFIX}/lib/
-    popd
-fi
-if [[ ${HOST} =~ .*linux.* ]]; then
-    pushd ${PREFIX}/lib
-    ${AR} -M <<EOM
-        CREATE libarchive_and_deps.a
-        ADDLIB libarchive.a
-        ADDLIB libbz2.a
-        ADDLIB libz.a
-        ADDLIB libzstd.a
-        SAVE
-        END
-EOM
-    ranlib libarchive_and_deps.a
-    popd
-fi
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000..2c84fd73
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,9 @@
+api module
+==========
+
+```{eval-rst}
+.. automodule:: conda_package_handling.api
+   :members:
+   :undoc-members:
+   :show-inheritance:
+```
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..5060dae9
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,55 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath(".."))
+
+
+# -- Project information -----------------------------------------------------
+
+project = "conda-package-handling"
+copyright = "Anaconda, Inc."
+author = "Anaconda, Inc."
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    "sphinx.ext.autodoc",
+    "myst_parser",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = "furo"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..8a037628
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,26 @@
+# Welcome to conda-package-handling's documentation!
+
+`conda-package-handling` is the library `conda` uses to handle `.conda` and
+`.tar.bz2` [conda
+packages](https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/packages.html).
+
+It includes a `cph` command-line utility for manipulating new- and old-format
+conda packages.
+
+See also
+[conda-package-streaming](https://github.com/conda-incubator/conda-package-streaming)
+
+```{include} ../README.md
+```
+
+```{toctree}
+:caption: 'Contents:'
+:maxdepth: 2
+modules
+```
+
+# Indices and tables
+
+- {ref}`genindex`
+- {ref}`modindex`
+- {ref}`search`
diff --git a/docs/modules.md b/docs/modules.md
new file mode 100644
index 00000000..b8405ab2
--- /dev/null
+++ b/docs/modules.md
@@ -0,0 +1,9 @@
+# conda_package_handling
+
+
+
+```{toctree}
+:maxdepth: 4
+
+api
+```
diff --git a/make.bat b/make.bat
new file mode 100644
index 00000000..daee51b4
--- /dev/null
+++ b/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=docs
+set BUILDDIR=build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.https://www.sphinx-doc.org/
+	exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/news/quiet b/news/quiet
new file mode 100644
index 00000000..ada2378b
--- /dev/null
+++ b/news/quiet
@@ -0,0 +1,19 @@
+### Enhancements
+
+* Add `cph transmute --quiet` to disable progress bars.
+
+### Bug fixes
+
+* <news item>
+
+### Deprecations
+
+* <news item>
+
+### Docs
+
+* <news item>
+
+### Other
+
+* <news item>
diff --git a/news/streaming b/news/streaming
new file mode 100644
index 00000000..4f81cd75
--- /dev/null
+++ b/news/streaming
@@ -0,0 +1,24 @@
+### Enhancements
+
+* Based on conda-package-streaming >= 0.5.0 instead of libarchive.
+* Requires the `python-zstandard` (`zstandard`) library.
+* Threadsafe `extract()` function.
+* More efficient `.conda` handling.
+
+### Bug fixes
+
+* <news item>
+
+### Deprecations
+
+* Remove broken `verify` subcommand.
+* Remove support for `binsort` (was supposed to help with `tar.bz2`
+  compression). (Use `.conda` instead.)
+
+### Docs
+
+* Add sphinx documentation.
+
+### Other
+
+* <news item>
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..2b567f1c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.black]
+# consider reverting to standard line length
+line-length = 99
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..18676580
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+# used in sphinx documentation build
+conda-package-streaming>=0.6.0
+# docs
+furo
+sphinx
+myst-parser
+mdit-py-plugins>=0.3.0
diff --git a/rever.xsh b/rever.xsh
index a0d145ce..94c489de 100644
--- a/rever.xsh
+++ b/rever.xsh
@@ -1,4 +1,4 @@
-$ACTIVITIES = ["authors", "changelog"]
+$ACTIVITIES = ["version_bump", "authors", "changelog"]
 
 # Basic settings
 $PROJECT = $GITHUB_REPO = $(basename $(git remote get-url origin)).split('.')[0]
@@ -26,3 +26,7 @@ $CHANGELOG_CATEGORIES = [
 $CHANGELOG_CATEGORY_TITLE_FORMAT = "### {category}\n\n"
 $CHANGELOG_AUTHORS_TITLE = "Contributors"
 $CHANGELOG_AUTHORS_FORMAT = "* @{github}\n"
+
+$VERSION_BUMP_PATTERNS = [  # These note where/how to find the version numbers
+    ('src/conda_package_handling/__init__.py', r'__version__\s*=.*', '__version__ = "$VERSION"'),
+]
diff --git a/setup.cfg b/setup.cfg
index 0b665843..18b45861 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -11,16 +11,9 @@ addopts =
     --ignore run_test.py
     --cov-report term-missing
     --tb native
-    --strict
+    --strict-markers
     --durations=20
 env =
     PYTHONHASHSEED=0
 markers =
     serial: execute test serially (to avoid race conditions)
-
-[versioneer]
-VCS = git
-versionfile_source = src/conda_package_handling/_version.py
-versionfile_build = conda_package_handling/_version.py
-tag_prefix =
-parentdir_prefix = conda-package-handling-
diff --git a/setup.py b/setup.py
index b4e67f85..01eedcda 100644
--- a/setup.py
+++ b/setup.py
@@ -1,38 +1,31 @@
-import sys
+import importlib.util
+import pathlib
 
-from Cython.Build import cythonize
 from setuptools import find_packages, setup
-from setuptools.extension import Extension
 
-import versioneer
-
-_libraries = ["archive_and_deps"]
-if sys.platform == "win32":
-    _libraries.append("advapi32")
-    _libraries.append("user32")
-archive_utils_cy_extension = Extension(
-    name="conda_package_handling.archive_utils_cy",
-    sources=["src/conda_package_handling/archive_utils_cy.pyx"],
-    libraries=_libraries,
+spec = importlib.util.spec_from_file_location(
+    "conda_package_handling", pathlib.Path("src/conda_package_handling/__init__.py")
 )
-
+module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(module)
+version = module.__version__
 
 setup(
     name="conda-package-handling",
-    version=versioneer.get_version(),
-    cmdclass=versioneer.get_cmdclass(),
+    version=version,
     description="Create and extract conda packages of various formats",
     author="Anaconda, Inc.",
     author_email="conda@anaconda.com",
     url="https://github.com/conda/conda-package-handling",
-    ext_modules=cythonize([archive_utils_cy_extension]),
     packages=find_packages("src", exclude=["tests"]),
     package_dir={"": "src"},
     entry_points={"console_scripts": ["cph=conda_package_handling.cli:main"]},
     keywords="conda-package-handling",
-    classifiers=[
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-    ],
+    classifiers=["Programming Language :: Python :: 3"],
+    python_requires=">=3.7",
+    install_requires=["conda-package-streaming >= 0.6.0"],
+    extras_require={
+        "docs": ["furo", "sphinx", "myst-parser", "mdit-py-plugins>=0.3.0"],
+        "test": ["mock", "pytest", "pytest-cov", "pytest-mock"],
+    },
 )
diff --git a/src/conda_package_handling/__init__.py b/src/conda_package_handling/__init__.py
index 4d52a61e..981a129f 100644
--- a/src/conda_package_handling/__init__.py
+++ b/src/conda_package_handling/__init__.py
@@ -1,3 +1,1 @@
-from . import _version
-
-__version__ = _version.get_versions()["version"]
+__version__ = "2.0.0a4"
diff --git a/src/conda_package_handling/__main__.py b/src/conda_package_handling/__main__.py
index 79e6d00c..dde91ff0 100644
--- a/src/conda_package_handling/__main__.py
+++ b/src/conda_package_handling/__main__.py
@@ -1,3 +1,3 @@
 from conda_package_handling import cli
 
-cli.cli()
+cli.main(args=None)
diff --git a/src/conda_package_handling/_version.py b/src/conda_package_handling/_version.py
deleted file mode 100644
index 90365a8f..00000000
--- a/src/conda_package_handling/_version.py
+++ /dev/null
@@ -1,658 +0,0 @@
-
-# This file helps to compute a version number in source trees obtained from
-# git-archive tarball (such as those provided by githubs download-from-tag
-# feature). Distribution tarballs (built by setup.py sdist) and build
-# directories (produced by setup.py build) will contain a much shorter file
-# that just contains the computed version number.
-
-# This file is released into the public domain. Generated by
-# versioneer-0.22 (https://github.com/python-versioneer/python-versioneer)
-
-"""Git implementation of _version.py."""
-
-import errno
-import os
-import re
-import subprocess
-import sys
-from typing import Callable, Dict
-import functools
-
-
-def get_keywords():
-    """Get the keywords needed to look up the version information."""
-    # these strings will be replaced by git during git-archive.
-    # setup.py/versioneer.py will grep for the variable names, so they must
-    # each be defined on a line of their own. _version.py will just call
-    # get_keywords().
-    git_refnames = "$Format:%d$"
-    git_full = "$Format:%H$"
-    git_date = "$Format:%ci$"
-    keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
-    return keywords
-
-
-class VersioneerConfig:
-    """Container for Versioneer configuration parameters."""
-
-
-def get_config():
-    """Create, populate and return the VersioneerConfig() object."""
-    # these strings are filled in when 'setup.py versioneer' creates
-    # _version.py
-    cfg = VersioneerConfig()
-    cfg.VCS = "git"
-    cfg.style = ""
-    cfg.tag_prefix = ""
-    cfg.parentdir_prefix = "conda-package-handling-"
-    cfg.versionfile_source = "src/conda_package_handling/_version.py"
-    cfg.verbose = False
-    return cfg
-
-
-class NotThisMethod(Exception):
-    """Exception raised if a method is not valid for the current scenario."""
-
-
-LONG_VERSION_PY: Dict[str, str] = {}
-HANDLERS: Dict[str, Dict[str, Callable]] = {}
-
-
-def register_vcs_handler(vcs, method):  # decorator
-    """Create decorator to mark a method as the handler of a VCS."""
-    def decorate(f):
-        """Store f in HANDLERS[vcs][method]."""
-        if vcs not in HANDLERS:
-            HANDLERS[vcs] = {}
-        HANDLERS[vcs][method] = f
-        return f
-    return decorate
-
-
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
-                env=None):
-    """Call the given command(s)."""
-    assert isinstance(commands, list)
-    process = None
-
-    popen_kwargs = {}
-    if sys.platform == "win32":
-        # This hides the console window if pythonw.exe is used
-        startupinfo = subprocess.STARTUPINFO()
-        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
-        popen_kwargs["startupinfo"] = startupinfo
-
-    for command in commands:
-        try:
-            dispcmd = str([command] + args)
-            # remember shell=False, so use git.cmd on windows, not just git
-            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
-                                       stdout=subprocess.PIPE,
-                                       stderr=(subprocess.PIPE if hide_stderr
-                                               else None), **popen_kwargs)
-            break
-        except OSError:
-            e = sys.exc_info()[1]
-            if e.errno == errno.ENOENT:
-                continue
-            if verbose:
-                print("unable to run %s" % dispcmd)
-                print(e)
-            return None, None
-    else:
-        if verbose:
-            print("unable to find command, tried %s" % (commands,))
-        return None, None
-    stdout = process.communicate()[0].strip().decode()
-    if process.returncode != 0:
-        if verbose:
-            print("unable to run %s (error)" % dispcmd)
-            print("stdout was %s" % stdout)
-        return None, process.returncode
-    return stdout, process.returncode
-
-
-def versions_from_parentdir(parentdir_prefix, root, verbose):
-    """Try to determine the version from the parent directory name.
-
-    Source tarballs conventionally unpack into a directory that includes both
-    the project name and a version string. We will also support searching up
-    two directory levels for an appropriately named parent directory
-    """
-    rootdirs = []
-
-    for _ in range(3):
-        dirname = os.path.basename(root)
-        if dirname.startswith(parentdir_prefix):
-            return {"version": dirname[len(parentdir_prefix):],
-                    "full-revisionid": None,
-                    "dirty": False, "error": None, "date": None}
-        rootdirs.append(root)
-        root = os.path.dirname(root)  # up a level
-
-    if verbose:
-        print("Tried directories %s but none started with prefix %s" %
-              (str(rootdirs), parentdir_prefix))
-    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-@register_vcs_handler("git", "get_keywords")
-def git_get_keywords(versionfile_abs):
-    """Extract version information from the given file."""
-    # the code embedded in _version.py can just fetch the value of these
-    # keywords. When used from setup.py, we don't want to import _version.py,
-    # so we do it with a regexp instead. This function is not used from
-    # _version.py.
-    keywords = {}
-    try:
-        with open(versionfile_abs, "r") as fobj:
-            for line in fobj:
-                if line.strip().startswith("git_refnames ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["refnames"] = mo.group(1)
-                if line.strip().startswith("git_full ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["full"] = mo.group(1)
-                if line.strip().startswith("git_date ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["date"] = mo.group(1)
-    except OSError:
-        pass
-    return keywords
-
-
-@register_vcs_handler("git", "keywords")
-def git_versions_from_keywords(keywords, tag_prefix, verbose):
-    """Get version information from git keywords."""
-    if "refnames" not in keywords:
-        raise NotThisMethod("Short version file found")
-    date = keywords.get("date")
-    if date is not None:
-        # Use only the last line.  Previous lines may contain GPG signature
-        # information.
-        date = date.splitlines()[-1]
-
-        # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
-        # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
-        # -like" string, which we must then edit to make compliant), because
-        # it's been around since git-1.5.3, and it's too difficult to
-        # discover which version we're using, or to work around using an
-        # older one.
-        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-    refnames = keywords["refnames"].strip()
-    if refnames.startswith("$Format"):
-        if verbose:
-            print("keywords are unexpanded, not using")
-        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
-    refs = {r.strip() for r in refnames.strip("()").split(",")}
-    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
-    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
-    TAG = "tag: "
-    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
-    if not tags:
-        # Either we're using git < 1.8.3, or there really are no tags. We use
-        # a heuristic: assume all version tags have a digit. The old git %d
-        # expansion behaves like git log --decorate=short and strips out the
-        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
-        # between branches and tags. By ignoring refnames without digits, we
-        # filter out many common branch names like "release" and
-        # "stabilization", as well as "HEAD" and "master".
-        tags = {r for r in refs if re.search(r'\d', r)}
-        if verbose:
-            print("discarding '%s', no digits" % ",".join(refs - tags))
-    if verbose:
-        print("likely tags: %s" % ",".join(sorted(tags)))
-    for ref in sorted(tags):
-        # sorting will prefer e.g. "2.0" over "2.0rc1"
-        if ref.startswith(tag_prefix):
-            r = ref[len(tag_prefix):]
-            # Filter out refs that exactly match prefix or that don't start
-            # with a number once the prefix is stripped (mostly a concern
-            # when prefix is '')
-            if not re.match(r'\d', r):
-                continue
-            if verbose:
-                print("picking %s" % r)
-            return {"version": r,
-                    "full-revisionid": keywords["full"].strip(),
-                    "dirty": False, "error": None,
-                    "date": date}
-    # no suitable tags, so version is "0+unknown", but full hex is still there
-    if verbose:
-        print("no suitable tags, using unknown + full revision id")
-    return {"version": "0+unknown",
-            "full-revisionid": keywords["full"].strip(),
-            "dirty": False, "error": "no suitable tags", "date": None}
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
-    """Get version from 'git describe' in the root of the source tree.
-
-    This only gets called if the git-archive 'subst' keywords were *not*
-    expanded, and _version.py hasn't already been rewritten with a short
-    version string, meaning we're inside a checked out source tree.
-    """
-    GITS = ["git"]
-    if sys.platform == "win32":
-        GITS = ["git.cmd", "git.exe"]
-
-    # GIT_DIR can interfere with correct operation of Versioneer.
-    # It may be intended to be passed to the Versioneer-versioned project,
-    # but that should not change where we get our version from.
-    env = os.environ.copy()
-    env.pop("GIT_DIR", None)
-    runner = functools.partial(runner, env=env)
-
-    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
-                   hide_stderr=True)
-    if rc != 0:
-        if verbose:
-            print("Directory %s not under git control" % root)
-        raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
-    MATCH_ARGS = ["--match", "%s*" % tag_prefix] if tag_prefix else []
-
-    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
-    # if there isn't one, this yields HEX[-dirty] (no NUM)
-    describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
-                                     "--always", "--long", *MATCH_ARGS],
-                              cwd=root)
-    # --long was added in git-1.5.5
-    if describe_out is None:
-        raise NotThisMethod("'git describe' failed")
-    describe_out = describe_out.strip()
-    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
-    if full_out is None:
-        raise NotThisMethod("'git rev-parse' failed")
-    full_out = full_out.strip()
-
-    pieces = {}
-    pieces["long"] = full_out
-    pieces["short"] = full_out[:7]  # maybe improved later
-    pieces["error"] = None
-
-    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
-                             cwd=root)
-    # --abbrev-ref was added in git-1.6.3
-    if rc != 0 or branch_name is None:
-        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
-    branch_name = branch_name.strip()
-
-    if branch_name == "HEAD":
-        # If we aren't exactly on a branch, pick a branch which represents
-        # the current commit. If all else fails, we are on a branchless
-        # commit.
-        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
-        # --contains was added in git-1.5.4
-        if rc != 0 or branches is None:
-            raise NotThisMethod("'git branch --contains' returned error")
-        branches = branches.split("\n")
-
-        # Remove the first line if we're running detached
-        if "(" in branches[0]:
-            branches.pop(0)
-
-        # Strip off the leading "* " from the list of branches.
-        branches = [branch[2:] for branch in branches]
-        if "master" in branches:
-            branch_name = "master"
-        elif not branches:
-            branch_name = None
-        else:
-            # Pick the first branch that is returned. Good or bad.
-            branch_name = branches[0]
-
-    pieces["branch"] = branch_name
-
-    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
-    # TAG might have hyphens.
-    git_describe = describe_out
-
-    # look for -dirty suffix
-    dirty = git_describe.endswith("-dirty")
-    pieces["dirty"] = dirty
-    if dirty:
-        git_describe = git_describe[:git_describe.rindex("-dirty")]
-
-    # now we have TAG-NUM-gHEX or HEX
-
-    if "-" in git_describe:
-        # TAG-NUM-gHEX
-        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
-        if not mo:
-            # unparsable. Maybe git-describe is misbehaving?
-            pieces["error"] = ("unable to parse git-describe output: '%s'"
-                               % describe_out)
-            return pieces
-
-        # tag
-        full_tag = mo.group(1)
-        if not full_tag.startswith(tag_prefix):
-            if verbose:
-                fmt = "tag '%s' doesn't start with prefix '%s'"
-                print(fmt % (full_tag, tag_prefix))
-            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
-                               % (full_tag, tag_prefix))
-            return pieces
-        pieces["closest-tag"] = full_tag[len(tag_prefix):]
-
-        # distance: number of commits since tag
-        pieces["distance"] = int(mo.group(2))
-
-        # commit: short hex revision ID
-        pieces["short"] = mo.group(3)
-
-    else:
-        # HEX: no tags
-        pieces["closest-tag"] = None
-        count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
-        pieces["distance"] = int(count_out)  # total number of commits
-
-    # commit date: see ISO-8601 comment in git_versions_from_keywords()
-    date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
-    # Use only the last line.  Previous lines may contain GPG signature
-    # information.
-    date = date.splitlines()[-1]
-    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-
-    return pieces
-
-
-def plus_or_dot(pieces):
-    """Return a + if we don't already have one, else return a ."""
-    if "+" in pieces.get("closest-tag", ""):
-        return "."
-    return "+"
-
-
-def render_pep440(pieces):
-    """Build up version string, with post-release "local version identifier".
-
-    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
-    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
-    Exceptions:
-    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += plus_or_dot(pieces)
-            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
-                                          pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def render_pep440_branch(pieces):
-    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
-
-    The ".dev0" means not master branch. Note that .dev0 sorts backwards
-    (a feature branch will appear "older" than the master branch).
-
-    Exceptions:
-    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            if pieces["branch"] != "master":
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0"
-        if pieces["branch"] != "master":
-            rendered += ".dev0"
-        rendered += "+untagged.%d.g%s" % (pieces["distance"],
-                                          pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def pep440_split_post(ver):
-    """Split pep440 version string at the post-release segment.
-
-    Returns the release segments before the post-release and the
-    post-release version number (or -1 if no post-release segment is present).
-    """
-    vc = str.split(ver, ".post")
-    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
-
-
-def render_pep440_pre(pieces):
-    """TAG[.postN.devDISTANCE] -- No -dirty.
-
-    Exceptions:
-    1: no tags. 0.post0.devDISTANCE
-    """
-    if pieces["closest-tag"]:
-        if pieces["distance"]:
-            # update the post release segment
-            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
-            rendered = tag_version
-            if post_version is not None:
-                rendered += ".post%d.dev%d" % (post_version+1, pieces["distance"])
-            else:
-                rendered += ".post0.dev%d" % (pieces["distance"])
-        else:
-            # no commits, use the tag as the version
-            rendered = pieces["closest-tag"]
-    else:
-        # exception #1
-        rendered = "0.post0.dev%d" % pieces["distance"]
-    return rendered
-
-
-def render_pep440_post(pieces):
-    """TAG[.postDISTANCE[.dev0]+gHEX] .
-
-    The ".dev0" means dirty. Note that .dev0 sorts backwards
-    (a dirty tree will appear "older" than the corresponding clean one),
-    but you shouldn't be releasing software with -dirty anyways.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%s" % pieces["short"]
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-        rendered += "+g%s" % pieces["short"]
-    return rendered
-
-
-def render_pep440_post_branch(pieces):
-    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
-
-    The ".dev0" means not master branch.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["branch"] != "master":
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%s" % pieces["short"]
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["branch"] != "master":
-            rendered += ".dev0"
-        rendered += "+g%s" % pieces["short"]
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def render_pep440_old(pieces):
-    """TAG[.postDISTANCE[.dev0]] .
-
-    The ".dev0" means dirty.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-    return rendered
-
-
-def render_git_describe(pieces):
-    """TAG[-DISTANCE-gHEX][-dirty].
-
-    Like 'git describe --tags --dirty --always'.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"]:
-            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render_git_describe_long(pieces):
-    """TAG-DISTANCE-gHEX[-dirty].
-
-    Like 'git describe --tags --dirty --always -long'.
-    The distance/hash is unconditional.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render(pieces, style):
-    """Render the given version pieces into the requested style."""
-    if pieces["error"]:
-        return {"version": "unknown",
-                "full-revisionid": pieces.get("long"),
-                "dirty": None,
-                "error": pieces["error"],
-                "date": None}
-
-    if not style or style == "default":
-        style = "pep440"  # the default
-
-    if style == "pep440":
-        rendered = render_pep440(pieces)
-    elif style == "pep440-branch":
-        rendered = render_pep440_branch(pieces)
-    elif style == "pep440-pre":
-        rendered = render_pep440_pre(pieces)
-    elif style == "pep440-post":
-        rendered = render_pep440_post(pieces)
-    elif style == "pep440-post-branch":
-        rendered = render_pep440_post_branch(pieces)
-    elif style == "pep440-old":
-        rendered = render_pep440_old(pieces)
-    elif style == "git-describe":
-        rendered = render_git_describe(pieces)
-    elif style == "git-describe-long":
-        rendered = render_git_describe_long(pieces)
-    else:
-        raise ValueError("unknown style '%s'" % style)
-
-    return {"version": rendered, "full-revisionid": pieces["long"],
-            "dirty": pieces["dirty"], "error": None,
-            "date": pieces.get("date")}
-
-
-def get_versions():
-    """Get version information or return default if unable to do so."""
-    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
-    # __file__, we can work backwards from there to the root. Some
-    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
-    # case we can only use expanded keywords.
-
-    cfg = get_config()
-    verbose = cfg.verbose
-
-    try:
-        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
-                                          verbose)
-    except NotThisMethod:
-        pass
-
-    try:
-        root = os.path.realpath(__file__)
-        # versionfile_source is the relative path from the top of the source
-        # tree (where the .git directory might live) to this file. Invert
-        # this to find the root from __file__.
-        for _ in cfg.versionfile_source.split('/'):
-            root = os.path.dirname(root)
-    except NameError:
-        return {"version": "0+unknown", "full-revisionid": None,
-                "dirty": None,
-                "error": "unable to find root of source tree",
-                "date": None}
-
-    try:
-        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
-        return render(pieces, cfg.style)
-    except NotThisMethod:
-        pass
-
-    try:
-        if cfg.parentdir_prefix:
-            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
-    except NotThisMethod:
-        pass
-
-    return {"version": "0+unknown", "full-revisionid": None,
-            "dirty": None,
-            "error": "unable to compute version", "date": None}
diff --git a/src/conda_package_handling/api.py b/src/conda_package_handling/api.py
index a06ed00c..39fc08ed 100644
--- a/src/conda_package_handling/api.py
+++ b/src/conda_package_handling/api.py
@@ -1,21 +1,34 @@
+from __future__ import annotations
+
 import functools as _functools
 import os as _os
-import tempfile as _tempfile
+import warnings as _warnings
 from glob import glob as _glob
 
-import tqdm as _tqdm
-
-from .conda_fmt import CondaFormat_v2 as _CondaFormat_v2
-
 # expose these two exceptions as part of the API.  Everything else should feed into these.
 from .exceptions import ConversionError, InvalidArchiveError  # NOQA
-from .tarball import CondaTarBZ2 as _CondaTarBZ2  # NOQA
-from .tarball import libarchive_enabled
-from .utils import TemporaryDirectory as _TemporaryDirectory
+from .interface import AbstractBaseFormat
+from .tarball import CondaTarBZ2 as _CondaTarBZ2
+from .utils import filter_info_files
 from .utils import get_executor as _get_executor
 from .utils import rm_rf as _rm_rf
 
-SUPPORTED_EXTENSIONS = {".tar.bz2": _CondaTarBZ2, ".conda": _CondaFormat_v2}
+SUPPORTED_EXTENSIONS: dict[str, type[AbstractBaseFormat]] = {".tar.bz2": _CondaTarBZ2}
+
+libarchive_enabled = False  #: Old API meaning "can extract .conda" (now without libarchive)
+
+try:
+    from .conda_fmt import ZSTD_COMPRESS_LEVEL, ZSTD_COMPRESS_THREADS
+    from .conda_fmt import CondaFormat_v2 as _CondaFormat_v2
+
+    SUPPORTED_EXTENSIONS[".conda"] = _CondaFormat_v2
+
+    libarchive_enabled = True
+
+except ImportError:
+    _warnings.warn("Install zstandard Python bindings for .conda support")
+
+THREADSAFE_EXTRACT = True  #: Not present in conda-package-handling<2.0.
 
 
 def _collect_paths(prefix):
@@ -52,7 +65,8 @@ def extract(fn, dest_dir=None, components=None, prefix=None):
             dest_dir = _os.path.normpath(_os.path.join(prefix or _os.getcwd(), dest_dir))
     else:
         dest_dir = _os.path.join(
-            prefix or _os.path.dirname(fn), get_default_extracted_folder(fn, abspath=False)
+            prefix or _os.path.dirname(fn),
+            get_default_extracted_folder(fn, abspath=False),
         )
 
     if not _os.path.isdir(dest_dir):
@@ -73,9 +87,14 @@ def extract(fn, dest_dir=None, components=None, prefix=None):
 def create(prefix, file_list, out_fn, out_folder=None, **kw):
     if not out_folder:
         out_folder = _os.getcwd()
+
+    # simplify arguments to format.create()
+    if _os.path.isabs(out_fn):
+        out_folder = _os.path.dirname(out_fn)
+        out_fn = _os.path.basename(out_fn)
+
     if file_list is None:
         file_list = _collect_paths(prefix)
-
     elif isinstance(file_list, str):
         try:
             with open(file_list) as f:
@@ -90,10 +109,11 @@ def create(prefix, file_list, out_fn, out_folder=None, **kw):
             try:
                 out = format.create(prefix, file_list, out_fn, out_folder, **kw)
                 break
-            except Exception as err:
+            except BaseException as err:
                 # don't leave broken files around
-                if out and _os.path.isfile(out):
-                    _rm_rf(out)
+                abs_out_fn = _os.path.join(out_folder, out_fn)
+                if _os.path.isfile(abs_out_fn):
+                    _rm_rf(abs_out_fn)
                 raise err
     else:
         raise ValueError(
@@ -105,30 +125,77 @@ def create(prefix, file_list, out_fn, out_folder=None, **kw):
     return out
 
 
-def _convert(fn, out_ext, out_folder, **kw):
+def _convert(
+    fn,
+    out_ext,
+    out_folder,
+    force=False,
+    zstd_compress_level=None,
+    zstd_compress_threads=None,
+    **kw,
+):
+    # allow package to work in degraded mode when zstandard is not available
+    import conda_package_streaming.transmute
+    import zstandard
+
     basename = get_default_extracted_folder(fn, abspath=False)
-    from .validate import validate_converted_files_match
+    from .validate import validate_converted_files_match_streaming
 
     if not basename:
-        print(
+        return (
+            fn,
+            "",
             "Input file %s doesn't have a supported extension (%s), skipping it"
-            % (fn, SUPPORTED_EXTENSIONS)
+            % (fn, SUPPORTED_EXTENSIONS),
         )
-        return
-    out_fn = _os.path.join(out_folder, basename + out_ext)
+    out_fn = str(_os.path.join(out_folder, basename + out_ext))
     errors = ""
-    if not _os.path.lexists(out_fn) or ("force" in kw and kw["force"]):
-        with _TemporaryDirectory(dir=out_folder) as tmp:
-            try:
-                extract(fn, dest_dir=tmp)
-                file_list = _collect_paths(tmp)
-
-                create(tmp, file_list, _os.path.basename(out_fn), out_folder=out_folder, **kw)
-                _, missing_files, mismatching_sizes = validate_converted_files_match(tmp, out_fn)
-                if missing_files or mismatching_sizes:
-                    errors = str(ConversionError(missing_files, mismatching_sizes))
-            except Exception as e:
-                errors = str(e)
+    if not _os.path.lexists(out_fn) or force:
+        if force and _os.path.lexists(out_fn):
+            _os.unlink(out_fn)
+
+        if out_ext == ".conda":
+            # ZSTD_COMPRESS_* constants are only defined if we have zstandard
+            if zstd_compress_level is None:
+                zstd_compress_level = ZSTD_COMPRESS_LEVEL
+            if zstd_compress_threads is None:
+                zstd_compress_threads = ZSTD_COMPRESS_THREADS
+
+            def compressor():
+                return zstandard.ZstdCompressor(
+                    level=zstd_compress_level, threads=zstd_compress_threads
+                )
+
+            def is_info(filename):
+                return filter_info_files([filename], prefix=".") == []
+
+            transmute = _functools.partial(
+                conda_package_streaming.transmute.transmute,
+                fn,
+                out_folder,
+                compressor=compressor,
+                is_info=is_info,
+            )
+
+        else:
+            transmute = _functools.partial(
+                conda_package_streaming.transmute.transmute_tar_bz2, fn, out_folder
+            )
+
+        try:
+            transmute()
+            result = validate_converted_files_match_streaming(out_fn, fn)
+            _, missing_files, mismatching_sizes = result
+            if missing_files or mismatching_sizes:
+                errors = str(ConversionError(missing_files, mismatching_sizes))
+        except BaseException as e:
+            # don't leave partial package around
+            if _os.path.isfile(out_fn):
+                _rm_rf(out_fn)
+            if not isinstance(e, Exception):
+                raise
+            errors = str(e)
+
     return fn, out_fn, errors
 
 
@@ -143,50 +210,15 @@ def transmute(in_file, out_ext, out_folder=None, processes=1, **kw):
         flist = flist - set(_glob(in_file.replace(".conda", out_ext)))
 
     failed_files = {}
-    with _tqdm.tqdm(total=len(flist), leave=False) as t:
-        with _get_executor(processes) as executor:
-            convert_f = _functools.partial(_convert, out_ext=out_ext, out_folder=out_folder, **kw)
-            for fn, out_fn, errors in executor.map(convert_f, flist):
-                t.set_description("Converted: %s" % fn)
-                t.update()
-                if errors:
-                    failed_files[fn] = errors
-                    _rm_rf(out_fn)
+    with _get_executor(processes) as executor:
+        convert_f = _functools.partial(_convert, out_ext=out_ext, out_folder=out_folder, **kw)
+        for fn, out_fn, errors in executor.map(convert_f, flist):
+            if errors:
+                failed_files[fn] = errors
+                _rm_rf(out_fn)
     return failed_files
 
 
-def verify_conversion(
-    glob_pattern, target_dir, reference_ext, tmpdir_root=_tempfile.gettempdir(), processes=None
-):
-    from .validate import validate_converted_files_match
-
-    if not glob_pattern.endswith(reference_ext):
-        glob_pattern = glob_pattern + reference_ext
-    file_sets_by_ext = {
-        ext: _glob(_os.path.join(target_dir, glob_pattern + ext)) for ext in SUPPORTED_EXTENSIONS
-    }
-    matches = {path.replace(ext, "") for ext, path in file_sets_by_ext[reference_ext]}
-    for ext, paths in file_sets_by_ext.items():
-        if ext == reference_ext:
-            continue
-        matches &= {path.replace(ext, "") for ext, path in paths}
-    other_exts = set(SUPPORTED_EXTENSIONS) - {reference_ext}
-
-    errors = {}
-    with _tqdm.tqdm(total=(len(matches) * len(SUPPORTED_EXTENSIONS) - 1), leave=False) as t:
-        with _Executor(max_workers=processes) as executor:
-            for other_ext in other_exts:
-                verify_fn = lambda fn: validate_converted_files_match(
-                    ref_ext=reference_ext, subject=fn + other_ext
-                )
-                for fn, missing, mismatching in executor.map(verify_fn, matches):
-                    t.set_description("Validating %s" % fn)
-                    t.update()
-                    if missing or mismatching:
-                        errors[fn] = str(ConversionError(missing, mismatching))
-    return errors
-
-
 def get_pkg_details(in_file):
     """For the new pkg format, we return the size and hashes of the inner pkg part of the file"""
     for format in SUPPORTED_EXTENSIONS.values():
@@ -194,5 +226,5 @@ def get_pkg_details(in_file):
             details = format.get_pkg_details(in_file)
             break
     else:
-        raise ValueError("Don't know what to do with file {}".format(in_file))
+        raise ValueError(f"Don't know what to do with file {in_file}")
     return details
diff --git a/src/conda_package_handling/archive_utils.py b/src/conda_package_handling/archive_utils.py
deleted file mode 100644
index 38026e43..00000000
--- a/src/conda_package_handling/archive_utils.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from . import archive_utils_cy
-from .exceptions import ArchiveCreationError, InvalidArchiveError
-
-
-def _to_bytes(s):
-    if isinstance(s, bytes):
-        return s
-    return s.encode("utf-8")
-
-
-def extract_file(tarball):
-    tarball_bytes = _to_bytes(tarball)
-    result, error_str = archive_utils_cy.extract_file(tarball_bytes)
-    if result:
-        raise InvalidArchiveError(tarball, error_str.decode("utf-8"))
-
-
-def create_archive(fullpath, files, compression_filter, compression_opts):
-    fullpath = _to_bytes(fullpath)
-    compression_filter = _to_bytes(compression_filter)
-    compression_opts = _to_bytes(compression_opts)
-    files = [_to_bytes(f) for f in files]
-    result, error_str, error_file = archive_utils_cy.create_archive(
-        fullpath, files, compression_filter, compression_opts
-    )
-    if result:
-        message = error_str.decode("utf-8")
-        if len(error_file):
-            message += " while writing file: " + error_file.decode("utf-8")
-        raise ArchiveCreationError(message)
diff --git a/src/conda_package_handling/archive_utils_c.c b/src/conda_package_handling/archive_utils_c.c
deleted file mode 100644
index 8888eee7..00000000
--- a/src/conda_package_handling/archive_utils_c.c
+++ /dev/null
@@ -1,207 +0,0 @@
-// Utilities for creating and extracting files from archives using libarchive
-// for use in conda-packaging-handling
-#define LIBARCHIVE_STATIC
-
-#include <fcntl.h>
-#include <archive.h>
-#include <archive_entry.h>
-
-#ifndef O_BINARY
-#define O_BINARY    0
-#endif
-
-const char * checked_strdup(const char * err) {
-    if (err == NULL) {
-        return NULL;
-    }
-    return strdup(err);
-}
-
-struct archive * prepare_gnutar_archive(
-    const char *outname, const char *filtername, const char *opts, const char **err_str)
-{
-    struct archive *a;
-    if (!err_str) {
-        return NULL;
-    }
-    a = archive_write_new();
-    if (a == NULL) {
-        return a;
-    }
-    if (archive_write_set_format_gnutar(a) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(a));
-        archive_write_close(a);
-        archive_write_free(a);
-        return NULL;
-    }
-    if (archive_write_add_filter_by_name(a, filtername) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(a));
-        archive_write_close(a);
-        archive_write_free(a);
-        return NULL;
-    }
-    if (archive_write_set_options(a, opts) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(a));
-        archive_write_close(a);
-        archive_write_free(a);
-        return NULL;
-    }
-    if (archive_write_open_filename(a, outname) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(a));
-        archive_write_close(a);
-        archive_write_free(a);
-        return NULL;
-    }
-    return a;
-}
-
-void close_archive(struct archive *a) {
-    archive_write_close(a);
-    archive_write_free(a);
-}
-
-struct archive_entry * prepare_entry(void) {
-    struct archive_entry *entry;
-    entry = archive_entry_new();
-    return entry;
-}
-
-void close_entry(struct archive_entry *entry) {
-    archive_entry_free(entry);
-}
-
-static int add_file(
-    struct archive *a, struct archive_entry *entry, const char *filename, const char **err_str)
-{
-    struct archive *disk;
-    char buff[8192];
-    int len;
-    int fd;
-    int flags;
-    flags = 0;
-
-    disk = archive_read_disk_new();
-    if (disk == NULL) {
-        return 1;
-    }
-    if (archive_read_disk_set_behavior(disk, flags) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(disk));
-        return 1;
-    }
-    if (archive_read_disk_open(disk, filename) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(disk));
-        return 1;
-    }
-    if (archive_read_next_header2(disk, entry) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(disk));
-        return 1;
-    }
-    if (archive_read_disk_descend(disk) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(disk));
-        return 1;
-    }
-    if (archive_write_header(a, entry) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(a));
-        return 1;
-    }
-    fd = open(filename, O_RDONLY | O_BINARY);
-    len = read(fd, buff, sizeof(buff));
-    while ( len > 0 ) {
-        archive_write_data(a, buff, len);
-        len = read(fd, buff, sizeof(buff));
-    }
-    close(fd);
-    if (archive_write_finish_entry(a) < ARCHIVE_OK) {
-        *err_str = checked_strdup(archive_error_string(a));
-        return 1;
-    }
-    archive_read_close(disk);
-    archive_read_free(disk);
-    archive_entry_clear(entry);
-    return 0;
-}
-
-
-static int copy_data(struct archive *ar, struct archive *aw)
-{
-    int r;
-    const void *buff;
-    size_t size;
-    la_int64_t offset;
-    for (;;) {
-        r = archive_read_data_block(ar, &buff, &size, &offset);
-        if (r == ARCHIVE_EOF)
-            return (ARCHIVE_OK);
-        if (r < ARCHIVE_OK)
-            return (r);
-        r = archive_write_data_block(aw, buff, size, (size_t)offset);
-        if (r < ARCHIVE_OK) {
-            return (r);
-    }
-  }
-}
-
-static int extract_file_c(const char *filename, const char **err_str) {
-    struct archive *a;
-    struct archive *ext;
-    struct archive_entry *entry;
-    int flags;
-    int r;
-
-    if (!err_str) {
-        return 0;
-    }
-    /* attributes we want to restore. */
-    flags = ARCHIVE_EXTRACT_TIME;
-    flags |= ARCHIVE_EXTRACT_PERM;
-    flags |= ARCHIVE_EXTRACT_SECURE_NODOTDOT;
-    flags |= ARCHIVE_EXTRACT_SECURE_SYMLINKS;
-    flags |= ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS;
-    flags |= ARCHIVE_EXTRACT_SPARSE;
-    flags |= ARCHIVE_EXTRACT_UNLINK;
-
-    a = archive_read_new();
-    archive_read_support_format_all(a);
-    archive_read_support_filter_all(a);
-    ext = archive_write_disk_new();
-    archive_write_disk_set_options(ext, flags);
-    archive_write_disk_set_standard_lookup(ext);
-    if ((r = archive_read_open_filename(a, filename, 10240))) {
-        *err_str = checked_strdup(archive_error_string(a));
-        return 1;
-    }
-    for (;;) {
-        r = archive_read_next_header(a, &entry);
-        if (r == ARCHIVE_EOF)
-            break;
-        if (r < ARCHIVE_WARN) {
-            *err_str = checked_strdup(archive_error_string(a));
-            return 1;
-        }
-        r = archive_write_header(ext, entry);
-        if (r < ARCHIVE_OK) {
-            *err_str = checked_strdup(archive_error_string(ext));
-            return 1;
-        }
-        else if (archive_entry_size(entry) > 0) {
-            r = copy_data(a, ext);
-            if (r < ARCHIVE_WARN) {
-                *err_str = checked_strdup(archive_error_string(ext));
-                if (*err_str == NULL) {
-                  *err_str = checked_strdup(archive_error_string(a));
-                }
-                return 1;
-            }
-        }
-        r = archive_write_finish_entry(ext);
-        if (r < ARCHIVE_WARN) {
-            *err_str = checked_strdup(archive_error_string(ext));
-            return 1;
-        }
-    }
-    archive_read_close(a);
-    archive_read_free(a);
-    archive_write_close(ext);
-    archive_write_free(ext);
-    return 0;
-}
diff --git a/src/conda_package_handling/archive_utils_cy.pyx b/src/conda_package_handling/archive_utils_cy.pyx
deleted file mode 100644
index 8cd7ff2e..00000000
--- a/src/conda_package_handling/archive_utils_cy.pyx
+++ /dev/null
@@ -1,41 +0,0 @@
-# Cython wrapper around archive_utils_c
-
-
-cdef extern from "archive_utils_c.c":
-    void * prepare_gnutar_archive(
-        const char *outname, const char *filtername, const char *opts, const char **err_str)
-    void close_archive(void *a)
-    void * prepare_entry()
-    void close_entry(void *entry)
-    int add_file(void *a, void *entry, const char *filename, const char **err_str)
-    int extract_file_c(const char *filename, const char **err_str)
-
-
-def extract_file(tarball):
-    """Extract a tarball into the current directory."""
-    cdef const char *err_str = NULL
-    result = extract_file_c(tarball, &err_str)
-    if result:
-        assert err_str != NULL
-        return 1, <bytes> err_str
-    return 0, b''
-
-
-def create_archive(fullpath, files, compression_filter, compression_opts):
-    """ Create a compressed gnutar archive. """
-    cdef void *a
-    cdef void *entry
-    cdef const char *err_str = NULL
-    a = prepare_gnutar_archive(fullpath, compression_filter, compression_opts, &err_str)
-    if a == NULL:
-        return 1, <bytes> err_str, b''
-    entry = prepare_entry()
-    if entry == NULL:
-        return 1, b'archive entry creation failed', b''
-    for f in files:
-        result = add_file(a, entry, f, &err_str)
-        if result:
-            return 1, <bytes> err_str, f
-    close_entry(entry)
-    close_archive(a)
-    return 0, b'', b''
diff --git a/src/conda_package_handling/cli.py b/src/conda_package_handling/cli.py
index 6006fd73..addacc2c 100644
--- a/src/conda_package_handling/cli.py
+++ b/src/conda_package_handling/cli.py
@@ -50,7 +50,8 @@ def parse_args(parse_this=None):
         "conda package",
     )
     create_parser.add_argument(
-        "out_fn", help="Filename of archive to be created.  Extension " "determines package type."
+        "out_fn",
+        help="Filename of archive to be created.  Extension determines package type.",
     )
     create_parser.add_argument(
         "--file-list",
@@ -60,42 +61,15 @@ def parse_args(parse_this=None):
     )
     create_parser.add_argument("--out-folder", help="Folder to dump final archive to")
 
-    verify_parser = sp.add_parser(
-        "verify", help="verify converted files against their reference", aliases=["v"]
-    )
-    verify_parser.add_argument(
-        "glob",
-        help="filename glob pattern to match pairs and verify.  Use"
-        "the --reference-ext argument to change which extension is used "
-        "as the ground truth, and which is considered corrupt in any "
-        "mismatch",
-    )
-    verify_parser.add_argument(
-        "--target-dir",
-        help="folder for finding pairs of files.  Defaults " "to cwd.",
-        default=os.getcwd(),
-    )
-    verify_parser.add_argument(
-        "--reference-ext",
-        "-r",
-        help="file extension to consider as "
-        "'ground truth' in comparison.  Use this with the --all flag.",
-        default=".tar.bz2",
-    )
-    verify_parser.add_argument(
-        "--processes",
-        type=int,
-        help="Max number of processes to use.  If " "not set, defaults to your CPU count.",
-    )
-
     convert_parser = sp.add_parser(
         "transmute", help="convert from one package type to another", aliases=["t"]
     )
     convert_parser.add_argument(
-        "in_file", help="existing file to convert from.  Glob patterns " "accepted."
+        "in_file", help="existing file to convert from.  Glob patterns accepted."
     )
     convert_parser.add_argument(
-        "out_ext", help="extension of file to convert to.  " "Examples: .tar.bz2, .conda"
+        "out_ext",
+        help="extension of file to convert to.  Examples: .tar.bz2, .conda",
     )
     convert_parser.add_argument("--out-folder", help="Folder to dump final archive to")
     convert_parser.add_argument(
@@ -104,7 +78,7 @@ def parse_args(parse_this=None):
     convert_parser.add_argument(
         "--processes",
         type=int,
-        help="Max number of processes to use.  If " "not set, defaults to your CPU count.",
+        help="Max number of processes to use.  If not set, defaults to 1.",
     )
     convert_parser.add_argument(
         "--zstd-compression-level",
@@ -113,9 +87,18 @@ def parse_args(parse_this=None):
             "conda-package-handling. Defaults to the maximum."
         ),
         type=int,
-        choices=range(1, 22),
+        choices=range(1, 23),
         default=22,
     )
+    convert_parser.add_argument(
+        "--zstd-compression-threads",
+        help=(
+            "When building v2 packages, set the compression threads used by "
+            "conda-package-handling. Defaults to 1. -1=automatic."
+        ),
+        type=int,
+        default=1,
+    )
     return parser.parse_args(parse_this)
 
 
@@ -139,23 +122,14 @@ def main(args=None):
             args.out_folder,
             args.processes or 1,
             force=args.force,
-            compression_tuple=(
-                ".tar.zst",
-                "zstd",
-                f"zstd:compression-level={args.zstd_compression_level}",
-            ),
+            zstd_compress_level=args.zstd_compression_level,
+            zstd_compress_threads=args.zstd_compression_threads,
         )
         if failed_files:
             print("failed files:")
             pprint(failed_files)
             sys.exit(1)
-    elif args.subcommand in ("verify", "v"):
-        failed_files = api.verify_conversion(args.glob, args.target_dir, args.reference_ext)
-        if failed_files:
-            print("failed files:")
-            pprint(failed_files)
-            sys.exit(1)
 
 
-if __name__ == "__main__":
+if __name__ == "__main__":  # pragma: no cover
     main(args=None)
diff --git a/src/conda_package_handling/conda_fmt.py b/src/conda_package_handling/conda_fmt.py
index d781f503..b09a1b9c 100644
--- a/src/conda_package_handling/conda_fmt.py
+++ b/src/conda_package_handling/conda_fmt.py
@@ -1,39 +1,30 @@
-"""The 'new' conda format, introduced in late 2018/early 2019.  Spec at
-https://anaconda.atlassian.net/wiki/spaces/AD/pages/90210540/Conda+package+format+v2"""
+"""
+The 'new' conda format, introduced in late 2018/early 2019.
+
+https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/packages.html
+"""
+
+from __future__ import annotations
 
 import json
 import os
-from tempfile import NamedTemporaryFile
-from zipfile import ZIP_STORED, BadZipFile, ZipFile
+import tarfile
+from typing import Callable
+from zipfile import ZIP_STORED, ZipFile
+
+import zstandard
 
 from . import utils
-from .exceptions import InvalidArchiveError
 from .interface import AbstractBaseFormat
-from .tarball import _tar_xf, create_compressed_tarball
+from .streaming import _extract
 
 CONDA_PACKAGE_FORMAT_VERSION = 2
 DEFAULT_COMPRESSION_TUPLE = (".tar.zst", "zstd", "zstd:compression-level=22")
 
-
-def _lookup_component_filename(zf, file_id, component_name):
-    contents = zf.namelist()
-    component_filename_without_ext = "-".join((component_name, file_id))
-    component_filename = [_ for _ in contents if _.startswith(component_filename_without_ext)]
-    return component_filename
-
-
-def _extract_component(fn, file_id, component_name, dest_dir):
-    try:
-        with ZipFile(fn, compression=ZIP_STORED) as zf:
-            with utils.TemporaryDirectory(dir=dest_dir) as tmpdir:
-                component_filename = _lookup_component_filename(zf, file_id, component_name)
-                if not component_filename:
-                    raise RuntimeError("didn't find {} component in {}".format(component_name, fn))
-                component_filename = component_filename[0]
-                zf.extract(component_filename, tmpdir)
-                _tar_xf(os.path.join(tmpdir, component_filename), dest_dir)
-    except BadZipFile as e:
-        raise InvalidArchiveError(fn, str(e))
+# increase to reduce speed and increase compression (22 = conda's default)
+ZSTD_COMPRESS_LEVEL = 22
+# increase to reduce compression (slightly) and increase speed
+ZSTD_COMPRESS_THREADS = 1
 
 
 class CondaFormat_v2(AbstractBaseFormat):
@@ -47,47 +38,78 @@ def supported(fn):
     @staticmethod
     def extract(fn, dest_dir, **kw):
         components = utils.ensure_list(kw.get("components")) or ("info", "pkg")
-        file_id = os.path.basename(fn).replace(".conda", "")
         if not os.path.isabs(fn):
             fn = os.path.normpath(os.path.join(os.getcwd(), fn))
         if not os.path.isdir(dest_dir):
             os.makedirs(dest_dir)
-        for component in components:
-            _extract_component(fn, file_id, component, dest_dir)
+
+        _extract(str(fn), str(dest_dir), components=components)
 
     @staticmethod
     def extract_info(fn, dest_dir=None):
         return CondaFormat_v2.extract(fn, dest_dir, components=["info"])
 
     @staticmethod
-    def create(prefix, file_list, out_fn, out_folder=os.getcwd(), **kw):
+    def create(
+        prefix,
+        file_list,
+        out_fn,
+        out_folder=os.getcwd(),
+        compressor: Callable[[], zstandard.ZstdCompressor] | None = None,
+        compression_tuple=(None, None, None),
+    ):
         if os.path.isabs(out_fn):
             out_folder = os.path.dirname(out_fn)
             out_fn = os.path.basename(out_fn)
         conda_pkg_fn = os.path.join(out_folder, out_fn)
-        out_fn = out_fn.replace(".conda", "")
+        file_id = out_fn.replace(".conda", "")
         pkg_files = utils.filter_info_files(file_list, prefix)
-        info_files = set(file_list) - set(pkg_files)
-        ext, comp_filter, filter_opts = kw.get("compression_tuple") or DEFAULT_COMPRESSION_TUPLE
+        # preserve order
+        pkg_files_set = set(pkg_files)
+        info_files = list(f for f in file_list if f not in pkg_files_set)
 
-        with utils.TemporaryDirectory(dir=out_folder) as tmpdir:
-            info_tarball = create_compressed_tarball(
-                prefix, info_files, tmpdir, "info-" + out_fn, ext, comp_filter, filter_opts
-            )
-            pkg_tarball = create_compressed_tarball(
-                prefix, pkg_files, tmpdir, "pkg-" + out_fn, ext, comp_filter, filter_opts
+        if compressor and (compression_tuple != (None, None, None)):
+            raise ValueError("Supply one of compressor= or (deprecated) compression_tuple=")
+
+        if compressor is None:
+            compressor = lambda: zstandard.ZstdCompressor(
+                level=ZSTD_COMPRESS_LEVEL,
+                threads=ZSTD_COMPRESS_THREADS,
             )
 
+            # legacy libarchive-ish compatibility
+            ext, comp_filter, filter_opts = compression_tuple
+            if filter_opts and filter_opts.startswith("zstd:compression-level="):
+                compressor = lambda: zstandard.ZstdCompressor(
+                    level=int(filter_opts.split("=", 1)[-1]),
+                    threads=ZSTD_COMPRESS_THREADS,
+                )
+
+        with ZipFile(conda_pkg_fn, "w", compression=ZIP_STORED) as conda_file, utils.tmp_chdir(
+            prefix
+        ):
+
             pkg_metadata = {"conda_pkg_format_version": CONDA_PACKAGE_FORMAT_VERSION}
+            conda_file.writestr("metadata.json", json.dumps(pkg_metadata))
+
+            components_files = (f"pkg-{file_id}.tar.zst", pkg_files), (
+                f"info-{file_id}.tar.zst",
+                info_files,
+            )
+
+            # put the info last, for parity with updated transmute.
+            for component, files in components_files:
+                compress = compressor()
+                with conda_file.open(component, "w") as component_file:
+                    component_stream = compress.stream_writer(component_file, closefd=False)
+                    component_tar = tarfile.TarFile(fileobj=component_stream, mode="w")
+
+                    for file in files:
+                        component_tar.add(file, filter=utils.anonymize_tarinfo)
+
+                    component_tar.close()
+                    component_stream.close()
 
-            with ZipFile(conda_pkg_fn, "w", compression=ZIP_STORED) as zf:
-                with NamedTemporaryFile(mode="w", delete=False) as tf:
-                    json.dump(pkg_metadata, tf)
-                    tf.flush()
-                    zf.write(tf.name, "metadata.json")
-                for pkg in (info_tarball, pkg_tarball):
-                    zf.write(pkg, os.path.basename(pkg))
-                utils.rm_rf(tf.name)
         return conda_pkg_fn
 
     @staticmethod
diff --git a/src/conda_package_handling/exceptions.py b/src/conda_package_handling/exceptions.py
index b03e6b50..1f3be929 100644
--- a/src/conda_package_handling/exceptions.py
+++ b/src/conda_package_handling/exceptions.py
@@ -7,10 +7,10 @@ class InvalidArchiveError(Exception):
     def __init__(self, fn, msg, *args, **kw):
         msg = (
             "Error with archive %s.  You probably need to delete and re-download "
-            "or re-create this file.  Message from libarchive was:\n\n%s" % (fn, msg)
+            "or re-create this file.  Message was:\n\n%s" % (fn, msg)
         )
         self.errno = ENOENT
-        super(InvalidArchiveError, self).__init__(msg)
+        super().__init__(msg)
 
 
 class ArchiveCreationError(Exception):
@@ -19,7 +19,7 @@ class ArchiveCreationError(Exception):
     pass
 
 
-class CaseInsensitiveFileSystemError(Exception):
+class CaseInsensitiveFileSystemError(InvalidArchiveError):
     def __init__(self, package_location, extract_location, **kwargs):
         message = """
         Cannot extract package to a case-insensitive file system. Your install
@@ -32,9 +32,9 @@ def __init__(self, package_location, extract_location, **kwargs):
           package location: %(package_location)s
           extract location: %(extract_location)s
         """
-        super(CaseInsensitiveFileSystemError, self).__init__(
-            message, package_location=package_location, extract_location=extract_location, **kwargs
-        )
+        self.package_location = package_location
+        self.extract_location = extract_location
+        super().__init__(package_location, message, **kwargs)
 
 
 class ConversionError(Exception):
@@ -49,4 +49,4 @@ def __init__(self, missing_files, mismatching_sizes, *args, **kw):
             + "Mismatching sizes (corruption) in converted package: %s" % self.mismatching_sizes
         )
 
-        super(ConversionError, self).__init__(errors, *args, **kw)
+        super().__init__(errors, *args, **kw)
diff --git a/src/conda_package_handling/interface.py b/src/conda_package_handling/interface.py
index d26d2aec..b5668c2f 100644
--- a/src/conda_package_handling/interface.py
+++ b/src/conda_package_handling/interface.py
@@ -5,20 +5,20 @@
 class AbstractBaseFormat(metaclass=abc.ABCMeta):
     @staticmethod
     @abc.abstractmethod
-    def supported(fn):
+    def supported(fn):  # pragma: no cover
         return False
 
     @staticmethod
     @abc.abstractmethod
-    def extract(fn, dest_dir, **kw):
+    def extract(fn, dest_dir, **kw):  # pragma: no cover
         raise NotImplementedError
 
     @staticmethod
     @abc.abstractmethod
-    def create(prefix, file_list, out_fn, out_folder=os.getcwd(), **kw):
+    def create(prefix, file_list, out_fn, out_folder=os.getcwd(), **kw):  # pragma: no cover
         raise NotImplementedError
 
     @staticmethod
     @abc.abstractmethod
-    def get_pkg_details(in_file):
+    def get_pkg_details(in_file):  # pragma: no cover
         raise NotImplementedError
diff --git a/src/conda_package_handling/streaming.py b/src/conda_package_handling/streaming.py
new file mode 100644
index 00000000..fde2ef5b
--- /dev/null
+++ b/src/conda_package_handling/streaming.py
@@ -0,0 +1,42 @@
+"""
+Exception-compatible adapter from conda_package_streaming.
+"""
+
+from __future__ import annotations
+
+from tarfile import TarError
+from zipfile import BadZipFile
+
+from conda_package_streaming.extract import exceptions as cps_exceptions
+from conda_package_streaming.extract import extract_stream, package_streaming
+
+from . import exceptions
+
+
+def _extract(filename: str, dest_dir: str, components: list[str]):
+    """
+    Extract .conda or .tar.bz2 package to dest_dir.
+
+    If it's a conda package, components may be ["pkg", "info"]
+
+    If it's a .tar.bz2 package, components must equal ["pkg"]
+
+    Internal. Skip directly to conda-package-streaming if you don't need
+    exception compatibility.
+    """
+
+    if str(filename).endswith(".tar.bz2"):
+        assert components == ["pkg"]
+
+    try:
+        with open(filename, "rb") as fileobj:
+            for component in components:
+                # will parse zipfile twice
+                stream = package_streaming.stream_conda_component(
+                    filename, fileobj, component=component
+                )
+                extract_stream(stream, dest_dir)
+    except cps_exceptions.CaseInsensitiveFileSystemError as e:
+        raise exceptions.CaseInsensitiveFileSystemError(filename, dest_dir) from e
+    except (OSError, TarError, BadZipFile) as e:
+        raise exceptions.InvalidArchiveError(filename, f"failed with error: {str(e)}") from e
diff --git a/src/conda_package_handling/tarball.py b/src/conda_package_handling/tarball.py
index b4c79e02..86f65bb8 100644
--- a/src/conda_package_handling/tarball.py
+++ b/src/conda_package_handling/tarball.py
@@ -1,20 +1,11 @@
 import logging
 import os
 import re
-import subprocess
-import sys
 import tarfile
 from errno import ELOOP
 from tempfile import NamedTemporaryFile
 
-try:
-    from . import archive_utils
-
-    libarchive_enabled = True
-except ImportError:
-    libarchive_enabled = False
-
-from . import utils
+from . import streaming, utils
 from .exceptions import CaseInsensitiveFileSystemError, InvalidArchiveError
 from .interface import AbstractBaseFormat
 
@@ -22,7 +13,7 @@
 
 
 def _sort_file_order(prefix, files):
-    """Sort by filesize or by binsort, to optimize compression"""
+    """Sort by filesize, to optimize compression?"""
     info_slash = "info" + os.path.sep
 
     def order(f):
@@ -41,44 +32,15 @@ def order(f):
                 info_order = 1 + abs(hash(ext)) % (10**8)
         return info_order, fsize
 
-    binsort = os.path.join(sys.prefix, "bin", "binsort")
-    if os.path.exists(binsort):
-        with NamedTemporaryFile(mode="w", suffix=".filelist", delete=False) as fl:
-            with utils.tmp_chdir(prefix):
-                fl.writelines(map(lambda x: "." + os.sep + x + "\n", files))
-                fl.close()
-                cmd = binsort + " -t 1 -q -d -o 1000 {}".format(fl.name)
-                out, _ = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
-                files_list = out.decode("utf-8").strip().split("\n")
-                # binsort returns the absolute paths.
-                files_list = [f.split(prefix + os.sep, 1)[-1] for f in files_list]
-                os.unlink(fl.name)
-        # Binsort does not handle symlinks gracefully. It will follow them. We must correct that.
-        s1 = set(files)
-        s2 = set(files_list)
-        followed = s2 - s1
-        for f in followed:
-            files_list.remove(f)
-        s2 = set(files_list)
-        if len(s1) > len(s2):
-            files_list.extend(s1 - s2)
-        # move info/ to front, otherwise preserving current order fi[0]
-        # (Python's sort algorithm is guaranteed to be stable, maintains
-        # existing order of items with the same sort key)
-        files_list = list(sorted(files, key=lambda f: not f.startswith(info_slash)))
-    else:
-        files_list = list(sorted(files, key=order))
+    files_list = list(sorted(files, key=order))
+
     return files_list
 
 
 def _create_no_libarchive(fullpath, files):
     with tarfile.open(fullpath, "w:bz2") as t:
         for f in files:
-            t.add(f)
-
-
-def _create_libarchive(fullpath, files, compression_filter, filter_opts):
-    archive_utils.create_archive(fullpath, files, compression_filter, filter_opts)
+            t.add(f, filter=utils.anonymize_tarinfo)
 
 
 def create_compressed_tarball(
@@ -92,57 +54,10 @@ def create_compressed_tarball(
     # possible large binary or data files
     fullpath = tmp_path + ext
     with utils.tmp_chdir(prefix):
-        if libarchive_enabled:
-            _create_libarchive(fullpath, files, compression_filter, filter_opts)
-        else:
-            _create_no_libarchive(fullpath, files)
+        _create_no_libarchive(fullpath, files)
     return fullpath
 
 
-def _tar_xf(tarball, dir_path):
-    if not os.path.isabs(tarball):
-        tarball = os.path.join(os.getcwd(), tarball)
-    with utils.tmp_chdir(dir_path):
-        archive_utils.extract_file(tarball)
-
-
-def _tar_xf_no_libarchive(tarball_full_path, destination_directory=None):
-    if destination_directory is None:
-        destination_directory = tarball_full_path[:-8]
-
-    with open(tarball_full_path, "rb") as fileobj:
-        with tarfile.open(fileobj=fileobj) as tar_file:
-            for member in tar_file.getmembers():
-                if os.path.isabs(member.name) or not os.path.realpath(member.name).startswith(
-                    os.getcwd()
-                ):
-                    raise InvalidArchiveError(
-                        tarball_full_path, "contains unsafe path: {}".format(member.name)
-                    )
-            try:
-                tar_file.extractall(path=destination_directory)
-            except IOError as e:
-                if e.errno == ELOOP:
-                    raise CaseInsensitiveFileSystemError(
-                        package_location=tarball_full_path,
-                        extract_location=destination_directory,
-                        caused_by=e,
-                    )
-                else:
-                    raise InvalidArchiveError(
-                        tarball_full_path, "failed with error: {}".format(str(e))
-                    )
-
-    if sys.platform.startswith("linux") and os.getuid() == 0:
-        # When extracting as root, tarfile will by restore ownership
-        # of extracted files.  However, we want root to be the owner
-        # (our implementation of --no-same-owner).
-        for root, dirs, files in os.walk(destination_directory):
-            for fn in files:
-                p = os.path.join(root, fn)
-                os.lchown(p, 0, 0)
-
-
 class CondaTarBZ2(AbstractBaseFormat):
     @staticmethod
     def supported(fn):
@@ -155,17 +70,7 @@ def extract(fn, dest_dir, **kw):
         if not os.path.isabs(fn):
             fn = os.path.normpath(os.path.join(os.getcwd(), fn))
 
-        if libarchive_enabled:
-            try:
-                _tar_xf(fn, dest_dir)
-            except InvalidArchiveError:
-                LOG.warning(
-                    "Failed extraction with libarchive... falling back to python implementation"
-                )
-                _tar_xf_no_libarchive(fn, dest_dir)
-
-        else:
-            _tar_xf_no_libarchive(fn, dest_dir)
+        streaming._extract(str(fn), str(dest_dir), components=["pkg"])
 
     @staticmethod
     def create(prefix, file_list, out_fn, out_folder=os.getcwd(), **kw):
diff --git a/src/conda_package_handling/utils.py b/src/conda_package_handling/utils.py
index 12b7caa0..2b55a367 100644
--- a/src/conda_package_handling/utils.py
+++ b/src/conda_package_handling/utils.py
@@ -129,23 +129,21 @@ def rmtree(path, *args, **kwargs):
             # out = check_output('DEL /F/Q/S *.* > NUL 2> NUL'.format(path), shell=True,
             #                    stderr=STDOUT, cwd=path)
 
-            out = check_output(
-                'RD /S /Q "{}" > NUL 2> NUL'.format(path), shell=True, stderr=STDOUT
-            )
+            out = check_output(f'RD /S /Q "{path}" > NUL 2> NUL', shell=True, stderr=STDOUT)
         except:
             try:
                 # Try to delete in Unicode
                 name = None
 
                 with NamedTemporaryFile(suffix=".bat", delete=False) as batch_file:
-                    batch_file.write("RD /S {}\n".format(quote_for_shell([path])))
+                    batch_file.write(f"RD /S {quote_for_shell([path])}\n")
                     batch_file.write("chcp 65001\n")
-                    batch_file.write("RD /S {}\n".format(quote_for_shell([path])))
+                    batch_file.write(f"RD /S {quote_for_shell([path])}\n")
                     batch_file.write("EXIT 0\n")
                     name = batch_file.name
                 # If the above is bugged we can end up deleting hard-drives, so we check
                 # that 'path' appears in it. This is not bulletproof but it could save you (me).
-                with open(name, "r") as contents:
+                with open(name) as contents:
                     content = contents.read()
                     assert path in content
                 comspec = os.environ["COMSPEC"]
@@ -169,9 +167,7 @@ def rmtree(path, *args, **kwargs):
                     )
                     raise
                 else:
-                    log.debug(
-                        "removing dir contents the fast way failed.  Output was: {}".format(out)
-                    )
+                    log.debug(f"removing dir contents the fast way failed.  Output was: {out}")
     else:
         try:
             os.makedirs(".empty")
@@ -195,7 +191,7 @@ def rmtree(path, *args, **kwargs):
                     stderr=STDOUT,
                 )
             except CalledProcessError:
-                log.debug("removing dir contents the fast way failed.  Output was: {}".format(out))
+                log.debug(f"removing dir contents the fast way failed.  Output was: {out}")
             shutil.rmtree(".empty")
     shutil.rmtree(path)
 
@@ -209,10 +205,10 @@ def unlink_or_rename_to_trash(path):
     try:
         make_writable(path)
         os.unlink(path)
-    except EnvironmentError:
+    except OSError:
         try:
             os.rename(path, path + ".conda_trash")
-        except EnvironmentError:
+        except OSError:
             if on_win:
                 # on windows, it is important to use the rename program, as just using python's
                 #    rename leads to permission errors when files are in use.
@@ -226,12 +222,19 @@ def unlink_or_rename_to_trash(path):
                     dest_fn = path + ".conda_trash"
                     counter = 1
                     while isfile(dest_fn):
-                        dest_fn = dest_fn.splitext[0] + ".conda_trash_{}".format(counter)
+                        dest_fn = dest_fn.splitext[0] + f".conda_trash_{counter}"
                         counter += 1
                     out = "< empty >"
                     try:
                         out = check_output(
-                            ["cmd.exe", "/C", trash_script, _dirname, _fn, basename(dest_fn)],
+                            [
+                                "cmd.exe",
+                                "/C",
+                                trash_script,
+                                _dirname,
+                                _fn,
+                                basename(dest_fn),
+                            ],
                             stderr=STDOUT,
                         )
                     except CalledProcessError:
@@ -289,7 +292,7 @@ def rm_rf(path, clean_empty_parents=False, *args, **kw):
 def delete_trash(prefix):
     if not prefix:
         prefix = sys.prefix
-    exclude = set(["envs"])
+    exclude = {"envs"}
     for root, dirs, files in os.walk(prefix, topdown=True):
         dirs[:] = [d for d in dirs if d not in exclude]
         for fn in files:
@@ -300,7 +303,7 @@ def delete_trash(prefix):
                 try:
                     os.unlink(filename)
                     remove_empty_parent_paths(filename)
-                except (OSError, IOError) as e:
+                except OSError as e:
                     log.debug("%r errno %d\nCannot unlink %s.", e, e.errno, filename)
 
 
@@ -320,7 +323,7 @@ def rmdir(dirpath):
 
 
 # we have our own TemporaryDirectory class because it's faster and handles disk issues better.
-class TemporaryDirectory(object):
+class TemporaryDirectory:
     """Create and return a temporary directory.  This has the same
     behavior as mkdtemp but can be used as a context manager.  For
     example:
@@ -340,7 +343,7 @@ def __init__(self, suffix="", prefix=".cph_tmp", dir=os.getcwd()):
         self.name = mkdtemp(suffix, prefix, dir)
 
     def __repr__(self):
-        return "<{} {!r}>".format(self.__class__.__name__, self.name)
+        return f"<{self.__class__.__name__} {self.name!r}>"
 
     def __enter__(self):
         return self.name
@@ -358,7 +361,8 @@ def cleanup(self, _warn=False, _warnings=_warnings):
             self._closed = True
             if _warn and _warnings.warn:
                 _warnings.warn(
-                    "Implicitly cleaning up {!r}".format(self), _warnings.ResourceWarning
+                    f"Implicitly cleaning up {self!r}",
+                    _warnings.ResourceWarning,
                 )
 
     def __exit__(self, exc, value, tb):
@@ -445,7 +449,7 @@ def filter_info_files(files_list, prefix):
 def _checksum(fd, algorithm, buffersize=65536):
     hash_impl = getattr(hashlib, algorithm)
     if not hash_impl:
-        raise ValueError("Unrecognized hash algorithm: {}".format(algorithm))
+        raise ValueError(f"Unrecognized hash algorithm: {algorithm}")
     else:
         hash_impl = hash_impl()
     for block in iter(lambda: fd.read(buffersize), b""):
@@ -477,3 +481,15 @@ def checksums(fn, algorithms, buffersize=1 << 18):
         # take care not to share hash_impl between threads
         results = [e.submit(checksum, fn, algorithm, buffersize) for algorithm in algorithms]
     return [result.result() for result in results]
+
+
+def anonymize_tarinfo(tarinfo):
+    """
+    Remove user id, name from tarinfo.
+    """
+    # also remove timestamps?
+    tarinfo.uid = 0
+    tarinfo.uname = ""
+    tarinfo.gid = 0
+    tarinfo.gname = ""
+    return tarinfo
diff --git a/src/conda_package_handling/validate.py b/src/conda_package_handling/validate.py
index f1f64ec7..cef6a9d2 100644
--- a/src/conda_package_handling/validate.py
+++ b/src/conda_package_handling/validate.py
@@ -1,12 +1,23 @@
+from __future__ import annotations
+
+import hashlib
 import os
+from itertools import chain
+from pathlib import Path
+
+from conda_package_streaming import package_streaming
 
 from .utils import TemporaryDirectory
 
 
-def validate_converted_files_match(src_file_or_folder, subject, reference_ext=""):
+def validate_converted_files_match(
+    src_file_or_folder, subject, reference_ext=""
+):  # pragma: nocover
+    # No longer used by conda-package-handling
     from .api import extract
 
     with TemporaryDirectory() as tmpdir:
+        assert tmpdir is not None
         if os.path.isdir(src_file_or_folder):
             src_folder = src_file_or_folder
         else:
@@ -29,3 +40,74 @@ def validate_converted_files_match(src_file_or_folder, subject, reference_ext=""
                     elif os.stat(absfile).st_size != os.stat(destpath).st_size:
                         mismatch_size.add(rp)
     return src_file_or_folder, missing_files, mismatch_size
+
+
+def hash_fn():
+    return hashlib.blake2b()
+
+
+IGNORE_FIELDS = {
+    "uid",
+    "gid",
+    "mtime",
+    "uname",
+    "gname",
+    "chksum",
+}  #: ignore if not strict
+
+
+def validate_converted_files_match_streaming(
+    src: str | Path, reference: str | Path, *, strict=True
+):
+    """
+    Check that two .tar.bz2 or .conda files (either of src_file and
+    reference_file can be either format) match exactly, down to the timestamps
+    etc.
+
+    Does not check outside of the info- and pkg- components of a .conda.
+    (conda's metadata.json, which gives the version "2" of the format)
+
+    If strict = True, also check for matching uid, gid, mtime, uname, gname.
+    """
+    source_set = {}
+    reference_set = {}
+    ignore_fields = {"chksum"} if strict else IGNORE_FIELDS
+
+    def get_fileset(filename: str | Path):
+        fileset = {}
+        components = ["info", "pkg"] if os.fspath(filename).endswith(".conda") else ["pkg"]
+        with open(filename, "rb") as conda_file:
+            for component in components:
+                for tar, member in package_streaming.stream_conda_component(
+                    filename, conda_file, component
+                ):
+
+                    info = {k: v for k, v in member.get_info().items() if k not in ignore_fields}
+
+                    if member.isfile():
+                        hasher = hash_fn()
+                        fd = tar.extractfile(member)
+                        assert fd is not None
+                        for block in iter(lambda: fd.read(1 << 18), b""):  # type: ignore
+                            hasher.update(block)
+
+                        info["digest"] = hasher.hexdigest()
+
+                    fileset[info["name"]] = info
+
+        return fileset
+
+    source_set = get_fileset(src)
+    reference_set = get_fileset(reference)
+
+    missing = []
+    mismatched = []
+
+    if source_set != reference_set:
+        for file in chain(source_set, reference_set):
+            if not (file in source_set and file in reference_set):
+                missing.append(file)
+            elif source_set[file] != reference_set[file]:
+                mismatched.append(file)
+
+    return src, missing, mismatched
diff --git a/update_usage.py b/update_usage.py
new file mode 100755
index 00000000..06c8f353
--- /dev/null
+++ b/update_usage.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+"""
+Replace usage section of README.md with current help text.
+"""
+
+import pathlib
+import subprocess
+import sys
+
+process = subprocess.run(["cph", "-h"], capture_output=True, encoding="utf-8")
+usage = process.stdout
+
+readme = pathlib.Path("README.md")
+text = readme.read_text()
+
+output = []
+
+lines = iter(text.splitlines())
+for line in lines:
+    output.append(line)
+    if line.startswith("```"):
+        break
+
+output.append(usage)
+output.append("```")
+for line in lines:
+    if line.startswith("```"):
+        break
+
+output.extend(lines)
+
+new_text = "\n".join(output) + "\n"
+if new_text != text:
+    readme.write_text(new_text)
+    sys.exit(0)
+
+sys.exit(1)
diff --git a/versioneer.py b/versioneer.py
deleted file mode 100644
index a142bf53..00000000
--- a/versioneer.py
+++ /dev/null
@@ -1,2140 +0,0 @@
-
-# Version: 0.22
-
-"""The Versioneer - like a rocketeer, but for versions.
-
-The Versioneer
-==============
-
-* like a rocketeer, but for versions!
-* https://github.com/python-versioneer/python-versioneer
-* Brian Warner
-* License: Public Domain
-* Compatible with: Python 3.6, 3.7, 3.8, 3.9, 3.10 and pypy3
-* [![Latest Version][pypi-image]][pypi-url]
-* [![Build Status][travis-image]][travis-url]
-
-This is a tool for managing a recorded version number in distutils/setuptools-based
-python projects. The goal is to remove the tedious and error-prone "update
-the embedded version string" step from your release process. Making a new
-release should be as easy as recording a new tag in your version-control
-system, and maybe making new tarballs.
-
-
-## Quick Install
-
-* `pip install versioneer` to somewhere in your $PATH
-* add a `[versioneer]` section to your setup.cfg (see [Install](INSTALL.md))
-* run `versioneer install` in your source tree, commit the results
-* Verify version information with `python setup.py version`
-
-## Version Identifiers
-
-Source trees come from a variety of places:
-
-* a version-control system checkout (mostly used by developers)
-* a nightly tarball, produced by build automation
-* a snapshot tarball, produced by a web-based VCS browser, like github's
-  "tarball from tag" feature
-* a release tarball, produced by "setup.py sdist", distributed through PyPI
-
-Within each source tree, the version identifier (either a string or a number,
-this tool is format-agnostic) can come from a variety of places:
-
-* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows
-  about recent "tags" and an absolute revision-id
-* the name of the directory into which the tarball was unpacked
-* an expanded VCS keyword ($Id$, etc)
-* a `_version.py` created by some earlier build step
-
-For released software, the version identifier is closely related to a VCS
-tag. Some projects use tag names that include more than just the version
-string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool
-needs to strip the tag prefix to extract the version identifier. For
-unreleased software (between tags), the version identifier should provide
-enough information to help developers recreate the same tree, while also
-giving them an idea of roughly how old the tree is (after version 1.2, before
-version 1.3). Many VCS systems can report a description that captures this,
-for example `git describe --tags --dirty --always` reports things like
-"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the
-0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has
-uncommitted changes).
-
-The version identifier is used for multiple purposes:
-
-* to allow the module to self-identify its version: `myproject.__version__`
-* to choose a name and prefix for a 'setup.py sdist' tarball
-
-## Theory of Operation
-
-Versioneer works by adding a special `_version.py` file into your source
-tree, where your `__init__.py` can import it. This `_version.py` knows how to
-dynamically ask the VCS tool for version information at import time.
-
-`_version.py` also contains `$Revision$` markers, and the installation
-process marks `_version.py` to have this marker rewritten with a tag name
-during the `git archive` command. As a result, generated tarballs will
-contain enough information to get the proper version.
-
-To allow `setup.py` to compute a version too, a `versioneer.py` is added to
-the top level of your source tree, next to `setup.py` and the `setup.cfg`
-that configures it. This overrides several distutils/setuptools commands to
-compute the version when invoked, and changes `setup.py build` and `setup.py
-sdist` to replace `_version.py` with a small static file that contains just
-the generated version data.
-
-## Installation
-
-See [INSTALL.md](./INSTALL.md) for detailed installation instructions.
-
-## Version-String Flavors
-
-Code which uses Versioneer can learn about its version string at runtime by
-importing `_version` from your main `__init__.py` file and running the
-`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can
-import the top-level `versioneer.py` and run `get_versions()`.
-
-Both functions return a dictionary with different flavors of version
-information:
-
-* `['version']`: A condensed version string, rendered using the selected
-  style. This is the most commonly used value for the project's version
-  string. The default "pep440" style yields strings like `0.11`,
-  `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section
-  below for alternative styles.
-
-* `['full-revisionid']`: detailed revision identifier. For Git, this is the
-  full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac".
-
-* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the
-  commit date in ISO 8601 format. This will be None if the date is not
-  available.
-
-* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that
-  this is only accurate if run in a VCS checkout, otherwise it is likely to
-  be False or None
-
-* `['error']`: if the version string could not be computed, this will be set
-  to a string describing the problem, otherwise it will be None. It may be
-  useful to throw an exception in setup.py if this is set, to avoid e.g.
-  creating tarballs with a version string of "unknown".
-
-Some variants are more useful than others. Including `full-revisionid` in a
-bug report should allow developers to reconstruct the exact code being tested
-(or indicate the presence of local changes that should be shared with the
-developers). `version` is suitable for display in an "about" box or a CLI
-`--version` output: it can be easily compared against release notes and lists
-of bugs fixed in various releases.
-
-The installer adds the following text to your `__init__.py` to place a basic
-version in `YOURPROJECT.__version__`:
-
-    from ._version import get_versions
-    __version__ = get_versions()['version']
-    del get_versions
-
-## Styles
-
-The setup.cfg `style=` configuration controls how the VCS information is
-rendered into a version string.
-
-The default style, "pep440", produces a PEP440-compliant string, equal to the
-un-prefixed tag name for actual releases, and containing an additional "local
-version" section with more detail for in-between builds. For Git, this is
-TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags
---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the
-tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and
-that this commit is two revisions ("+2") beyond the "0.11" tag. For released
-software (exactly equal to a known tag), the identifier will only contain the
-stripped tag, e.g. "0.11".
-
-Other styles are available. See [details.md](details.md) in the Versioneer
-source tree for descriptions.
-
-## Debugging
-
-Versioneer tries to avoid fatal errors: if something goes wrong, it will tend
-to return a version of "0+unknown". To investigate the problem, run `setup.py
-version`, which will run the version-lookup code in a verbose mode, and will
-display the full contents of `get_versions()` (including the `error` string,
-which may help identify what went wrong).
-
-## Known Limitations
-
-Some situations are known to cause problems for Versioneer. This details the
-most significant ones. More can be found on Github
-[issues page](https://github.com/python-versioneer/python-versioneer/issues).
-
-### Subprojects
-
-Versioneer has limited support for source trees in which `setup.py` is not in
-the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are
-two common reasons why `setup.py` might not be in the root:
-
-* Source trees which contain multiple subprojects, such as
-  [Buildbot](https://github.com/buildbot/buildbot), which contains both
-  "master" and "slave" subprojects, each with their own `setup.py`,
-  `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI
-  distributions (and upload multiple independently-installable tarballs).
-* Source trees whose main purpose is to contain a C library, but which also
-  provide bindings to Python (and perhaps other languages) in subdirectories.
-
-Versioneer will look for `.git` in parent directories, and most operations
-should get the right version string. However `pip` and `setuptools` have bugs
-and implementation details which frequently cause `pip install .` from a
-subproject directory to fail to find a correct version string (so it usually
-defaults to `0+unknown`).
-
-`pip install --editable .` should work correctly. `setup.py install` might
-work too.
-
-Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in
-some later version.
-
-[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking
-this issue. The discussion in
-[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the
-issue from the Versioneer side in more detail.
-[pip PR#3176](https://github.com/pypa/pip/pull/3176) and
-[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve
-pip to let Versioneer work correctly.
-
-Versioneer-0.16 and earlier only looked for a `.git` directory next to the
-`setup.cfg`, so subprojects were completely unsupported with those releases.
-
-### Editable installs with setuptools <= 18.5
-
-`setup.py develop` and `pip install --editable .` allow you to install a
-project into a virtualenv once, then continue editing the source code (and
-test) without re-installing after every change.
-
-"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a
-convenient way to specify executable scripts that should be installed along
-with the python package.
-
-These both work as expected when using modern setuptools. When using
-setuptools-18.5 or earlier, however, certain operations will cause
-`pkg_resources.DistributionNotFound` errors when running the entrypoint
-script, which must be resolved by re-installing the package. This happens
-when the install happens with one version, then the egg_info data is
-regenerated while a different version is checked out. Many setup.py commands
-cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into
-a different virtualenv), so this can be surprising.
-
-[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes
-this one, but upgrading to a newer version of setuptools should probably
-resolve it.
-
-
-## Updating Versioneer
-
-To upgrade your project to a new release of Versioneer, do the following:
-
-* install the new Versioneer (`pip install -U versioneer` or equivalent)
-* edit `setup.cfg`, if necessary, to include any new configuration settings
-  indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details.
-* re-run `versioneer install` in your source tree, to replace
-  `SRC/_version.py`
-* commit any changed files
-
-## Future Directions
-
-This tool is designed to make it easily extended to other version-control
-systems: all VCS-specific components are in separate directories like
-src/git/ . The top-level `versioneer.py` script is assembled from these
-components by running make-versioneer.py . In the future, make-versioneer.py
-will take a VCS name as an argument, and will construct a version of
-`versioneer.py` that is specific to the given VCS. It might also take the
-configuration arguments that are currently provided manually during
-installation by editing setup.py . Alternatively, it might go the other
-direction and include code from all supported VCS systems, reducing the
-number of intermediate scripts.
-
-## Similar projects
-
-* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time
-  dependency
-* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of
-  versioneer
-* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools
-  plugin
-
-## License
-
-To make Versioneer easier to embed, all its code is dedicated to the public
-domain. The `_version.py` that it creates is also in the public domain.
-Specifically, both are released under the Creative Commons "Public Domain
-Dedication" license (CC0-1.0), as described in
-https://creativecommons.org/publicdomain/zero/1.0/ .
-
-[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg
-[pypi-url]: https://pypi.python.org/pypi/versioneer/
-[travis-image]:
-https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg
-[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer
-
-"""
-# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring
-# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements
-# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error
-# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with
-# pylint:disable=attribute-defined-outside-init,too-many-arguments
-
-import configparser
-import errno
-import json
-import os
-import re
-import subprocess
-import sys
-from typing import Callable, Dict
-import functools
-
-
-class VersioneerConfig:
-    """Container for Versioneer configuration parameters."""
-
-
-def get_root():
-    """Get the project root directory.
-
-    We require that all commands are run from the project root, i.e. the
-    directory that contains setup.py, setup.cfg, and versioneer.py .
-    """
-    root = os.path.realpath(os.path.abspath(os.getcwd()))
-    setup_py = os.path.join(root, "setup.py")
-    versioneer_py = os.path.join(root, "versioneer.py")
-    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
-        # allow 'python path/to/setup.py COMMAND'
-        root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
-        setup_py = os.path.join(root, "setup.py")
-        versioneer_py = os.path.join(root, "versioneer.py")
-    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
-        err = ("Versioneer was unable to run the project root directory. "
-               "Versioneer requires setup.py to be executed from "
-               "its immediate directory (like 'python setup.py COMMAND'), "
-               "or in a way that lets it use sys.argv[0] to find the root "
-               "(like 'python path/to/setup.py COMMAND').")
-        raise VersioneerBadRootError(err)
-    try:
-        # Certain runtime workflows (setup.py install/develop in a setuptools
-        # tree) execute all dependencies in a single python process, so
-        # "versioneer" may be imported multiple times, and python's shared
-        # module-import table will cache the first one. So we can't use
-        # os.path.dirname(__file__), as that will find whichever
-        # versioneer.py was first imported, even in later projects.
-        my_path = os.path.realpath(os.path.abspath(__file__))
-        me_dir = os.path.normcase(os.path.splitext(my_path)[0])
-        vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0])
-        if me_dir != vsr_dir:
-            print("Warning: build in %s is using versioneer.py from %s"
-                  % (os.path.dirname(my_path), versioneer_py))
-    except NameError:
-        pass
-    return root
-
-
-def get_config_from_root(root):
-    """Read the project setup.cfg file to determine Versioneer config."""
-    # This might raise OSError (if setup.cfg is missing), or
-    # configparser.NoSectionError (if it lacks a [versioneer] section), or
-    # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
-    # the top of versioneer.py for instructions on writing your setup.cfg .
-    setup_cfg = os.path.join(root, "setup.cfg")
-    parser = configparser.ConfigParser()
-    with open(setup_cfg, "r") as cfg_file:
-        parser.read_file(cfg_file)
-    VCS = parser.get("versioneer", "VCS")  # mandatory
-
-    # Dict-like interface for non-mandatory entries
-    section = parser["versioneer"]
-
-    cfg = VersioneerConfig()
-    cfg.VCS = VCS
-    cfg.style = section.get("style", "")
-    cfg.versionfile_source = section.get("versionfile_source")
-    cfg.versionfile_build = section.get("versionfile_build")
-    cfg.tag_prefix = section.get("tag_prefix")
-    if cfg.tag_prefix in ("''", '""'):
-        cfg.tag_prefix = ""
-    cfg.parentdir_prefix = section.get("parentdir_prefix")
-    cfg.verbose = section.get("verbose")
-    return cfg
-
-
-class NotThisMethod(Exception):
-    """Exception raised if a method is not valid for the current scenario."""
-
-
-# these dictionaries contain VCS-specific tools
-LONG_VERSION_PY: Dict[str, str] = {}
-HANDLERS: Dict[str, Dict[str, Callable]] = {}
-
-
-def register_vcs_handler(vcs, method):  # decorator
-    """Create decorator to mark a method as the handler of a VCS."""
-    def decorate(f):
-        """Store f in HANDLERS[vcs][method]."""
-        HANDLERS.setdefault(vcs, {})[method] = f
-        return f
-    return decorate
-
-
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
-                env=None):
-    """Call the given command(s)."""
-    assert isinstance(commands, list)
-    process = None
-
-    popen_kwargs = {}
-    if sys.platform == "win32":
-        # This hides the console window if pythonw.exe is used
-        startupinfo = subprocess.STARTUPINFO()
-        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
-        popen_kwargs["startupinfo"] = startupinfo
-
-    for command in commands:
-        try:
-            dispcmd = str([command] + args)
-            # remember shell=False, so use git.cmd on windows, not just git
-            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
-                                       stdout=subprocess.PIPE,
-                                       stderr=(subprocess.PIPE if hide_stderr
-                                               else None), **popen_kwargs)
-            break
-        except OSError:
-            e = sys.exc_info()[1]
-            if e.errno == errno.ENOENT:
-                continue
-            if verbose:
-                print("unable to run %s" % dispcmd)
-                print(e)
-            return None, None
-    else:
-        if verbose:
-            print("unable to find command, tried %s" % (commands,))
-        return None, None
-    stdout = process.communicate()[0].strip().decode()
-    if process.returncode != 0:
-        if verbose:
-            print("unable to run %s (error)" % dispcmd)
-            print("stdout was %s" % stdout)
-        return None, process.returncode
-    return stdout, process.returncode
-
-
-LONG_VERSION_PY['git'] = r'''
-# This file helps to compute a version number in source trees obtained from
-# git-archive tarball (such as those provided by githubs download-from-tag
-# feature). Distribution tarballs (built by setup.py sdist) and build
-# directories (produced by setup.py build) will contain a much shorter file
-# that just contains the computed version number.
-
-# This file is released into the public domain. Generated by
-# versioneer-0.22 (https://github.com/python-versioneer/python-versioneer)
-
-"""Git implementation of _version.py."""
-
-import errno
-import os
-import re
-import subprocess
-import sys
-from typing import Callable, Dict
-import functools
-
-
-def get_keywords():
-    """Get the keywords needed to look up the version information."""
-    # these strings will be replaced by git during git-archive.
-    # setup.py/versioneer.py will grep for the variable names, so they must
-    # each be defined on a line of their own. _version.py will just call
-    # get_keywords().
-    git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
-    git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
-    git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s"
-    keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
-    return keywords
-
-
-class VersioneerConfig:
-    """Container for Versioneer configuration parameters."""
-
-
-def get_config():
-    """Create, populate and return the VersioneerConfig() object."""
-    # these strings are filled in when 'setup.py versioneer' creates
-    # _version.py
-    cfg = VersioneerConfig()
-    cfg.VCS = "git"
-    cfg.style = "%(STYLE)s"
-    cfg.tag_prefix = "%(TAG_PREFIX)s"
-    cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s"
-    cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s"
-    cfg.verbose = False
-    return cfg
-
-
-class NotThisMethod(Exception):
-    """Exception raised if a method is not valid for the current scenario."""
-
-
-LONG_VERSION_PY: Dict[str, str] = {}
-HANDLERS: Dict[str, Dict[str, Callable]] = {}
-
-
-def register_vcs_handler(vcs, method):  # decorator
-    """Create decorator to mark a method as the handler of a VCS."""
-    def decorate(f):
-        """Store f in HANDLERS[vcs][method]."""
-        if vcs not in HANDLERS:
-            HANDLERS[vcs] = {}
-        HANDLERS[vcs][method] = f
-        return f
-    return decorate
-
-
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
-                env=None):
-    """Call the given command(s)."""
-    assert isinstance(commands, list)
-    process = None
-
-    popen_kwargs = {}
-    if sys.platform == "win32":
-        # This hides the console window if pythonw.exe is used
-        startupinfo = subprocess.STARTUPINFO()
-        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
-        popen_kwargs["startupinfo"] = startupinfo
-
-    for command in commands:
-        try:
-            dispcmd = str([command] + args)
-            # remember shell=False, so use git.cmd on windows, not just git
-            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
-                                       stdout=subprocess.PIPE,
-                                       stderr=(subprocess.PIPE if hide_stderr
-                                               else None), **popen_kwargs)
-            break
-        except OSError:
-            e = sys.exc_info()[1]
-            if e.errno == errno.ENOENT:
-                continue
-            if verbose:
-                print("unable to run %%s" %% dispcmd)
-                print(e)
-            return None, None
-    else:
-        if verbose:
-            print("unable to find command, tried %%s" %% (commands,))
-        return None, None
-    stdout = process.communicate()[0].strip().decode()
-    if process.returncode != 0:
-        if verbose:
-            print("unable to run %%s (error)" %% dispcmd)
-            print("stdout was %%s" %% stdout)
-        return None, process.returncode
-    return stdout, process.returncode
-
-
-def versions_from_parentdir(parentdir_prefix, root, verbose):
-    """Try to determine the version from the parent directory name.
-
-    Source tarballs conventionally unpack into a directory that includes both
-    the project name and a version string. We will also support searching up
-    two directory levels for an appropriately named parent directory
-    """
-    rootdirs = []
-
-    for _ in range(3):
-        dirname = os.path.basename(root)
-        if dirname.startswith(parentdir_prefix):
-            return {"version": dirname[len(parentdir_prefix):],
-                    "full-revisionid": None,
-                    "dirty": False, "error": None, "date": None}
-        rootdirs.append(root)
-        root = os.path.dirname(root)  # up a level
-
-    if verbose:
-        print("Tried directories %%s but none started with prefix %%s" %%
-              (str(rootdirs), parentdir_prefix))
-    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-@register_vcs_handler("git", "get_keywords")
-def git_get_keywords(versionfile_abs):
-    """Extract version information from the given file."""
-    # the code embedded in _version.py can just fetch the value of these
-    # keywords. When used from setup.py, we don't want to import _version.py,
-    # so we do it with a regexp instead. This function is not used from
-    # _version.py.
-    keywords = {}
-    try:
-        with open(versionfile_abs, "r") as fobj:
-            for line in fobj:
-                if line.strip().startswith("git_refnames ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["refnames"] = mo.group(1)
-                if line.strip().startswith("git_full ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["full"] = mo.group(1)
-                if line.strip().startswith("git_date ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["date"] = mo.group(1)
-    except OSError:
-        pass
-    return keywords
-
-
-@register_vcs_handler("git", "keywords")
-def git_versions_from_keywords(keywords, tag_prefix, verbose):
-    """Get version information from git keywords."""
-    if "refnames" not in keywords:
-        raise NotThisMethod("Short version file found")
-    date = keywords.get("date")
-    if date is not None:
-        # Use only the last line.  Previous lines may contain GPG signature
-        # information.
-        date = date.splitlines()[-1]
-
-        # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant
-        # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601
-        # -like" string, which we must then edit to make compliant), because
-        # it's been around since git-1.5.3, and it's too difficult to
-        # discover which version we're using, or to work around using an
-        # older one.
-        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-    refnames = keywords["refnames"].strip()
-    if refnames.startswith("$Format"):
-        if verbose:
-            print("keywords are unexpanded, not using")
-        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
-    refs = {r.strip() for r in refnames.strip("()").split(",")}
-    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
-    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
-    TAG = "tag: "
-    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
-    if not tags:
-        # Either we're using git < 1.8.3, or there really are no tags. We use
-        # a heuristic: assume all version tags have a digit. The old git %%d
-        # expansion behaves like git log --decorate=short and strips out the
-        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
-        # between branches and tags. By ignoring refnames without digits, we
-        # filter out many common branch names like "release" and
-        # "stabilization", as well as "HEAD" and "master".
-        tags = {r for r in refs if re.search(r'\d', r)}
-        if verbose:
-            print("discarding '%%s', no digits" %% ",".join(refs - tags))
-    if verbose:
-        print("likely tags: %%s" %% ",".join(sorted(tags)))
-    for ref in sorted(tags):
-        # sorting will prefer e.g. "2.0" over "2.0rc1"
-        if ref.startswith(tag_prefix):
-            r = ref[len(tag_prefix):]
-            # Filter out refs that exactly match prefix or that don't start
-            # with a number once the prefix is stripped (mostly a concern
-            # when prefix is '')
-            if not re.match(r'\d', r):
-                continue
-            if verbose:
-                print("picking %%s" %% r)
-            return {"version": r,
-                    "full-revisionid": keywords["full"].strip(),
-                    "dirty": False, "error": None,
-                    "date": date}
-    # no suitable tags, so version is "0+unknown", but full hex is still there
-    if verbose:
-        print("no suitable tags, using unknown + full revision id")
-    return {"version": "0+unknown",
-            "full-revisionid": keywords["full"].strip(),
-            "dirty": False, "error": "no suitable tags", "date": None}
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
-    """Get version from 'git describe' in the root of the source tree.
-
-    This only gets called if the git-archive 'subst' keywords were *not*
-    expanded, and _version.py hasn't already been rewritten with a short
-    version string, meaning we're inside a checked out source tree.
-    """
-    GITS = ["git"]
-    if sys.platform == "win32":
-        GITS = ["git.cmd", "git.exe"]
-
-    # GIT_DIR can interfere with correct operation of Versioneer.
-    # It may be intended to be passed to the Versioneer-versioned project,
-    # but that should not change where we get our version from.
-    env = os.environ.copy()
-    env.pop("GIT_DIR", None)
-    runner = functools.partial(runner, env=env)
-
-    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
-                   hide_stderr=True)
-    if rc != 0:
-        if verbose:
-            print("Directory %%s not under git control" %% root)
-        raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
-    MATCH_ARGS = ["--match", "%%s*" %% tag_prefix] if tag_prefix else []
-
-    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
-    # if there isn't one, this yields HEX[-dirty] (no NUM)
-    describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
-                                     "--always", "--long", *MATCH_ARGS],
-                              cwd=root)
-    # --long was added in git-1.5.5
-    if describe_out is None:
-        raise NotThisMethod("'git describe' failed")
-    describe_out = describe_out.strip()
-    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
-    if full_out is None:
-        raise NotThisMethod("'git rev-parse' failed")
-    full_out = full_out.strip()
-
-    pieces = {}
-    pieces["long"] = full_out
-    pieces["short"] = full_out[:7]  # maybe improved later
-    pieces["error"] = None
-
-    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
-                             cwd=root)
-    # --abbrev-ref was added in git-1.6.3
-    if rc != 0 or branch_name is None:
-        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
-    branch_name = branch_name.strip()
-
-    if branch_name == "HEAD":
-        # If we aren't exactly on a branch, pick a branch which represents
-        # the current commit. If all else fails, we are on a branchless
-        # commit.
-        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
-        # --contains was added in git-1.5.4
-        if rc != 0 or branches is None:
-            raise NotThisMethod("'git branch --contains' returned error")
-        branches = branches.split("\n")
-
-        # Remove the first line if we're running detached
-        if "(" in branches[0]:
-            branches.pop(0)
-
-        # Strip off the leading "* " from the list of branches.
-        branches = [branch[2:] for branch in branches]
-        if "master" in branches:
-            branch_name = "master"
-        elif not branches:
-            branch_name = None
-        else:
-            # Pick the first branch that is returned. Good or bad.
-            branch_name = branches[0]
-
-    pieces["branch"] = branch_name
-
-    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
-    # TAG might have hyphens.
-    git_describe = describe_out
-
-    # look for -dirty suffix
-    dirty = git_describe.endswith("-dirty")
-    pieces["dirty"] = dirty
-    if dirty:
-        git_describe = git_describe[:git_describe.rindex("-dirty")]
-
-    # now we have TAG-NUM-gHEX or HEX
-
-    if "-" in git_describe:
-        # TAG-NUM-gHEX
-        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
-        if not mo:
-            # unparsable. Maybe git-describe is misbehaving?
-            pieces["error"] = ("unable to parse git-describe output: '%%s'"
-                               %% describe_out)
-            return pieces
-
-        # tag
-        full_tag = mo.group(1)
-        if not full_tag.startswith(tag_prefix):
-            if verbose:
-                fmt = "tag '%%s' doesn't start with prefix '%%s'"
-                print(fmt %% (full_tag, tag_prefix))
-            pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'"
-                               %% (full_tag, tag_prefix))
-            return pieces
-        pieces["closest-tag"] = full_tag[len(tag_prefix):]
-
-        # distance: number of commits since tag
-        pieces["distance"] = int(mo.group(2))
-
-        # commit: short hex revision ID
-        pieces["short"] = mo.group(3)
-
-    else:
-        # HEX: no tags
-        pieces["closest-tag"] = None
-        count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
-        pieces["distance"] = int(count_out)  # total number of commits
-
-    # commit date: see ISO-8601 comment in git_versions_from_keywords()
-    date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip()
-    # Use only the last line.  Previous lines may contain GPG signature
-    # information.
-    date = date.splitlines()[-1]
-    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-
-    return pieces
-
-
-def plus_or_dot(pieces):
-    """Return a + if we don't already have one, else return a ."""
-    if "+" in pieces.get("closest-tag", ""):
-        return "."
-    return "+"
-
-
-def render_pep440(pieces):
-    """Build up version string, with post-release "local version identifier".
-
-    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
-    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
-    Exceptions:
-    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += plus_or_dot(pieces)
-            rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"],
-                                          pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def render_pep440_branch(pieces):
-    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
-
-    The ".dev0" means not master branch. Note that .dev0 sorts backwards
-    (a feature branch will appear "older" than the master branch).
-
-    Exceptions:
-    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            if pieces["branch"] != "master":
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0"
-        if pieces["branch"] != "master":
-            rendered += ".dev0"
-        rendered += "+untagged.%%d.g%%s" %% (pieces["distance"],
-                                          pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def pep440_split_post(ver):
-    """Split pep440 version string at the post-release segment.
-
-    Returns the release segments before the post-release and the
-    post-release version number (or -1 if no post-release segment is present).
-    """
-    vc = str.split(ver, ".post")
-    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
-
-
-def render_pep440_pre(pieces):
-    """TAG[.postN.devDISTANCE] -- No -dirty.
-
-    Exceptions:
-    1: no tags. 0.post0.devDISTANCE
-    """
-    if pieces["closest-tag"]:
-        if pieces["distance"]:
-            # update the post release segment
-            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
-            rendered = tag_version
-            if post_version is not None:
-                rendered += ".post%%d.dev%%d" %% (post_version+1, pieces["distance"])
-            else:
-                rendered += ".post0.dev%%d" %% (pieces["distance"])
-        else:
-            # no commits, use the tag as the version
-            rendered = pieces["closest-tag"]
-    else:
-        # exception #1
-        rendered = "0.post0.dev%%d" %% pieces["distance"]
-    return rendered
-
-
-def render_pep440_post(pieces):
-    """TAG[.postDISTANCE[.dev0]+gHEX] .
-
-    The ".dev0" means dirty. Note that .dev0 sorts backwards
-    (a dirty tree will appear "older" than the corresponding clean one),
-    but you shouldn't be releasing software with -dirty anyways.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%%d" %% pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%%s" %% pieces["short"]
-    else:
-        # exception #1
-        rendered = "0.post%%d" %% pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-        rendered += "+g%%s" %% pieces["short"]
-    return rendered
-
-
-def render_pep440_post_branch(pieces):
-    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
-
-    The ".dev0" means not master branch.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%%d" %% pieces["distance"]
-            if pieces["branch"] != "master":
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%%s" %% pieces["short"]
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0.post%%d" %% pieces["distance"]
-        if pieces["branch"] != "master":
-            rendered += ".dev0"
-        rendered += "+g%%s" %% pieces["short"]
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def render_pep440_old(pieces):
-    """TAG[.postDISTANCE[.dev0]] .
-
-    The ".dev0" means dirty.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%%d" %% pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-    else:
-        # exception #1
-        rendered = "0.post%%d" %% pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-    return rendered
-
-
-def render_git_describe(pieces):
-    """TAG[-DISTANCE-gHEX][-dirty].
-
-    Like 'git describe --tags --dirty --always'.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"]:
-            rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render_git_describe_long(pieces):
-    """TAG-DISTANCE-gHEX[-dirty].
-
-    Like 'git describe --tags --dirty --always -long'.
-    The distance/hash is unconditional.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render(pieces, style):
-    """Render the given version pieces into the requested style."""
-    if pieces["error"]:
-        return {"version": "unknown",
-                "full-revisionid": pieces.get("long"),
-                "dirty": None,
-                "error": pieces["error"],
-                "date": None}
-
-    if not style or style == "default":
-        style = "pep440"  # the default
-
-    if style == "pep440":
-        rendered = render_pep440(pieces)
-    elif style == "pep440-branch":
-        rendered = render_pep440_branch(pieces)
-    elif style == "pep440-pre":
-        rendered = render_pep440_pre(pieces)
-    elif style == "pep440-post":
-        rendered = render_pep440_post(pieces)
-    elif style == "pep440-post-branch":
-        rendered = render_pep440_post_branch(pieces)
-    elif style == "pep440-old":
-        rendered = render_pep440_old(pieces)
-    elif style == "git-describe":
-        rendered = render_git_describe(pieces)
-    elif style == "git-describe-long":
-        rendered = render_git_describe_long(pieces)
-    else:
-        raise ValueError("unknown style '%%s'" %% style)
-
-    return {"version": rendered, "full-revisionid": pieces["long"],
-            "dirty": pieces["dirty"], "error": None,
-            "date": pieces.get("date")}
-
-
-def get_versions():
-    """Get version information or return default if unable to do so."""
-    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
-    # __file__, we can work backwards from there to the root. Some
-    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
-    # case we can only use expanded keywords.
-
-    cfg = get_config()
-    verbose = cfg.verbose
-
-    try:
-        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
-                                          verbose)
-    except NotThisMethod:
-        pass
-
-    try:
-        root = os.path.realpath(__file__)
-        # versionfile_source is the relative path from the top of the source
-        # tree (where the .git directory might live) to this file. Invert
-        # this to find the root from __file__.
-        for _ in cfg.versionfile_source.split('/'):
-            root = os.path.dirname(root)
-    except NameError:
-        return {"version": "0+unknown", "full-revisionid": None,
-                "dirty": None,
-                "error": "unable to find root of source tree",
-                "date": None}
-
-    try:
-        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
-        return render(pieces, cfg.style)
-    except NotThisMethod:
-        pass
-
-    try:
-        if cfg.parentdir_prefix:
-            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
-    except NotThisMethod:
-        pass
-
-    return {"version": "0+unknown", "full-revisionid": None,
-            "dirty": None,
-            "error": "unable to compute version", "date": None}
-'''
-
-
-@register_vcs_handler("git", "get_keywords")
-def git_get_keywords(versionfile_abs):
-    """Extract version information from the given file."""
-    # the code embedded in _version.py can just fetch the value of these
-    # keywords. When used from setup.py, we don't want to import _version.py,
-    # so we do it with a regexp instead. This function is not used from
-    # _version.py.
-    keywords = {}
-    try:
-        with open(versionfile_abs, "r") as fobj:
-            for line in fobj:
-                if line.strip().startswith("git_refnames ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["refnames"] = mo.group(1)
-                if line.strip().startswith("git_full ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["full"] = mo.group(1)
-                if line.strip().startswith("git_date ="):
-                    mo = re.search(r'=\s*"(.*)"', line)
-                    if mo:
-                        keywords["date"] = mo.group(1)
-    except OSError:
-        pass
-    return keywords
-
-
-@register_vcs_handler("git", "keywords")
-def git_versions_from_keywords(keywords, tag_prefix, verbose):
-    """Get version information from git keywords."""
-    if "refnames" not in keywords:
-        raise NotThisMethod("Short version file found")
-    date = keywords.get("date")
-    if date is not None:
-        # Use only the last line.  Previous lines may contain GPG signature
-        # information.
-        date = date.splitlines()[-1]
-
-        # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
-        # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
-        # -like" string, which we must then edit to make compliant), because
-        # it's been around since git-1.5.3, and it's too difficult to
-        # discover which version we're using, or to work around using an
-        # older one.
-        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-    refnames = keywords["refnames"].strip()
-    if refnames.startswith("$Format"):
-        if verbose:
-            print("keywords are unexpanded, not using")
-        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
-    refs = {r.strip() for r in refnames.strip("()").split(",")}
-    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
-    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
-    TAG = "tag: "
-    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
-    if not tags:
-        # Either we're using git < 1.8.3, or there really are no tags. We use
-        # a heuristic: assume all version tags have a digit. The old git %d
-        # expansion behaves like git log --decorate=short and strips out the
-        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
-        # between branches and tags. By ignoring refnames without digits, we
-        # filter out many common branch names like "release" and
-        # "stabilization", as well as "HEAD" and "master".
-        tags = {r for r in refs if re.search(r'\d', r)}
-        if verbose:
-            print("discarding '%s', no digits" % ",".join(refs - tags))
-    if verbose:
-        print("likely tags: %s" % ",".join(sorted(tags)))
-    for ref in sorted(tags):
-        # sorting will prefer e.g. "2.0" over "2.0rc1"
-        if ref.startswith(tag_prefix):
-            r = ref[len(tag_prefix):]
-            # Filter out refs that exactly match prefix or that don't start
-            # with a number once the prefix is stripped (mostly a concern
-            # when prefix is '')
-            if not re.match(r'\d', r):
-                continue
-            if verbose:
-                print("picking %s" % r)
-            return {"version": r,
-                    "full-revisionid": keywords["full"].strip(),
-                    "dirty": False, "error": None,
-                    "date": date}
-    # no suitable tags, so version is "0+unknown", but full hex is still there
-    if verbose:
-        print("no suitable tags, using unknown + full revision id")
-    return {"version": "0+unknown",
-            "full-revisionid": keywords["full"].strip(),
-            "dirty": False, "error": "no suitable tags", "date": None}
-
-
-@register_vcs_handler("git", "pieces_from_vcs")
-def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
-    """Get version from 'git describe' in the root of the source tree.
-
-    This only gets called if the git-archive 'subst' keywords were *not*
-    expanded, and _version.py hasn't already been rewritten with a short
-    version string, meaning we're inside a checked out source tree.
-    """
-    GITS = ["git"]
-    if sys.platform == "win32":
-        GITS = ["git.cmd", "git.exe"]
-
-    # GIT_DIR can interfere with correct operation of Versioneer.
-    # It may be intended to be passed to the Versioneer-versioned project,
-    # but that should not change where we get our version from.
-    env = os.environ.copy()
-    env.pop("GIT_DIR", None)
-    runner = functools.partial(runner, env=env)
-
-    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
-                   hide_stderr=True)
-    if rc != 0:
-        if verbose:
-            print("Directory %s not under git control" % root)
-        raise NotThisMethod("'git rev-parse --git-dir' returned error")
-
-    MATCH_ARGS = ["--match", "%s*" % tag_prefix] if tag_prefix else []
-
-    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
-    # if there isn't one, this yields HEX[-dirty] (no NUM)
-    describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
-                                     "--always", "--long", *MATCH_ARGS],
-                              cwd=root)
-    # --long was added in git-1.5.5
-    if describe_out is None:
-        raise NotThisMethod("'git describe' failed")
-    describe_out = describe_out.strip()
-    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
-    if full_out is None:
-        raise NotThisMethod("'git rev-parse' failed")
-    full_out = full_out.strip()
-
-    pieces = {}
-    pieces["long"] = full_out
-    pieces["short"] = full_out[:7]  # maybe improved later
-    pieces["error"] = None
-
-    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
-                             cwd=root)
-    # --abbrev-ref was added in git-1.6.3
-    if rc != 0 or branch_name is None:
-        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
-    branch_name = branch_name.strip()
-
-    if branch_name == "HEAD":
-        # If we aren't exactly on a branch, pick a branch which represents
-        # the current commit. If all else fails, we are on a branchless
-        # commit.
-        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
-        # --contains was added in git-1.5.4
-        if rc != 0 or branches is None:
-            raise NotThisMethod("'git branch --contains' returned error")
-        branches = branches.split("\n")
-
-        # Remove the first line if we're running detached
-        if "(" in branches[0]:
-            branches.pop(0)
-
-        # Strip off the leading "* " from the list of branches.
-        branches = [branch[2:] for branch in branches]
-        if "master" in branches:
-            branch_name = "master"
-        elif not branches:
-            branch_name = None
-        else:
-            # Pick the first branch that is returned. Good or bad.
-            branch_name = branches[0]
-
-    pieces["branch"] = branch_name
-
-    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
-    # TAG might have hyphens.
-    git_describe = describe_out
-
-    # look for -dirty suffix
-    dirty = git_describe.endswith("-dirty")
-    pieces["dirty"] = dirty
-    if dirty:
-        git_describe = git_describe[:git_describe.rindex("-dirty")]
-
-    # now we have TAG-NUM-gHEX or HEX
-
-    if "-" in git_describe:
-        # TAG-NUM-gHEX
-        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
-        if not mo:
-            # unparsable. Maybe git-describe is misbehaving?
-            pieces["error"] = ("unable to parse git-describe output: '%s'"
-                               % describe_out)
-            return pieces
-
-        # tag
-        full_tag = mo.group(1)
-        if not full_tag.startswith(tag_prefix):
-            if verbose:
-                fmt = "tag '%s' doesn't start with prefix '%s'"
-                print(fmt % (full_tag, tag_prefix))
-            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
-                               % (full_tag, tag_prefix))
-            return pieces
-        pieces["closest-tag"] = full_tag[len(tag_prefix):]
-
-        # distance: number of commits since tag
-        pieces["distance"] = int(mo.group(2))
-
-        # commit: short hex revision ID
-        pieces["short"] = mo.group(3)
-
-    else:
-        # HEX: no tags
-        pieces["closest-tag"] = None
-        count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
-        pieces["distance"] = int(count_out)  # total number of commits
-
-    # commit date: see ISO-8601 comment in git_versions_from_keywords()
-    date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
-    # Use only the last line.  Previous lines may contain GPG signature
-    # information.
-    date = date.splitlines()[-1]
-    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
-
-    return pieces
-
-
-def do_vcs_install(manifest_in, versionfile_source, ipy):
-    """Git-specific installation logic for Versioneer.
-
-    For Git, this means creating/changing .gitattributes to mark _version.py
-    for export-subst keyword substitution.
-    """
-    GITS = ["git"]
-    if sys.platform == "win32":
-        GITS = ["git.cmd", "git.exe"]
-    files = [manifest_in, versionfile_source]
-    if ipy:
-        files.append(ipy)
-    try:
-        my_path = __file__
-        if my_path.endswith(".pyc") or my_path.endswith(".pyo"):
-            my_path = os.path.splitext(my_path)[0] + ".py"
-        versioneer_file = os.path.relpath(my_path)
-    except NameError:
-        versioneer_file = "versioneer.py"
-    files.append(versioneer_file)
-    present = False
-    try:
-        with open(".gitattributes", "r") as fobj:
-            for line in fobj:
-                if line.strip().startswith(versionfile_source):
-                    if "export-subst" in line.strip().split()[1:]:
-                        present = True
-                        break
-    except OSError:
-        pass
-    if not present:
-        with open(".gitattributes", "a+") as fobj:
-            fobj.write(f"{versionfile_source} export-subst\n")
-        files.append(".gitattributes")
-    run_command(GITS, ["add", "--"] + files)
-
-
-def versions_from_parentdir(parentdir_prefix, root, verbose):
-    """Try to determine the version from the parent directory name.
-
-    Source tarballs conventionally unpack into a directory that includes both
-    the project name and a version string. We will also support searching up
-    two directory levels for an appropriately named parent directory
-    """
-    rootdirs = []
-
-    for _ in range(3):
-        dirname = os.path.basename(root)
-        if dirname.startswith(parentdir_prefix):
-            return {"version": dirname[len(parentdir_prefix):],
-                    "full-revisionid": None,
-                    "dirty": False, "error": None, "date": None}
-        rootdirs.append(root)
-        root = os.path.dirname(root)  # up a level
-
-    if verbose:
-        print("Tried directories %s but none started with prefix %s" %
-              (str(rootdirs), parentdir_prefix))
-    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
-
-
-SHORT_VERSION_PY = """
-# This file was generated by 'versioneer.py' (0.22) from
-# revision-control system data, or from the parent directory name of an
-# unpacked source archive. Distribution tarballs contain a pre-generated copy
-# of this file.
-
-import json
-
-version_json = '''
-%s
-'''  # END VERSION_JSON
-
-
-def get_versions():
-    return json.loads(version_json)
-"""
-
-
-def versions_from_file(filename):
-    """Try to determine the version from _version.py if present."""
-    try:
-        with open(filename) as f:
-            contents = f.read()
-    except OSError:
-        raise NotThisMethod("unable to read _version.py")
-    mo = re.search(r"version_json = '''\n(.*)'''  # END VERSION_JSON",
-                   contents, re.M | re.S)
-    if not mo:
-        mo = re.search(r"version_json = '''\r\n(.*)'''  # END VERSION_JSON",
-                       contents, re.M | re.S)
-    if not mo:
-        raise NotThisMethod("no version_json in _version.py")
-    return json.loads(mo.group(1))
-
-
-def write_to_version_file(filename, versions):
-    """Write the given version number to the given _version.py file."""
-    os.unlink(filename)
-    contents = json.dumps(versions, sort_keys=True,
-                          indent=1, separators=(",", ": "))
-    with open(filename, "w") as f:
-        f.write(SHORT_VERSION_PY % contents)
-
-    print("set %s to '%s'" % (filename, versions["version"]))
-
-
-def plus_or_dot(pieces):
-    """Return a + if we don't already have one, else return a ."""
-    if "+" in pieces.get("closest-tag", ""):
-        return "."
-    return "+"
-
-
-def render_pep440(pieces):
-    """Build up version string, with post-release "local version identifier".
-
-    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
-    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
-
-    Exceptions:
-    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += plus_or_dot(pieces)
-            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
-                                          pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def render_pep440_branch(pieces):
-    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
-
-    The ".dev0" means not master branch. Note that .dev0 sorts backwards
-    (a feature branch will appear "older" than the master branch).
-
-    Exceptions:
-    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            if pieces["branch"] != "master":
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0"
-        if pieces["branch"] != "master":
-            rendered += ".dev0"
-        rendered += "+untagged.%d.g%s" % (pieces["distance"],
-                                          pieces["short"])
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def pep440_split_post(ver):
-    """Split pep440 version string at the post-release segment.
-
-    Returns the release segments before the post-release and the
-    post-release version number (or -1 if no post-release segment is present).
-    """
-    vc = str.split(ver, ".post")
-    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
-
-
-def render_pep440_pre(pieces):
-    """TAG[.postN.devDISTANCE] -- No -dirty.
-
-    Exceptions:
-    1: no tags. 0.post0.devDISTANCE
-    """
-    if pieces["closest-tag"]:
-        if pieces["distance"]:
-            # update the post release segment
-            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
-            rendered = tag_version
-            if post_version is not None:
-                rendered += ".post%d.dev%d" % (post_version+1, pieces["distance"])
-            else:
-                rendered += ".post0.dev%d" % (pieces["distance"])
-        else:
-            # no commits, use the tag as the version
-            rendered = pieces["closest-tag"]
-    else:
-        # exception #1
-        rendered = "0.post0.dev%d" % pieces["distance"]
-    return rendered
-
-
-def render_pep440_post(pieces):
-    """TAG[.postDISTANCE[.dev0]+gHEX] .
-
-    The ".dev0" means dirty. Note that .dev0 sorts backwards
-    (a dirty tree will appear "older" than the corresponding clean one),
-    but you shouldn't be releasing software with -dirty anyways.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%s" % pieces["short"]
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-        rendered += "+g%s" % pieces["short"]
-    return rendered
-
-
-def render_pep440_post_branch(pieces):
-    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
-
-    The ".dev0" means not master branch.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["branch"] != "master":
-                rendered += ".dev0"
-            rendered += plus_or_dot(pieces)
-            rendered += "g%s" % pieces["short"]
-            if pieces["dirty"]:
-                rendered += ".dirty"
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["branch"] != "master":
-            rendered += ".dev0"
-        rendered += "+g%s" % pieces["short"]
-        if pieces["dirty"]:
-            rendered += ".dirty"
-    return rendered
-
-
-def render_pep440_old(pieces):
-    """TAG[.postDISTANCE[.dev0]] .
-
-    The ".dev0" means dirty.
-
-    Exceptions:
-    1: no tags. 0.postDISTANCE[.dev0]
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"] or pieces["dirty"]:
-            rendered += ".post%d" % pieces["distance"]
-            if pieces["dirty"]:
-                rendered += ".dev0"
-    else:
-        # exception #1
-        rendered = "0.post%d" % pieces["distance"]
-        if pieces["dirty"]:
-            rendered += ".dev0"
-    return rendered
-
-
-def render_git_describe(pieces):
-    """TAG[-DISTANCE-gHEX][-dirty].
-
-    Like 'git describe --tags --dirty --always'.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        if pieces["distance"]:
-            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render_git_describe_long(pieces):
-    """TAG-DISTANCE-gHEX[-dirty].
-
-    Like 'git describe --tags --dirty --always -long'.
-    The distance/hash is unconditional.
-
-    Exceptions:
-    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
-    """
-    if pieces["closest-tag"]:
-        rendered = pieces["closest-tag"]
-        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
-    else:
-        # exception #1
-        rendered = pieces["short"]
-    if pieces["dirty"]:
-        rendered += "-dirty"
-    return rendered
-
-
-def render(pieces, style):
-    """Render the given version pieces into the requested style."""
-    if pieces["error"]:
-        return {"version": "unknown",
-                "full-revisionid": pieces.get("long"),
-                "dirty": None,
-                "error": pieces["error"],
-                "date": None}
-
-    if not style or style == "default":
-        style = "pep440"  # the default
-
-    if style == "pep440":
-        rendered = render_pep440(pieces)
-    elif style == "pep440-branch":
-        rendered = render_pep440_branch(pieces)
-    elif style == "pep440-pre":
-        rendered = render_pep440_pre(pieces)
-    elif style == "pep440-post":
-        rendered = render_pep440_post(pieces)
-    elif style == "pep440-post-branch":
-        rendered = render_pep440_post_branch(pieces)
-    elif style == "pep440-old":
-        rendered = render_pep440_old(pieces)
-    elif style == "git-describe":
-        rendered = render_git_describe(pieces)
-    elif style == "git-describe-long":
-        rendered = render_git_describe_long(pieces)
-    else:
-        raise ValueError("unknown style '%s'" % style)
-
-    return {"version": rendered, "full-revisionid": pieces["long"],
-            "dirty": pieces["dirty"], "error": None,
-            "date": pieces.get("date")}
-
-
-class VersioneerBadRootError(Exception):
-    """The project root directory is unknown or missing key files."""
-
-
-def get_versions(verbose=False):
-    """Get the project version from whatever source is available.
-
-    Returns dict with two keys: 'version' and 'full'.
-    """
-    if "versioneer" in sys.modules:
-        # see the discussion in cmdclass.py:get_cmdclass()
-        del sys.modules["versioneer"]
-
-    root = get_root()
-    cfg = get_config_from_root(root)
-
-    assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg"
-    handlers = HANDLERS.get(cfg.VCS)
-    assert handlers, "unrecognized VCS '%s'" % cfg.VCS
-    verbose = verbose or cfg.verbose
-    assert cfg.versionfile_source is not None, \
-        "please set versioneer.versionfile_source"
-    assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
-
-    versionfile_abs = os.path.join(root, cfg.versionfile_source)
-
-    # extract version from first of: _version.py, VCS command (e.g. 'git
-    # describe'), parentdir. This is meant to work for developers using a
-    # source checkout, for users of a tarball created by 'setup.py sdist',
-    # and for users of a tarball/zipball created by 'git archive' or github's
-    # download-from-tag feature or the equivalent in other VCSes.
-
-    get_keywords_f = handlers.get("get_keywords")
-    from_keywords_f = handlers.get("keywords")
-    if get_keywords_f and from_keywords_f:
-        try:
-            keywords = get_keywords_f(versionfile_abs)
-            ver = from_keywords_f(keywords, cfg.tag_prefix, verbose)
-            if verbose:
-                print("got version from expanded keyword %s" % ver)
-            return ver
-        except NotThisMethod:
-            pass
-
-    try:
-        ver = versions_from_file(versionfile_abs)
-        if verbose:
-            print("got version from file %s %s" % (versionfile_abs, ver))
-        return ver
-    except NotThisMethod:
-        pass
-
-    from_vcs_f = handlers.get("pieces_from_vcs")
-    if from_vcs_f:
-        try:
-            pieces = from_vcs_f(cfg.tag_prefix, root, verbose)
-            ver = render(pieces, cfg.style)
-            if verbose:
-                print("got version from VCS %s" % ver)
-            return ver
-        except NotThisMethod:
-            pass
-
-    try:
-        if cfg.parentdir_prefix:
-            ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
-            if verbose:
-                print("got version from parentdir %s" % ver)
-            return ver
-    except NotThisMethod:
-        pass
-
-    if verbose:
-        print("unable to compute version")
-
-    return {"version": "0+unknown", "full-revisionid": None,
-            "dirty": None, "error": "unable to compute version",
-            "date": None}
-
-
-def get_version():
-    """Get the short version string for this project."""
-    return get_versions()["version"]
-
-
-def get_cmdclass(cmdclass=None):
-    """Get the custom setuptools/distutils subclasses used by Versioneer.
-
-    If the package uses a different cmdclass (e.g. one from numpy), it
-    should be provide as an argument.
-    """
-    if "versioneer" in sys.modules:
-        del sys.modules["versioneer"]
-        # this fixes the "python setup.py develop" case (also 'install' and
-        # 'easy_install .'), in which subdependencies of the main project are
-        # built (using setup.py bdist_egg) in the same python process. Assume
-        # a main project A and a dependency B, which use different versions
-        # of Versioneer. A's setup.py imports A's Versioneer, leaving it in
-        # sys.modules by the time B's setup.py is executed, causing B to run
-        # with the wrong versioneer. Setuptools wraps the sub-dep builds in a
-        # sandbox that restores sys.modules to it's pre-build state, so the
-        # parent is protected against the child's "import versioneer". By
-        # removing ourselves from sys.modules here, before the child build
-        # happens, we protect the child from the parent's versioneer too.
-        # Also see https://github.com/python-versioneer/python-versioneer/issues/52
-
-    cmds = {} if cmdclass is None else cmdclass.copy()
-
-    # we add "version" to both distutils and setuptools
-    try:
-        from setuptools import Command
-    except ImportError:
-        from distutils.core import Command
-
-    class cmd_version(Command):
-        description = "report generated version string"
-        user_options = []
-        boolean_options = []
-
-        def initialize_options(self):
-            pass
-
-        def finalize_options(self):
-            pass
-
-        def run(self):
-            vers = get_versions(verbose=True)
-            print("Version: %s" % vers["version"])
-            print(" full-revisionid: %s" % vers.get("full-revisionid"))
-            print(" dirty: %s" % vers.get("dirty"))
-            print(" date: %s" % vers.get("date"))
-            if vers["error"]:
-                print(" error: %s" % vers["error"])
-    cmds["version"] = cmd_version
-
-    # we override "build_py" in both distutils and setuptools
-    #
-    # most invocation pathways end up running build_py:
-    #  distutils/build -> build_py
-    #  distutils/install -> distutils/build ->..
-    #  setuptools/bdist_wheel -> distutils/install ->..
-    #  setuptools/bdist_egg -> distutils/install_lib -> build_py
-    #  setuptools/install -> bdist_egg ->..
-    #  setuptools/develop -> ?
-    #  pip install:
-    #   copies source tree to a tempdir before running egg_info/etc
-    #   if .git isn't copied too, 'git describe' will fail
-    #   then does setup.py bdist_wheel, or sometimes setup.py install
-    #  setup.py egg_info -> ?
-
-    # we override different "build_py" commands for both environments
-    if 'build_py' in cmds:
-        _build_py = cmds['build_py']
-    elif "setuptools" in sys.modules:
-        from setuptools.command.build_py import build_py as _build_py
-    else:
-        from distutils.command.build_py import build_py as _build_py
-
-    class cmd_build_py(_build_py):
-        def run(self):
-            root = get_root()
-            cfg = get_config_from_root(root)
-            versions = get_versions()
-            _build_py.run(self)
-            # now locate _version.py in the new build/ directory and replace
-            # it with an updated value
-            if cfg.versionfile_build:
-                target_versionfile = os.path.join(self.build_lib,
-                                                  cfg.versionfile_build)
-                print("UPDATING %s" % target_versionfile)
-                write_to_version_file(target_versionfile, versions)
-    cmds["build_py"] = cmd_build_py
-
-    if 'build_ext' in cmds:
-        _build_ext = cmds['build_ext']
-    elif "setuptools" in sys.modules:
-        from setuptools.command.build_ext import build_ext as _build_ext
-    else:
-        from distutils.command.build_ext import build_ext as _build_ext
-
-    class cmd_build_ext(_build_ext):
-        def run(self):
-            root = get_root()
-            cfg = get_config_from_root(root)
-            versions = get_versions()
-            _build_ext.run(self)
-            if self.inplace:
-                # build_ext --inplace will only build extensions in
-                # build/lib<..> dir with no _version.py to write to.
-                # As in place builds will already have a _version.py
-                # in the module dir, we do not need to write one.
-                return
-            # now locate _version.py in the new build/ directory and replace
-            # it with an updated value
-            target_versionfile = os.path.join(self.build_lib,
-                                              cfg.versionfile_build)
-            print("UPDATING %s" % target_versionfile)
-            write_to_version_file(target_versionfile, versions)
-    cmds["build_ext"] = cmd_build_ext
-
-    if "cx_Freeze" in sys.modules:  # cx_freeze enabled?
-        from cx_Freeze.dist import build_exe as _build_exe
-        # nczeczulin reports that py2exe won't like the pep440-style string
-        # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g.
-        # setup(console=[{
-        #   "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION
-        #   "product_version": versioneer.get_version(),
-        #   ...
-
-        class cmd_build_exe(_build_exe):
-            def run(self):
-                root = get_root()
-                cfg = get_config_from_root(root)
-                versions = get_versions()
-                target_versionfile = cfg.versionfile_source
-                print("UPDATING %s" % target_versionfile)
-                write_to_version_file(target_versionfile, versions)
-
-                _build_exe.run(self)
-                os.unlink(target_versionfile)
-                with open(cfg.versionfile_source, "w") as f:
-                    LONG = LONG_VERSION_PY[cfg.VCS]
-                    f.write(LONG %
-                            {"DOLLAR": "$",
-                             "STYLE": cfg.style,
-                             "TAG_PREFIX": cfg.tag_prefix,
-                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
-                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
-                             })
-        cmds["build_exe"] = cmd_build_exe
-        del cmds["build_py"]
-
-    if 'py2exe' in sys.modules:  # py2exe enabled?
-        from py2exe.distutils_buildexe import py2exe as _py2exe
-
-        class cmd_py2exe(_py2exe):
-            def run(self):
-                root = get_root()
-                cfg = get_config_from_root(root)
-                versions = get_versions()
-                target_versionfile = cfg.versionfile_source
-                print("UPDATING %s" % target_versionfile)
-                write_to_version_file(target_versionfile, versions)
-
-                _py2exe.run(self)
-                os.unlink(target_versionfile)
-                with open(cfg.versionfile_source, "w") as f:
-                    LONG = LONG_VERSION_PY[cfg.VCS]
-                    f.write(LONG %
-                            {"DOLLAR": "$",
-                             "STYLE": cfg.style,
-                             "TAG_PREFIX": cfg.tag_prefix,
-                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
-                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
-                             })
-        cmds["py2exe"] = cmd_py2exe
-
-    # we override different "sdist" commands for both environments
-    if 'sdist' in cmds:
-        _sdist = cmds['sdist']
-    elif "setuptools" in sys.modules:
-        from setuptools.command.sdist import sdist as _sdist
-    else:
-        from distutils.command.sdist import sdist as _sdist
-
-    class cmd_sdist(_sdist):
-        def run(self):
-            versions = get_versions()
-            self._versioneer_generated_versions = versions
-            # unless we update this, the command will keep using the old
-            # version
-            self.distribution.metadata.version = versions["version"]
-            return _sdist.run(self)
-
-        def make_release_tree(self, base_dir, files):
-            root = get_root()
-            cfg = get_config_from_root(root)
-            _sdist.make_release_tree(self, base_dir, files)
-            # now locate _version.py in the new base_dir directory
-            # (remembering that it may be a hardlink) and replace it with an
-            # updated value
-            target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
-            print("UPDATING %s" % target_versionfile)
-            write_to_version_file(target_versionfile,
-                                  self._versioneer_generated_versions)
-    cmds["sdist"] = cmd_sdist
-
-    return cmds
-
-
-CONFIG_ERROR = """
-setup.cfg is missing the necessary Versioneer configuration. You need
-a section like:
-
- [versioneer]
- VCS = git
- style = pep440
- versionfile_source = src/myproject/_version.py
- versionfile_build = myproject/_version.py
- tag_prefix =
- parentdir_prefix = myproject-
-
-You will also need to edit your setup.py to use the results:
-
- import versioneer
- setup(version=versioneer.get_version(),
-       cmdclass=versioneer.get_cmdclass(), ...)
-
-Please read the docstring in ./versioneer.py for configuration instructions,
-edit setup.cfg, and re-run the installer or 'python versioneer.py setup'.
-"""
-
-SAMPLE_CONFIG = """
-# See the docstring in versioneer.py for instructions. Note that you must
-# re-run 'versioneer.py setup' after changing this section, and commit the
-# resulting files.
-
-[versioneer]
-#VCS = git
-#style = pep440
-#versionfile_source =
-#versionfile_build =
-#tag_prefix =
-#parentdir_prefix =
-
-"""
-
-OLD_SNIPPET = """
-from ._version import get_versions
-__version__ = get_versions()['version']
-del get_versions
-"""
-
-INIT_PY_SNIPPET = """
-from . import {0}
-__version__ = {0}.get_versions()['version']
-"""
-
-
-def do_setup():
-    """Do main VCS-independent setup function for installing Versioneer."""
-    root = get_root()
-    try:
-        cfg = get_config_from_root(root)
-    except (OSError, configparser.NoSectionError,
-            configparser.NoOptionError) as e:
-        if isinstance(e, (OSError, configparser.NoSectionError)):
-            print("Adding sample versioneer config to setup.cfg",
-                  file=sys.stderr)
-            with open(os.path.join(root, "setup.cfg"), "a") as f:
-                f.write(SAMPLE_CONFIG)
-        print(CONFIG_ERROR, file=sys.stderr)
-        return 1
-
-    print(" creating %s" % cfg.versionfile_source)
-    with open(cfg.versionfile_source, "w") as f:
-        LONG = LONG_VERSION_PY[cfg.VCS]
-        f.write(LONG % {"DOLLAR": "$",
-                        "STYLE": cfg.style,
-                        "TAG_PREFIX": cfg.tag_prefix,
-                        "PARENTDIR_PREFIX": cfg.parentdir_prefix,
-                        "VERSIONFILE_SOURCE": cfg.versionfile_source,
-                        })
-
-    ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
-                       "__init__.py")
-    if os.path.exists(ipy):
-        try:
-            with open(ipy, "r") as f:
-                old = f.read()
-        except OSError:
-            old = ""
-        module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0]
-        snippet = INIT_PY_SNIPPET.format(module)
-        if OLD_SNIPPET in old:
-            print(" replacing boilerplate in %s" % ipy)
-            with open(ipy, "w") as f:
-                f.write(old.replace(OLD_SNIPPET, snippet))
-        elif snippet not in old:
-            print(" appending to %s" % ipy)
-            with open(ipy, "a") as f:
-                f.write(snippet)
-        else:
-            print(" %s unmodified" % ipy)
-    else:
-        print(" %s doesn't exist, ok" % ipy)
-        ipy = None
-
-    # Make sure both the top-level "versioneer.py" and versionfile_source
-    # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so
-    # they'll be copied into source distributions. Pip won't be able to
-    # install the package without this.
-    manifest_in = os.path.join(root, "MANIFEST.in")
-    simple_includes = set()
-    try:
-        with open(manifest_in, "r") as f:
-            for line in f:
-                if line.startswith("include "):
-                    for include in line.split()[1:]:
-                        simple_includes.add(include)
-    except OSError:
-        pass
-    # That doesn't cover everything MANIFEST.in can do
-    # (http://docs.python.org/2/distutils/sourcedist.html#commands), so
-    # it might give some false negatives. Appending redundant 'include'
-    # lines is safe, though.
-    if "versioneer.py" not in simple_includes:
-        print(" appending 'versioneer.py' to MANIFEST.in")
-        with open(manifest_in, "a") as f:
-            f.write("include versioneer.py\n")
-    else:
-        print(" 'versioneer.py' already in MANIFEST.in")
-    if cfg.versionfile_source not in simple_includes:
-        print(" appending versionfile_source ('%s') to MANIFEST.in" %
-              cfg.versionfile_source)
-        with open(manifest_in, "a") as f:
-            f.write("include %s\n" % cfg.versionfile_source)
-    else:
-        print(" versionfile_source already in MANIFEST.in")
-
-    # Make VCS-specific changes. For git, this means creating/changing
-    # .gitattributes to mark _version.py for export-subst keyword
-    # substitution.
-    do_vcs_install(manifest_in, cfg.versionfile_source, ipy)
-    return 0
-
-
-def scan_setup_py():
-    """Validate the contents of setup.py against Versioneer's expectations."""
-    found = set()
-    setters = False
-    errors = 0
-    with open("setup.py", "r") as f:
-        for line in f.readlines():
-            if "import versioneer" in line:
-                found.add("import")
-            if "versioneer.get_cmdclass()" in line:
-                found.add("cmdclass")
-            if "versioneer.get_version()" in line:
-                found.add("get_version")
-            if "versioneer.VCS" in line:
-                setters = True
-            if "versioneer.versionfile_source" in line:
-                setters = True
-    if len(found) != 3:
-        print("")
-        print("Your setup.py appears to be missing some important items")
-        print("(but I might be wrong). Please make sure it has something")
-        print("roughly like the following:")
-        print("")
-        print(" import versioneer")
-        print(" setup( version=versioneer.get_version(),")
-        print("        cmdclass=versioneer.get_cmdclass(),  ...)")
-        print("")
-        errors += 1
-    if setters:
-        print("You should remove lines like 'versioneer.VCS = ' and")
-        print("'versioneer.versionfile_source = ' . This configuration")
-        print("now lives in setup.cfg, and should be removed from setup.py")
-        print("")
-        errors += 1
-    return errors
-
-
-if __name__ == "__main__":
-    cmd = sys.argv[1]
-    if cmd == "setup":
-        errors = do_setup()
-        errors += scan_setup_py()
-        if errors:
-            sys.exit(1)


Test patch:
diff --git a/.github/workflows/CI.yaml b/.github/workflows/tests.yml
similarity index 96%
rename from .github/workflows/CI.yaml
rename to .github/workflows/tests.yml
index 4506ad3f..8cb93c40 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/tests.yml
@@ -46,7 +46,7 @@ jobs:
       # conda setup requires this special shell
       shell: bash -l {0}
       run: |
-        conda build conda.recipe
+        conda build -c conda conda.recipe
 
 # TODO: Re-enable codecov when we figure out how to grab coverage from the conda build
 #       environment
diff --git a/conda.recipe/libarchive-patches/0003-VC9-compatibility-test-for-BCryptDeriveKeyPBKDF2.patch b/conda.recipe/libarchive-patches/0003-VC9-compatibility-test-for-BCryptDeriveKeyPBKDF2.patch
deleted file mode 100644
index 158c5d94..00000000
--- a/conda.recipe/libarchive-patches/0003-VC9-compatibility-test-for-BCryptDeriveKeyPBKDF2.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-diff -urN work.orig/CMakeLists.txt work/CMakeLists.txt
---- work.orig/CMakeLists.txt	2018-04-22 06:34:09.127722200 -0500
-+++ work/CMakeLists.txt	2018-04-22 06:37:35.445261100 -0500
-@@ -635,6 +635,8 @@
- # Following files need windows.h, so we should test it after windows.h test.
- LA_CHECK_INCLUDE_FILE("wincrypt.h" HAVE_WINCRYPT_H)
- LA_CHECK_INCLUDE_FILE("winioctl.h" HAVE_WINIOCTL_H)
-+# Only available on Windows 7 and above, so not possible with vc9.
-+CHECK_FUNCTION_EXISTS(BCryptDeriveKeyPBKDF2 HAVE_BCRYPT_DERIVE_KEY_PBKDF2)
- 
- #
- # Check whether use of __EXTENSIONS__ is safe.
-diff -urN work.orig/libarchive/archive_cryptor.c work/libarchive/archive_cryptor.c
---- work.orig/libarchive/archive_cryptor.c	2018-04-22 06:33:58.190129600 -0500
-+++ work/libarchive/archive_cryptor.c	2018-04-22 06:38:51.330777500 -0500
-@@ -57,7 +57,7 @@
- 	return 0;
- }
- 
--#elif defined(_WIN32) && !defined(__CYGWIN__) && defined(HAVE_BCRYPT_H)
-+#elif defined(_WIN32) && !defined(__CYGWIN__) && defined(HAVE_BCRYPT_H) && defined(HAVE_BCRYPT_DERIVE_KEY_PBKDF2)
- #ifdef _MSC_VER
- #pragma comment(lib, "Bcrypt.lib")
- #endif
diff --git a/tests/data/mock-2.0.0-py37_1000/info/LICENSE.txt b/tests/data/mock-2.0.0-py37_1000/info/LICENSE.txt
deleted file mode 100644
index c8209243..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/LICENSE.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-Copyright (c) 2003-2013, Michael Foord & the mock team
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
-    * Redistributions of source code must retain the above copyright
-      notice, this list of conditions and the following disclaimer.
-
-    * Redistributions in binary form must reproduce the above
-      copyright notice, this list of conditions and the following
-      disclaimer in the documentation and/or other materials provided
-      with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tests/data/mock-2.0.0-py37_1000/info/about.json b/tests/data/mock-2.0.0-py37_1000/info/about.json
deleted file mode 100644
index 363c2a08..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/about.json
+++ /dev/null
@@ -1,85 +0,0 @@
-{
-  "channels": [
-    "conda-forge",
-    "defaults"
-  ],
-  "conda_build_version": "3.15.1",
-  "conda_private": false,
-  "conda_version": "4.5.11",
-  "env_vars": {
-    "CIO_TEST": "<not set>"
-  },
-  "extra": {
-    "copy_test_source_files": true,
-    "final": true,
-    "recipe-maintainers": [
-      "jakirkham",
-      "ocefpaf"
-    ]
-  },
-  "home": "https://github.com/testing-cabal/mock",
-  "identifiers": [],
-  "keywords": [],
-  "license": "BSD 2-Clause",
-  "license_file": "LICENSE.txt",
-  "root_pkgs": [
-    "anaconda-client 1.7.2 py36_0",
-    "asn1crypto 0.24.0 py36_0",
-    "beautifulsoup4 4.6.3 py36_0",
-    "ca-certificates 2018.03.07 0",
-    "certifi 2018.8.24 py36_1",
-    "cffi 1.11.5 py36h6174b99_1",
-    "chardet 3.0.4 py36_1",
-    "click 7.0 py36_0",
-    "clyent 1.2.2 py36_1",
-    "conda 4.5.11 py36_0",
-    "conda-build 3.15.1 py36_0",
-    "conda-env 2.6.0 1",
-    "conda-forge-ci-setup 2.0.8 py36_0",
-    "cryptography 2.3.1 py36hdbc3d79_0",
-    "decorator 4.3.0 py36_0",
-    "filelock 3.0.8 py36_0",
-    "glob2 0.6 py36_1",
-    "idna 2.7 py36_0",
-    "ipython_genutils 0.2.0 py36h241746c_0",
-    "jinja2 2.10 py36_0",
-    "jsonschema 2.6.0 py36hb385e00_0",
-    "jupyter_core 4.4.0 py36_0",
-    "libcxx 4.0.1 h579ed51_0",
-    "libcxxabi 4.0.1 hebd6815_0",
-    "libedit 3.1.20170329 hb402a30_2",
-    "libffi 3.2.1 h475c297_4",
-    "markupsafe 1.0 py36h1de35cc_1",
-    "nbformat 4.4.0 py36h827af21_0",
-    "ncurses 6.1 h0a44026_0",
-    "openssl 1.0.2p h1de35cc_0",
-    "pip 10.0.1 py36_0",
-    "pkginfo 1.4.2 py36_1",
-    "psutil 5.4.7 py36h1de35cc_0",
-    "pycosat 0.6.3 py36h1de35cc_0",
-    "pycparser 2.19 py36_0",
-    "pyopenssl 18.0.0 py36_0",
-    "pysocks 1.6.8 py36_0",
-    "python 3.6.6 hc167b69_0",
-    "python-dateutil 2.7.3 py36_0",
-    "python.app 2 py36_9",
-    "pytz 2018.5 py36_0",
-    "pyyaml 3.13 py36h1de35cc_0",
-    "readline 7.0 h1de35cc_5",
-    "requests 2.19.1 py36_0",
-    "ruamel_yaml 0.15.46 py36h1de35cc_0",
-    "setuptools 40.4.3 py36_0",
-    "six 1.11.0 py36_1",
-    "sqlite 3.24.0 ha441bb4_0",
-    "tk 8.6.8 ha441bb4_0",
-    "tqdm 4.26.0 py36h28b3542_0",
-    "traitlets 4.3.2 py36h65bd3ce_0",
-    "urllib3 1.23 py36_0",
-    "wheel 0.32.0 py36_0",
-    "xz 5.2.4 h1de35cc_4",
-    "yaml 0.1.7 hc338f04_2",
-    "zlib 1.2.11 hf3cbc9b_2"
-  ],
-  "summary": "A library for testing in Python",
-  "tags": []
-}
\ No newline at end of file
diff --git a/tests/data/mock-2.0.0-py37_1000/info/files b/tests/data/mock-2.0.0-py37_1000/info/files
deleted file mode 100644
index 89beefdc..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/files
+++ /dev/null
@@ -1,32 +0,0 @@
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/PKG-INFO
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/SOURCES.txt
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/dependency_links.txt
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/installed-files.txt
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/not-zip-safe
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/pbr.json
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/requires.txt
-lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/top_level.txt
-lib/python3.7/site-packages/mock/__init__.py
-lib/python3.7/site-packages/mock/__pycache__/__init__.cpython-37.pyc
-lib/python3.7/site-packages/mock/__pycache__/mock.cpython-37.pyc
-lib/python3.7/site-packages/mock/mock.py
-lib/python3.7/site-packages/mock/tests/__init__.py
-lib/python3.7/site-packages/mock/tests/__main__.py
-lib/python3.7/site-packages/mock/tests/__pycache__/__init__.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/__main__.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/support.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testcallable.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testhelpers.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testmagicmethods.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testmock.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testpatch.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testsentinel.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/__pycache__/testwith.cpython-37.pyc
-lib/python3.7/site-packages/mock/tests/support.py
-lib/python3.7/site-packages/mock/tests/testcallable.py
-lib/python3.7/site-packages/mock/tests/testhelpers.py
-lib/python3.7/site-packages/mock/tests/testmagicmethods.py
-lib/python3.7/site-packages/mock/tests/testmock.py
-lib/python3.7/site-packages/mock/tests/testpatch.py
-lib/python3.7/site-packages/mock/tests/testsentinel.py
-lib/python3.7/site-packages/mock/tests/testwith.py
diff --git a/tests/data/mock-2.0.0-py37_1000/info/git b/tests/data/mock-2.0.0-py37_1000/info/git
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/data/mock-2.0.0-py37_1000/info/hash_input.json b/tests/data/mock-2.0.0-py37_1000/info/hash_input.json
deleted file mode 100644
index 9e26dfee..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/hash_input.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{}
\ No newline at end of file
diff --git a/tests/data/mock-2.0.0-py37_1000/info/index.json b/tests/data/mock-2.0.0-py37_1000/info/index.json
deleted file mode 100644
index e9e5d42f..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/index.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  "arch": "x86_64",
-  "build": "py37_1000",
-  "build_number": 1000,
-  "depends": [
-    "pbr >=1.3",
-    "python >=3.7,<3.8.0a0",
-    "six"
-  ],
-  "license": "BSD 2-Clause",
-  "name": "mock",
-  "platform": "osx",
-  "subdir": "osx-64",
-  "timestamp": 1538654520670,
-  "version": "2.0.0"
-}
\ No newline at end of file
diff --git a/tests/data/mock-2.0.0-py37_1000/info/paths.json b/tests/data/mock-2.0.0-py37_1000/info/paths.json
deleted file mode 100644
index 73fde176..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/paths.json
+++ /dev/null
@@ -1,197 +0,0 @@
-{
-  "paths": [
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/PKG-INFO",
-      "path_type": "hardlink",
-      "sha256": "3ede1c4415ae4e6180065d91049b7df3bcc15e353bc7fe0abe28cfde609817a9",
-      "size_in_bytes": 2852
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/SOURCES.txt",
-      "path_type": "hardlink",
-      "sha256": "1a9de2fba3a1cc68140fe184598f6d46481e7853866bbb3be9175bb309ee8562",
-      "size_in_bytes": 712
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/dependency_links.txt",
-      "path_type": "hardlink",
-      "sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
-      "size_in_bytes": 1
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/installed-files.txt",
-      "path_type": "hardlink",
-      "sha256": "d6526358ba73ef98006fd4e2bc6bdc4d678f467e7edd78be0ee73394b871eede",
-      "size_in_bytes": 1009
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/not-zip-safe",
-      "path_type": "hardlink",
-      "sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b",
-      "size_in_bytes": 1
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/pbr.json",
-      "path_type": "hardlink",
-      "sha256": "714afbdd81ba16a9def62e515616794eb01ee090eff775517f3e881b1c81695c",
-      "size_in_bytes": 46
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/requires.txt",
-      "path_type": "hardlink",
-      "sha256": "12ca807142eefd58e756935a875764d1dae3e7ca5e27a815138adf5bdacddec8",
-      "size_in_bytes": 186
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock-2.0.0-py3.7.egg-info/top_level.txt",
-      "path_type": "hardlink",
-      "sha256": "61b1946176b6d6031da0cac42e0f359e77e47c121de491809bb2bc6cc53c8564",
-      "size_in_bytes": 5
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/__init__.py",
-      "path_type": "hardlink",
-      "sha256": "179e321b2773287d0eb834f61661a167136f2274e3233d949e81cb7929b4c64b",
-      "size_in_bytes": 207
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/__pycache__/__init__.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "367f6c99b0bfb5e687d519408f073fcce466f9006681e5f0ca6afa328add8023",
-      "size_in_bytes": 251
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/__pycache__/mock.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "43284024b4ae20357e374d8c1ea7bd910f17baf305c9f9a79f29ec9ab00b787b",
-      "size_in_bytes": 63640
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/mock.py",
-      "path_type": "hardlink",
-      "sha256": "7a2603661e0ddcb40d8a53c3ed4186c6859d26c7d56f69aec501281db67afb5e",
-      "size_in_bytes": 83651
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__init__.py",
-      "path_type": "hardlink",
-      "sha256": "2fed2b162d97b427d72e6339869268392d77a3fade357b7b06c0b2421badf59e",
-      "size_in_bytes": 146
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__main__.py",
-      "path_type": "hardlink",
-      "sha256": "bbbc16cb8b42e626b7562a7a9c8febd810c759f844bbedab2d3425b674da47a0",
-      "size_in_bytes": 623
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/__init__.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "8884b2e34e208c40a593a0800ece7366336f8c633cb1cfb39008148e7f6ec807",
-      "size_in_bytes": 137
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/__main__.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "075411eb675de614152fceb23507b3b1f4c933adabcc75170c5bb103ee6b31c4",
-      "size_in_bytes": 554
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/support.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "52bdd3faf443d6929e451aab866d18ba037ab4a9efd04a42b6cc78a4c093f3c7",
-      "size_in_bytes": 1172
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testcallable.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "e93e704e84a0b02ae67963a2cc47449843481d6dc63748698161d483653aaebe",
-      "size_in_bytes": 5697
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testhelpers.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "cf54ca6845d213bcb3e81a35d3a1ee367bdebd9a76a0b0156b2826db58d089e0",
-      "size_in_bytes": 33282
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testmagicmethods.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "91f630fccf6829b13db41d82853fbb00be2fe740a17163b14ccd2bd58194f265",
-      "size_in_bytes": 21342
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testmock.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "a9b6876541e2228522a9a1cf2d250c5a5529b3b9dc0f3299333803e659f329a1",
-      "size_in_bytes": 45487
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testpatch.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "da94c52823a0df6214e1a311663dd79a7b701b1a1108a00b89901af750c90e4f",
-      "size_in_bytes": 67757
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testsentinel.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "41fc081052442e7d09bc6fd22124f0f51bbecbd589ce848023b2d7518fa2b890",
-      "size_in_bytes": 1382
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/__pycache__/testwith.cpython-37.pyc",
-      "path_type": "hardlink",
-      "sha256": "e88889d491289786df1d5f0c22c75feaf201514ed10c1425f6351b4bcd773719",
-      "size_in_bytes": 9608
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/support.py",
-      "path_type": "hardlink",
-      "sha256": "a481fe1a4a677a5f4ad56d62fdd68d97bc16e878964e4c16245b980a87f51eca",
-      "size_in_bytes": 552
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testcallable.py",
-      "path_type": "hardlink",
-      "sha256": "2bfbed9cab0f11cd7b837ef370bb61bcf4825ec9235de8d3ec248e0a5ba0ca6c",
-      "size_in_bytes": 4431
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testhelpers.py",
-      "path_type": "hardlink",
-      "sha256": "b7614ecaa40d33a3890c0085e6b8dc841e5110b7a26db7b5a5fc11692bbf184e",
-      "size_in_bytes": 28697
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testmagicmethods.py",
-      "path_type": "hardlink",
-      "sha256": "87bdf2fb53268da4f4b8b343fadf2b0fc4cf5d74c6c2942de8434fb8a93bf260",
-      "size_in_bytes": 16379
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testmock.py",
-      "path_type": "hardlink",
-      "sha256": "ff7391d6892e268db7b168dcd28267875cf5c20e8820b29f066b6c5e0fa09d79",
-      "size_in_bytes": 50470
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testpatch.py",
-      "path_type": "hardlink",
-      "sha256": "33debf24024d77d5d9ff261dee5801964d84a2c7ce8a26c6ab44cca2e61fe5b4",
-      "size_in_bytes": 56347
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testsentinel.py",
-      "path_type": "hardlink",
-      "sha256": "9dc09b80d9af801ad2e5b684f051ec150776047a971ed811456fa7d68e49c343",
-      "size_in_bytes": 976
-    },
-    {
-      "_path": "lib/python3.7/site-packages/mock/tests/testwith.py",
-      "path_type": "hardlink",
-      "sha256": "395c69382eaed333835ca9be417f0e3f8a9d0f786d4cbacd95a8709d4671d645",
-      "size_in_bytes": 10587
-    }
-  ],
-  "paths_version": 1
-}
\ No newline at end of file
diff --git a/tests/data/mock-2.0.0-py37_1000/info/recipe/conda_build_config.yaml b/tests/data/mock-2.0.0-py37_1000/info/recipe/conda_build_config.yaml
deleted file mode 100644
index a3b26ccc..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/recipe/conda_build_config.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-CONDA_BUILD_SYSROOT: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk
-MACOSX_DEPLOYMENT_TARGET: '10.9'
-build_number_decrement: '0'
-c_compiler: clang
-channel_sources: conda-forge,defaults
-channel_targets: conda-forge main
-cpu_optimization_target: nocona
-cran_mirror: https://cran.r-project.org
-cxx_compiler: clangxx
-docker_image: condaforge/linux-anvil
-fortran_compiler: gfortran
-ignore_build_only_deps: python
-lua: '5'
-macos_machine: x86_64-apple-darwin13.4.0
-macos_min_version: '10.9'
-numpy: '1.11'
-perl: 5.26.0
-pin_run_as_build:
-  python: {max_pin: x.x, min_pin: x.x}
-  r-base: {min_pin: x.x.x, max_pin: x.x.x}
-python: '3.7'
-r_base: '3.5'
-target_platform: osx-64
-zip_keys:
-- [channel_sources, channel_targets, build_number_decrement]
diff --git a/tests/data/mock-2.0.0-py37_1000/info/recipe/meta.yaml b/tests/data/mock-2.0.0-py37_1000/info/recipe/meta.yaml
deleted file mode 100644
index 0e6b8ad3..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/recipe/meta.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-# This file created by conda-build 3.15.1
-# meta.yaml template originally from:
-# /Users/travis/build/conda-forge/mock-feedstock/recipe, last modified Thu Oct  4 11:58:58 2018
-# ------------------------------------------------
-
-package:
-    name: mock
-    version: 2.0.0
-source:
-    md5: 0febfafd14330c9dcaa40de2d82d40ad
-    url: https://pypi.python.org/packages/source/m/mock/mock-2.0.0.tar.gz
-build:
-    number: '1000'
-    script: /Users/travis/miniconda3/conda-bld/mock_1538654483896/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_plac/bin/python
-        -m pip install . --no-deps --ignore-installed --no-cache-dir -vvv
-    string: py37_1000
-requirements:
-    host:
-        - ca-certificates 2018.8.24 ha4d7672_0
-        - certifi 2018.8.24 py37_1001
-        - libffi 3.2.1 hfc679d8_5
-        - ncurses 6.1 hfc679d8_1
-        - openssl 1.0.2p h470a237_0
-        - pbr 4.2.0 py_0
-        - pip 18.0 py37_1001
-        - python 3.7.0 h4eca856_1
-        - readline 7.0 haf1bffa_1
-        - setuptools 40.4.3 py37_0
-        - sqlite 3.25.2 hb1c47c0_0
-        - tk 8.6.8 ha92aebf_0
-        - wheel 0.32.0 py37_1000
-        - xz 5.2.4 h470a237_1
-        - zlib 1.2.11 h470a237_3
-    run:
-        - pbr >=1.3
-        - python >=3.7,<3.8.0a0
-        - six
-test:
-    imports:
-        - mock
-about:
-    home: https://github.com/testing-cabal/mock
-    license: BSD 2-Clause
-    license_file: LICENSE.txt
-    summary: A library for testing in Python
-extra:
-    copy_test_source_files: true
-    final: true
-    recipe-maintainers:
-        - jakirkham
-        - ocefpaf
diff --git a/tests/data/mock-2.0.0-py37_1000/info/recipe/meta.yaml.template b/tests/data/mock-2.0.0-py37_1000/info/recipe/meta.yaml.template
deleted file mode 100644
index 93e61be6..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/recipe/meta.yaml.template
+++ /dev/null
@@ -1,40 +0,0 @@
-{% set version = "2.0.0" %}
-
-package:
-  name: mock
-  version: {{ version }}
-
-source:
-  url: https://pypi.python.org/packages/source/m/mock/mock-{{ version }}.tar.gz
-  md5: 0febfafd14330c9dcaa40de2d82d40ad
-
-build:
-  number: 1000
-  script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed --no-cache-dir -vvv"
-
-requirements:
-  host:
-    - python
-    - pip
-    - pbr >=1.3
-
-  run:
-    - python
-    - pbr >=1.3
-    - funcsigs    # [py<33]
-    - six
-
-test:
-  imports:
-    - mock
-
-about:
-  home: https://github.com/testing-cabal/mock
-  license: BSD 2-Clause
-  license_file: LICENSE.txt
-  summary: A library for testing in Python
-
-extra:
-  recipe-maintainers:
-    - jakirkham
-    - ocefpaf
diff --git a/tests/data/mock-2.0.0-py37_1000/info/repodata_record.json b/tests/data/mock-2.0.0-py37_1000/info/repodata_record.json
deleted file mode 100644
index b6d5f452..00000000
--- a/tests/data/mock-2.0.0-py37_1000/info/repodata_record.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-  "arch": "x86_64",
-  "build": "py37_1000",
-  "build_number": 1000,
-  "channel": "https://conda.anaconda.org/conda-forge/osx-64",
-  "constrains": [],
-  "depends": [
-    "pbr >=1.3",
-    "python >=3.7,<3.8.0a0",
-    "six"
-  ],
-  "features": "",
-  "fn": "mock-2.0.0-py37_1000.tar.bz2",
-  "license": "BSD 2-Clause",
-  "md5": "0f9cce120a73803a70abb14bd4d4900b",
-  "name": "mock",
-  "platform": "darwin",
-  "size": 106576,
-  "subdir": "osx-64",
-  "timestamp": 1538654520670,
-  "track_features": "",
-  "url": "https://conda.anaconda.org/conda-forge/osx-64/mock-2.0.0-py37_1000.tar.bz2",
-  "version": "2.0.0"
-}
\ No newline at end of file
diff --git a/tests/test_api.py b/tests/test_api.py
index f1592b8c..c22a5808 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,23 +1,39 @@
 import json
 import os
+import pathlib
+import platform
 import shutil
 import sys
 import tarfile
+import time
 import zipfile
 from datetime import datetime
 from tempfile import TemporaryDirectory
 
 import pytest
 
+import conda_package_handling
 import conda_package_handling.tarball
-from conda_package_handling import api
+from conda_package_handling import api, exceptions
 
 this_dir = os.path.dirname(__file__)
 data_dir = os.path.join(this_dir, "data")
+version_file = pathlib.Path(this_dir).parent / "src" / "conda_package_handling" / "__init__.py"
 test_package_name = "mock-2.0.0-py37_1000"
 test_package_name_2 = "cph_test_data-0.0.1-0"
 
 
+@pytest.mark.skipif(
+    bool(os.environ.get("GITHUB_ACTIONS", False)), reason="Fails on GitHub Actions"
+)
+@pytest.mark.skipif(not version_file.exists(), reason=f"Could not find {version_file}")
+def test_correct_version():
+    """
+    Prevent accidentally running tests against a globally installed different version.
+    """
+    assert conda_package_handling.__version__ in version_file.read_text()
+
+
 def test_api_extract_tarball_implicit_path(testing_workdir):
     tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2")
     local_tarfile = os.path.join(testing_workdir, os.path.basename(tarfile))
@@ -51,20 +67,6 @@ def test_api_extract_tarball_explicit_path(testing_workdir):
     assert os.path.isfile(os.path.join(testing_workdir, "manual_path", "info", "index.json"))
 
 
-def test_api_extract_tarball_with_libarchive_import_error(testing_workdir, mocker):
-    try:
-        api.libarchive_enabled = False
-        conda_package_handling.tarball.libarchive_enabled = False
-        tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2")
-        local_tarfile = os.path.join(testing_workdir, os.path.basename(tarfile))
-        shutil.copy2(tarfile, local_tarfile)
-        api.extract(local_tarfile, "manual_path")
-        assert os.path.isfile(os.path.join(testing_workdir, "manual_path", "info", "index.json"))
-    finally:
-        api.libarchive_enabled = True
-        conda_package_handling.tarball.libarchive_enabled = True
-
-
 def test_api_extract_conda_v2_implicit_path(testing_workdir):
     condafile = os.path.join(data_dir, test_package_name + ".conda")
     local_condafile = os.path.join(testing_workdir, os.path.basename(condafile))
@@ -90,11 +92,11 @@ def test_api_extract_conda_v2_no_destdir_relative_path(testing_workdir):
 
 
 def test_api_extract_conda_v2_explicit_path(testing_workdir):
-    tarfile = os.path.join(data_dir, test_package_name + ".conda")
-    local_tarfile = os.path.join(testing_workdir, os.path.basename(tarfile))
-    shutil.copy2(tarfile, local_tarfile)
+    condafile = os.path.join(data_dir, test_package_name + ".conda")
+    local_condafile = os.path.join(testing_workdir, os.path.basename(condafile))
+    shutil.copy2(condafile, local_condafile)
 
-    api.extract(tarfile, "manual_path")
+    api.extract(condafile, "manual_path")
     assert os.path.isfile(os.path.join(testing_workdir, "manual_path", "info", "index.json"))
 
 
@@ -132,7 +134,9 @@ def check_conda_v2_metadata(condafile):
 
 def test_api_transmute_tarball_to_conda_v2(testing_workdir):
     tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2")
-    errors = api.transmute(tarfile, ".conda", testing_workdir)
+    # lower compress level makes the test run much faster, even 15 is much
+    # better than 22
+    errors = api.transmute(tarfile, ".conda", testing_workdir, zstd_compress_level=3)
     assert not errors
     condafile = os.path.join(testing_workdir, test_package_name + ".conda")
     assert os.path.isfile(condafile)
@@ -146,10 +150,13 @@ def test_api_transmute_tarball_info_sorts_first(testing_workdir):
         test_packages += test_packages_with_symlinks
     for test_package in test_packages:
         test_file = os.path.join(data_dir, test_package + ".tar.bz2")
-        # skip 'don't transmute to same extension' logic
-        fn, out_fn, errors = api._convert(test_file, ".tar.bz2", testing_workdir)
-        assert fn == test_file
-        assert not errors
+
+        # transmute/convert doesn't re-sort files; extract to folder.
+        api.extract(test_file, testing_workdir)
+        out_fn = os.path.join(testing_workdir, test_package + ".tar.bz2")
+        out = api.create(testing_workdir, None, out_fn)
+        assert out == out_fn
+
         # info must be first
         with tarfile.open(out_fn, "r:bz2") as repacked:
             info_seen = False
@@ -176,7 +183,7 @@ def _walk(path):
 
     tar_path = os.path.join(data_dir, test_package_name_2 + ".tar.bz2")
     conda_path = os.path.join(testing_workdir, test_package_name_2 + ".conda")
-    api.transmute(tar_path, ".conda", testing_workdir)
+    api.transmute(tar_path, ".conda", testing_workdir, zstd_compress_level=3)
 
     # Verify original contents were all put in the right place
     pkg_tarbz2 = tarfile.open(tar_path, mode="r:bz2")
@@ -222,8 +229,18 @@ def _walk(path):
 
 def test_api_transmute_conda_v2_to_tarball(testing_workdir):
     condafile = os.path.join(data_dir, test_package_name + ".conda")
-    api.transmute(condafile, ".tar.bz2", testing_workdir)
-    assert os.path.isfile(os.path.join(testing_workdir, test_package_name + ".tar.bz2"))
+    outfile = pathlib.Path(testing_workdir, test_package_name + ".tar.bz2")
+    # one quiet=True in the test suite for coverage
+    api.transmute(condafile, ".tar.bz2", testing_workdir, quiet=True)
+    assert outfile.is_file()
+
+    # test that no-force keeps file, and force overwrites file
+    for force in False, True:
+        mtime = outfile.stat().st_mtime
+        time.sleep(2 if platform.platform() == "Windows" else 0)
+        api.transmute(condafile, ".tar.bz2", testing_workdir, force=force)
+        mtime2 = outfile.stat().st_mtime
+        assert (mtime2 == mtime) != force
 
 
 def test_warning_when_bundling_no_metadata(testing_workdir):
@@ -244,14 +261,24 @@ def test_create_package_with_uncommon_conditions_captures_all_content(testing_wo
     os.symlink("../a_folder/text_file", "src/symlink_stuff/symlink_to_text_file")
 
     with tarfile.open("pinkie.tar.bz2", "w:bz2") as tf:
-        tf.add("src/empty_folder", "empty_folder")
-        tf.add("src/empty_file", "empty_file")
-        tf.add("src/a_folder", "a_folder")
-        tf.add("src/a_folder/text_file", "a_folder/text_file")
-        tf.add("src/a_folder/hardlink_to_text_file", "a_folder/hardlink_to_text_file")
-        tf.add("src/symlink_stuff/symlink_to_a", "symlink_stuff/symlink_to_a")
-        tf.add("src/symlink_stuff/symlink_to_empty_file", "symlink_stuff/symlink_to_empty_file")
-        tf.add("src/symlink_stuff/symlink_to_text_file", "symlink_stuff/symlink_to_text_file")
+
+        def add(source, target):
+            tf.add(source, target, recursive=False)
+
+        add("src/empty_folder", "empty_folder")
+        add("src/empty_file", "empty_file")
+        add("src/a_folder", "a_folder")
+        add("src/a_folder/text_file", "a_folder/text_file")
+        add("src/a_folder/hardlink_to_text_file", "a_folder/hardlink_to_text_file")
+        add("src/symlink_stuff/symlink_to_a", "symlink_stuff/symlink_to_a")
+        add(
+            "src/symlink_stuff/symlink_to_empty_file",
+            "symlink_stuff/symlink_to_empty_file",
+        )
+        add(
+            "src/symlink_stuff/symlink_to_text_file",
+            "symlink_stuff/symlink_to_text_file",
+        )
 
     cph_created = api.create("src", None, "thebrain.tar.bz2")
 
@@ -322,3 +349,131 @@ def tests_secure_refusal_to_extract_dotdot(testing_workdir):
 
     with pytest.raises(api.InvalidArchiveError):
         api.extract("pinkie.tar.bz2")
+
+
+def test_api_bad_filename(testing_workdir):
+    with pytest.raises(ValueError):
+        api.extract("pinkie.rar", testing_workdir)
+
+
+def test_details_bad_extension():
+    with pytest.raises(ValueError):
+        # TODO this function should not exist
+        api.get_pkg_details("pinkie.rar")
+
+
+def test_convert_bad_extension(testing_workdir):
+    api._convert("pinkie.rar", ".conda", testing_workdir)
+
+
+def test_convert_keyerror(tmpdir, mocker):
+    tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2")
+
+    mocker.patch(
+        "conda_package_streaming.transmute.transmute",
+        side_effect=KeyboardInterrupt(),
+    )
+
+    # interrupted before ".conda" was created
+    with pytest.raises(KeyboardInterrupt):
+        api._convert(tarfile, ".conda", tmpdir)
+
+    def create_file_and_raise(*args, **kwargs):
+        out_fn = pathlib.Path(tmpdir, pathlib.Path(tarfile[: -len(".tar.bz2")] + ".conda").name)
+        print("out fn", out_fn)
+        out_fn.write_text("")
+        raise KeyboardInterrupt()
+
+    mocker.patch("conda_package_streaming.transmute.transmute", side_effect=create_file_and_raise)
+
+    # interrupted after ".conda" was created
+    with pytest.raises(KeyboardInterrupt):
+        api._convert(tarfile, ".conda", tmpdir)
+
+
+def test_create_filelist(tmpdir, mocker):
+    # another bad API, tested for coverage
+    filelist = pathlib.Path(tmpdir, "filelist.txt")
+    filelist.write_text("\n".join(["filelist.txt", "anotherfile"]))
+
+    # when looking for filelist-not-found.txt
+    with pytest.raises(FileNotFoundError):
+        api.create(str(tmpdir), "filelist-not-found.txt", str(tmpdir / "newconda.conda"))
+
+    # when adding anotherfile
+    with pytest.raises(FileNotFoundError):
+        api.create(str(tmpdir), str(filelist), str(tmpdir / "newconda.conda"))
+
+    # unrecognized target extension
+    with pytest.raises(ValueError):
+        api.create(str(tmpdir), str(filelist), str(tmpdir / "newpackage.rar"))
+
+    def create_file_and_raise(prefix, file_list, out_fn, *args, **kwargs):
+        pathlib.Path(prefix, out_fn).write_text("")
+        raise KeyboardInterrupt()
+
+    mocker.patch(
+        "conda_package_handling.conda_fmt.CondaFormat_v2.create",
+        side_effect=create_file_and_raise,
+    )
+
+    # failure inside inner create()
+    with pytest.raises(KeyboardInterrupt):
+        api.create(str(tmpdir), str(filelist), str(tmpdir / "newpackage.conda"))
+
+
+def test_api_transmute_fail_validation(tmpdir, mocker):
+    package = os.path.join(data_dir, test_package_name + ".conda")
+
+    # this code is only called for .conda -> .tar.bz2; a streaming validate for
+    # .tar.bz2 -> .conda would be a good idea.
+    mocker.patch(
+        "conda_package_handling.validate.validate_converted_files_match_streaming",
+        return_value=(str(package), {"missing-file.txt"}, {"mismatched-size.txt"}),
+    )
+
+    errors = api.transmute(package, ".tar.bz2", tmpdir)
+    assert errors
+
+
+def test_api_transmute_fail_validation_to_conda(tmpdir, mocker):
+    package = os.path.join(data_dir, test_package_name + ".tar.bz2")
+
+    mocker.patch(
+        "conda_package_handling.validate.validate_converted_files_match_streaming",
+        return_value=(str(package), {"missing-file.txt"}, {"mismatched-size.txt"}),
+    )
+
+    errors = api.transmute(package, ".conda", tmpdir, zstd_compress_level=3)
+    assert errors
+
+
+def test_api_transmute_fail_validation_2(tmpdir, mocker):
+    package = os.path.join(data_dir, test_package_name + ".conda")
+    tmptarfile = tmpdir / pathlib.Path(package).name
+    shutil.copy(package, tmptarfile)
+
+    mocker.patch(
+        "conda_package_handling.validate.validate_converted_files_match_streaming",
+        side_effect=Exception("not today"),
+    )
+
+    # run with out_folder=None
+    errors = api.transmute(str(tmptarfile), ".tar.bz2")
+    assert errors
+
+
+def test_api_translates_exception(mocker, tmpdir):
+    from conda_package_streaming.extract import exceptions as cps_exceptions
+
+    tarfile = os.path.join(data_dir, test_package_name + ".tar.bz2")
+
+    # translates their exception to our exception of the same name
+    mocker.patch(
+        "conda_package_streaming.package_streaming.stream_conda_component",
+        side_effect=cps_exceptions.CaseInsensitiveFileSystemError(),
+    )
+
+    # should this be exported from the api or inherit from InvalidArchiveError?
+    with pytest.raises(exceptions.CaseInsensitiveFileSystemError):
+        api.extract(tarfile, tmpdir)
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 00000000..7f981d81
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,48 @@
+from pathlib import Path
+
+import pytest
+
+import conda_package_handling.cli as cli
+
+from .test_api import data_dir, test_package_name
+
+
+def test_cli(tmpdir, mocker):
+    """
+    Code coverage for the cli.
+    """
+    for command in [
+        ["x", str(Path(data_dir, test_package_name + ".tar.bz2")), f"--prefix={tmpdir}"],
+        [
+            "x",
+            str(Path(data_dir, test_package_name + ".conda")),
+            "--info",
+            f"--prefix={tmpdir}",
+        ],
+        ["c", str(Path(tmpdir, test_package_name)), ".tar.bz2", f"--out-folder={tmpdir}"],
+    ]:
+        cli.main(args=command)
+
+    # XXX difficult to get to this error handling code through the actual CLI;
+    # for example, a .tar.bz2 that can't be extracted raises OSError instead of
+    # returning errors. Designed for .tar.bz2 -> .conda conversions that somehow
+    # omit files?
+    mocker.patch(
+        "conda_package_handling.api.transmute", return_value=set("that is why you fail".split())
+    )
+    with pytest.raises(SystemExit):
+        command = [
+            "t",
+            str(Path(data_dir, test_package_name + ".tar.bz2")),
+            ".conda",
+            f"--out-folder={tmpdir}",
+        ]
+        cli.main(args=command)
+
+
+def test_import_main():
+    """
+    e.g. python -m conda_package_handling
+    """
+    with pytest.raises(SystemExit):
+        import conda_package_handling.__main__
diff --git a/tests/test_degraded.py b/tests/test_degraded.py
new file mode 100644
index 00000000..6da50706
--- /dev/null
+++ b/tests/test_degraded.py
@@ -0,0 +1,47 @@
+"""
+Test conda-package-handling can work in `.tar.bz2`-only mode if zstandard is not
+available. (Giving the user a chance to immediately install zstandard.)
+"""
+
+import importlib
+import subprocess
+import sys
+
+
+def test_degraded():
+    try:
+        sys.modules["zstandard"] = None  # type: ignore
+        sys.modules["conda_package_streaming.transmute"] = None  # type: ignore
+        sys.modules["conda_package_handling.conda_fmt"] = None  # type: ignore
+
+        # this is only testing conda_package_handling's code, and does not test
+        # that conda_package_streaming works without zstandard.
+
+        import conda_package_handling.api
+
+        importlib.reload(conda_package_handling.api)
+
+        assert conda_package_handling.api.libarchive_enabled == False
+
+    finally:
+        sys.modules.pop("zstandard", None)
+        sys.modules.pop("conda_package_handling.conda_fmt", None)
+        sys.modules.pop("conda_package_streaming.transmute", None)
+        import conda_package_handling.api
+
+        importlib.reload(conda_package_handling.api)
+
+        assert conda_package_handling.api.libarchive_enabled == True
+
+
+def test_degraded_subprocess():
+    """
+    More reliable way to mock 'zstandard not available'
+    """
+    subprocess.check_call(
+        [
+            sys.executable,
+            "-c",
+            "import sys; sys.modules['zstandard'] = None; import conda_package_handling.api",
+        ]
+    )
diff --git a/tests/test_interface.py b/tests/test_interface.py
new file mode 100644
index 00000000..65f4a675
--- /dev/null
+++ b/tests/test_interface.py
@@ -0,0 +1,53 @@
+"""
+Test format classes.
+
+(Some of their code is unreachable through api.py)
+"""
+
+import os
+from pathlib import Path
+
+import pytest
+
+from conda_package_handling.conda_fmt import CondaFormat_v2
+from conda_package_handling.tarball import CondaTarBZ2
+
+from .test_api import data_dir, test_package_name
+
+TEST_CONDA = Path(data_dir, test_package_name + ".conda")
+TEST_TARBZ = Path(data_dir, test_package_name + ".tar.bz2")
+
+
+def test_extract_create(tmpdir):
+    for format, infile, outfile in (
+        (CondaFormat_v2, TEST_CONDA, "newmock.conda"),
+        (CondaTarBZ2, TEST_TARBZ, "newmock.tar.bz2"),
+    ):
+
+        both_path = Path(tmpdir, f"mkdirs-{outfile.split('.', 1)[-1]}")
+
+        # these old APIs don't guarantee Path-like's
+        format.extract(infile, str(both_path))
+        assert sorted(os.listdir(both_path)) == sorted(["lib", "info"])
+
+        if format == CondaFormat_v2:
+            info_path = Path(tmpdir, "info-only")
+            format.extract_info(TEST_CONDA, str(info_path))  # type: ignore
+            assert os.listdir(info_path) == ["info"]
+
+        filelist = [str(p.relative_to(both_path)) for p in both_path.rglob("*")]
+        format.create(
+            both_path,
+            filelist,
+            tmpdir / outfile,
+            # compression_tuple is for libarchive compatibility. Instead, pass
+            # compressor=(compressor factory function)
+            compression_tuple=(".tar.zst", "zstd", "zstd:compression-level=1"),
+        )
+
+        assert (tmpdir / outfile).exists()
+
+        with pytest.raises(ValueError):
+            CondaFormat_v2.create(
+                "", [], "", compressor=True, compression_tuple=("1", "2", "3")  # type: ignore
+            )
diff --git a/tests/test_validate.py b/tests/test_validate.py
new file mode 100644
index 00000000..58ac7708
--- /dev/null
+++ b/tests/test_validate.py
@@ -0,0 +1,50 @@
+from pathlib import Path
+
+from conda_package_handling.validate import validate_converted_files_match_streaming
+
+from .test_api import data_dir, test_package_name, test_package_name_2
+
+
+def test_validate_streaming():
+    assert validate_converted_files_match_streaming(
+        Path(data_dir, test_package_name + ".conda"),
+        Path(data_dir, test_package_name + ".tar.bz2"),
+        strict=False,
+    ) == (Path(data_dir, test_package_name + ".conda"), [], [])
+
+    # old converted files don't match uname, gname, mtime
+    assert validate_converted_files_match_streaming(
+        Path(data_dir, test_package_name + ".conda"),
+        Path(data_dir, test_package_name + ".tar.bz2"),
+        strict=True,
+    ) != (Path(data_dir, test_package_name + ".conda"), [], [])
+
+    src, missing, mismatched = validate_converted_files_match_streaming(
+        Path(data_dir, test_package_name_2 + ".tar.bz2"),
+        Path(data_dir, test_package_name + ".conda"),
+        strict=False,
+    )
+
+    assert src == Path(data_dir, test_package_name_2 + ".tar.bz2")
+    # not that critical exactly what mismatches; we are comparing separate packages
+    assert len(missing) == 47
+    assert mismatched == [
+        "info/hash_input.json",
+        "info/files",
+        "info/index.json",
+        "info/paths.json",
+        "info/about.json",
+        "info/git",
+        "info/recipe/meta.yaml",
+        "info/recipe/conda_build_config.yaml",
+        "info/recipe/meta.yaml.template",
+        "info/hash_input.json",
+        "info/index.json",
+        "info/files",
+        "info/about.json",
+        "info/paths.json",
+        "info/git",
+        "info/recipe/meta.yaml.template",
+        "info/recipe/conda_build_config.yaml",
+        "info/recipe/meta.yaml",
+    ]