[Reproducible-commits] [wheel] 01/03: imported from wheel_0.24.0.orig.tar.gz

Holger Levsen holger at moszumanska.debian.org
Mon Mar 23 12:57:33 UTC 2015


This is an automated email from the git hooks/post-receive script.

holger pushed a commit to branch pu/reproducible_builds
in repository wheel.

commit 6571c17da82a9106807b2f25b6688e106b74d2a3
Author: Holger Levsen <holger at layer-acht.org>
Date:   Mon Mar 23 13:55:38 2015 +0100

    imported from wheel_0.24.0.orig.tar.gz
---
 CHANGES.txt                                     | 230 ++++++++++++
 LICENSE.txt                                     |  22 ++
 MANIFEST.in                                     |   7 +
 PKG-INFO                                        | 294 +++++++++++++++
 README.txt                                      |  41 ++
 setup.cfg                                       |  14 +
 setup.py                                        |  57 +++
 vendorize.sh                                    |   3 +
 wheel.egg-info/PKG-INFO                         | 294 +++++++++++++++
 wheel.egg-info/SOURCES.txt                      |  52 +++
 wheel.egg-info/dependency_links.txt             |   1 +
 wheel.egg-info/entry_points.txt                 |   5 +
 wheel.egg-info/not-zip-safe                     |   1 +
 wheel.egg-info/requires.txt                     |  16 +
 wheel.egg-info/top_level.txt                    |   1 +
 wheel/__init__.py                               |   2 +
 wheel/__main__.py                               |  17 +
 wheel/archive.py                                |  61 +++
 wheel/bdist_wheel.py                            | 446 ++++++++++++++++++++++
 wheel/decorator.py                              |  19 +
 wheel/egg2wheel.py                              |  73 ++++
 wheel/eggnames.txt                              |  87 +++++
 wheel/install.py                                | 480 ++++++++++++++++++++++++
 wheel/metadata.py                               | 304 +++++++++++++++
 wheel/paths.py                                  |  41 ++
 wheel/pep425tags.py                             |  97 +++++
 wheel/pkginfo.py                                |  44 +++
 wheel/signatures/__init__.py                    | 106 ++++++
 wheel/signatures/djbec.py                       | 270 +++++++++++++
 wheel/signatures/ed25519py.py                   |  52 +++
 wheel/signatures/keys.py                        |  99 +++++
 wheel/test/__init__.py                          |   1 +
 wheel/test/complex-dist/complexdist/__init__.py |   2 +
 wheel/test/complex-dist/setup.py                |  25 ++
 wheel/test/headers.dist/header.h                |   0
 wheel/test/headers.dist/headersdist.py          |   0
 wheel/test/headers.dist/setup.py                |  16 +
 wheel/test/pydist-schema.json                   | 362 ++++++++++++++++++
 wheel/test/simple.dist/setup.py                 |  17 +
 wheel/test/simple.dist/simpledist/__init__.py   |   0
 wheel/test/test-1.0-py2.py3-none-win32.whl      | Bin 0 -> 5226 bytes
 wheel/test/test_basic.py                        | 176 +++++++++
 wheel/test/test_install.py                      |  55 +++
 wheel/test/test_keys.py                         |  98 +++++
 wheel/test/test_paths.py                        |   6 +
 wheel/test/test_ranking.py                      |  43 +++
 wheel/test/test_signatures.py                   |  47 +++
 wheel/test/test_tagopt.py                       | 112 ++++++
 wheel/test/test_tool.py                         |  28 ++
 wheel/test/test_wheelfile.py                    |  69 ++++
 wheel/tool/__init__.py                          | 362 ++++++++++++++++++
 wheel/util.py                                   | 146 +++++++
 wheel/wininst2wheel.py                          | 187 +++++++++
 53 files changed, 4988 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..26754c9
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,230 @@
+0.24.0
+======
+- The python tag used for pure-python packages is now .pyN (major version
+  only). This change actually occurred in 0.23.0 when the --python-tag
+  option was added, but was not explicitly mentioned in the changelog then.
+- wininst2wheel and egg2wheel removed. Use "wheel convert [archive]"
+  instead.
+- Wheel now supports setuptools style conditional requirements via the
+  extras_require={} syntax. Separate 'extra' names from conditions using
+  the : character. Wheel's own setup.py does this. (The empty-string
+  extra is the same as install_requires.) These conditional requirements
+  should work the same whether the package is installed by wheel or
+  by setup.py.
+
+0.23.0
+======
+- Compatibiltiy tag flags added to the bdist_wheel command
+- sdist should include files necessary for tests
+- 'wheel convert' can now also convert unpacked eggs to wheel
+- Rename pydist.json to metadata.json to avoid stepping on the PEP
+- The --skip-scripts option has been removed, and not generating scripts is now
+  the default. The option was a temporary approach until installers could
+  generate scripts themselves. That is now the case with pip 1.5 and later.
+  Note that using pip 1.4 to install a wheel without scripts will leave the
+  installation without entry-point wrappers. The "wheel install-scripts"
+  command can be used to generate the scripts in such cases.
+- Thank you contributors
+
+0.22.0
+======
+- Include entry_points.txt, scripts a.k.a. commands, in experimental
+  pydist.json
+- Improved test_requires parsing
+- Python 2.6 fixes, "wheel version" command courtesy pombredanne
+
+0.21.0
+======
+- Pregenerated scripts are the default again.
+- "setup.py bdist_wheel --skip-scripts" turns them off.
+- setuptools is no longer a listed requirement for the 'wheel'
+  package. It is of course still required in order for bdist_wheel
+  to work.
+- "python -m wheel" avoids importing pkg_resources until it's necessary.
+
+0.20.0
+======
+- No longer include console_scripts in wheels. Ordinary scripts (shell files,
+  standalone Python files) are included as usual.
+- Include new command "python -m wheel install-scripts [distribution
+  [distribution ...]]" to install the console_scripts (setuptools-style
+  scripts using pkg_resources) for a distribution.
+
+0.19.0
+======
+- pymeta.json becomes pydist.json
+
+0.18.0
+======
+- Python 3 Unicode improvements
+
+0.17.0
+======
+- Support latest PEP-426 "pymeta.json" (json-format metadata)
+
+0.16.0
+======
+- Python 2.6 compatibility bugfix (thanks John McFarlane)
+- Non-prerelease version number
+
+1.0.0a2
+=======
+- Bugfix for C-extension tags for CPython 3.3 (using SOABI)
+
+1.0.0a1
+=======
+- Bugfix for bdist_wininst converter "wheel convert"
+- Bugfix for dists where "is pure" is None instead of True or False
+
+1.0.0a0
+=======
+- Update for version 1.0 of Wheel (PEP accepted).
+- Python 3 fix for moving Unicode Description to metadata body
+- Include rudimentary API documentation in Sphinx (thanks Kevin Horn)
+
+0.15.0
+======
+- Various improvements
+
+0.14.0
+======
+- Changed the signature format to better comply with the current JWS spec.
+  Breaks all existing signatures.
+- Include ``wheel unsign`` command to remove RECORD.jws from an archive.
+- Put the description in the newly allowed payload section of PKG-INFO
+  (METADATA) files.
+
+0.13.0
+======
+- Use distutils instead of sysconfig to get installation paths; can install
+  headers.
+- Improve WheelFile() sort.
+- Allow bootstrap installs without any pkg_resources.
+
+0.12.0
+======
+- Unit test for wheel.tool.install
+
+0.11.0
+======
+- API cleanup
+
+0.10.3
+======
+- Scripts fixer fix
+
+0.10.2
+======
+- Fix keygen
+
+0.10.1
+======
+- Preserve attributes on install.
+
+0.10.0
+======
+- Include a copy of pkg_resources. Wheel can now install into a virtualenv
+  that does not have distribute (though most packages still require
+  pkg_resources to actually work; wheel install distribute)
+- Define a new setup.cfg section [wheel]. universal=1 will
+  apply the py2.py3-none-any tag for pure python wheels.
+
+0.9.7
+=====
+- Only import dirspec when needed. dirspec is only needed to find the
+  configuration for keygen/signing operations.
+
+0.9.6
+=====
+- requires-dist from setup.cfg overwrites any requirements from setup.py
+  Care must be taken that the requirements are the same in both cases,
+  or just always install from wheel.
+- drop dirspec requirement on win32
+- improved command line utility, adds 'wheel convert [egg or wininst]' to
+  convert legacy binary formats to wheel
+
+0.9.5
+=====
+- Wheel's own wheel file can be executed by Python, and can install itself:
+  ``python wheel-0.9.5-py27-none-any/wheel install ...``
+- Use argparse; basic ``wheel install`` command should run with only stdlib
+  dependencies.
+- Allow requires_dist in setup.cfg's [metadata] section. In addition to
+  dependencies in setup.py, but will only be interpreted when installing
+  from wheel, not from sdist. Can be qualified with environment markers.
+
+0.9.4
+=====
+- Fix wheel.signatures in sdist
+
+0.9.3
+=====
+- Integrated digital signatures support without C extensions.
+- Integrated "wheel install" command (single package, no dependency
+  resolution) including compatibility check.
+- Support Python 3.3
+- Use Metadata 1.3 (PEP 426)
+
+0.9.2
+=====
+- Automatic signing if WHEEL_TOOL points to the wheel binary
+- Even more Python 3 fixes
+
+0.9.1
+=====
+- 'wheel sign' uses the keys generated by 'wheel keygen' (instead of generating
+  a new key at random each time)
+- Python 2/3 encoding/decoding fixes
+- Run tests on Python 2.6 (without signature verification)
+
+0.9
+===
+- Updated digital signatures scheme
+- Python 3 support for digital signatures
+- Always verify RECORD hashes on extract
+- "wheel" command line tool to sign, verify, unpack wheel files
+
+0.8
+===
+- none/any draft pep tags update
+- improved wininst2wheel script
+- doc changes and other improvements
+
+0.7
+===
+- sort .dist-info at end of wheel archive
+- Windows & Python 3 fixes from Paul Moore
+- pep8
+- scripts to convert wininst & egg to wheel
+
+0.6
+===
+- require distribute >= 0.6.28
+- stop using verlib
+
+0.5
+===
+- working pretty well
+
+0.4.2
+=====
+- hyphenated name fix
+
+0.4
+===
+- improve test coverage
+- improve Windows compatibility
+- include tox.ini courtesy of Marc Abramowitz
+- draft hmac sha-256 signing function
+
+0.3
+===
+- prototype egg2wheel conversion script
+
+0.2
+===
+- Python 3 compatibility
+
+0.1
+===
+- Initial version
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..c3441e6
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,22 @@
+"wheel" copyright (c) 2012-2014 Daniel Holth <dholth at fastmail.fm> and
+contributors.
+
+The MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..990e55d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,7 @@
+include wheel/*.txt *.txt *.sh
+recursive-include wheel/test *.py
+include wheel/test/test-1.0-py2.py3-none-win32.whl
+include wheel/test/headers.dist/header.h
+include wheel/test/pydist-schema.json
+prune wheel/test/*/dist
+prune wheel/test/*/build
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..d499ecf
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,294 @@
+Metadata-Version: 1.1
+Name: wheel
+Version: 0.24.0
+Summary: A built-package format for Python.
+Home-page: http://bitbucket.org/pypa/wheel/
+Author: Daniel Holth
+Author-email: dholth at fastmail.fm
+License: MIT
+Description: Wheel
+        =====
+        
+        A built-package format for Python.
+        
+        A wheel is a ZIP-format archive with a specially formatted filename
+        and the .whl extension. It is designed to contain all the files for a
+        PEP 376 compatible install in a way that is very close to the on-disk
+        format. Many packages will be properly installed with only the "Unpack"
+        step (simply extracting the file onto sys.path), and the unpacked archive
+        preserves enough information to "Spread" (copy data and scripts to their
+        final locations) at any later time.
+        
+        The wheel project provides a `bdist_wheel` command for setuptools
+        (requires setuptools >= 0.8.0). Wheel files can be installed with a
+        newer `pip` from https://github.com/pypa/pip or with wheel's own command
+        line utility.
+        
+        The wheel documentation is at http://wheel.rtfd.org/. The file format
+        is documented in PEP 427 (http://www.python.org/dev/peps/pep-0427/).
+        
+        The reference implementation is at https://bitbucket.org/pypa/wheel
+        
+        Why not egg?
+        ------------
+        
+        Python's egg format predates the packaging related standards we have
+        today, the most important being PEP 376 "Database of Installed Python
+        Distributions" which specifies the .dist-info directory (instead of
+        .egg-info) and PEP 426 "Metadata for Python Software Packages 2.0"
+        which specifies how to express dependencies (instead of requires.txt
+        in .egg-info).
+        
+        Wheel implements these things. It also provides a richer file naming
+        convention that communicates the Python implementation and ABI as well
+        as simply the language version used in a particular package.
+        
+        Unlike .egg, wheel will be a fully-documented standard at the binary
+        level that is truly easy to install even if you do not want to use the
+        reference implementation.
+        
+        
+        
+        0.24.0
+        ======
+        - The python tag used for pure-python packages is now .pyN (major version
+          only). This change actually occurred in 0.23.0 when the --python-tag
+          option was added, but was not explicitly mentioned in the changelog then.
+        - wininst2wheel and egg2wheel removed. Use "wheel convert [archive]"
+          instead.
+        - Wheel now supports setuptools style conditional requirements via the
+          extras_require={} syntax. Separate 'extra' names from conditions using
+          the : character. Wheel's own setup.py does this. (The empty-string
+          extra is the same as install_requires.) These conditional requirements
+          should work the same whether the package is installed by wheel or
+          by setup.py.
+        
+        0.23.0
+        ======
+        - Compatibiltiy tag flags added to the bdist_wheel command
+        - sdist should include files necessary for tests
+        - 'wheel convert' can now also convert unpacked eggs to wheel
+        - Rename pydist.json to metadata.json to avoid stepping on the PEP
+        - The --skip-scripts option has been removed, and not generating scripts is now
+          the default. The option was a temporary approach until installers could
+          generate scripts themselves. That is now the case with pip 1.5 and later.
+          Note that using pip 1.4 to install a wheel without scripts will leave the
+          installation without entry-point wrappers. The "wheel install-scripts"
+          command can be used to generate the scripts in such cases.
+        - Thank you contributors
+        
+        0.22.0
+        ======
+        - Include entry_points.txt, scripts a.k.a. commands, in experimental
+          pydist.json
+        - Improved test_requires parsing
+        - Python 2.6 fixes, "wheel version" command courtesy pombredanne
+        
+        0.21.0
+        ======
+        - Pregenerated scripts are the default again.
+        - "setup.py bdist_wheel --skip-scripts" turns them off.
+        - setuptools is no longer a listed requirement for the 'wheel'
+          package. It is of course still required in order for bdist_wheel
+          to work.
+        - "python -m wheel" avoids importing pkg_resources until it's necessary.
+        
+        0.20.0
+        ======
+        - No longer include console_scripts in wheels. Ordinary scripts (shell files,
+          standalone Python files) are included as usual.
+        - Include new command "python -m wheel install-scripts [distribution
+          [distribution ...]]" to install the console_scripts (setuptools-style
+          scripts using pkg_resources) for a distribution.
+        
+        0.19.0
+        ======
+        - pymeta.json becomes pydist.json
+        
+        0.18.0
+        ======
+        - Python 3 Unicode improvements
+        
+        0.17.0
+        ======
+        - Support latest PEP-426 "pymeta.json" (json-format metadata)
+        
+        0.16.0
+        ======
+        - Python 2.6 compatibility bugfix (thanks John McFarlane)
+        - Non-prerelease version number
+        
+        1.0.0a2
+        =======
+        - Bugfix for C-extension tags for CPython 3.3 (using SOABI)
+        
+        1.0.0a1
+        =======
+        - Bugfix for bdist_wininst converter "wheel convert"
+        - Bugfix for dists where "is pure" is None instead of True or False
+        
+        1.0.0a0
+        =======
+        - Update for version 1.0 of Wheel (PEP accepted).
+        - Python 3 fix for moving Unicode Description to metadata body
+        - Include rudimentary API documentation in Sphinx (thanks Kevin Horn)
+        
+        0.15.0
+        ======
+        - Various improvements
+        
+        0.14.0
+        ======
+        - Changed the signature format to better comply with the current JWS spec.
+          Breaks all existing signatures.
+        - Include ``wheel unsign`` command to remove RECORD.jws from an archive.
+        - Put the description in the newly allowed payload section of PKG-INFO
+          (METADATA) files.
+        
+        0.13.0
+        ======
+        - Use distutils instead of sysconfig to get installation paths; can install
+          headers.
+        - Improve WheelFile() sort.
+        - Allow bootstrap installs without any pkg_resources.
+        
+        0.12.0
+        ======
+        - Unit test for wheel.tool.install
+        
+        0.11.0
+        ======
+        - API cleanup
+        
+        0.10.3
+        ======
+        - Scripts fixer fix
+        
+        0.10.2
+        ======
+        - Fix keygen
+        
+        0.10.1
+        ======
+        - Preserve attributes on install.
+        
+        0.10.0
+        ======
+        - Include a copy of pkg_resources. Wheel can now install into a virtualenv
+          that does not have distribute (though most packages still require
+          pkg_resources to actually work; wheel install distribute)
+        - Define a new setup.cfg section [wheel]. universal=1 will
+          apply the py2.py3-none-any tag for pure python wheels.
+        
+        0.9.7
+        =====
+        - Only import dirspec when needed. dirspec is only needed to find the
+          configuration for keygen/signing operations.
+        
+        0.9.6
+        =====
+        - requires-dist from setup.cfg overwrites any requirements from setup.py
+          Care must be taken that the requirements are the same in both cases,
+          or just always install from wheel.
+        - drop dirspec requirement on win32
+        - improved command line utility, adds 'wheel convert [egg or wininst]' to
+          convert legacy binary formats to wheel
+        
+        0.9.5
+        =====
+        - Wheel's own wheel file can be executed by Python, and can install itself:
+          ``python wheel-0.9.5-py27-none-any/wheel install ...``
+        - Use argparse; basic ``wheel install`` command should run with only stdlib
+          dependencies.
+        - Allow requires_dist in setup.cfg's [metadata] section. In addition to
+          dependencies in setup.py, but will only be interpreted when installing
+          from wheel, not from sdist. Can be qualified with environment markers.
+        
+        0.9.4
+        =====
+        - Fix wheel.signatures in sdist
+        
+        0.9.3
+        =====
+        - Integrated digital signatures support without C extensions.
+        - Integrated "wheel install" command (single package, no dependency
+          resolution) including compatibility check.
+        - Support Python 3.3
+        - Use Metadata 1.3 (PEP 426)
+        
+        0.9.2
+        =====
+        - Automatic signing if WHEEL_TOOL points to the wheel binary
+        - Even more Python 3 fixes
+        
+        0.9.1
+        =====
+        - 'wheel sign' uses the keys generated by 'wheel keygen' (instead of generating
+          a new key at random each time)
+        - Python 2/3 encoding/decoding fixes
+        - Run tests on Python 2.6 (without signature verification)
+        
+        0.9
+        ===
+        - Updated digital signatures scheme
+        - Python 3 support for digital signatures
+        - Always verify RECORD hashes on extract
+        - "wheel" command line tool to sign, verify, unpack wheel files
+        
+        0.8
+        ===
+        - none/any draft pep tags update
+        - improved wininst2wheel script
+        - doc changes and other improvements
+        
+        0.7
+        ===
+        - sort .dist-info at end of wheel archive
+        - Windows & Python 3 fixes from Paul Moore
+        - pep8
+        - scripts to convert wininst & egg to wheel
+        
+        0.6
+        ===
+        - require distribute >= 0.6.28
+        - stop using verlib
+        
+        0.5
+        ===
+        - working pretty well
+        
+        0.4.2
+        =====
+        - hyphenated name fix
+        
+        0.4
+        ===
+        - improve test coverage
+        - improve Windows compatibility
+        - include tox.ini courtesy of Marc Abramowitz
+        - draft hmac sha-256 signing function
+        
+        0.3
+        ===
+        - prototype egg2wheel conversion script
+        
+        0.2
+        ===
+        - Python 3 compatibility
+        
+        0.1
+        ===
+        - Initial version
+        
+Keywords: wheel,packaging
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..4b14821
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,41 @@
+Wheel
+=====
+
+A built-package format for Python.
+
+A wheel is a ZIP-format archive with a specially formatted filename
+and the .whl extension. It is designed to contain all the files for a
+PEP 376 compatible install in a way that is very close to the on-disk
+format. Many packages will be properly installed with only the "Unpack"
+step (simply extracting the file onto sys.path), and the unpacked archive
+preserves enough information to "Spread" (copy data and scripts to their
+final locations) at any later time.
+
+The wheel project provides a `bdist_wheel` command for setuptools
+(requires setuptools >= 0.8.0). Wheel files can be installed with a
+newer `pip` from https://github.com/pypa/pip or with wheel's own command
+line utility.
+
+The wheel documentation is at http://wheel.rtfd.org/. The file format
+is documented in PEP 427 (http://www.python.org/dev/peps/pep-0427/).
+
+The reference implementation is at https://bitbucket.org/pypa/wheel
+
+Why not egg?
+------------
+
+Python's egg format predates the packaging related standards we have
+today, the most important being PEP 376 "Database of Installed Python
+Distributions" which specifies the .dist-info directory (instead of
+.egg-info) and PEP 426 "Metadata for Python Software Packages 2.0"
+which specifies how to express dependencies (instead of requires.txt
+in .egg-info).
+
+Wheel implements these things. It also provides a richer file naming
+convention that communicates the Python implementation and ABI as well
+as simply the language version used in a particular package.
+
+Unlike .egg, wheel will be a fully-documented standard at the binary
+level that is truly easy to install even if you do not want to use the
+reference implementation.
+
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..216112b
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,14 @@
+[pytest]
+addopts = --ignore=dist --ignore=build --cov=wheel
+
+[metadata]
+license-file = LICENSE.txt
+
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..4956a0a
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,57 @@
+import os.path, codecs, re
+
+from setuptools import setup
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = codecs.open(os.path.join(here, 'README.txt'), encoding='utf8').read()
+CHANGES = codecs.open(os.path.join(here, 'CHANGES.txt'), encoding='utf8').read()
+
+with codecs.open(os.path.join(os.path.dirname(__file__), 'wheel', '__init__.py'),
+                 encoding='utf8') as version_file:
+    metadata = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", version_file.read()))
+
+setup(name='wheel',
+      version=metadata['version'],
+      description='A built-package format for Python.',
+      long_description=README + '\n\n' +  CHANGES,
+      classifiers=[
+        "Development Status :: 4 - Beta",
+        "Intended Audience :: Developers",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.2",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        ],
+      author='Daniel Holth',
+      author_email='dholth at fastmail.fm',
+      url='http://bitbucket.org/pypa/wheel/',
+      keywords=['wheel', 'packaging'],
+      license='MIT',
+      packages=[
+          'wheel',
+          'wheel.test',
+          'wheel.tool',
+          'wheel.signatures'
+          ],
+      extras_require={
+          ':python_version=="2.6"': ['argparse'],
+          'signatures': ['keyring'],
+          'signatures:sys_platform!="win32"': ['pyxdg'],
+          'faster-signatures': ['ed25519ll'],
+          'tool': []
+          },
+      tests_require=['jsonschema', 'pytest', 'coverage', 'pytest-cov'],
+      include_package_data=True,
+      zip_safe=False,
+      entry_points = """\
+[console_scripts]
+wheel = wheel.tool:main
+
+[distutils.commands]
+bdist_wheel = wheel.bdist_wheel:bdist_wheel"""
+      )
+
diff --git a/vendorize.sh b/vendorize.sh
new file mode 100755
index 0000000..3402172
--- /dev/null
+++ b/vendorize.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Vendorize pep 425 tagging scheme
+cp ../pep425/pep425tags.py wheel
diff --git a/wheel.egg-info/PKG-INFO b/wheel.egg-info/PKG-INFO
new file mode 100644
index 0000000..d499ecf
--- /dev/null
+++ b/wheel.egg-info/PKG-INFO
@@ -0,0 +1,294 @@
+Metadata-Version: 1.1
+Name: wheel
+Version: 0.24.0
+Summary: A built-package format for Python.
+Home-page: http://bitbucket.org/pypa/wheel/
+Author: Daniel Holth
+Author-email: dholth at fastmail.fm
+License: MIT
+Description: Wheel
+        =====
+        
+        A built-package format for Python.
+        
+        A wheel is a ZIP-format archive with a specially formatted filename
+        and the .whl extension. It is designed to contain all the files for a
+        PEP 376 compatible install in a way that is very close to the on-disk
+        format. Many packages will be properly installed with only the "Unpack"
+        step (simply extracting the file onto sys.path), and the unpacked archive
+        preserves enough information to "Spread" (copy data and scripts to their
+        final locations) at any later time.
+        
+        The wheel project provides a `bdist_wheel` command for setuptools
+        (requires setuptools >= 0.8.0). Wheel files can be installed with a
+        newer `pip` from https://github.com/pypa/pip or with wheel's own command
+        line utility.
+        
+        The wheel documentation is at http://wheel.rtfd.org/. The file format
+        is documented in PEP 427 (http://www.python.org/dev/peps/pep-0427/).
+        
+        The reference implementation is at https://bitbucket.org/pypa/wheel
+        
+        Why not egg?
+        ------------
+        
+        Python's egg format predates the packaging related standards we have
+        today, the most important being PEP 376 "Database of Installed Python
+        Distributions" which specifies the .dist-info directory (instead of
+        .egg-info) and PEP 426 "Metadata for Python Software Packages 2.0"
+        which specifies how to express dependencies (instead of requires.txt
+        in .egg-info).
+        
+        Wheel implements these things. It also provides a richer file naming
+        convention that communicates the Python implementation and ABI as well
+        as simply the language version used in a particular package.
+        
+        Unlike .egg, wheel will be a fully-documented standard at the binary
+        level that is truly easy to install even if you do not want to use the
+        reference implementation.
+        
+        
+        
+        0.24.0
+        ======
+        - The python tag used for pure-python packages is now .pyN (major version
+          only). This change actually occurred in 0.23.0 when the --python-tag
+          option was added, but was not explicitly mentioned in the changelog then.
+        - wininst2wheel and egg2wheel removed. Use "wheel convert [archive]"
+          instead.
+        - Wheel now supports setuptools style conditional requirements via the
+          extras_require={} syntax. Separate 'extra' names from conditions using
+          the : character. Wheel's own setup.py does this. (The empty-string
+          extra is the same as install_requires.) These conditional requirements
+          should work the same whether the package is installed by wheel or
+          by setup.py.
+        
+        0.23.0
+        ======
+        - Compatibiltiy tag flags added to the bdist_wheel command
+        - sdist should include files necessary for tests
+        - 'wheel convert' can now also convert unpacked eggs to wheel
+        - Rename pydist.json to metadata.json to avoid stepping on the PEP
+        - The --skip-scripts option has been removed, and not generating scripts is now
+          the default. The option was a temporary approach until installers could
+          generate scripts themselves. That is now the case with pip 1.5 and later.
+          Note that using pip 1.4 to install a wheel without scripts will leave the
+          installation without entry-point wrappers. The "wheel install-scripts"
+          command can be used to generate the scripts in such cases.
+        - Thank you contributors
+        
+        0.22.0
+        ======
+        - Include entry_points.txt, scripts a.k.a. commands, in experimental
+          pydist.json
+        - Improved test_requires parsing
+        - Python 2.6 fixes, "wheel version" command courtesy pombredanne
+        
+        0.21.0
+        ======
+        - Pregenerated scripts are the default again.
+        - "setup.py bdist_wheel --skip-scripts" turns them off.
+        - setuptools is no longer a listed requirement for the 'wheel'
+          package. It is of course still required in order for bdist_wheel
+          to work.
+        - "python -m wheel" avoids importing pkg_resources until it's necessary.
+        
+        0.20.0
+        ======
+        - No longer include console_scripts in wheels. Ordinary scripts (shell files,
+          standalone Python files) are included as usual.
+        - Include new command "python -m wheel install-scripts [distribution
+          [distribution ...]]" to install the console_scripts (setuptools-style
+          scripts using pkg_resources) for a distribution.
+        
+        0.19.0
+        ======
+        - pymeta.json becomes pydist.json
+        
+        0.18.0
+        ======
+        - Python 3 Unicode improvements
+        
+        0.17.0
+        ======
+        - Support latest PEP-426 "pymeta.json" (json-format metadata)
+        
+        0.16.0
+        ======
+        - Python 2.6 compatibility bugfix (thanks John McFarlane)
+        - Non-prerelease version number
+        
+        1.0.0a2
+        =======
+        - Bugfix for C-extension tags for CPython 3.3 (using SOABI)
+        
+        1.0.0a1
+        =======
+        - Bugfix for bdist_wininst converter "wheel convert"
+        - Bugfix for dists where "is pure" is None instead of True or False
+        
+        1.0.0a0
+        =======
+        - Update for version 1.0 of Wheel (PEP accepted).
+        - Python 3 fix for moving Unicode Description to metadata body
+        - Include rudimentary API documentation in Sphinx (thanks Kevin Horn)
+        
+        0.15.0
+        ======
+        - Various improvements
+        
+        0.14.0
+        ======
+        - Changed the signature format to better comply with the current JWS spec.
+          Breaks all existing signatures.
+        - Include ``wheel unsign`` command to remove RECORD.jws from an archive.
+        - Put the description in the newly allowed payload section of PKG-INFO
+          (METADATA) files.
+        
+        0.13.0
+        ======
+        - Use distutils instead of sysconfig to get installation paths; can install
+          headers.
+        - Improve WheelFile() sort.
+        - Allow bootstrap installs without any pkg_resources.
+        
+        0.12.0
+        ======
+        - Unit test for wheel.tool.install
+        
+        0.11.0
+        ======
+        - API cleanup
+        
+        0.10.3
+        ======
+        - Scripts fixer fix
+        
+        0.10.2
+        ======
+        - Fix keygen
+        
+        0.10.1
+        ======
+        - Preserve attributes on install.
+        
+        0.10.0
+        ======
+        - Include a copy of pkg_resources. Wheel can now install into a virtualenv
+          that does not have distribute (though most packages still require
+          pkg_resources to actually work; wheel install distribute)
+        - Define a new setup.cfg section [wheel]. universal=1 will
+          apply the py2.py3-none-any tag for pure python wheels.
+        
+        0.9.7
+        =====
+        - Only import dirspec when needed. dirspec is only needed to find the
+          configuration for keygen/signing operations.
+        
+        0.9.6
+        =====
+        - requires-dist from setup.cfg overwrites any requirements from setup.py
+          Care must be taken that the requirements are the same in both cases,
+          or just always install from wheel.
+        - drop dirspec requirement on win32
+        - improved command line utility, adds 'wheel convert [egg or wininst]' to
+          convert legacy binary formats to wheel
+        
+        0.9.5
+        =====
+        - Wheel's own wheel file can be executed by Python, and can install itself:
+          ``python wheel-0.9.5-py27-none-any/wheel install ...``
+        - Use argparse; basic ``wheel install`` command should run with only stdlib
+          dependencies.
+        - Allow requires_dist in setup.cfg's [metadata] section. In addition to
+          dependencies in setup.py, but will only be interpreted when installing
+          from wheel, not from sdist. Can be qualified with environment markers.
+        
+        0.9.4
+        =====
+        - Fix wheel.signatures in sdist
+        
+        0.9.3
+        =====
+        - Integrated digital signatures support without C extensions.
+        - Integrated "wheel install" command (single package, no dependency
+          resolution) including compatibility check.
+        - Support Python 3.3
+        - Use Metadata 1.3 (PEP 426)
+        
+        0.9.2
+        =====
+        - Automatic signing if WHEEL_TOOL points to the wheel binary
+        - Even more Python 3 fixes
+        
+        0.9.1
+        =====
+        - 'wheel sign' uses the keys generated by 'wheel keygen' (instead of generating
+          a new key at random each time)
+        - Python 2/3 encoding/decoding fixes
+        - Run tests on Python 2.6 (without signature verification)
+        
+        0.9
+        ===
+        - Updated digital signatures scheme
+        - Python 3 support for digital signatures
+        - Always verify RECORD hashes on extract
+        - "wheel" command line tool to sign, verify, unpack wheel files
+        
+        0.8
+        ===
+        - none/any draft pep tags update
+        - improved wininst2wheel script
+        - doc changes and other improvements
+        
+        0.7
+        ===
+        - sort .dist-info at end of wheel archive
+        - Windows & Python 3 fixes from Paul Moore
+        - pep8
+        - scripts to convert wininst & egg to wheel
+        
+        0.6
+        ===
+        - require distribute >= 0.6.28
+        - stop using verlib
+        
+        0.5
+        ===
+        - working pretty well
+        
+        0.4.2
+        =====
+        - hyphenated name fix
+        
+        0.4
+        ===
+        - improve test coverage
+        - improve Windows compatibility
+        - include tox.ini courtesy of Marc Abramowitz
+        - draft hmac sha-256 signing function
+        
+        0.3
+        ===
+        - prototype egg2wheel conversion script
+        
+        0.2
+        ===
+        - Python 3 compatibility
+        
+        0.1
+        ===
+        - Initial version
+        
+Keywords: wheel,packaging
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
diff --git a/wheel.egg-info/SOURCES.txt b/wheel.egg-info/SOURCES.txt
new file mode 100644
index 0000000..9e1be12
--- /dev/null
+++ b/wheel.egg-info/SOURCES.txt
@@ -0,0 +1,52 @@
+CHANGES.txt
+LICENSE.txt
+MANIFEST.in
+README.txt
+setup.cfg
+setup.py
+vendorize.sh
+wheel/__init__.py
+wheel/__main__.py
+wheel/archive.py
+wheel/bdist_wheel.py
+wheel/decorator.py
+wheel/egg2wheel.py
+wheel/eggnames.txt
+wheel/install.py
+wheel/metadata.py
+wheel/paths.py
+wheel/pep425tags.py
+wheel/pkginfo.py
+wheel/util.py
+wheel/wininst2wheel.py
+wheel.egg-info/PKG-INFO
+wheel.egg-info/SOURCES.txt
+wheel.egg-info/dependency_links.txt
+wheel.egg-info/entry_points.txt
+wheel.egg-info/not-zip-safe
+wheel.egg-info/requires.txt
+wheel.egg-info/top_level.txt
+wheel/signatures/__init__.py
+wheel/signatures/djbec.py
+wheel/signatures/ed25519py.py
+wheel/signatures/keys.py
+wheel/test/__init__.py
+wheel/test/pydist-schema.json
+wheel/test/test-1.0-py2.py3-none-win32.whl
+wheel/test/test_basic.py
+wheel/test/test_install.py
+wheel/test/test_keys.py
+wheel/test/test_paths.py
+wheel/test/test_ranking.py
+wheel/test/test_signatures.py
+wheel/test/test_tagopt.py
+wheel/test/test_tool.py
+wheel/test/test_wheelfile.py
+wheel/test/complex-dist/setup.py
+wheel/test/complex-dist/complexdist/__init__.py
+wheel/test/headers.dist/header.h
+wheel/test/headers.dist/headersdist.py
+wheel/test/headers.dist/setup.py
+wheel/test/simple.dist/setup.py
+wheel/test/simple.dist/simpledist/__init__.py
+wheel/tool/__init__.py
\ No newline at end of file
diff --git a/wheel.egg-info/dependency_links.txt b/wheel.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/wheel.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/wheel.egg-info/entry_points.txt b/wheel.egg-info/entry_points.txt
new file mode 100644
index 0000000..f57b8c0
--- /dev/null
+++ b/wheel.egg-info/entry_points.txt
@@ -0,0 +1,5 @@
+[console_scripts]
+wheel = wheel.tool:main
+
+[distutils.commands]
+bdist_wheel = wheel.bdist_wheel:bdist_wheel
\ No newline at end of file
diff --git a/wheel.egg-info/not-zip-safe b/wheel.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/wheel.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/wheel.egg-info/requires.txt b/wheel.egg-info/requires.txt
new file mode 100644
index 0000000..94efb8c
--- /dev/null
+++ b/wheel.egg-info/requires.txt
@@ -0,0 +1,16 @@
+
+
+[tool]
+
+
+[signatures]
+keyring
+
+[faster-signatures]
+ed25519ll
+
+[:python_version=="2.6"]
+argparse
+
+[signatures:sys_platform!="win32"]
+pyxdg
\ No newline at end of file
diff --git a/wheel.egg-info/top_level.txt b/wheel.egg-info/top_level.txt
new file mode 100644
index 0000000..2309722
--- /dev/null
+++ b/wheel.egg-info/top_level.txt
@@ -0,0 +1 @@
+wheel
diff --git a/wheel/__init__.py b/wheel/__init__.py
new file mode 100644
index 0000000..11c7a39
--- /dev/null
+++ b/wheel/__init__.py
@@ -0,0 +1,2 @@
+# __variables__ with double-quoted values will be available in setup.py:
+__version__ = "0.24.0"
diff --git a/wheel/__main__.py b/wheel/__main__.py
new file mode 100644
index 0000000..889359c
--- /dev/null
+++ b/wheel/__main__.py
@@ -0,0 +1,17 @@
+"""
+Wheel command line tool (enable python -m wheel syntax)
+"""
+
+import sys
+
+def main(): # needed for console script
+    if __package__ == '':
+        # To be able to run 'python wheel-0.9.whl/wheel':
+        import os.path
+        path = os.path.dirname(os.path.dirname(__file__))
+        sys.path[0:0] = [path]
+    import wheel.tool
+    sys.exit(wheel.tool.main())
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/wheel/archive.py b/wheel/archive.py
new file mode 100644
index 0000000..225d295
--- /dev/null
+++ b/wheel/archive.py
@@ -0,0 +1,61 @@
+"""
+Archive tools for wheel.
+"""
+
+import logging
+import os.path
+import zipfile
+
+log = logging.getLogger("wheel")
+
+
+def archive_wheelfile(base_name, base_dir):
+    '''Archive all files under `base_dir` in a whl file and name it like
+    `base_name`.
+    '''
+    olddir = os.path.abspath(os.curdir)
+    base_name = os.path.abspath(base_name)
+    try:
+        os.chdir(base_dir)
+        return make_wheelfile_inner(base_name)
+    finally:
+        os.chdir(olddir)
+
+
+def make_wheelfile_inner(base_name, base_dir='.'):
+    """Create a whl file from all the files under 'base_dir'.
+
+    Places .dist-info at the end of the archive."""
+
+    zip_filename = base_name + ".whl"
+
+    log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
+
+    # XXX support bz2, xz when available
+    zip = zipfile.ZipFile(open(zip_filename, "wb+"), "w",
+                          compression=zipfile.ZIP_DEFLATED)
+
+    score = {'WHEEL': 1, 'METADATA': 2, 'RECORD': 3}
+    deferred = []
+
+    def writefile(path):
+        zip.write(path, path)
+        log.info("adding '%s'" % path)
+
+    for dirpath, dirnames, filenames in os.walk(base_dir):
+        for name in filenames:
+            path = os.path.normpath(os.path.join(dirpath, name))
+
+            if os.path.isfile(path):
+                if dirpath.endswith('.dist-info'):
+                    deferred.append((score.get(name, 0), path))
+                else:
+                    writefile(path)
+
+    deferred.sort()
+    for score, path in deferred:
+        writefile(path)
+
+    zip.close()
+
+    return zip_filename
diff --git a/wheel/bdist_wheel.py b/wheel/bdist_wheel.py
new file mode 100644
index 0000000..df3575c
--- /dev/null
+++ b/wheel/bdist_wheel.py
@@ -0,0 +1,446 @@
+"""
+Create a wheel (.whl) distribution.
+
+A wheel is a built archive format.
+"""
+
+import csv
+import hashlib
+import os
+import subprocess
+import warnings
+import shutil
+import json
+import wheel
+
+try:
+    import sysconfig
+except ImportError:  # pragma nocover
+    # Python < 2.7
+    import distutils.sysconfig as sysconfig
+
+import pkg_resources
+
+safe_name = pkg_resources.safe_name
+safe_version = pkg_resources.safe_version
+
+from shutil import rmtree
+from email.generator import Generator
+
+from distutils.util import get_platform
+from distutils.core import Command
+from distutils.sysconfig import get_python_version
+
+from distutils import log as logger
+
+from .pep425tags import get_abbr_impl, get_impl_ver
+from .util import native, open_for_csv
+from .archive import archive_wheelfile
+from .pkginfo import read_pkg_info, write_pkg_info
+from .metadata import pkginfo_to_dict
+from . import pep425tags, metadata
+
+def safer_name(name):
+    return safe_name(name).replace('-', '_')
+
+def safer_version(version):
+    return safe_version(version).replace('-', '_')
+
+class bdist_wheel(Command):
+
+    description = 'create a wheel distribution'
+
+    user_options = [('bdist-dir=', 'b',
+                     "temporary directory for creating the distribution"),
+                    ('plat-name=', 'p',
+                     "platform name to embed in generated filenames "
+                     "(default: %s)" % get_platform()),
+                    ('keep-temp', 'k',
+                     "keep the pseudo-installation tree around after " +
+                     "creating the distribution archive"),
+                    ('dist-dir=', 'd',
+                     "directory to put final built distributions in"),
+                    ('skip-build', None,
+                     "skip rebuilding everything (for testing/debugging)"),
+                    ('relative', None,
+                     "build the archive using relative paths"
+                     "(default: false)"),
+                    ('owner=', 'u',
+                     "Owner name used when creating a tar file"
+                     " [default: current user]"),
+                    ('group=', 'g',
+                     "Group name used when creating a tar file"
+                     " [default: current group]"),
+                    ('universal', None,
+                     "make a universal wheel"
+                     " (default: false)"),
+                    ('python-tag=', None,
+                     "Python implementation compatibility tag"
+                     " (default: py%s)" % get_impl_ver()[0]),
+                    ]
+
+    boolean_options = ['keep-temp', 'skip-build', 'relative', 'universal']
+
+    def initialize_options(self):
+        self.bdist_dir = None
+        self.data_dir = None
+        self.plat_name = None
+        self.format = 'zip'
+        self.keep_temp = False
+        self.dist_dir = None
+        self.distinfo_dir = None
+        self.egginfo_dir = None
+        self.root_is_purelib = None
+        self.skip_build = None
+        self.relative = False
+        self.owner = None
+        self.group = None
+        self.universal = False
+        self.python_tag = 'py' + get_impl_ver()[0]
+
+    def finalize_options(self):
+        if self.bdist_dir is None:
+            bdist_base = self.get_finalized_command('bdist').bdist_base
+            self.bdist_dir = os.path.join(bdist_base, 'wheel')
+
+        self.data_dir = self.wheel_dist_name + '.data'
+
+        need_options = ('dist_dir', 'plat_name', 'skip_build')
+
+        self.set_undefined_options('bdist',
+                                   *zip(need_options, need_options))
+
+        self.root_is_purelib = self.distribution.is_pure()
+
+        # Support legacy [wheel] section for setting universal
+        wheel = self.distribution.get_option_dict('wheel')
+        if 'universal' in wheel:
+            # please don't define this in your global configs
+            val = wheel['universal'][1].strip()
+            if val.lower() in ('1', 'true', 'yes'):
+                self.universal = True
+
+    @property
+    def wheel_dist_name(self):
+        """Return distribution full name with - replaced with _"""
+        return '-'.join((safer_name(self.distribution.get_name()),
+                         safer_version(self.distribution.get_version())))
+
+    def get_tag(self):
+        supported_tags = pep425tags.get_supported()
+
+        if self.distribution.is_pure():
+            if self.universal:
+                impl = 'py2.py3'
+            else:
+                impl = self.python_tag
+            tag = (impl, 'none', 'any')
+        else:
+            plat_name = self.plat_name
+            if plat_name is None:
+                plat_name = get_platform()
+            plat_name = plat_name.replace('-', '_').replace('.', '_')
+            impl_name = get_abbr_impl()
+            impl_ver = get_impl_ver()
+            # PEP 3149 -- no SOABI in Py 2
+            # For PyPy?
+            # "pp%s%s" % (sys.pypy_version_info.major,
+            # sys.pypy_version_info.minor)
+            abi_tag = sysconfig.get_config_vars().get('SOABI', 'none')
+            if abi_tag.startswith('cpython-'):
+                abi_tag = 'cp' + abi_tag.rsplit('-', 1)[-1]
+
+            tag = (impl_name + impl_ver, abi_tag, plat_name)
+            # XXX switch to this alternate implementation for non-pure:
+            assert tag == supported_tags[0]
+        return tag
+
+    def get_archive_basename(self):
+        """Return archive name without extension"""
+
+        impl_tag, abi_tag, plat_tag = self.get_tag()
+
+        archive_basename = "%s-%s-%s-%s" % (
+            self.wheel_dist_name,
+            impl_tag,
+            abi_tag,
+            plat_tag)
+        return archive_basename
+
+    def run(self):
+        build_scripts = self.reinitialize_command('build_scripts')
+        build_scripts.executable = 'python'
+
+        if not self.skip_build:
+            self.run_command('build')
+
+        install = self.reinitialize_command('install',
+                                            reinit_subcommands=True)
+        install.root = self.bdist_dir
+        install.compile = False
+        install.skip_build = self.skip_build
+        install.warn_dir = False
+
+        # A wheel without setuptools scripts is more cross-platform.
+        # Use the (undocumented) `no_ep` option to setuptools'
+        # install_scripts command to avoid creating entry point scripts.
+        install_scripts = self.reinitialize_command('install_scripts')
+        install_scripts.no_ep = True
+
+        # Use a custom scheme for the archive, because we have to decide
+        # at installation time which scheme to use.
+        for key in ('headers', 'scripts', 'data', 'purelib', 'platlib'):
+            setattr(install,
+                    'install_' + key,
+                    os.path.join(self.data_dir, key))
+
+        basedir_observed = ''
+
+        if os.name == 'nt':
+            # win32 barfs if any of these are ''; could be '.'?
+            # (distutils.command.install:change_roots bug)
+            basedir_observed = os.path.join(self.data_dir, '..')
+            self.install_libbase = self.install_lib = basedir_observed
+
+        setattr(install,
+                'install_purelib' if self.root_is_purelib else 'install_platlib',
+                basedir_observed)
+
+        logger.info("installing to %s", self.bdist_dir)
+
+        self.run_command('install')
+
+        archive_basename = self.get_archive_basename()
+
+        pseudoinstall_root = os.path.join(self.dist_dir, archive_basename)
+        if not self.relative:
+            archive_root = self.bdist_dir
+        else:
+            archive_root = os.path.join(
+                self.bdist_dir,
+                self._ensure_relative(install.install_base))
+
+        self.set_undefined_options(
+            'install_egg_info', ('target', 'egginfo_dir'))
+        self.distinfo_dir = os.path.join(self.bdist_dir,
+                                         '%s.dist-info' % self.wheel_dist_name)
+        self.egg2dist(self.egginfo_dir,
+                      self.distinfo_dir)
+
+        self.write_wheelfile(self.distinfo_dir)
+
+        self.write_record(self.bdist_dir, self.distinfo_dir)
+
+        # Make the archive
+        if not os.path.exists(self.dist_dir):
+            os.makedirs(self.dist_dir)
+        wheel_name = archive_wheelfile(pseudoinstall_root, archive_root)
+
+        # Sign the archive
+        if 'WHEEL_TOOL' in os.environ:
+            subprocess.call([os.environ['WHEEL_TOOL'], 'sign', wheel_name])
+
+        # Add to 'Distribution.dist_files' so that the "upload" command works
+        getattr(self.distribution, 'dist_files', []).append(
+            ('bdist_wheel', get_python_version(), wheel_name))
+
+        if not self.keep_temp:
+            if self.dry_run:
+                logger.info('removing %s', self.bdist_dir)
+            else:
+                rmtree(self.bdist_dir)
+
+    def write_wheelfile(self, wheelfile_base, generator='bdist_wheel (' + wheel.__version__ + ')'):
+        from email.message import Message
+        msg = Message()
+        msg['Wheel-Version'] = '1.0'  # of the spec
+        msg['Generator'] = generator
+        msg['Root-Is-Purelib'] = str(self.root_is_purelib).lower()
+
+        # Doesn't work for bdist_wininst
+        impl_tag, abi_tag, plat_tag = self.get_tag()
+        for impl in impl_tag.split('.'):
+            for abi in abi_tag.split('.'):
+                for plat in plat_tag.split('.'):
+                    msg['Tag'] = '-'.join((impl, abi, plat))
+
+        wheelfile_path = os.path.join(wheelfile_base, 'WHEEL')
+        logger.info('creating %s', wheelfile_path)
+        with open(wheelfile_path, 'w') as f:
+            Generator(f, maxheaderlen=0).flatten(msg)
+
+    def _ensure_relative(self, path):
+        # copied from dir_util, deleted
+        drive, path = os.path.splitdrive(path)
+        if path[0:1] == os.sep:
+            path = drive + path[1:]
+        return path
+
+    def _pkginfo_to_metadata(self, egg_info_path, pkginfo_path):
+        return metadata.pkginfo_to_metadata(egg_info_path, pkginfo_path)
+
+    def license_file(self):
+        """Return license filename from a license-file key in setup.cfg, or None."""
+        metadata = self.distribution.get_option_dict('metadata')
+        if not 'license_file' in metadata:
+            return None
+        return metadata['license_file'][1]
+
+    def setupcfg_requirements(self):
+        """Generate requirements from setup.cfg as
+        ('Requires-Dist', 'requirement; qualifier') tuples. From a metadata
+        section in setup.cfg:
+
+        [metadata]
+        provides-extra = extra1
+            extra2
+        requires-dist = requirement; qualifier
+            another; qualifier2
+            unqualified
+
+        Yields
+
+        ('Provides-Extra', 'extra1'),
+        ('Provides-Extra', 'extra2'),
+        ('Requires-Dist', 'requirement; qualifier'),
+        ('Requires-Dist', 'another; qualifier2'),
+        ('Requires-Dist', 'unqualified')
+        """
+        metadata = self.distribution.get_option_dict('metadata')
+
+        # our .ini parser folds - to _ in key names:
+        for key, title in (('provides_extra', 'Provides-Extra'),
+                           ('requires_dist', 'Requires-Dist')):
+            if not key in metadata:
+                continue
+            field = metadata[key]
+            for line in field[1].splitlines():
+                line = line.strip()
+                if not line:
+                    continue
+                yield (title, line)
+
+    def add_requirements(self, metadata_path):
+        """Add additional requirements from setup.cfg to file metadata_path"""
+        additional = list(self.setupcfg_requirements())
+        if not additional: return
+        pkg_info = read_pkg_info(metadata_path)
+        if 'Provides-Extra' in pkg_info or 'Requires-Dist' in pkg_info:
+            warnings.warn('setup.cfg requirements overwrite values from setup.py')
+            del pkg_info['Provides-Extra']
+            del pkg_info['Requires-Dist']
+        for k, v in additional:
+            pkg_info[k] = v
+        write_pkg_info(metadata_path, pkg_info)
+
+    def egg2dist(self, egginfo_path, distinfo_path):
+        """Convert an .egg-info directory into a .dist-info directory"""
+        def adios(p):
+            """Appropriately delete directory, file or link."""
+            if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
+                shutil.rmtree(p)
+            elif os.path.exists(p):
+                os.unlink(p)
+
+        adios(distinfo_path)
+
+        if not os.path.exists(egginfo_path):
+            # There is no egg-info. This is probably because the egg-info
+            # file/directory is not named matching the distribution name used
+            # to name the archive file. Check for this case and report
+            # accordingly.
+            import glob
+            pat = os.path.join(os.path.dirname(egginfo_path), '*.egg-info')
+            possible = glob.glob(pat)
+            err = "Egg metadata expected at %s but not found" % (egginfo_path,)
+            if possible:
+                alt = os.path.basename(possible[0])
+                err += " (%s found - possible misnamed archive file?)" % (alt,)
+
+            raise ValueError(err)
+
+        if os.path.isfile(egginfo_path):
+            # .egg-info is a single file
+            pkginfo_path = egginfo_path
+            pkg_info = self._pkginfo_to_metadata(egginfo_path, egginfo_path)
+            os.mkdir(distinfo_path)
+        else:
+            # .egg-info is a directory
+            pkginfo_path = os.path.join(egginfo_path, 'PKG-INFO')
+            pkg_info = self._pkginfo_to_metadata(egginfo_path, pkginfo_path)
+
+            # ignore common egg metadata that is useless to wheel
+            shutil.copytree(egginfo_path, distinfo_path,
+                            ignore=lambda x, y: set(('PKG-INFO',
+                                                     'requires.txt',
+                                                     'SOURCES.txt',
+                                                     'not-zip-safe',)))
+
+            # delete dependency_links if it is only whitespace
+            dependency_links = os.path.join(distinfo_path, 'dependency_links.txt')
+            if not open(dependency_links, 'r').read().strip():
+                adios(dependency_links)
+
+        write_pkg_info(os.path.join(distinfo_path, 'METADATA'), pkg_info)
+
+        # XXX deprecated. Still useful for current distribute/setuptools.
+        metadata_path = os.path.join(distinfo_path, 'METADATA')
+        self.add_requirements(metadata_path)
+
+        # XXX intentionally a different path than the PEP.
+        metadata_json_path = os.path.join(distinfo_path, 'metadata.json')
+        pymeta = pkginfo_to_dict(metadata_path,
+                                 distribution=self.distribution)
+
+        if 'description' in pymeta:
+            description_filename = 'DESCRIPTION.rst'
+            description_text = pymeta.pop('description')
+            description_path = os.path.join(distinfo_path,
+                                            description_filename)
+            with open(description_path, "wb") as description_file:
+                description_file.write(description_text.encode('utf-8'))
+            pymeta['extensions']['python.details']['document_names']['description'] = description_filename
+
+        # XXX heuristically copy any LICENSE/LICENSE.txt?
+        license = self.license_file()
+        if license:
+            license_filename = 'LICENSE.txt'
+            shutil.copy(license, os.path.join(self.distinfo_dir, license_filename))
+            pymeta['extensions']['python.details']['document_names']['license'] = license_filename
+
+        with open(metadata_json_path, "w") as metadata_json:
+            json.dump(pymeta, metadata_json)
+
+        adios(egginfo_path)
+
+    def write_record(self, bdist_dir, distinfo_dir):
+        from wheel.util import urlsafe_b64encode
+
+        record_path = os.path.join(distinfo_dir, 'RECORD')
+        record_relpath = os.path.relpath(record_path, bdist_dir)
+
+        def walk():
+            for dir, dirs, files in os.walk(bdist_dir):
+                for f in files:
+                    yield os.path.join(dir, f)
+
+        def skip(path):
+            """Wheel hashes every possible file."""
+            return (path == record_relpath)
+
+        with open_for_csv(record_path, 'w+') as record_file:
+            writer = csv.writer(record_file)
+            for path in walk():
+                relpath = os.path.relpath(path, bdist_dir)
+                if skip(relpath):
+                    hash = ''
+                    size = ''
+                else:
+                    with open(path, 'rb') as f:
+                        data = f.read()
+                    digest = hashlib.sha256(data).digest()
+                    hash = 'sha256=' + native(urlsafe_b64encode(digest))
+                    size = len(data)
+                record_path = os.path.relpath(
+                    path, bdist_dir).replace(os.path.sep, '/')
+                writer.writerow((record_path, hash, size))
diff --git a/wheel/decorator.py b/wheel/decorator.py
new file mode 100644
index 0000000..e4b56d1
--- /dev/null
+++ b/wheel/decorator.py
@@ -0,0 +1,19 @@
+# from Pyramid
+
+
+class reify(object):
+    """Put the result of a method which uses this (non-data)
+    descriptor decorator in the instance dict after the first call,
+    effectively replacing the decorator with an instance variable.
+    """
+
+    def __init__(self, wrapped):
+        self.wrapped = wrapped
+        self.__doc__ = wrapped.__doc__
+
+    def __get__(self, inst, objtype=None):
+        if inst is None:
+            return self
+        val = self.wrapped(inst)
+        setattr(inst, self.wrapped.__name__, val)
+        return val
diff --git a/wheel/egg2wheel.py b/wheel/egg2wheel.py
new file mode 100755
index 0000000..bf919c4
--- /dev/null
+++ b/wheel/egg2wheel.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+import os.path
+import re
+import sys
+import tempfile
+import zipfile
+import wheel.bdist_wheel
+import shutil
+import distutils.dist
+from distutils.archive_util import make_archive
+from argparse import ArgumentParser
+from glob import iglob
+
+egg_info_re = re.compile(r'''(?P<name>.+?)-(?P<ver>.+?)
+    (-(?P<pyver>.+?))?(-(?P<arch>.+?))?.egg''', re.VERBOSE)
+
+def egg2wheel(egg_path, dest_dir):
+    egg_info = egg_info_re.match(os.path.basename(egg_path)).groupdict()
+    dir = tempfile.mkdtemp(suffix="_e2w")
+    if os.path.isfile(egg_path):
+        # assume we have a bdist_egg otherwise
+        egg = zipfile.ZipFile(egg_path)
+        egg.extractall(dir)
+    else:
+        # support buildout-style installed eggs directories
+        for pth in os.listdir(egg_path):
+            src = os.path.join(egg_path, pth)
+            if os.path.isfile(src):
+                shutil.copy2(src, dir)
+            else:
+                shutil.copytree(src, os.path.join(dir, pth))
+
+    dist_info = "%s-%s" % (egg_info['name'], egg_info['ver'])
+    abi = 'none'
+    pyver = egg_info['pyver'].replace('.', '')
+    arch = (egg_info['arch'] or 'any').replace('.', '_').replace('-', '_')
+    if arch != 'any':
+        # assume all binary eggs are for CPython
+        pyver = 'cp' + pyver[2:]
+    wheel_name = '-'.join((
+                          dist_info,
+                          pyver,
+                          abi,
+                          arch
+                          ))
+    bw = wheel.bdist_wheel.bdist_wheel(distutils.dist.Distribution())
+    bw.root_is_purelib = egg_info['arch'] is None
+    dist_info_dir = os.path.join(dir, '%s.dist-info' % dist_info)
+    bw.egg2dist(os.path.join(dir, 'EGG-INFO'),
+                dist_info_dir)
+    bw.write_wheelfile(dist_info_dir, generator='egg2wheel')
+    bw.write_record(dir, dist_info_dir)
+    filename = make_archive(os.path.join(dest_dir, wheel_name), 'zip', root_dir=dir)
+    os.rename(filename, filename[:-3] + 'whl')
+    shutil.rmtree(dir)
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument('eggs', nargs='*', help="Eggs to convert")
+    parser.add_argument('--dest-dir', '-d', default=os.path.curdir,
+            help="Directory to store wheels (default %(default)s)")
+    parser.add_argument('--verbose', '-v', action='store_true')
+    args = parser.parse_args()
+    for pat in args.eggs:
+        for egg in iglob(pat):
+            if args.verbose:
+                sys.stdout.write("{0}... ".format(egg))
+            egg2wheel(egg, args.dest_dir)
+            if args.verbose:
+                sys.stdout.write("OK\n")
+
+if __name__ == "__main__":
+    main()
diff --git a/wheel/eggnames.txt b/wheel/eggnames.txt
new file mode 100644
index 0000000..d422120
--- /dev/null
+++ b/wheel/eggnames.txt
@@ -0,0 +1,87 @@
+vcard-0.7.8-py2.7.egg
+qtalchemy-0.7.1-py2.7.egg
+AMQPDeliver-0.1-py2.7.egg
+infi.registry-0.1.1-py2.7.egg
+infi.instruct-0.5.5-py2.7.egg
+infi.devicemanager-0.1.2-py2.7.egg
+TracTixSummary-1.0-py2.7.egg
+ToscaWidgets-0.9.12-py2.7.egg
+archipel_agent_iphone_notification-0.5.0beta-py2.7.egg
+archipel_agent_action_scheduler-0.5.0beta-py2.7.egg
+ao.social-1.0.2-py2.7.egg
+apgl-0.7-py2.7.egg
+satchmo_payment_payworld-0.1.1-py2.7.egg
+snmpsim-0.1.3-py2.7.egg
+sshim-0.2-py2.7.egg
+shove-0.3.4-py2.7.egg
+simpleavro-0.3.0-py2.7.egg
+wkhtmltopdf-0.2-py2.7.egg
+wokkel-0.7.0-py2.7.egg
+jmbo_social-0.0.6-py2.7.egg
+jmbo_post-0.0.6-py2.7.egg
+jcrack-0.0.2-py2.7.egg
+riak-1.4.0-py2.7.egg
+restclient-0.10.2-py2.7.egg
+Sutekh-0.8.1-py2.7.egg
+trayify-0.0.1-py2.7.egg
+tweepy-1.9-py2.7.egg
+topzootools-0.2.1-py2.7.egg
+haystack-0.16-py2.7.egg
+zope.interface-4.0.1-py2.7-win32.egg
+neuroshare-0.8.5-py2.7-macosx-10.7-intel.egg
+ndg_httpsclient-0.2.0-py2.7.egg
+libtele-0.3-py2.7.egg
+litex.cxpool-1.0.2-py2.7.egg
+obspy.iris-0.5.1-py2.7.egg
+obspy.mseed-0.6.1-py2.7-win32.egg
+obspy.core-0.6.2-py2.7.egg
+CorePost-0.0.3-py2.7.egg
+fnordstalk-0.0.3-py2.7.egg
+Persistence-2.13.2-py2.7-win32.egg
+Pydap-3.1.RC1-py2.7.egg
+PyExecJS-1.0.4-py2.7.egg
+Wally-0.7.2-py2.7.egg
+ExtensionClass-4.0a1-py2.7-win32.egg
+Feedjack-0.9.16-py2.7.egg
+Mars24-0.3.9-py2.7.egg
+HalWeb-0.6.0-py2.7.egg
+DARE-0.7.140-py2.7.egg
+macholib-1.3-py2.7.egg
+marrow.wsgi.egress.compression-1.1-py2.7.egg
+mcs-0.3.7-py2.7.egg
+Kook-0.6.0-py2.7.egg
+er-0.1-py2.7.egg
+evasion_director-1.1.4-py2.7.egg
+djquery-0.1a-py2.7.egg
+django_factory-0.7-py2.7.egg
+django_gizmo-0.0.3-py2.7.egg
+django_category-0.1-py2.7.egg
+dbwrap-0.3.2-py2.7.egg
+django_supergeneric-1.0-py2.7.egg
+django_dynamo-0.25-py2.7.egg
+django_acollabauth-0.1-py2.7.egg
+django_qrlink-0.1.0-py2.7.egg
+django_addons-0.6.6-py2.7.egg
+cover_grabber-1.1.2-py2.7.egg
+chem-1.1-py2.7.egg
+crud-0.1-py2.7.egg
+bongo-0.1-py2.7.egg
+bytecodehacks-April2000-py2.7.egg
+greenlet-0.3.4-py2.7-win32.egg
+ginvoke-0.3.1-py2.7.egg
+pyobjc_framework_ScriptingBridge-2.3-py2.7.egg
+pecan-0.2.0a-py2.7.egg
+pyress-0.2.0-py2.7.egg
+pyobjc_framework_PubSub-2.3-py2.7.egg
+pyobjc_framework_ExceptionHandling-2.3-py2.7.egg
+pywps-trunk-py2.7.egg
+pyobjc_framework_CFNetwork-2.3-py2.7-macosx-10.6-fat.egg
+py.saunter-0.40-py2.7.egg
+pyfnordmetric-0.0.1-py2.7.egg
+pyws-1.1.1-py2.7.egg
+prestapyt-0.4.0-py2.7.egg
+passlib-1.5.3-py2.7.egg
+pyga-2.1-py2.7.egg
+pygithub3-0.3-py2.7.egg
+pyobjc_framework_OpenDirectory-2.3-py2.7.egg
+yaposib-0.2.75-py2.7-linux-x86_64.egg
diff --git a/wheel/install.py b/wheel/install.py
new file mode 100644
index 0000000..3af6d0c
--- /dev/null
+++ b/wheel/install.py
@@ -0,0 +1,480 @@
+"""
+Operations on existing wheel files, including basic installation. 
+"""
+# XXX see patched pip to install
+
+import sys
+import warnings
+import os.path
+import re
+import zipfile
+import hashlib
+import csv
+
+import shutil
+
+try:
+    _big_number = sys.maxsize
+except NameError:
+    _big_number = sys.maxint
+
+from wheel.decorator import reify
+from wheel.util import (urlsafe_b64encode, from_json, urlsafe_b64decode,
+                        native, binary, HashingFile)
+from wheel import signatures
+from wheel.pkginfo import read_pkg_info_bytes
+from wheel.util import open_for_csv
+
+from .pep425tags import get_supported
+from .paths import get_install_paths
+
+# The next major version after this version of the 'wheel' tool:
+VERSION_TOO_HIGH = (1, 0)
+
+# Non-greedy matching of an optional build number may be too clever (more
+# invalid wheel filenames will match). Separate regex for .dist-info?
+WHEEL_INFO_RE = re.compile(
+    r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>\d.+?))?)
+    ((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
+    \.whl|\.dist-info)$""",
+    re.VERBOSE).match
+
+def parse_version(version):
+    """Use parse_version from pkg_resources or distutils as available."""
+    global parse_version
+    try:
+        from pkg_resources import parse_version
+    except ImportError:
+        from distutils.version import LooseVersion as parse_version
+    return parse_version(version)
+
+class BadWheelFile(ValueError):
+    pass
+
+
+class WheelFile(object):
+    """Parse wheel-specific attributes from a wheel (.whl) file and offer
+    basic installation and verification support.
+    
+    WheelFile can be used to simply parse a wheel filename by avoiding the
+    methods that require the actual file contents."""
+
+    WHEEL_INFO = "WHEEL"
+    RECORD = "RECORD"
+
+    def __init__(self,
+                 filename,
+                 fp=None,
+                 append=False,
+                 context=get_supported):
+        """
+        :param fp: A seekable file-like object or None to open(filename).
+        :param append: Open archive in append mode.
+        :param context: Function returning list of supported tags. Wheels
+        must have the same context to be sortable.
+        """
+        self.filename = filename
+        self.fp = fp
+        self.append = append
+        self.context = context
+        basename = os.path.basename(filename)
+        self.parsed_filename = WHEEL_INFO_RE(basename)
+        if not basename.endswith('.whl') or self.parsed_filename is None:
+            raise BadWheelFile("Bad filename '%s'" % filename)
+
+    def __repr__(self):
+        return self.filename
+
+    @property
+    def distinfo_name(self):
+        return "%s.dist-info" % self.parsed_filename.group('namever')
+
+    @property
+    def datadir_name(self):
+        return "%s.data" % self.parsed_filename.group('namever')
+
+    @property
+    def record_name(self):
+        return "%s/%s" % (self.distinfo_name, self.RECORD)
+
+    @property
+    def wheelinfo_name(self):
+        return "%s/%s" % (self.distinfo_name, self.WHEEL_INFO)
+
+    @property
+    def tags(self):
+        """A wheel file is compatible with the Cartesian product of the
+        period-delimited tags in its filename.
+        To choose a wheel file among several candidates having the same
+        distribution version 'ver', an installer ranks each triple of
+        (pyver, abi, plat) that its Python installation can run, sorting
+        the wheels by the best-ranked tag it supports and then by their
+        arity which is just len(list(compatibility_tags)).
+        """
+        tags = self.parsed_filename.groupdict()
+        for pyver in tags['pyver'].split('.'):
+            for abi in tags['abi'].split('.'):
+                for plat in tags['plat'].split('.'):
+                    yield (pyver, abi, plat)
+
+    compatibility_tags = tags
+
+    @property
+    def arity(self):
+        """The number of compatibility tags the wheel declares."""
+        return len(list(self.compatibility_tags))
+
+    @property
+    def rank(self):
+        """
+        Lowest index of any of this wheel's tags in self.context(), and the
+        arity e.g. (0, 1)
+        """
+        return self.compatibility_rank(self.context())
+
+    @property
+    def compatible(self):
+        return self.rank[0] != _big_number  # bad API!
+
+    # deprecated:
+    def compatibility_rank(self, supported):
+        """Rank the wheel against the supported tags. Smaller ranks are more
+        compatible!
+
+        :param supported: A list of compatibility tags that the current
+            Python implemenation can run.
+        """
+        preferences = []
+        for tag in self.compatibility_tags:
+            try:
+                preferences.append(supported.index(tag))
+            # Tag not present
+            except ValueError:
+                pass
+        if len(preferences):
+            return (min(preferences), self.arity)
+        return (_big_number, 0)
+
+    # deprecated
+    def supports_current_python(self, x):
+        assert self.context == x, 'context mismatch'
+        return self.compatible
+
+    # Comparability.
+    # Wheels are equal if they refer to the same file.
+    # If two wheels are not equal, compare based on (in this order):
+    #   1. Name
+    #   2. Version
+    #   3. Compatibility rank
+    #   4. Filename (as a tiebreaker)
+    @property
+    def _sort_key(self):
+        return (self.parsed_filename.group('name'),
+                parse_version(self.parsed_filename.group('ver')),
+                tuple(-x for x in self.rank),
+                self.filename)
+
+    def __eq__(self, other):
+        return self.filename == other.filename
+
+    def __ne__(self, other):
+        return self.filename != other.filename
+
+    def __lt__(self, other):
+        if self.context != other.context:
+            raise TypeError("{0}.context != {1}.context".format(self, other))
+
+        return self._sort_key < other._sort_key
+
+        # XXX prune
+
+        sn = self.parsed_filename.group('name')
+        on = other.parsed_filename.group('name')
+        if sn != on:
+            return sn < on
+        sv = parse_version(self.parsed_filename.group('ver'))
+        ov = parse_version(other.parsed_filename.group('ver'))
+        if sv != ov:
+            return sv < ov
+        # Compatibility
+        if self.context != other.context:
+            raise TypeError("{0}.context != {1}.context".format(self, other))
+        sc = self.rank
+        oc = other.rank
+        if sc != None and oc != None and sc != oc:
+            # Smaller compatibility ranks are "better" than larger ones,
+            # so we have to reverse the sense of the comparison here!
+            return sc > oc
+        elif sc == None and oc != None:
+            return False
+        return self.filename < other.filename
+
+    def __gt__(self, other):
+        return other < self
+
+    def __le__(self, other):
+        return self == other or self < other
+
+    def __ge__(self, other):
+        return self == other or other < self
+
+    #
+    # Methods using the file's contents:
+    #
+
+    @reify
+    def zipfile(self):
+        mode = "r"
+        if self.append:
+            mode = "a"
+        vzf = VerifyingZipFile(self.fp if self.fp else self.filename, mode)
+        if not self.append:
+            self.verify(vzf)
+        return vzf
+
+    @reify
+    def parsed_wheel_info(self):
+        """Parse wheel metadata (the .data/WHEEL file)"""
+        return read_pkg_info_bytes(self.zipfile.read(self.wheelinfo_name))
+
+    def check_version(self):
+        version = self.parsed_wheel_info['Wheel-Version']
+        if tuple(map(int, version.split('.'))) >= VERSION_TOO_HIGH:
+            raise ValueError("Wheel version is too high")
+
+    @reify
+    def install_paths(self):
+        """
+        Consult distutils to get the install paths for our dist.  A dict with
+        ('purelib', 'platlib', 'headers', 'scripts', 'data').
+        
+        We use the name from our filename as the dist name, which means headers
+        could be installed in the wrong place if the filesystem-escaped name
+        is different than the Name.  Who cares? 
+        """
+        name = self.parsed_filename.group('name')
+        return get_install_paths(name)
+
+    def install(self, force=False, overrides={}):
+        """
+        Install the wheel into site-packages.
+        """
+
+        # Utility to get the target directory for a particular key
+        def get_path(key):
+            return overrides.get(key) or self.install_paths[key]
+
+        # The base target location is either purelib or platlib
+        if self.parsed_wheel_info['Root-Is-Purelib'] == 'true':
+            root = get_path('purelib')
+        else:
+            root = get_path('platlib')
+
+        # Parse all the names in the archive
+        name_trans = {}
+        for info in self.zipfile.infolist():
+            name = info.filename
+            # Zip files can contain entries representing directories.
+            # These end in a '/'.
+            # We ignore these, as we create directories on demand.
+            if name.endswith('/'):
+                continue
+
+            # Pathnames in a zipfile namelist are always /-separated.
+            # In theory, paths could start with ./ or have other oddities
+            # but this won't happen in practical cases of well-formed wheels.
+            # We'll cover the simple case of an initial './' as it's both easy
+            # to do and more common than most other oddities.
+            if name.startswith('./'):
+                name = name[2:]
+
+            # Split off the base directory to identify files that are to be
+            # installed in non-root locations
+            basedir, sep, filename = name.partition('/')
+            if sep and basedir == self.datadir_name:
+                # Data file. Target destination is elsewhere
+                key, sep, filename = filename.partition('/')
+                if not sep:
+                    raise ValueError("Invalid filename in wheel: {0}".format(name))
+                target = get_path(key)
+            else:
+                # Normal file. Target destination is root
+                key = ''
+                target = root
+                filename = name
+
+            # Map the actual filename from the zipfile to its intended target
+            # directory and the pathname relative to that directory.
+            dest = os.path.normpath(os.path.join(target, filename))
+            name_trans[info] = (key, target, filename, dest)
+
+        # We're now ready to start processing the actual install. The process
+        # is as follows:
+        #   1. Prechecks - is the wheel valid, is its declared architecture
+        #      OK, etc. [[Responsibility of the caller]]
+        #   2. Overwrite check - do any of the files to be installed already
+        #      exist?
+        #   3. Actual install - put the files in their target locations.
+        #   4. Update RECORD - write a suitably modified RECORD file to
+        #      reflect the actual installed paths.
+
+        if not force:
+            for info, v in name_trans.items():
+                k = info.filename
+                key, target, filename, dest = v
+                if os.path.exists(dest):
+                    raise ValueError("Wheel file {0} would overwrite {1}. Use force if this is intended".format(k, dest))
+
+        # Get the name of our executable, for use when replacing script
+        # wrapper hashbang lines.
+        # We encode it using getfilesystemencoding, as that is "the name of
+        # the encoding used to convert Unicode filenames into system file
+        # names".
+        exename = sys.executable.encode(sys.getfilesystemencoding())
+        record_data = []
+        record_name = self.distinfo_name + '/RECORD'
+        for info, (key, target, filename, dest) in name_trans.items():
+            name = info.filename
+            source = self.zipfile.open(info)
+            # Skip the RECORD file
+            if name == record_name:
+                continue
+            ddir = os.path.dirname(dest)
+            if not os.path.isdir(ddir):
+                os.makedirs(ddir)
+            destination = HashingFile(open(dest, 'wb'))
+            if key == 'scripts':
+                hashbang = source.readline()
+                if hashbang.startswith(b'#!python'):
+                    hashbang = b'#!' + exename + binary(os.linesep)
+                destination.write(hashbang)
+            shutil.copyfileobj(source, destination)
+            reldest = os.path.relpath(dest, root)
+            reldest.replace(os.sep, '/')
+            record_data.append((reldest, destination.digest(), destination.length))
+            destination.close()
+            source.close()
+            # preserve attributes (especially +x bit for scripts)
+            attrs = info.external_attr >> 16
+            if attrs:  # tends to be 0 if Windows.
+                os.chmod(dest, info.external_attr >> 16)
+
+        record_name = os.path.join(root, self.record_name)
+        writer = csv.writer(open_for_csv(record_name, 'w+'))
+        for reldest, digest, length in sorted(record_data):
+            writer.writerow((reldest, digest, length))
+        writer.writerow((self.record_name, '', ''))
+
+    def verify(self, zipfile=None):
+        """Configure the VerifyingZipFile `zipfile` by verifying its signature 
+        and setting expected hashes for every hash in RECORD.
+        Caller must complete the verification process by completely reading 
+        every file in the archive (e.g. with extractall)."""
+        sig = None
+        if zipfile is None:
+            zipfile = self.zipfile
+        zipfile.strict = True
+
+        record_name = '/'.join((self.distinfo_name, 'RECORD'))
+        sig_name = '/'.join((self.distinfo_name, 'RECORD.jws'))
+        # tolerate s/mime signatures:
+        smime_sig_name = '/'.join((self.distinfo_name, 'RECORD.p7s'))
+        zipfile.set_expected_hash(record_name, None)
+        zipfile.set_expected_hash(sig_name, None)
+        zipfile.set_expected_hash(smime_sig_name, None)
+        record = zipfile.read(record_name)
+
+        record_digest = urlsafe_b64encode(hashlib.sha256(record).digest())
+        try:
+            sig = from_json(native(zipfile.read(sig_name)))
+        except KeyError:  # no signature
+            pass
+        if sig:
+            headers, payload = signatures.verify(sig)
+            if payload['hash'] != "sha256=" + native(record_digest):
+                msg = "RECORD.sig claimed RECORD hash {0} != computed hash {1}."
+                raise BadWheelFile(msg.format(payload['hash'],
+                                              native(record_digest)))
+
+        reader = csv.reader((native(r) for r in record.splitlines()))
+
+        for row in reader:
+            filename = row[0]
+            hash = row[1]
+            if not hash:
+                if filename not in (record_name, sig_name):
+                    sys.stderr.write("%s has no hash!\n" % filename)
+                continue
+            algo, data = row[1].split('=', 1)
+            assert algo == "sha256", "Unsupported hash algorithm"
+            zipfile.set_expected_hash(filename, urlsafe_b64decode(binary(data)))
+
+
+class VerifyingZipFile(zipfile.ZipFile):
+    """ZipFile that can assert that each of its extracted contents matches
+    an expected sha256 hash. Note that each file must be completly read in 
+    order for its hash to be checked."""
+
+    def __init__(self, file, mode="r",
+                 compression=zipfile.ZIP_STORED,
+                 allowZip64=False):
+        zipfile.ZipFile.__init__(self, file, mode, compression, allowZip64)
+
+        self.strict = False
+        self._expected_hashes = {}
+        self._hash_algorithm = hashlib.sha256
+
+    def set_expected_hash(self, name, hash):
+        """
+        :param name: name of zip entry
+        :param hash: bytes of hash (or None for "don't care")
+        """
+        self._expected_hashes[name] = hash
+
+    def open(self, name_or_info, mode="r", pwd=None):
+        """Return file-like object for 'name'."""
+        # A non-monkey-patched version would contain most of zipfile.py
+        ef = zipfile.ZipFile.open(self, name_or_info, mode, pwd)
+        if isinstance(name_or_info, zipfile.ZipInfo):
+            name = name_or_info.filename
+        else:
+            name = name_or_info
+        if (name in self._expected_hashes
+            and self._expected_hashes[name] != None):
+            expected_hash = self._expected_hashes[name]
+            try:
+                _update_crc_orig = ef._update_crc
+            except AttributeError:
+                warnings.warn('Need ZipExtFile._update_crc to implement '
+                              'file hash verification (in Python >= 2.7)')
+                return ef
+            running_hash = self._hash_algorithm()
+            if hasattr(ef, '_eof'):  # py33
+                def _update_crc(data):
+                    _update_crc_orig(data)
+                    running_hash.update(data)
+                    if ef._eof and running_hash.digest() != expected_hash:
+                        raise BadWheelFile("Bad hash for file %r" % ef.name)
+            else:
+                def _update_crc(data, eof=None):
+                    _update_crc_orig(data, eof=eof)
+                    running_hash.update(data)
+                    if eof and running_hash.digest() != expected_hash:
+                        raise BadWheelFile("Bad hash for file %r" % ef.name)
+            ef._update_crc = _update_crc
+        elif self.strict and name not in self._expected_hashes:
+            raise BadWheelFile("No expected hash for file %r" % ef.name)
+        return ef
+
+    def pop(self):
+        """Truncate the last file off this zipfile.
+        Assumes infolist() is in the same order as the files (true for
+        ordinary zip files created by Python)"""
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to pop from ZIP archive that was already closed")
+        last = self.infolist().pop()
+        del self.NameToInfo[last.filename]
+        self.fp.seek(last.header_offset, os.SEEK_SET)
+        self.fp.truncate()
+        self._didModify = True
diff --git a/wheel/metadata.py b/wheel/metadata.py
new file mode 100644
index 0000000..b2318d4
--- /dev/null
+++ b/wheel/metadata.py
@@ -0,0 +1,304 @@
+"""
+Tools for converting old- to new-style metadata.
+"""
+
+from collections import defaultdict, namedtuple
+from .pkginfo import read_pkg_info
+
+import re
+import os.path
+import textwrap
+import pkg_resources
+import email.parser
+import wheel
+
+METADATA_VERSION = "2.0"
+
+PLURAL_FIELDS = { "classifier" : "classifiers",
+                  "provides_dist" : "provides",
+                  "provides_extra" : "extras" }
+
+SKIP_FIELDS = set()
+
+CONTACT_FIELDS = (({"email":"author_email", "name": "author"},
+                    "author"),
+                  ({"email":"maintainer_email", "name": "maintainer"},
+                    "maintainer"))
+
+# commonly filled out as "UNKNOWN" by distutils:
+UNKNOWN_FIELDS = set(("author", "author_email", "platform", "home_page",
+                      "license"))
+
+# Wheel itself is probably the only program that uses non-extras markers
+# in METADATA/PKG-INFO. Support its syntax with the extra at the end only.
+EXTRA_RE = re.compile("""^(?P<package>.*?)(;\s*(?P<condition>.*?)(extra == '(?P<extra>.*?)')?)$""")
+KEYWORDS_RE = re.compile("[\0-,]+")
+
+MayRequiresKey = namedtuple('MayRequiresKey', ('condition', 'extra'))
+
+def unique(iterable):
+    """
+    Yield unique values in iterable, preserving order.
+    """
+    seen = set()
+    for value in iterable:
+        if not value in seen:
+            seen.add(value)
+            yield value
+
+
+def handle_requires(metadata, pkg_info, key):
+    """
+    Place the runtime requirements from pkg_info into metadata.
+    """
+    may_requires = defaultdict(list)
+    for value in pkg_info.get_all(key):
+        extra_match = EXTRA_RE.search(value)
+        if extra_match:
+            groupdict = extra_match.groupdict()
+            condition = groupdict['condition']
+            extra = groupdict['extra']
+            package = groupdict['package']
+            if condition.endswith(' and '):
+                condition = condition[:-5]
+        else:
+            condition, extra = None, None
+            package = value
+        key = MayRequiresKey(condition, extra)
+        may_requires[key].append(package)
+
+    if may_requires:
+        metadata['run_requires'] = []
+        for key, value in may_requires.items():
+            may_requirement = {'requires':value}
+            if key.extra:
+                may_requirement['extra'] = key.extra
+            if key.condition:
+                may_requirement['environment'] = key.condition
+            metadata['run_requires'].append(may_requirement)
+
+        if not 'extras' in metadata:
+            metadata['extras'] = []
+        metadata['extras'].extend([key.extra for key in may_requires.keys() if key.extra])
+
+
+def pkginfo_to_dict(path, distribution=None):
+    """
+    Convert PKG-INFO to a prototype Metadata 2.0 (PEP 426) dict.
+
+    The description is included under the key ['description'] rather than
+    being written to a separate file.
+
+    path: path to PKG-INFO file
+    distribution: optional distutils Distribution()
+    """
+
+    metadata = defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
+    metadata["generator"] = "bdist_wheel (" + wheel.__version__ + ")"
+    try:
+        unicode
+        pkg_info = read_pkg_info(path)
+    except NameError:
+        pkg_info = email.parser.Parser().parsestr(open(path, 'rb').read().decode('utf-8'))
+    description = None
+
+    if pkg_info['Summary']:
+        metadata['summary'] = pkginfo_unicode(pkg_info, 'Summary')
+        del pkg_info['Summary']
+
+    if pkg_info['Description']:
+        description = dedent_description(pkg_info)
+        del pkg_info['Description']
+    else:
+        payload = pkg_info.get_payload()
+        if isinstance(payload, bytes):
+            # Avoid a Python 2 Unicode error.
+            # We still suffer ? glyphs on Python 3.
+            payload = payload.decode('utf-8')
+        if payload:
+            description = payload
+
+    if description:
+        pkg_info['description'] = description
+
+    for key in unique(k.lower() for k in pkg_info.keys()):
+        low_key = key.replace('-', '_')
+
+        if low_key in SKIP_FIELDS:
+            continue
+
+        if low_key in UNKNOWN_FIELDS and pkg_info.get(key) == 'UNKNOWN':
+            continue
+
+        if low_key in PLURAL_FIELDS:
+            metadata[PLURAL_FIELDS[low_key]] = pkg_info.get_all(key)
+
+        elif low_key == "requires_dist":
+            handle_requires(metadata, pkg_info, key)
+
+        elif low_key == 'provides_extra':
+            if not 'extras' in metadata:
+                metadata['extras'] = []
+            metadata['extras'].extend(pkg_info.get_all(key))
+
+        elif low_key == 'home_page':
+            metadata['extensions']['python.details']['project_urls'] = {'Home':pkg_info[key]}
+
+        elif low_key == 'keywords':
+            metadata['keywords'] = KEYWORDS_RE.split(pkg_info[key])
+
+        else:
+            metadata[low_key] = pkg_info[key]
+
+    metadata['metadata_version'] = METADATA_VERSION
+
+    if 'extras' in metadata:
+        metadata['extras'] = sorted(set(metadata['extras']))
+
+    # include more information if distribution is available
+    if distribution:
+        for requires, attr in (('test_requires', 'tests_require'),):
+            try:
+                requirements = getattr(distribution, attr)
+                if isinstance(requirements, list):
+                    new_requirements = list(convert_requirements(requirements))
+                    metadata[requires] = [{'requires':new_requirements}]
+            except AttributeError:
+                pass
+
+    # handle contacts
+    contacts = []
+    for contact_type, role in CONTACT_FIELDS:
+        contact = {}
+        for key in contact_type:
+            if contact_type[key] in metadata:
+                contact[key] = metadata.pop(contact_type[key])
+        if contact:
+            contact['role'] = role
+            contacts.append(contact)
+    if contacts:
+        metadata['extensions']['python.details']['contacts'] = contacts
+
+    # convert entry points to exports
+    try:
+        with open(os.path.join(os.path.dirname(path), "entry_points.txt"), "r") as ep_file:
+            ep_map = pkg_resources.EntryPoint.parse_map(ep_file.read())
+        exports = {}
+        for group, items in ep_map.items():
+            exports[group] = {}
+            for item in items.values():
+                name, export = str(item).split(' = ', 1)
+                exports[group][name] = export
+        if exports:
+            metadata['extensions']['python.exports'] = exports
+    except IOError:
+        pass
+
+    # copy console_scripts entry points to commands
+    if 'python.exports' in metadata['extensions']:
+        for (ep_script, wrap_script) in (('console_scripts', 'wrap_console'),
+                                         ('gui_scripts', 'wrap_gui')):
+            if ep_script in metadata['extensions']['python.exports']:
+                metadata['extensions']['python.commands'][wrap_script] = \
+                    metadata['extensions']['python.exports'][ep_script]
+
+    return metadata
+
+def requires_to_requires_dist(requirement):
+    """Compose the version predicates for requirement in PEP 345 fashion."""
+    requires_dist = []
+    for op, ver in requirement.specs:
+        requires_dist.append(op + ver)
+    if not requires_dist:
+        return ''
+    return " (%s)" % ','.join(requires_dist)
+
+def convert_requirements(requirements):
+    """Yield Requires-Dist: strings for parsed requirements strings."""
+    for req in requirements:
+        parsed_requirement = pkg_resources.Requirement.parse(req)
+        spec = requires_to_requires_dist(parsed_requirement)
+        extras = ",".join(parsed_requirement.extras)
+        if extras:
+            extras = "[%s]" % extras
+        yield (parsed_requirement.project_name + extras + spec)
+
+def pkginfo_to_metadata(egg_info_path, pkginfo_path):
+    """
+    Convert .egg-info directory with PKG-INFO to the Metadata 1.3 aka
+    old-draft Metadata 2.0 format.
+    """
+    pkg_info = read_pkg_info(pkginfo_path)
+    pkg_info.replace_header('Metadata-Version', '2.0')
+    requires_path = os.path.join(egg_info_path, 'requires.txt')
+    if os.path.exists(requires_path):
+        requires = open(requires_path).read()
+        for extra, reqs in pkg_resources.split_sections(requires):
+            condition = ''
+            if extra and ':' in extra: # setuptools extra:condition syntax
+                extra, condition = extra.split(':', 1)
+            if extra:
+                pkg_info['Provides-Extra'] = extra
+                if condition:
+                    condition += " and "
+                condition += 'extra == %s' % repr(extra)
+            if condition:
+                condition = '; ' + condition
+            for new_req in convert_requirements(reqs):
+                pkg_info['Requires-Dist'] = new_req + condition
+
+    description = pkg_info['Description']
+    if description:
+        pkg_info.set_payload(dedent_description(pkg_info))
+        del pkg_info['Description']
+
+    return pkg_info
+
+
+def pkginfo_unicode(pkg_info, field):
+    """Hack to coax Unicode out of an email Message() - Python 3.3+"""
+    text = pkg_info[field]
+    field = field.lower()
+    if not isinstance(text, str):
+        if not hasattr(pkg_info, 'raw_items'):  # Python 3.2
+            return str(text)
+        for item in pkg_info.raw_items():
+            if item[0].lower() == field:
+                text = item[1].encode('ascii', 'surrogateescape')\
+                                      .decode('utf-8')
+                break
+
+    return text
+
+
+def dedent_description(pkg_info):
+    """
+    Dedent and convert pkg_info['Description'] to Unicode.
+    """
+    description = pkg_info['Description']
+
+    # Python 3 Unicode handling, sorta.
+    surrogates = False
+    if not isinstance(description, str):
+        surrogates = True
+        description = pkginfo_unicode(pkg_info, 'Description')
+
+    description_lines = description.splitlines()
+    description_dedent = '\n'.join(
+            # if the first line of long_description is blank,
+            # the first line here will be indented.
+            (description_lines[0].lstrip(),
+             textwrap.dedent('\n'.join(description_lines[1:])),
+             '\n'))
+
+    if surrogates:
+        description_dedent = description_dedent\
+                .encode("utf8")\
+                .decode("ascii", "surrogateescape")
+
+    return description_dedent
+
+
+if __name__ == "__main__":
+    import sys, pprint
+    pprint.pprint(pkginfo_to_dict(sys.argv[1]))
diff --git a/wheel/paths.py b/wheel/paths.py
new file mode 100644
index 0000000..fe3dfd6
--- /dev/null
+++ b/wheel/paths.py
@@ -0,0 +1,41 @@
+"""
+Installation paths.
+
+Map the .data/ subdirectory names to install paths.
+"""
+
+import os.path
+import sys
+import distutils.dist as dist
+import distutils.command.install as install
+
+def get_install_command(name):
+    # late binding due to potential monkeypatching
+    d = dist.Distribution({'name':name})
+    i = install.install(d)
+    i.finalize_options()
+    return i
+
+def get_install_paths(name):
+    """
+    Return the (distutils) install paths for the named dist.
+    
+    A dict with ('purelib', 'platlib', 'headers', 'scripts', 'data') keys.
+    """
+    paths = {}
+
+    i = get_install_command(name)
+
+    for key in install.SCHEME_KEYS:
+        paths[key] = getattr(i, 'install_' + key)
+
+    # pip uses a similar path as an alternative to the system's (read-only)
+    # include directory:
+    if hasattr(sys, 'real_prefix'):  # virtualenv
+        paths['headers'] = os.path.join(sys.prefix,
+                                        'include',
+                                        'site',
+                                        'python' + sys.version[:3],
+                                        name)
+
+    return paths
diff --git a/wheel/pep425tags.py b/wheel/pep425tags.py
new file mode 100644
index 0000000..3af04a1
--- /dev/null
+++ b/wheel/pep425tags.py
@@ -0,0 +1,97 @@
+"""Generate and work with PEP 425 Compatibility Tags."""
+
+import sys
+
+try:
+    import sysconfig
+except ImportError:  # pragma nocover
+    # Python < 2.7
+    import distutils.sysconfig as sysconfig
+import distutils.util
+
+
+def get_abbr_impl():
+    """Return abbreviated implementation name."""
+    if hasattr(sys, 'pypy_version_info'):
+        pyimpl = 'pp'
+    elif sys.platform.startswith('java'):
+        pyimpl = 'jy'
+    elif sys.platform == 'cli':
+        pyimpl = 'ip'
+    else:
+        pyimpl = 'cp'
+    return pyimpl
+
+
+def get_impl_ver():
+    """Return implementation version."""
+    impl_ver = sysconfig.get_config_var("py_version_nodot")
+    if not impl_ver:
+        impl_ver = ''.join(map(str, sys.version_info[:2]))
+    return impl_ver
+
+
+def get_platform():
+    """Return our platform name 'win32', 'linux_x86_64'"""
+    # XXX remove distutils dependency
+    return distutils.util.get_platform().replace('.', '_').replace('-', '_')
+
+
+def get_supported(versions=None):
+    """Return a list of supported tags for each version specified in
+    `versions`.
+
+    :param versions: a list of string versions, of the form ["33", "32"], 
+        or None. The first version will be assumed to support our ABI.
+    """
+    supported = []
+    
+    # Versions must be given with respect to the preference
+    if versions is None:
+        versions = []
+        major = sys.version_info[0]
+        # Support all previous minor Python versions.
+        for minor in range(sys.version_info[1], -1, -1):
+            versions.append(''.join(map(str, (major, minor))))
+            
+    impl = get_abbr_impl()
+    
+    abis = []
+
+    soabi = sysconfig.get_config_var('SOABI')
+    if soabi and soabi.startswith('cpython-'):
+        abis[0:0] = ['cp' + soabi.split('-', 1)[-1]]
+ 
+    abi3s = set()
+    import imp
+    for suffix in imp.get_suffixes():
+        if suffix[0].startswith('.abi'):
+            abi3s.add(suffix[0].split('.', 2)[1])
+
+    abis.extend(sorted(list(abi3s)))
+
+    abis.append('none')
+
+    arch = get_platform()
+    
+    # Current version, current API (built specifically for our Python):
+    for abi in abis:
+        supported.append(('%s%s' % (impl, versions[0]), abi, arch))
+            
+    # No abi / arch, but requires our implementation:
+    for i, version in enumerate(versions):
+        supported.append(('%s%s' % (impl, version), 'none', 'any'))
+        if i == 0:
+            # Tagged specifically as being cross-version compatible 
+            # (with just the major version specified)
+            supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any')) 
+            
+    # No abi / arch, generic Python
+    for i, version in enumerate(versions):
+        supported.append(('py%s' % (version,), 'none', 'any'))
+        if i == 0:
+            supported.append(('py%s' % (version[0]), 'none', 'any'))
+        
+    return supported
+
+
diff --git a/wheel/pkginfo.py b/wheel/pkginfo.py
new file mode 100644
index 0000000..8a4aca3
--- /dev/null
+++ b/wheel/pkginfo.py
@@ -0,0 +1,44 @@
+"""Tools for reading and writing PKG-INFO / METADATA without caring
+about the encoding."""
+
+from email.parser import Parser
+
+try:
+    unicode
+    _PY3 = False
+except NameError:
+    _PY3 = True
+
+if not _PY3:
+    from email.generator import Generator
+    
+    def read_pkg_info_bytes(bytestr):
+        return Parser().parsestr(bytestr)
+
+    def read_pkg_info(path):
+        with open(path, "r") as headers:
+            message = Parser().parse(headers)
+        return message
+
+    def write_pkg_info(path, message):
+        with open(path, 'w') as metadata:
+            Generator(metadata, maxheaderlen=0).flatten(message) 
+
+else:
+    from email.generator import BytesGenerator
+    def read_pkg_info_bytes(bytestr):
+        headers = bytestr.decode(encoding="ascii", errors="surrogateescape")
+        message = Parser().parsestr(headers)
+        return message
+
+    def read_pkg_info(path):
+        with open(path, "r", 
+                  encoding="ascii", 
+                  errors="surrogateescape") as headers:
+            message = Parser().parse(headers)
+        return message
+
+    def write_pkg_info(path, message):
+        with open(path, "wb") as out:
+            BytesGenerator(out, maxheaderlen=0).flatten(message)
+
diff --git a/wheel/signatures/__init__.py b/wheel/signatures/__init__.py
new file mode 100644
index 0000000..3f21b50
--- /dev/null
+++ b/wheel/signatures/__init__.py
@@ -0,0 +1,106 @@
+"""
+Create and verify jws-js format Ed25519 signatures.
+"""
+
+__all__ = [ 'sign', 'verify' ]
+
+import json
+from ..util import urlsafe_b64decode, urlsafe_b64encode, native, binary
+
+ed25519ll = None
+
+ALG = "Ed25519"
+
+def get_ed25519ll():
+    """Lazy import-and-test of ed25519 module"""
+    global ed25519ll
+    
+    if not ed25519ll:
+        try:
+            import ed25519ll # fast (thousands / s)
+        except (ImportError, OSError): # pragma nocover
+            from . import ed25519py as ed25519ll # pure Python (hundreds / s)
+        test()
+    
+    return ed25519ll
+
+def sign(payload, keypair):
+    """Return a JWS-JS format signature given a JSON-serializable payload and 
+    an Ed25519 keypair."""
+    get_ed25519ll()
+    #
+    header = {
+                "alg": ALG,
+                "jwk": {
+                    "kty": ALG, # alg -> kty in jwk-08.
+                    "vk": native(urlsafe_b64encode(keypair.vk))
+                }
+             }
+    
+    encoded_header = urlsafe_b64encode(binary(json.dumps(header, sort_keys=True)))
+    encoded_payload = urlsafe_b64encode(binary(json.dumps(payload, sort_keys=True)))
+    secured_input = b".".join((encoded_header, encoded_payload))
+    sig_msg = ed25519ll.crypto_sign(secured_input, keypair.sk)
+    signature = sig_msg[:ed25519ll.SIGNATUREBYTES]
+    encoded_signature = urlsafe_b64encode(signature)
+    
+    return {"recipients": 
+            [{"header":native(encoded_header),
+             "signature":native(encoded_signature)}],
+            "payload": native(encoded_payload)}
+
+def assertTrue(condition, message=""):
+    if not condition:
+        raise ValueError(message)
+    
+def verify(jwsjs):
+    """Return (decoded headers, payload) if all signatures in jwsjs are
+    consistent, else raise ValueError.
+    
+    Caller must decide whether the keys are actually trusted."""
+    get_ed25519ll()    
+    # XXX forbid duplicate keys in JSON input using object_pairs_hook (2.7+)
+    recipients = jwsjs["recipients"]
+    encoded_payload = binary(jwsjs["payload"])
+    headers = []
+    for recipient in recipients:
+        assertTrue(len(recipient) == 2, "Unknown recipient key {0}".format(recipient))
+        h = binary(recipient["header"])
+        s = binary(recipient["signature"])
+        header = json.loads(native(urlsafe_b64decode(h)))
+        assertTrue(header["alg"] == ALG, 
+                "Unexpected algorithm {0}".format(header["alg"]))
+        if "alg" in header["jwk"] and not "kty" in header["jwk"]:
+            header["jwk"]["kty"] = header["jwk"]["alg"] # b/w for JWK < -08
+        assertTrue(header["jwk"]["kty"] == ALG, # true for Ed25519
+                "Unexpected key type {0}".format(header["jwk"]["kty"]))
+        vk = urlsafe_b64decode(binary(header["jwk"]["vk"]))
+        secured_input = b".".join((h, encoded_payload))
+        sig = urlsafe_b64decode(s)
+        sig_msg = sig+secured_input
+        verified_input = native(ed25519ll.crypto_sign_open(sig_msg, vk))
+        verified_header, verified_payload = verified_input.split('.')
+        verified_header = binary(verified_header)
+        decoded_header = native(urlsafe_b64decode(verified_header))
+        headers.append(json.loads(decoded_header))
+
+    verified_payload = binary(verified_payload)
+
+    # only return header, payload that have passed through the crypto library.
+    payload = json.loads(native(urlsafe_b64decode(verified_payload)))
+
+    return headers, payload
+
+def test():
+    kp = ed25519ll.crypto_sign_keypair()
+    payload = {'test': 'onstartup'}
+    jwsjs = json.loads(json.dumps(sign(payload, kp)))
+    verify(jwsjs)
+    jwsjs['payload'] += 'x'
+    try:
+        verify(jwsjs)
+    except ValueError:
+        pass
+    else: # pragma no cover
+        raise RuntimeError("No error from bad wheel.signatures payload.")
+
diff --git a/wheel/signatures/djbec.py b/wheel/signatures/djbec.py
new file mode 100644
index 0000000..56efe44
--- /dev/null
+++ b/wheel/signatures/djbec.py
@@ -0,0 +1,270 @@
+# Ed25519 digital signatures
+# Based on http://ed25519.cr.yp.to/python/ed25519.py
+# See also http://ed25519.cr.yp.to/software.html
+# Adapted by Ron Garret
+# Sped up considerably using coordinate transforms found on:
+# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
+# Specifically add-2008-hwcd-4 and dbl-2008-hwcd
+
+try: # pragma nocover
+    unicode
+    PY3 = False
+    def asbytes(b):
+        """Convert array of integers to byte string"""
+        return ''.join(chr(x) for x in b)
+    def joinbytes(b):
+        """Convert array of bytes to byte string"""
+        return ''.join(b)
+    def bit(h, i):
+        """Return i'th bit of bytestring h"""
+        return (ord(h[i//8]) >> (i%8)) & 1
+
+except NameError: # pragma nocover
+    PY3 = True
+    asbytes = bytes
+    joinbytes = bytes
+    def bit(h, i):
+        return (h[i//8] >> (i%8)) & 1
+
+import hashlib
+
+b = 256
+q = 2**255 - 19
+l = 2**252 + 27742317777372353535851937790883648493
+
+def H(m):
+    return hashlib.sha512(m).digest()
+
+def expmod(b, e, m):
+    if e == 0: return 1
+    t = expmod(b, e // 2, m) ** 2 % m
+    if e & 1: t = (t * b) % m
+    return t
+
+# Can probably get some extra speedup here by replacing this with
+# an extended-euclidean, but performance seems OK without that
+def inv(x):
+    return expmod(x, q-2, q)
+
+d = -121665 * inv(121666)
+I = expmod(2,(q-1)//4,q)
+
+def xrecover(y):
+    xx = (y*y-1) * inv(d*y*y+1)
+    x = expmod(xx,(q+3)//8,q)
+    if (x*x - xx) % q != 0: x = (x*I) % q
+    if x % 2 != 0: x = q-x
+    return x
+
+By = 4 * inv(5)
+Bx = xrecover(By)
+B = [Bx % q,By % q]
+
+#def edwards(P,Q):
+#    x1 = P[0]
+#    y1 = P[1]
+#    x2 = Q[0]
+#    y2 = Q[1]
+#    x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2)
+#    y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2)
+#    return (x3 % q,y3 % q)
+
+#def scalarmult(P,e):
+#    if e == 0: return [0,1]
+#    Q = scalarmult(P,e/2)
+#    Q = edwards(Q,Q)
+#    if e & 1: Q = edwards(Q,P)
+#    return Q
+
+# Faster (!) version based on:
+# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
+
+def xpt_add(pt1, pt2):
+    (X1, Y1, Z1, T1) = pt1
+    (X2, Y2, Z2, T2) = pt2
+    A = ((Y1-X1)*(Y2+X2)) % q
+    B = ((Y1+X1)*(Y2-X2)) % q
+    C = (Z1*2*T2) % q
+    D = (T1*2*Z2) % q
+    E = (D+C) % q
+    F = (B-A) % q
+    G = (B+A) % q
+    H = (D-C) % q
+    X3 = (E*F) % q
+    Y3 = (G*H) % q
+    Z3 = (F*G) % q
+    T3 = (E*H) % q
+    return (X3, Y3, Z3, T3)
+
+def xpt_double (pt):
+    (X1, Y1, Z1, _) = pt
+    A = (X1*X1)
+    B = (Y1*Y1)
+    C = (2*Z1*Z1)
+    D = (-A) % q
+    J = (X1+Y1) % q
+    E = (J*J-A-B) % q
+    G = (D+B) % q
+    F = (G-C) % q
+    H = (D-B) % q
+    X3 = (E*F) % q
+    Y3 = (G*H) % q
+    Z3 = (F*G) % q
+    T3 = (E*H) % q
+    return (X3, Y3, Z3, T3)
+
+def pt_xform (pt):
+    (x, y) = pt
+    return (x, y, 1, (x*y)%q)
+
+def pt_unxform (pt):
+    (x, y, z, _) = pt
+    return ((x*inv(z))%q, (y*inv(z))%q)
+
+def xpt_mult (pt, n):
+    if n==0: return pt_xform((0,1))
+    _ = xpt_double(xpt_mult(pt, n>>1))
+    return xpt_add(_, pt) if n&1 else _
+
+def scalarmult(pt, e):
+    return pt_unxform(xpt_mult(pt_xform(pt), e))
+
+def encodeint(y):
+    bits = [(y >> i) & 1 for i in range(b)]
+    e = [(sum([bits[i * 8 + j] << j for j in range(8)]))
+                                    for i in range(b//8)]
+    return asbytes(e)
+
+def encodepoint(P):
+    x = P[0]
+    y = P[1]
+    bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1]
+    e = [(sum([bits[i * 8 + j] << j for j in range(8)]))
+                                    for i in range(b//8)]
+    return asbytes(e)
+    
+def publickey(sk):
+    h = H(sk)
+    a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2))
+    A = scalarmult(B,a)
+    return encodepoint(A)
+
+def Hint(m):
+    h = H(m)
+    return sum(2**i * bit(h,i) for i in range(2*b))
+
+def signature(m,sk,pk):
+    h = H(sk)
+    a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2))
+    inter = joinbytes([h[i] for i in range(b//8,b//4)])
+    r = Hint(inter + m)
+    R = scalarmult(B,r)
+    S = (r + Hint(encodepoint(R) + pk + m) * a) % l
+    return encodepoint(R) + encodeint(S)
+
+def isoncurve(P):
+    x = P[0]
+    y = P[1]
+    return (-x*x + y*y - 1 - d*x*x*y*y) % q == 0
+
+def decodeint(s):
+    return sum(2**i * bit(s,i) for i in range(0,b))
+
+def decodepoint(s):
+    y = sum(2**i * bit(s,i) for i in range(0,b-1))
+    x = xrecover(y)
+    if x & 1 != bit(s,b-1): x = q-x
+    P = [x,y]
+    if not isoncurve(P): raise Exception("decoding point that is not on curve")
+    return P
+
+def checkvalid(s, m, pk):
+    if len(s) != b//4: raise Exception("signature length is wrong")
+    if len(pk) != b//8: raise Exception("public-key length is wrong")
+    R = decodepoint(s[0:b//8])
+    A = decodepoint(pk)
+    S = decodeint(s[b//8:b//4])
+    h = Hint(encodepoint(R) + pk + m)
+    v1 = scalarmult(B,S)
+#  v2 = edwards(R,scalarmult(A,h))
+    v2 = pt_unxform(xpt_add(pt_xform(R), pt_xform(scalarmult(A, h))))
+    return v1==v2
+
+##########################################################
+#
+# Curve25519 reference implementation by Matthew Dempsky, from:
+# http://cr.yp.to/highspeed/naclcrypto-20090310.pdf
+
+# P = 2 ** 255 - 19
+P = q
+A = 486662
+
+#def expmod(b, e, m):
+#    if e == 0: return 1
+#    t = expmod(b, e / 2, m) ** 2 % m
+#    if e & 1: t = (t * b) % m
+#    return t
+
+# def inv(x): return expmod(x, P - 2, P)
+
+def add(n, m, d):
+    (xn, zn) = n
+    (xm, zm) = m 
+    (xd, zd) = d
+    x = 4 * (xm * xn - zm * zn) ** 2 * zd
+    z = 4 * (xm * zn - zm * xn) ** 2 * xd
+    return (x % P, z % P)
+
+def double(n):
+    (xn, zn) = n
+    x = (xn ** 2 - zn ** 2) ** 2
+    z = 4 * xn * zn * (xn ** 2 + A * xn * zn + zn ** 2)
+    return (x % P, z % P)
+
+def curve25519(n, base=9):
+    one = (base,1)
+    two = double(one)
+    # f(m) evaluates to a tuple
+    # containing the mth multiple and the
+    # (m+1)th multiple of base.
+    def f(m):
+        if m == 1: return (one, two)
+        (pm, pm1) = f(m // 2)
+        if (m & 1):
+            return (add(pm, pm1, one), double(pm1))
+        return (double(pm), add(pm, pm1, one))
+    ((x,z), _) = f(n)
+    return (x * inv(z)) % P
+
+import random
+
+def genkey(n=0):
+    n = n or random.randint(0,P)
+    n &= ~7
+    n &= ~(128 << 8 * 31)
+    n |= 64 << 8 * 31
+    return n
+
+#def str2int(s):
+#    return int(hexlify(s), 16)
+#    # return sum(ord(s[i]) << (8 * i) for i in range(32))
+#
+#def int2str(n):
+#    return unhexlify("%x" % n)
+#    # return ''.join([chr((n >> (8 * i)) & 255) for i in range(32)])
+
+#################################################
+
+def dsa_test():
+    import os
+    msg = str(random.randint(q,q+q)).encode('utf-8')
+    sk = os.urandom(32)
+    pk = publickey(sk)
+    sig = signature(msg, sk, pk)
+    return checkvalid(sig, msg, pk)
+
+def dh_test():
+    sk1 = genkey()
+    sk2 = genkey()
+    return curve25519(sk1, curve25519(sk2)) == curve25519(sk2, curve25519(sk1))
+
diff --git a/wheel/signatures/ed25519py.py b/wheel/signatures/ed25519py.py
new file mode 100644
index 0000000..55eba2e
--- /dev/null
+++ b/wheel/signatures/ed25519py.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+import warnings
+import os
+
+from collections import namedtuple
+from . import djbec
+
+__all__ = ['crypto_sign', 'crypto_sign_open', 'crypto_sign_keypair', 'Keypair',
+           'PUBLICKEYBYTES', 'SECRETKEYBYTES', 'SIGNATUREBYTES']
+
+PUBLICKEYBYTES=32
+SECRETKEYBYTES=64
+SIGNATUREBYTES=64
+
+Keypair = namedtuple('Keypair', ('vk', 'sk')) # verifying key, secret key
+
+def crypto_sign_keypair(seed=None):
+    """Return (verifying, secret) key from a given seed, or os.urandom(32)"""    
+    if seed is None:
+        seed = os.urandom(PUBLICKEYBYTES)
+    else:
+        warnings.warn("ed25519ll should choose random seed.",
+                      RuntimeWarning)
+    if len(seed) != 32:
+        raise ValueError("seed must be 32 random bytes or None.")
+    skbytes = seed
+    vkbytes = djbec.publickey(skbytes)
+    return Keypair(vkbytes, skbytes+vkbytes)
+
+
+def crypto_sign(msg, sk):
+    """Return signature+message given message and secret key.
+    The signature is the first SIGNATUREBYTES bytes of the return value.
+    A copy of msg is in the remainder."""
+    if len(sk) != SECRETKEYBYTES:
+        raise ValueError("Bad signing key length %d" % len(sk))
+    vkbytes = sk[PUBLICKEYBYTES:]
+    skbytes = sk[:PUBLICKEYBYTES]
+    sig = djbec.signature(msg, skbytes, vkbytes)
+    return sig + msg
+
+
+def crypto_sign_open(signed, vk):
+    """Return message given signature+message and the verifying key."""
+    if len(vk) != PUBLICKEYBYTES:
+        raise ValueError("Bad verifying key length %d" % len(vk))
+    rc = djbec.checkvalid(signed[:SIGNATUREBYTES], signed[SIGNATUREBYTES:], vk)
+    if not rc:
+        raise ValueError("rc != True", rc)    
+    return signed[SIGNATUREBYTES:]
+
diff --git a/wheel/signatures/keys.py b/wheel/signatures/keys.py
new file mode 100644
index 0000000..1dde4bf
--- /dev/null
+++ b/wheel/signatures/keys.py
@@ -0,0 +1,99 @@
+"""Store and retrieve wheel signing / verifying keys.
+
+Given a scope (a package name, + meaning "all packages", or - meaning 
+"no packages"), return a list of verifying keys that are trusted for that 
+scope.
+
+Given a package name, return a list of (scope, key) suggested keys to sign
+that package (only the verifying keys; the private signing key is stored
+elsewhere).
+
+Keys here are represented as urlsafe_b64encoded strings with no padding.
+
+Tentative command line interface:
+
+# list trusts
+wheel trust
+# trust a particular key for all
+wheel trust + key
+# trust key for beaglevote
+wheel trust beaglevote key
+# stop trusting a key for all
+wheel untrust + key
+
+# generate a key pair
+wheel keygen
+
+# import a signing key from a file
+wheel import keyfile
+
+# export a signing key
+wheel export key
+"""
+
+import json
+import os.path
+from wheel.util import native, load_config_paths, save_config_path
+
+class WheelKeys(object):
+    SCHEMA = 1
+    CONFIG_NAME = 'wheel.json'
+    
+    def __init__(self):
+        self.data = {'signers':[], 'verifiers':[]}
+        
+    def load(self):
+        # XXX JSON is not a great database
+        for path in load_config_paths('wheel'):
+            conf = os.path.join(native(path), self.CONFIG_NAME)
+            if os.path.exists(conf):
+                with open(conf, 'r') as infile:
+                    self.data = json.load(infile)
+                    for x in ('signers', 'verifiers'):
+                        if not x in self.data:
+                            self.data[x] = []
+                    if 'schema' not in self.data:
+                        self.data['schema'] = self.SCHEMA
+                    elif self.data['schema'] != self.SCHEMA:
+                        raise ValueError(
+                            "Bad wheel.json version {0}, expected {1}".format(
+                                self.data['schema'], self.SCHEMA))
+                break
+        return self
+
+    def save(self):
+        # Try not to call this a very long time after load() 
+        path = save_config_path('wheel')
+        conf = os.path.join(native(path), self.CONFIG_NAME)
+        with open(conf, 'w+') as out:
+            json.dump(self.data, out, indent=2)
+        return self
+    
+    def trust(self, scope, vk):
+        """Start trusting a particular key for given scope."""
+        self.data['verifiers'].append({'scope':scope, 'vk':vk})
+        return self
+    
+    def untrust(self, scope, vk):
+        """Stop trusting a particular key for given scope."""
+        self.data['verifiers'].remove({'scope':scope, 'vk':vk})
+        return self
+        
+    def trusted(self, scope=None):
+        """Return list of [(scope, trusted key), ...] for given scope."""
+        trust = [(x['scope'], x['vk']) for x in self.data['verifiers'] if x['scope'] in (scope, '+')]
+        trust.sort(key=lambda x: x[0])
+        trust.reverse()
+        return trust
+    
+    def signers(self, scope):
+        """Return list of signing key(s)."""
+        sign = [(x['scope'], x['vk']) for x in self.data['signers'] if x['scope'] in (scope, '+')]
+        sign.sort(key=lambda x: x[0])
+        sign.reverse()
+        return sign
+    
+    def add_signer(self, scope, vk):
+        """Remember verifying key vk as being valid for signing in scope."""
+        self.data['signers'].append({'scope':scope, 'vk':vk})
+    
diff --git a/wheel/test/__init__.py b/wheel/test/__init__.py
new file mode 100644
index 0000000..4287ca8
--- /dev/null
+++ b/wheel/test/__init__.py
@@ -0,0 +1 @@
+#
\ No newline at end of file
diff --git a/wheel/test/complex-dist/complexdist/__init__.py b/wheel/test/complex-dist/complexdist/__init__.py
new file mode 100644
index 0000000..559fbb7
--- /dev/null
+++ b/wheel/test/complex-dist/complexdist/__init__.py
@@ -0,0 +1,2 @@
+def main():
+    return
diff --git a/wheel/test/complex-dist/setup.py b/wheel/test/complex-dist/setup.py
new file mode 100644
index 0000000..802dbaf
--- /dev/null
+++ b/wheel/test/complex-dist/setup.py
@@ -0,0 +1,25 @@
+from setuptools import setup
+
+try:
+    unicode
+    def u8(s):
+        return s.decode('unicode-escape')
+except NameError:
+    def u8(s):
+        return s
+
+setup(name='complex-dist',
+      version='0.1',
+      description=u8('Another testing distribution \N{SNOWMAN}'),
+      long_description=u8('Another testing distribution \N{SNOWMAN}'),
+      author="Illustrious Author",
+      author_email="illustrious at example.org",
+      url="http://example.org/exemplary",
+      packages=['complexdist'],
+      setup_requires=["wheel", "setuptools"],
+      install_requires=["quux", "splort"],
+      extras_require={'simple':['simple.dist']},
+      tests_require=["foo", "bar>=10.0.0"],
+      entry_points={'console_scripts':['complex-dist=complexdist:main']}
+      )
+
diff --git a/wheel/test/headers.dist/header.h b/wheel/test/headers.dist/header.h
new file mode 100644
index 0000000..e69de29
diff --git a/wheel/test/headers.dist/headersdist.py b/wheel/test/headers.dist/headersdist.py
new file mode 100644
index 0000000..e69de29
diff --git a/wheel/test/headers.dist/setup.py b/wheel/test/headers.dist/setup.py
new file mode 100644
index 0000000..2704f01
--- /dev/null
+++ b/wheel/test/headers.dist/setup.py
@@ -0,0 +1,16 @@
+from setuptools import setup
+
+try:
+    unicode
+    def u8(s):
+        return s.decode('unicode-escape').encode('utf-8')
+except NameError:
+    def u8(s):
+        return s.encode('utf-8')
+
+setup(name='headers.dist',
+      version='0.1',
+      description=u8('A distribution with headers'),
+      headers=['header.h']
+      )
+
diff --git a/wheel/test/pydist-schema.json b/wheel/test/pydist-schema.json
new file mode 100644
index 0000000..566f3a4
--- /dev/null
+++ b/wheel/test/pydist-schema.json
@@ -0,0 +1,362 @@
+{
+  "id": "http://www.python.org/dev/peps/pep-0426/",
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Metadata for Python Software Packages 2.0",
+  "type": "object",
+  "properties": {
+    "metadata_version": {
+      "description": "Version of the file format",
+      "type": "string",
+      "pattern": "^(\\d+(\\.\\d+)*)$"
+    },
+    "generator": {
+      "description": "Name and version of the program that produced this file.",
+      "type": "string",
+      "pattern": "^[0-9A-Za-z]([0-9A-Za-z_.-]*[0-9A-Za-z])( \\(.*\\))?$"
+    },
+    "name": {
+      "description": "The name of the distribution.",
+      "type": "string",
+      "$ref": "#/definitions/distribution_name"
+    },
+    "version": {
+      "description": "The distribution's public version identifier",
+      "type": "string",
+      "pattern": "^(\\d+(\\.\\d+)*)((a|b|c|rc)(\\d+))?(\\.(post)(\\d+))?(\\.(dev)(\\d+))?$"
+    },
+    "source_label": {
+      "description": "A constrained identifying text string",
+      "type": "string",
+      "pattern": "^[0-9a-z_.-+]+$"
+    },
+    "source_url": {
+      "description": "A string containing a full URL where the source for this specific version of the distribution can be downloaded.",
+      "type": "string",
+      "format": "uri"
+    },
+    "summary": {
+      "description": "A one-line summary of what the distribution does.",
+      "type": "string"
+    },
+    "extras": {
+      "description": "A list of optional sets of dependencies that may be used to define conditional dependencies in \"may_require\" and similar fields.",
+      "type": "array",
+      "items": {
+        "type": "string",
+        "$ref": "#/definitions/extra_name"
+      }
+    },
+    "meta_requires": {
+      "description": "A list of subdistributions made available through this metadistribution.",
+      "type": "array",
+      "$ref": "#/definitions/dependencies"
+    },
+    "run_requires": {
+      "description": "A list of other distributions needed to run this distribution.",
+      "type": "array",
+      "$ref": "#/definitions/dependencies"
+    },
+    "test_requires": {
+      "description": "A list of other distributions needed when this distribution is tested.",
+      "type": "array",
+      "$ref": "#/definitions/dependencies"
+    },
+    "build_requires": {
+      "description": "A list of other distributions needed when this distribution is built.",
+      "type": "array",
+      "$ref": "#/definitions/dependencies"
+    },
+    "dev_requires": {
+      "description": "A list of other distributions needed when this distribution is developed.",
+      "type": "array",
+      "$ref": "#/definitions/dependencies"
+    },
+    "provides": {
+      "description": "A list of strings naming additional dependency requirements that are satisfied by installing this distribution. These strings must be of the form Name or Name (Version)",
+      "type": "array",
+      "items": {
+        "type": "string",
+        "$ref": "#/definitions/provides_declaration"
+      }
+    },
+    "modules": {
+      "description": "A list of modules and/or packages available for import after installing this distribution.",
+      "type": "array",
+      "items": {
+        "type": "string",
+        "$ref": "#/definitions/qualified_name"
+      }
+    },
+    "namespaces": {
+      "description": "A list of namespace packages this distribution contributes to",
+      "type": "array",
+      "items": {
+        "type": "string",
+        "$ref": "#/definitions/qualified_name"
+      }
+    },
+    "obsoleted_by": {
+      "description": "A string that indicates that this project is no longer being developed. The named project provides a substitute or replacement.",
+      "type": "string",
+      "$ref": "#/definitions/requirement"
+    },
+    "supports_environments": {
+      "description": "A list of strings specifying the environments that the distribution explicitly supports.",
+      "type": "array",
+      "items": {
+        "type": "string",
+        "$ref": "#/definitions/environment_marker"
+      }
+    },
+    "install_hooks": {
+      "description": "The install_hooks field is used to define various operations that may be invoked on a distribution in a platform independent manner.",
+      "type": "object",
+      "properties": {
+        "postinstall": {
+          "type": "string",
+          "$ref": "#/definitions/export_specifier"
+        },
+        "preuninstall": {
+          "type": "string",
+          "$ref": "#/definitions/export_specifier"
+        }
+      }
+    },
+    "extensions": {
+      "description": "Extensions to the metadata may be present in a mapping under the 'extensions' key.",
+      "type": "object",
+      "$ref": "#/definitions/extensions"
+    }
+  },
+
+  "required": ["metadata_version", "name", "version", "summary"],
+  "additionalProperties": false,
+
+  "definitions": {
+    "contact": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "email": {
+          "type": "string"
+        },
+        "url": {
+          "type": "string"
+        },
+        "role": {
+          "type": "string"
+        }
+      },
+      "required": ["name"],
+      "additionalProperties": false
+    },
+    "dependencies": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "$ref": "#/definitions/dependency"
+      }
+    },
+    "dependency": {
+      "type": "object",
+      "properties": {
+        "extra": {
+          "type": "string",
+          "$ref": "#/definitions/extra_name"
+        },
+        "environment": {
+          "type": "string",
+          "$ref": "#/definitions/environment_marker"
+        },
+        "requires": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "$ref": "#/definitions/requirement"
+          }
+        }
+      },
+      "required": ["requires"],
+      "additionalProperties": false
+    },
+    "extensions": {
+      "type": "object",
+      "patternProperties": {
+        "^[A-Za-z][0-9A-Za-z_]*([.][0-9A-Za-z_]*)*$": {}
+      },
+      "properties": {
+        "python.details" : {
+          "description": "More information regarding the distribution.",
+          "type": "object",
+          "properties": {
+            "document_names": {
+              "description": "Names of supporting metadata documents",
+              "type": "object",
+              "properties": {
+                "description": {
+                  "type": "string",
+                  "$ref": "#/definitions/document_name"
+                },
+                "changelog": {
+                  "type": "string",
+                  "$ref": "#/definitions/document_name"
+                },
+                "license": {
+                  "type": "string",
+                  "$ref": "#/definitions/document_name"
+                }
+              },
+              "additionalProperties": false
+            },
+            "keywords": {
+              "description": "A list of additional keywords to be used to assist searching for the distribution in a larger catalog.",
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            },
+            "license": {
+              "description": "A string indicating the license covering the distribution.",
+              "type": "string"
+            },
+            "classifiers": {
+              "description": "A list of strings, with each giving a single classification value for the distribution.",
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          }
+        },
+        "python.project" : {
+          "description": "More information regarding the creation and maintenance of the distribution.",
+          "$ref": "#/definitions/project_or_integrator"
+        },
+        "python.integrator" : {
+          "description": "More information regarding the downstream redistributor of the distribution.",
+          "$ref": "#/definitions/project_or_integrator"
+        },
+        "python.commands" : {
+          "description": "Command line interfaces provided by this distribution",
+          "type": "object",
+          "$ref": "#/definitions/commands"
+        },
+        "python.exports" : {
+          "description": "Other exported interfaces provided by this distribution",
+          "type": "object",
+          "$ref": "#/definitions/exports"
+        }
+      },
+      "additionalProperties": false
+    },
+    "commands": {
+      "type": "object",
+      "properties": {
+        "wrap_console": {
+          "type": "object",
+          "$ref": "#/definitions/command_map"
+        },
+        "wrap_gui": {
+          "type": "object",
+          "$ref": "#/definitions/command_map"
+        },
+        "prebuilt": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "$ref": "#/definitions/relative_path"
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    "exports": {
+      "type": "object",
+      "patternProperties": {
+        "^[A-Za-z][0-9A-Za-z_]*([.][0-9A-Za-z_]*)*$": {
+          "type": "object",
+          "patternProperties": {
+            ".": {
+              "type": "string",
+              "$ref": "#/definitions/export_specifier"
+            }
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+    "command_map": {
+      "type": "object",
+      "patternProperties": {
+        "^[0-9A-Za-z]([0-9A-Za-z_.-]*[0-9A-Za-z])?$": {
+          "type": "string",
+          "$ref": "#/definitions/export_specifier"
+        }
+      },
+      "additionalProperties": false
+    },
+    "project_or_integrator" : {
+      "type": "object",
+      "properties" : {
+        "contacts": {
+          "description": "A list of contributor entries giving the recommended contact points for getting more information about the project.",
+          "type": "array",
+          "items": {
+            "type": "object",
+            "$ref": "#/definitions/contact"
+          }
+        },
+        "contributors": {
+          "description": "A list of contributor entries for other contributors not already listed as current project points of contact.",
+          "type": "array",
+          "items": {
+            "type": "object",
+            "$ref": "#/definitions/contact"
+          }
+        },
+        "project_urls": {
+          "description": "A mapping of arbitrary text labels to additional URLs relevant to the project.",
+          "type": "object"
+        }
+      }
+    },
+    "distribution_name": {
+      "type": "string",
+      "pattern": "^[0-9A-Za-z]([0-9A-Za-z_.-]*[0-9A-Za-z])?$"
+    },
+    "requirement": {
+      "type": "string"
+    },
+    "provides_declaration": {
+      "type": "string"
+    },
+    "environment_marker": {
+      "type": "string"
+    },
+    "document_name": {
+      "type": "string"
+    },
+    "extra_name" : {
+      "type": "string",
+      "pattern": "^[0-9A-Za-z]([0-9A-Za-z_.-]*[0-9A-Za-z])?$"
+    },
+    "relative_path" : {
+      "type": "string"
+    },
+    "export_specifier": {
+      "type": "string",
+      "pattern": "^([A-Za-z_][A-Za-z_0-9]*([.][A-Za-z_][A-Za-z_0-9]*)*)(:[A-Za-z_][A-Za-z_0-9]*([.][A-Za-z_][A-Za-z_0-9]*)*)?(\\[[0-9A-Za-z]([0-9A-Za-z_.-]*[0-9A-Za-z])?\\])?$"
+    },
+    "qualified_name" : {
+      "type": "string",
+      "pattern": "^[A-Za-z_][A-Za-z_0-9]*([.][A-Za-z_][A-Za-z_0-9]*)*$"
+    },
+    "prefixed_name" : {
+      "type": "string",
+      "pattern": "^[A-Za-z_][A-Za-z_0-9]*([.][A-Za-z_0-9]*)*$"
+    }
+  }
+}
diff --git a/wheel/test/simple.dist/setup.py b/wheel/test/simple.dist/setup.py
new file mode 100644
index 0000000..50c909f
--- /dev/null
+++ b/wheel/test/simple.dist/setup.py
@@ -0,0 +1,17 @@
+from setuptools import setup
+
+try:
+    unicode
+    def u8(s):
+        return s.decode('unicode-escape').encode('utf-8')
+except NameError:
+    def u8(s):
+        return s.encode('utf-8')
+
+setup(name='simple.dist',
+      version='0.1',
+      description=u8('A testing distribution \N{SNOWMAN}'),
+      packages=['simpledist'],
+      extras_require={'voting': ['beaglevote']},
+      )
+
diff --git a/wheel/test/simple.dist/simpledist/__init__.py b/wheel/test/simple.dist/simpledist/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/wheel/test/test-1.0-py2.py3-none-win32.whl b/wheel/test/test-1.0-py2.py3-none-win32.whl
new file mode 100644
index 0000000..dfd3070
Binary files /dev/null and b/wheel/test/test-1.0-py2.py3-none-win32.whl differ
diff --git a/wheel/test/test_basic.py b/wheel/test/test_basic.py
new file mode 100644
index 0000000..e69fef9
--- /dev/null
+++ b/wheel/test/test_basic.py
@@ -0,0 +1,176 @@
+"""
+Basic wheel tests.
+"""
+
+import os
+import pkg_resources
+import json
+import sys
+
+from pkg_resources import resource_filename
+
+import wheel.util
+import wheel.tool
+
+from wheel import egg2wheel
+from wheel.install import WheelFile
+from zipfile import ZipFile
+from shutil import rmtree
+
+test_distributions = ("complex-dist", "simple.dist", "headers.dist")
+
+def teardown_module():
+    """Delete eggs/wheels created by tests."""
+    base = pkg_resources.resource_filename('wheel.test', '')
+    for dist in test_distributions:
+        for subdir in ('build', 'dist'):
+            try:
+                rmtree(os.path.join(base, dist, subdir))
+            except OSError:
+                pass
+
+def setup_module():
+    build_wheel()
+    build_egg()
+
+def build_wheel():
+    """Build wheels from test distributions."""
+    for dist in test_distributions:
+        pwd = os.path.abspath(os.curdir)
+        distdir = pkg_resources.resource_filename('wheel.test', dist)
+        os.chdir(distdir)
+        try:
+            sys.argv = ['', 'bdist_wheel']
+            exec(compile(open('setup.py').read(), 'setup.py', 'exec'))
+        finally:
+            os.chdir(pwd)
+
+def build_egg():
+    """Build eggs from test distributions."""
+    for dist in test_distributions:
+        pwd = os.path.abspath(os.curdir)
+        distdir = pkg_resources.resource_filename('wheel.test', dist)
+        os.chdir(distdir)
+        try:
+            sys.argv = ['', 'bdist_egg']
+            exec(compile(open('setup.py').read(), 'setup.py', 'exec'))
+        finally:
+            os.chdir(pwd)
+
+def test_findable():
+    """Make sure pkg_resources can find us."""
+    assert pkg_resources.working_set.by_key['wheel'].version
+
+def test_egg_re():
+    """Make sure egg_info_re matches."""
+    egg_names = open(pkg_resources.resource_filename('wheel', 'eggnames.txt'))
+    for line in egg_names:
+        line = line.strip()
+        if not line:
+            continue
+        assert egg2wheel.egg_info_re.match(line), line
+
+def test_compatibility_tags():
+    """Test compatibilty tags are working."""
+    wf = WheelFile("package-1.0.0-cp32.cp33-noabi-noarch.whl")
+    assert (list(wf.compatibility_tags) ==
+                 [('cp32', 'noabi', 'noarch'), ('cp33', 'noabi', 'noarch')])
+    assert (wf.arity == 2)
+
+    wf2 = WheelFile("package-1.0.0-1st-cp33-noabi-noarch.whl")
+    wf2_info = wf2.parsed_filename.groupdict()
+    assert wf2_info['build'] == '1st', wf2_info
+
+def test_convert_egg():
+    base = pkg_resources.resource_filename('wheel.test', '')
+    for dist in test_distributions:
+        distdir = os.path.join(base, dist, 'dist')
+        eggs = [e for e in os.listdir(distdir) if e.endswith('.egg')]
+        wheel.tool.convert(eggs, distdir, verbose=False)
+
+def test_unpack():
+    """
+    Make sure 'wheel unpack' works.
+    This also verifies the integrity of our testing wheel files.
+    """
+    for dist in test_distributions:
+        distdir = pkg_resources.resource_filename('wheel.test',
+                                                  os.path.join(dist, 'dist'))
+        for wheelfile in (w for w in os.listdir(distdir) if w.endswith('.whl')):
+            wheel.tool.unpack(os.path.join(distdir, wheelfile), distdir)
+
+def test_no_scripts():
+    """Make sure entry point scripts are not generated."""
+    dist = "complex-dist"
+    basedir = pkg_resources.resource_filename('wheel.test', dist)
+    for (dirname, subdirs, filenames) in os.walk(basedir):
+        for filename in filenames:
+            if filename.endswith('.whl'):
+                whl = ZipFile(os.path.join(dirname, filename))
+                for entry in whl.infolist():
+                    assert not '.data/scripts/' in entry.filename
+
+def test_pydist():
+    """Make sure pydist.json exists and validates against our schema."""
+    # XXX this test may need manual cleanup of older wheels
+
+    import jsonschema
+
+    def open_json(filename):
+        return json.loads(open(filename, 'rb').read().decode('utf-8'))
+
+    pymeta_schema = open_json(resource_filename('wheel.test',
+                                                'pydist-schema.json'))
+    valid = 0
+    for dist in ("simple.dist", "complex-dist"):
+        basedir = pkg_resources.resource_filename('wheel.test', dist)
+        for (dirname, subdirs, filenames) in os.walk(basedir):
+            for filename in filenames:
+                if filename.endswith('.whl'):
+                    whl = ZipFile(os.path.join(dirname, filename))
+                    for entry in whl.infolist():
+                        if entry.filename.endswith('/metadata.json'):
+                            pymeta = json.loads(whl.read(entry).decode('utf-8'))
+                            jsonschema.validate(pymeta, pymeta_schema)
+                            valid += 1
+    assert valid > 0, "No metadata.json found"
+
+def test_util():
+    """Test functions in util.py."""
+    for i in range(10):
+        before = b'*' * i
+        encoded = wheel.util.urlsafe_b64encode(before)
+        assert not encoded.endswith(b'=')
+        after = wheel.util.urlsafe_b64decode(encoded)
+        assert before == after
+
+
+def test_pick_best():
+    """Test the wheel ranking algorithm."""
+    def get_tags(res):
+        info = res[-1].parsed_filename.groupdict()
+        return info['pyver'], info['abi'], info['plat']
+
+    cand_tags = [('py27', 'noabi', 'noarch'), ('py26', 'noabi', 'noarch'),
+                 ('cp27', 'noabi', 'linux_i686'),
+                 ('cp26', 'noabi', 'linux_i686'),
+                 ('cp27', 'noabi', 'linux_x86_64'),
+                 ('cp26', 'noabi', 'linux_x86_64')]
+    cand_wheels = [WheelFile('testpkg-1.0-%s-%s-%s.whl' % t)
+                   for t in cand_tags]
+
+    supported = [('cp27', 'noabi', 'linux_i686'), ('py27', 'noabi', 'noarch')]
+    supported2 = [('cp27', 'noabi', 'linux_i686'), ('py27', 'noabi', 'noarch'),
+                  ('cp26', 'noabi', 'linux_i686'), ('py26', 'noabi', 'noarch')]
+    supported3 = [('cp26', 'noabi', 'linux_i686'), ('py26', 'noabi', 'noarch'),
+                  ('cp27', 'noabi', 'linux_i686'), ('py27', 'noabi', 'noarch')]
+
+    for supp in (supported, supported2, supported3):
+        context = lambda: list(supp)
+        for wheel in cand_wheels:
+            wheel.context = context
+        best = max(cand_wheels)
+        assert list(best.tags)[0] == supp[0]
+
+        # assert_equal(
+        #     list(map(get_tags, pick_best(cand_wheels, supp, top=False))), supp)
diff --git a/wheel/test/test_install.py b/wheel/test/test_install.py
new file mode 100644
index 0000000..ddcddf5
--- /dev/null
+++ b/wheel/test/test_install.py
@@ -0,0 +1,55 @@
+# Test wheel.
+# The file has the following contents:
+#   hello.pyd
+#   hello/hello.py
+#   hello/__init__.py
+#   test-1.0.data/data/hello.dat
+#   test-1.0.data/headers/hello.dat
+#   test-1.0.data/scripts/hello.sh
+#   test-1.0.dist-info/WHEEL
+#   test-1.0.dist-info/METADATA
+#   test-1.0.dist-info/RECORD
+# The root is PLATLIB
+# So, some in PLATLIB, and one in each of DATA, HEADERS and SCRIPTS.
+
+import wheel.tool
+import wheel.pep425tags
+from wheel.install import WheelFile
+from tempfile import mkdtemp
+import shutil
+import os
+
+THISDIR = os.path.dirname(__file__)
+TESTWHEEL = os.path.join(THISDIR, 'test-1.0-py2.py3-none-win32.whl')
+
+def check(*path):
+    return os.path.exists(os.path.join(*path))
+
+def test_install():
+    tempdir = mkdtemp()
+    def get_supported():
+        return list(wheel.pep425tags.get_supported()) + [('py3', 'none', 'win32')]
+    whl = WheelFile(TESTWHEEL, context=get_supported)
+    assert whl.supports_current_python(get_supported)
+    try:
+        locs = {}
+        for key in ('purelib', 'platlib', 'scripts', 'headers', 'data'):
+            locs[key] = os.path.join(tempdir, key)
+            os.mkdir(locs[key])
+        whl.install(overrides=locs)
+        assert len(os.listdir(locs['purelib'])) == 0
+        assert check(locs['platlib'], 'hello.pyd')
+        assert check(locs['platlib'], 'hello', 'hello.py')
+        assert check(locs['platlib'], 'hello', '__init__.py')
+        assert check(locs['data'], 'hello.dat')
+        assert check(locs['headers'], 'hello.dat')
+        assert check(locs['scripts'], 'hello.sh')
+        assert check(locs['platlib'], 'test-1.0.dist-info', 'RECORD')
+    finally:
+        shutil.rmtree(tempdir)
+
+def test_install_tool():
+    """Slightly improve coverage of wheel.install"""
+    wheel.tool.install([TESTWHEEL], force=True, dry_run=True)
+
+    
\ No newline at end of file
diff --git a/wheel/test/test_keys.py b/wheel/test/test_keys.py
new file mode 100644
index 0000000..f96166b
--- /dev/null
+++ b/wheel/test/test_keys.py
@@ -0,0 +1,98 @@
+import tempfile
+import os.path
+import unittest
+import json
+
+from wheel.signatures import keys
+
+wheel_json = """
+{
+  "verifiers": [
+    {
+      "scope": "+", 
+      "vk": "bp-bjK2fFgtA-8DhKKAAPm9-eAZcX_u03oBv2RlKOBc"
+    }, 
+    {
+      "scope": "+", 
+      "vk": "KAHZBfyqFW3OcFDbLSG4nPCjXxUPy72phP9I4Rn9MAo"
+    },
+    {
+      "scope": "+", 
+      "vk": "tmAYCrSfj8gtJ10v3VkvW7jOndKmQIYE12hgnFu3cvk"
+    } 
+  ], 
+  "signers": [
+    {
+      "scope": "+", 
+      "vk": "tmAYCrSfj8gtJ10v3VkvW7jOndKmQIYE12hgnFu3cvk"
+    }, 
+    {
+      "scope": "+", 
+      "vk": "KAHZBfyqFW3OcFDbLSG4nPCjXxUPy72phP9I4Rn9MAo"
+    }
+  ], 
+  "schema": 1
+}
+"""
+
+class TestWheelKeys(unittest.TestCase):
+    def setUp(self):
+        self.config = tempfile.NamedTemporaryFile(suffix='.json')
+        self.config.close()
+        
+        self.config_path, self.config_filename = os.path.split(self.config.name) 
+        def load(*args):
+            return [self.config_path]
+        def save(*args):
+            return self.config_path
+        keys.load_config_paths = load
+        keys.save_config_path = save
+        self.wk = keys.WheelKeys()
+        self.wk.CONFIG_NAME = self.config_filename
+        
+    def tearDown(self):
+        os.unlink(self.config.name)
+        
+    def test_load_save(self):
+        self.wk.data = json.loads(wheel_json)
+        
+        self.wk.add_signer('+', '67890')
+        self.wk.add_signer('scope', 'abcdefg')
+        
+        self.wk.trust('epocs', 'gfedcba')
+        self.wk.trust('+', '12345')
+        
+        self.wk.save()
+        
+        del self.wk.data
+        self.wk.load()
+        
+        signers = self.wk.signers('scope')
+        self.assertTrue(signers[0] == ('scope', 'abcdefg'), self.wk.data['signers'])
+        self.assertTrue(signers[1][0] == '+', self.wk.data['signers'])
+        
+        trusted = self.wk.trusted('epocs')
+        self.assertTrue(trusted[0] == ('epocs', 'gfedcba'))
+        self.assertTrue(trusted[1][0] == '+')
+        
+        self.wk.untrust('epocs', 'gfedcba')
+        trusted = self.wk.trusted('epocs')
+        self.assertTrue(('epocs', 'gfedcba') not in trusted)
+        
+    def test_load_save_incomplete(self):
+        self.wk.data = json.loads(wheel_json)
+        del self.wk.data['signers']
+        self.wk.data['schema'] = self.wk.SCHEMA+1
+        self.wk.save()
+        try:
+            self.wk.load()
+        except ValueError:
+            pass
+        else:
+            raise Exception("Expected ValueError")
+        
+        del self.wk.data['schema']
+        self.wk.save()
+        self.wk.load()
+    
+    
diff --git a/wheel/test/test_paths.py b/wheel/test/test_paths.py
new file mode 100644
index 0000000..a23d506
--- /dev/null
+++ b/wheel/test/test_paths.py
@@ -0,0 +1,6 @@
+import wheel.paths
+from distutils.command.install import SCHEME_KEYS
+
+def test_path():
+    d = wheel.paths.get_install_paths('wheel')
+    assert len(d) == len(SCHEME_KEYS)
diff --git a/wheel/test/test_ranking.py b/wheel/test/test_ranking.py
new file mode 100644
index 0000000..1632a13
--- /dev/null
+++ b/wheel/test/test_ranking.py
@@ -0,0 +1,43 @@
+import unittest
+
+from wheel.pep425tags import get_supported
+from wheel.install import WheelFile
+
+WHEELPAT = "%(name)s-%(ver)s-%(pyver)s-%(abi)s-%(arch)s.whl"
+def make_wheel(name, ver, pyver, abi, arch):
+    name = WHEELPAT % dict(name=name, ver=ver, pyver=pyver, abi=abi,
+            arch=arch)
+    return WheelFile(name)
+
+# This relies on the fact that generate_supported will always return the
+# exact pyver, abi, and architecture for its first (best) match.
+sup = get_supported()
+pyver, abi, arch = sup[0]
+genver = 'py' + pyver[2:]
+majver = genver[:3]
+
+COMBINATIONS = (
+    ('bar', '0.9', 'py2.py3', 'none', 'any'),
+    ('bar', '0.9', majver, 'none', 'any'),
+    ('bar', '0.9', genver, 'none', 'any'),
+    ('bar', '0.9', pyver, abi, arch),
+    ('bar', '1.3.2', majver, 'none', 'any'),
+    ('bar', '3.1', genver, 'none', 'any'),
+    ('bar', '3.1', pyver, abi, arch),
+    ('foo', '1.0', majver, 'none', 'any'),
+    ('foo', '1.1', pyver, abi, arch),
+    ('foo', '2.1', majver + '0', 'none', 'any'),
+    # This will not be compatible for Python x.0. Beware when we hit Python
+    # 4.0, and don't test with 3.0!!!
+    ('foo', '2.1', majver + '1', 'none', 'any'),
+    ('foo', '2.1', pyver , 'none', 'any'),
+    ('foo', '2.1', pyver , abi, arch),
+)
+
+WHEELS = [ make_wheel(*args) for args in COMBINATIONS ]
+
+class TestRanking(unittest.TestCase):
+    def test_comparison(self):
+        for i in range(len(WHEELS)-1):
+            for j in range(i):
+                self.assertTrue(WHEELS[j]<WHEELS[i])
diff --git a/wheel/test/test_signatures.py b/wheel/test/test_signatures.py
new file mode 100644
index 0000000..0af19a7
--- /dev/null
+++ b/wheel/test/test_signatures.py
@@ -0,0 +1,47 @@
+from wheel import signatures
+from wheel.signatures import djbec, ed25519py
+from wheel.util import binary
+
+def test_getlib():
+    signatures.get_ed25519ll()
+
+def test_djbec():
+    djbec.dsa_test()    
+    djbec.dh_test()
+    
+def test_ed25519py():
+    kp0 = ed25519py.crypto_sign_keypair(binary(' '*32))
+    kp = ed25519py.crypto_sign_keypair()
+        
+    signed = ed25519py.crypto_sign(binary('test'), kp.sk)
+    
+    ed25519py.crypto_sign_open(signed, kp.vk)
+    
+    try:
+        ed25519py.crypto_sign_open(signed, kp0.vk)
+    except ValueError:
+        pass
+    else:
+        raise Exception("Expected ValueError")
+    
+    try:
+        ed25519py.crypto_sign_keypair(binary(' '*33))
+    except ValueError:
+        pass
+    else:
+        raise Exception("Expected ValueError")
+    
+    try:
+        ed25519py.crypto_sign(binary(''), binary(' ')*31)
+    except ValueError:
+        pass
+    else:
+        raise Exception("Expected ValueError")
+    
+    try:
+        ed25519py.crypto_sign_open(binary(''), binary(' ')*31)
+    except ValueError:
+        pass
+    else:
+        raise Exception("Expected ValueError")
+    
\ No newline at end of file
diff --git a/wheel/test/test_tagopt.py b/wheel/test/test_tagopt.py
new file mode 100644
index 0000000..300fcf9
--- /dev/null
+++ b/wheel/test/test_tagopt.py
@@ -0,0 +1,112 @@
+"""
+Tests for the bdist_wheel tag options (--python-tag and --universal)
+"""
+
+import sys
+import shutil
+import pytest
+import py.path
+import tempfile
+import subprocess
+
+SETUP_PY = """\
+from setuptools import setup
+
+setup(
+    name="Test",
+    version="1.0",
+    author_email="author at example.com",
+    py_modules=["test"],
+)
+"""
+
+ at pytest.fixture
+def temp_pkg(request):
+    tempdir = tempfile.mkdtemp()
+    def fin():
+        shutil.rmtree(tempdir)
+    request.addfinalizer(fin)
+    temppath = py.path.local(tempdir)
+    temppath.join('test.py').write('print("Hello, world")')
+    temppath.join('setup.py').write(SETUP_PY)
+    return temppath
+
+def test_default_tag(temp_pkg):
+    subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel'],
+            cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py%s-' % (sys.version[0],))
+    assert wheels[0].ext == '.whl'
+
+def test_explicit_tag(temp_pkg):
+    subprocess.check_call(
+        [sys.executable, 'setup.py', 'bdist_wheel', '--python-tag=py32'],
+        cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py32-')
+    assert wheels[0].ext == '.whl'
+
+def test_universal_tag(temp_pkg):
+    subprocess.check_call(
+        [sys.executable, 'setup.py', 'bdist_wheel', '--universal'],
+        cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py2.py3-')
+    assert wheels[0].ext == '.whl'
+
+def test_universal_beats_explicit_tag(temp_pkg):
+    subprocess.check_call(
+        [sys.executable, 'setup.py', 'bdist_wheel', '--universal', '--python-tag=py32'],
+        cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py2.py3-')
+    assert wheels[0].ext == '.whl'
+
+def test_universal_in_setup_cfg(temp_pkg):
+    temp_pkg.join('setup.cfg').write('[bdist_wheel]\nuniversal=1')
+    subprocess.check_call(
+        [sys.executable, 'setup.py', 'bdist_wheel'],
+        cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py2.py3-')
+    assert wheels[0].ext == '.whl'
+
+def test_pythontag_in_setup_cfg(temp_pkg):
+    temp_pkg.join('setup.cfg').write('[bdist_wheel]\npython_tag=py32')
+    subprocess.check_call(
+        [sys.executable, 'setup.py', 'bdist_wheel'],
+        cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py32-')
+    assert wheels[0].ext == '.whl'
+
+def test_legacy_wheel_section_in_setup_cfg(temp_pkg):
+    temp_pkg.join('setup.cfg').write('[wheel]\nuniversal=1')
+    subprocess.check_call(
+        [sys.executable, 'setup.py', 'bdist_wheel'],
+        cwd=str(temp_pkg))
+    dist_dir = temp_pkg.join('dist')
+    assert dist_dir.check(dir=1)
+    wheels = dist_dir.listdir()
+    assert len(wheels) == 1
+    assert wheels[0].basename.startswith('Test-1.0-py2.py3-')
+    assert wheels[0].ext == '.whl'
+
diff --git a/wheel/test/test_tool.py b/wheel/test/test_tool.py
new file mode 100644
index 0000000..31a927c
--- /dev/null
+++ b/wheel/test/test_tool.py
@@ -0,0 +1,28 @@
+from .. import tool
+
+def test_keygen():    
+    def get_keyring():
+        WheelKeys, keyring = tool.get_keyring()
+        
+        class WheelKeysTest(WheelKeys):
+            def save(self):
+                pass
+        
+        class keyringTest:
+            backend = keyring.backend
+            class backends:
+                file = keyring.backends.file
+            @classmethod
+            def get_keyring(cls):
+                class keyringTest2:
+                    pw = None
+                    def set_password(self, a, b, c):
+                        self.pw = c
+                    def get_password(self, a, b):
+                        return self.pw
+                    
+                return keyringTest2()
+        
+        return WheelKeysTest, keyringTest
+    
+    tool.keygen(get_keyring=get_keyring)
diff --git a/wheel/test/test_wheelfile.py b/wheel/test/test_wheelfile.py
new file mode 100644
index 0000000..e362ceb
--- /dev/null
+++ b/wheel/test/test_wheelfile.py
@@ -0,0 +1,69 @@
+import wheel.install
+import hashlib
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import BytesIO as StringIO
+import zipfile
+import pytest
+
+def test_verifying_zipfile():
+    if not hasattr(zipfile.ZipExtFile, '_update_crc'):
+        pytest.skip('No ZIP verification. Missing ZipExtFile._update_crc.')
+    
+    sio = StringIO()
+    zf = zipfile.ZipFile(sio, 'w')
+    zf.writestr("one", b"first file")
+    zf.writestr("two", b"second file")
+    zf.writestr("three", b"third file")
+    zf.close()
+    
+    # In default mode, VerifyingZipFile checks the hash of any read file
+    # mentioned with set_expected_hash(). Files not mentioned with
+    # set_expected_hash() are not checked.
+    vzf = wheel.install.VerifyingZipFile(sio, 'r')
+    vzf.set_expected_hash("one", hashlib.sha256(b"first file").digest())
+    vzf.set_expected_hash("three", "blurble")
+    vzf.open("one").read()
+    vzf.open("two").read()
+    try:
+        vzf.open("three").read()
+    except wheel.install.BadWheelFile:
+        pass
+    else:
+        raise Exception("expected exception 'BadWheelFile()'")
+    
+    # In strict mode, VerifyingZipFile requires every read file to be
+    # mentioned with set_expected_hash().
+    vzf.strict = True
+    try:
+        vzf.open("two").read()
+    except wheel.install.BadWheelFile:
+        pass
+    else:
+        raise Exception("expected exception 'BadWheelFile()'")
+        
+    vzf.set_expected_hash("two", None)
+    vzf.open("two").read()
+    
+def test_pop_zipfile():
+    sio = StringIO()
+    zf = wheel.install.VerifyingZipFile(sio, 'w')
+    zf.writestr("one", b"first file")
+    zf.writestr("two", b"second file")
+    zf.close()
+    
+    try:
+        zf.pop()
+    except RuntimeError:
+        pass # already closed
+    else:
+        raise Exception("expected RuntimeError")
+    
+    zf = wheel.install.VerifyingZipFile(sio, 'a')
+    zf.pop()
+    zf.close()
+    
+    zf = wheel.install.VerifyingZipFile(sio, 'r')
+    assert len(zf.infolist()) == 1
+    
\ No newline at end of file
diff --git a/wheel/tool/__init__.py b/wheel/tool/__init__.py
new file mode 100644
index 0000000..a997d1f
--- /dev/null
+++ b/wheel/tool/__init__.py
@@ -0,0 +1,362 @@
+"""
+Wheel command-line utility.
+"""
+
+import os
+import hashlib
+import sys
+import json
+import wheel.paths
+
+from glob import iglob
+from .. import signatures
+from ..util import (urlsafe_b64decode, urlsafe_b64encode, native, binary, 
+                    matches_requirement)
+from ..install import WheelFile
+
+def require_pkgresources(name):
+    try:
+        import pkg_resources
+    except ImportError:
+        raise RuntimeError("'{0}' needs pkg_resources (part of setuptools).".format(name))
+
+import argparse
+
+class WheelError(Exception): pass
+
+# For testability
+def get_keyring():
+    try:
+        from ..signatures import keys
+        import keyring
+    except ImportError:
+        raise WheelError("Install wheel[signatures] (requires keyring, pyxdg) for signatures.")
+    return keys.WheelKeys, keyring
+
+def keygen(get_keyring=get_keyring):
+    """Generate a public/private key pair."""
+    WheelKeys, keyring = get_keyring()
+
+    ed25519ll = signatures.get_ed25519ll()
+
+    wk = WheelKeys().load()
+
+    keypair = ed25519ll.crypto_sign_keypair()
+    vk = native(urlsafe_b64encode(keypair.vk))
+    sk = native(urlsafe_b64encode(keypair.sk))
+    kr = keyring.get_keyring()
+    kr.set_password("wheel", vk, sk)
+    sys.stdout.write("Created Ed25519 keypair with vk={0}\n".format(vk))
+    if isinstance(kr, keyring.backends.file.BaseKeyring):
+        sys.stdout.write("in {0}\n".format(kr.file_path))
+    else:
+        sys.stdout.write("in %r\n" % kr.__class__)
+
+    sk2 = kr.get_password('wheel', vk)
+    if sk2 != sk:
+        raise WheelError("Keyring is broken. Could not retrieve secret key.")
+
+    sys.stdout.write("Trusting {0} to sign and verify all packages.\n".format(vk))
+    wk.add_signer('+', vk)
+    wk.trust('+', vk)
+    wk.save()
+
+def sign(wheelfile, replace=False, get_keyring=get_keyring):
+    """Sign a wheel"""
+    WheelKeys, keyring = get_keyring()
+
+    ed25519ll = signatures.get_ed25519ll()
+
+    wf = WheelFile(wheelfile, append=True)
+    wk = WheelKeys().load()
+
+    name = wf.parsed_filename.group('name')
+    sign_with = wk.signers(name)[0]
+    sys.stdout.write("Signing {0} with {1}\n".format(name, sign_with[1]))
+
+    vk = sign_with[1]
+    kr = keyring.get_keyring()
+    sk = kr.get_password('wheel', vk)
+    keypair = ed25519ll.Keypair(urlsafe_b64decode(binary(vk)),
+                                urlsafe_b64decode(binary(sk)))
+
+
+    record_name = wf.distinfo_name + '/RECORD'
+    sig_name = wf.distinfo_name + '/RECORD.jws'
+    if sig_name in wf.zipfile.namelist():
+        raise WheelError("Wheel is already signed.")
+    record_data = wf.zipfile.read(record_name)
+    payload = {"hash":"sha256=" + native(urlsafe_b64encode(hashlib.sha256(record_data).digest()))}
+    sig = signatures.sign(payload, keypair)
+    wf.zipfile.writestr(sig_name, json.dumps(sig, sort_keys=True))
+    wf.zipfile.close()
+
+def unsign(wheelfile):
+    """
+    Remove RECORD.jws from a wheel by truncating the zip file.
+    
+    RECORD.jws must be at the end of the archive. The zip file must be an 
+    ordinary archive, with the compressed files and the directory in the same 
+    order, and without any non-zip content after the truncation point.
+    """
+    import wheel.install
+    vzf = wheel.install.VerifyingZipFile(wheelfile, "a")
+    info = vzf.infolist()
+    if not (len(info) and info[-1].filename.endswith('/RECORD.jws')):
+        raise WheelError("RECORD.jws not found at end of archive.")
+    vzf.pop()
+    vzf.close()
+
+def verify(wheelfile):
+    """Verify a wheel.
+    
+    The signature will be verified for internal consistency ONLY and printed. 
+    Wheel's own unpack/install commands verify the manifest against the
+    signature and file contents.
+    """
+    wf = WheelFile(wheelfile)
+    sig_name = wf.distinfo_name + '/RECORD.jws'
+    sig = json.loads(native(wf.zipfile.open(sig_name).read()))
+    verified = signatures.verify(sig)
+    sys.stderr.write("Signatures are internally consistent.\n")
+    sys.stdout.write(json.dumps(verified, indent=2))
+    sys.stdout.write('\n')
+
+def unpack(wheelfile, dest='.'):
+    """Unpack a wheel.
+
+    Wheel content will be unpacked to {dest}/{name}-{ver}, where {name}
+    is the package name and {ver} its version.
+
+    :param wheelfile: The path to the wheel.
+    :param dest: Destination directory (default to current directory).
+    """
+    wf = WheelFile(wheelfile)
+    namever = wf.parsed_filename.group('namever')
+    destination = os.path.join(dest, namever)
+    sys.stderr.write("Unpacking to: %s\n" % (destination))
+    wf.zipfile.extractall(destination)
+    wf.zipfile.close()
+
+def install(requirements, requirements_file=None,
+            wheel_dirs=None, force=False, list_files=False,
+            dry_run=False):
+    """Install wheels.
+    
+    :param requirements: A list of requirements or wheel files to install.
+    :param requirements_file: A file containing requirements to install.
+    :param wheel_dirs: A list of directories to search for wheels.
+    :param force: Install a wheel file even if it is not compatible.
+    :param list_files: Only list the files to install, don't install them.
+    :param dry_run: Do everything but the actual install.
+    """
+
+    # If no wheel directories specified, use the WHEELPATH environment
+    # variable, or the current directory if that is not set.
+    if not wheel_dirs:
+        wheelpath = os.getenv("WHEELPATH")
+        if wheelpath:
+            wheel_dirs = wheelpath.split(os.pathsep)
+        else:
+            wheel_dirs = [ os.path.curdir ]
+
+    # Get a list of all valid wheels in wheel_dirs
+    all_wheels = []
+    for d in wheel_dirs:
+        for w in os.listdir(d):
+            if w.endswith('.whl'):
+                wf = WheelFile(os.path.join(d, w))
+                if wf.compatible:
+                    all_wheels.append(wf)
+
+    # If there is a requirements file, add it to the list of requirements
+    if requirements_file:
+        # If the file doesn't exist, search for it in wheel_dirs
+        # This allows standard requirements files to be stored with the
+        # wheels.
+        if not os.path.exists(requirements_file):
+            for d in wheel_dirs:
+                name = os.path.join(d, requirements_file)
+                if os.path.exists(name):
+                    requirements_file = name
+                    break
+
+        with open(requirements_file) as fd:
+            requirements.extend(fd)
+
+    to_install = []
+    for req in requirements:
+        if req.endswith('.whl'):
+            # Explicitly specified wheel filename
+            if os.path.exists(req):
+                wf = WheelFile(req)
+                if wf.compatible or force:
+                    to_install.append(wf)
+                else:
+                    msg = ("{0} is not compatible with this Python. "
+                           "--force to install anyway.".format(req))
+                    raise WheelError(msg)
+            else:
+                # We could search on wheel_dirs, but it's probably OK to
+                # assume the user has made an error.
+                raise WheelError("No such wheel file: {}".format(req))
+            continue
+
+        # We have a requirement spec
+        # If we don't have pkg_resources, this will raise an exception
+        matches = matches_requirement(req, all_wheels)
+        if not matches:
+            raise WheelError("No match for requirement {}".format(req))
+        to_install.append(max(matches))
+
+    # We now have a list of wheels to install
+    if list_files:
+        sys.stdout.write("Installing:\n")
+
+    if dry_run:
+        return
+
+    for wf in to_install:
+        if list_files:
+            sys.stdout.write("    {0}\n".format(wf.filename))
+            continue
+        wf.install(force=force)
+        wf.zipfile.close()
+
+def install_scripts(distributions):
+    """
+    Regenerate the entry_points console_scripts for the named distribution.
+    """
+    try:
+        from setuptools.command import easy_install
+        import pkg_resources
+    except ImportError:
+        raise RuntimeError("'wheel install_scripts' needs setuptools.")
+
+    for dist in distributions:
+        pkg_resources_dist = pkg_resources.get_distribution(dist)
+        install = wheel.paths.get_install_command(dist)
+        command = easy_install.easy_install(install.distribution)
+        command.args = ['wheel'] # dummy argument
+        command.finalize_options()
+        command.install_egg_scripts(pkg_resources_dist)
+
+def convert(installers, dest_dir, verbose):
+    require_pkgresources('wheel convert')
+    
+    # Only support wheel convert if pkg_resources is present
+    from ..wininst2wheel import bdist_wininst2wheel
+    from ..egg2wheel import egg2wheel
+
+    for pat in installers:
+        for installer in iglob(pat):
+            if os.path.splitext(installer)[1] == '.egg':
+                conv = egg2wheel
+            else:
+                conv = bdist_wininst2wheel
+            if verbose:
+                sys.stdout.write("{0}... ".format(installer))
+                sys.stdout.flush()
+            conv(installer, dest_dir)
+            if verbose:
+                sys.stdout.write("OK\n")
+
+def parser():
+    p = argparse.ArgumentParser()
+    s = p.add_subparsers(help="commands")
+
+    def keygen_f(args):
+        keygen()
+    keygen_parser = s.add_parser('keygen', help='Generate signing key')
+    keygen_parser.set_defaults(func=keygen_f)
+
+    def sign_f(args):
+        sign(args.wheelfile)
+    sign_parser = s.add_parser('sign', help='Sign wheel')
+    sign_parser.add_argument('wheelfile', help='Wheel file')
+    sign_parser.set_defaults(func=sign_f)
+
+    def unsign_f(args):
+        unsign(args.wheelfile)
+    unsign_parser = s.add_parser('unsign', help=unsign.__doc__)
+    unsign_parser.add_argument('wheelfile', help='Wheel file')
+    unsign_parser.set_defaults(func=unsign_f)
+
+    def verify_f(args):
+        verify(args.wheelfile)
+    verify_parser = s.add_parser('verify', help=verify.__doc__)
+    verify_parser.add_argument('wheelfile', help='Wheel file')
+    verify_parser.set_defaults(func=verify_f)
+
+    def unpack_f(args):
+        unpack(args.wheelfile, args.dest)
+    unpack_parser = s.add_parser('unpack', help='Unpack wheel')
+    unpack_parser.add_argument('--dest', '-d', help='Destination directory',
+                               default='.')
+    unpack_parser.add_argument('wheelfile', help='Wheel file')
+    unpack_parser.set_defaults(func=unpack_f)
+
+    def install_f(args):
+        install(args.requirements, args.requirements_file,
+                args.wheel_dirs, args.force, args.list_files)
+    install_parser = s.add_parser('install', help='Install wheels')
+    install_parser.add_argument('requirements', nargs='*',
+                                help='Requirements to install.')
+    install_parser.add_argument('--force', default=False,
+                                action='store_true',
+                                help='Install incompatible wheel files.')
+    install_parser.add_argument('--wheel-dir', '-d', action='append',
+                                dest='wheel_dirs',
+                                help='Directories containing wheels.')
+    install_parser.add_argument('--requirements-file', '-r',
+                                help="A file containing requirements to "
+                                "install.")
+    install_parser.add_argument('--list', '-l', default=False,
+                                dest='list_files',
+                                action='store_true',
+                                help="List wheels which would be installed, "
+                                "but don't actually install anything.")
+    install_parser.set_defaults(func=install_f)
+
+    def install_scripts_f(args):        
+        install_scripts(args.distributions)
+    install_scripts_parser = s.add_parser('install-scripts', help='Install console_scripts')
+    install_scripts_parser.add_argument('distributions', nargs='*',
+                                        help='Regenerate console_scripts for these distributions')
+    install_scripts_parser.set_defaults(func=install_scripts_f)
+
+    def convert_f(args):
+        convert(args.installers, args.dest_dir, args.verbose)
+    convert_parser = s.add_parser('convert', help='Convert egg or wininst to wheel')
+    convert_parser.add_argument('installers', nargs='*', help='Installers to convert')
+    convert_parser.add_argument('--dest-dir', '-d', default=os.path.curdir,
+            help="Directory to store wheels (default %(default)s)")
+    convert_parser.add_argument('--verbose', '-v', action='store_true')
+    convert_parser.set_defaults(func=convert_f)
+
+    def version_f(args):
+        from .. import __version__
+        sys.stdout.write("wheel %s\n" % __version__)
+    version_parser = s.add_parser('version', help='Print version and exit')
+    version_parser.set_defaults(func=version_f)
+
+    def help_f(args):
+        p.print_help()
+    help_parser = s.add_parser('help', help='Show this help')
+    help_parser.set_defaults(func=help_f)
+
+    return p
+
+def main():
+    p = parser()
+    args = p.parse_args()
+    if not hasattr(args, 'func'):
+        p.print_help()
+    else:
+        # XXX on Python 3.3 we get 'args has no func' rather than short help.
+        try:
+            args.func(args)
+            return 0
+        except WheelError as e:
+            sys.stderr.write(e.message + "\n")
+    return 1
diff --git a/wheel/util.py b/wheel/util.py
new file mode 100644
index 0000000..bc2eb5e
--- /dev/null
+++ b/wheel/util.py
@@ -0,0 +1,146 @@
+"""Utility functions."""
+
+import sys
+import os
+import base64
+import json
+import hashlib
+
+__all__ = ['urlsafe_b64encode', 'urlsafe_b64decode', 'utf8',
+           'to_json', 'from_json', 'matches_requirement']
+
+def urlsafe_b64encode(data):
+    """urlsafe_b64encode without padding"""
+    return base64.urlsafe_b64encode(data).rstrip(binary('='))
+
+
+def urlsafe_b64decode(data):
+    """urlsafe_b64decode without padding"""
+    pad = b'=' * (4 - (len(data) & 3))
+    return base64.urlsafe_b64decode(data + pad)
+
+
+def to_json(o):
+    '''Convert given data to JSON.'''
+    return json.dumps(o, sort_keys=True)
+
+
+def from_json(j):
+    '''Decode a JSON payload.'''
+    return json.loads(j)
+
+def open_for_csv(name, mode):
+    if sys.version_info[0] < 3:
+        nl = {}
+        bin = 'b'
+    else:
+        nl = { 'newline': '' }
+        bin = ''
+    return open(name, mode + bin, **nl)
+
+try:
+    unicode
+
+    def utf8(data):
+        '''Utf-8 encode data.'''
+        if isinstance(data, unicode):
+            return data.encode('utf-8')
+        return data
+except NameError:
+    def utf8(data):
+        '''Utf-8 encode data.'''
+        if isinstance(data, str):
+            return data.encode('utf-8')
+        return data
+
+
+try:
+    # For encoding ascii back and forth between bytestrings, as is repeatedly
+    # necessary in JSON-based crypto under Python 3
+    unicode
+    def native(s):
+        return s
+    def binary(s):
+        if isinstance(s, unicode):
+            return s.encode('ascii')
+        return s
+except NameError:
+    def native(s):
+        if isinstance(s, bytes):
+            return s.decode('ascii')
+        return s
+    def binary(s):
+        if isinstance(s, str):
+            return s.encode('ascii')
+
+class HashingFile(object):
+    def __init__(self, fd, hashtype='sha256'):
+        self.fd = fd
+        self.hashtype = hashtype
+        self.hash = hashlib.new(hashtype)
+        self.length = 0
+    def write(self, data):
+        self.hash.update(data)
+        self.length += len(data)
+        self.fd.write(data)
+    def close(self):
+        self.fd.close()
+    def digest(self):
+        if self.hashtype == 'md5':
+            return self.hash.hexdigest()
+        digest = self.hash.digest()
+        return self.hashtype + '=' + native(urlsafe_b64encode(digest))
+
+if sys.platform == 'win32':
+    import ctypes.wintypes
+    # CSIDL_APPDATA for reference - not used here for compatibility with
+    # dirspec, which uses LOCAL_APPDATA and COMMON_APPDATA in that order
+    csidl = dict(CSIDL_APPDATA=26, CSIDL_LOCAL_APPDATA=28,
+            CSIDL_COMMON_APPDATA=35)
+    def get_path(name):
+        SHGFP_TYPE_CURRENT = 0
+        buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
+        ctypes.windll.shell32.SHGetFolderPathW(0, csidl[name], 0, SHGFP_TYPE_CURRENT, buf)
+        return buf.value
+
+    def save_config_path(*resource):
+        appdata = get_path("CSIDL_LOCAL_APPDATA")
+        path = os.path.join(appdata, *resource)
+        if not os.path.isdir(path):
+            os.makedirs(path)
+        return path
+    def load_config_paths(*resource):
+        ids = ["CSIDL_LOCAL_APPDATA", "CSIDL_COMMON_APPDATA"]
+        for id in ids:
+            base = get_path(id)
+            path = os.path.join(base, *resource)
+            if os.path.exists(path):
+                yield path
+else:
+    def save_config_path(*resource):
+        import xdg.BaseDirectory
+        return xdg.BaseDirectory.save_config_path(*resource)
+    def load_config_paths(*resource):
+        import xdg.BaseDirectory
+        return xdg.BaseDirectory.load_config_paths(*resource)
+
+def matches_requirement(req, wheels):
+    """List of wheels matching a requirement.
+
+    :param req: The requirement to satisfy
+    :param wheels: List of wheels to search.
+    """
+    try:
+        from pkg_resources import Distribution, Requirement
+    except ImportError:
+        raise RuntimeError("Cannot use requirements without pkg_resources")
+
+    req = Requirement.parse(req)
+
+    selected = []
+    for wf in wheels:
+        f = wf.parsed_filename
+        dist = Distribution(project_name=f.group("name"), version=f.group("ver"))
+        if dist in req:
+            selected.append(wf)
+    return selected
diff --git a/wheel/wininst2wheel.py b/wheel/wininst2wheel.py
new file mode 100755
index 0000000..297f8d1
--- /dev/null
+++ b/wheel/wininst2wheel.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python
+import os.path
+import re
+import sys
+import tempfile
+import zipfile
+import wheel.bdist_wheel
+import distutils.dist
+from distutils.archive_util import make_archive
+from shutil import rmtree
+from wheel.archive import archive_wheelfile
+from argparse import ArgumentParser
+from glob import iglob
+
+egg_info_re = re.compile(r'''(^|/)(?P<name>[^/]+?)-(?P<ver>.+?)
+    (-(?P<pyver>.+?))?(-(?P<arch>.+?))?.egg-info(/|$)''', re.VERBOSE)
+
+def parse_info(wininfo_name, egginfo_name):
+    """Extract metadata from filenames.
+    
+    Extracts the 4 metadataitems needed (name, version, pyversion, arch) from
+    the installer filename and the name of the egg-info directory embedded in
+    the zipfile (if any).
+
+    The egginfo filename has the format::
+
+        name-ver(-pyver)(-arch).egg-info
+
+    The installer filename has the format::
+
+        name-ver.arch(-pyver).exe
+
+    Some things to note:
+
+    1. The installer filename is not definitive. An installer can be renamed
+       and work perfectly well as an installer. So more reliable data should
+       be used whenever possible.
+    2. The egg-info data should be preferred for the name and version, because
+       these come straight from the distutils metadata, and are mandatory.
+    3. The pyver from the egg-info data should be ignored, as it is
+       constructed from the version of Python used to build the installer,
+       which is irrelevant - the installer filename is correct here (even to
+       the point that when it's not there, any version is implied).
+    4. The architecture must be taken from the installer filename, as it is
+       not included in the egg-info data.
+    5. Architecture-neutral installers still have an architecture because the
+       installer format itself (being executable) is architecture-specific. We
+       should therefore ignore the architecture if the content is pure-python.
+    """
+
+    egginfo = None
+    if egginfo_name:
+        egginfo = egg_info_re.search(egginfo_name)
+        if not egginfo:
+            raise ValueError("Egg info filename %s is not valid" %
+                    (egginfo_name,))
+
+    # Parse the wininst filename
+    # 1. Distribution name (up to the first '-')
+    w_name, sep, rest = wininfo_name.partition('-')
+    if not sep:
+        raise ValueError("Installer filename %s is not valid" %
+                (wininfo_name,))
+    # Strip '.exe'
+    rest = rest[:-4]
+    # 2. Python version (from the last '-', must start with 'py')
+    rest2, sep, w_pyver = rest.rpartition('-')
+    if sep and w_pyver.startswith('py'):
+        rest = rest2
+        w_pyver = w_pyver.replace('.', '')
+    else:
+        # Not version specific - use py2.py3. While it is possible that
+        # pure-Python code is not compatible with both Python 2 and 3, there
+        # is no way of knowing from the wininst format, so we assume the best
+        # here (the user can always manually rename the wheel to be more
+        # restrictive if needed).
+        w_pyver = 'py2.py3'
+    # 3. Version and architecture
+    w_ver, sep, w_arch = rest.rpartition('.')
+    if not sep:
+        raise ValueError("Installer filename %s is not valid" %
+                (wininfo_name,))
+
+    if egginfo:
+        w_name = egginfo.group('name')
+        w_ver = egginfo.group('ver')
+
+    return dict(name=w_name, ver=w_ver, arch=w_arch, pyver=w_pyver)
+
+def bdist_wininst2wheel(path, dest_dir=os.path.curdir):
+    bdw = zipfile.ZipFile(path)
+
+    # Search for egg-info in the archive
+    egginfo_name = None
+    for filename in bdw.namelist():
+        if '.egg-info' in filename:
+            egginfo_name = filename
+            break
+
+    info = parse_info(os.path.basename(path), egginfo_name)
+
+    root_is_purelib = True
+    for zipinfo in bdw.infolist():
+        if zipinfo.filename.startswith('PLATLIB'):
+            root_is_purelib = False
+            break
+    if root_is_purelib:
+        paths = {'purelib': ''}
+    else:
+        paths = {'platlib': ''}
+
+    dist_info = "%(name)s-%(ver)s" % info
+    datadir = "%s.data/" % dist_info
+
+    # rewrite paths to trick ZipFile into extracting an egg
+    # XXX grab wininst .ini - between .exe, padding, and first zip file.
+    members = []
+    egginfo_name = ''
+    for zipinfo in bdw.infolist():
+        key, basename = zipinfo.filename.split('/', 1)
+        key = key.lower()
+        basepath = paths.get(key, None)
+        if basepath is None:
+            basepath = datadir + key.lower() + '/'
+        oldname = zipinfo.filename
+        newname = basepath + basename
+        zipinfo.filename = newname
+        del bdw.NameToInfo[oldname]
+        bdw.NameToInfo[newname] = zipinfo
+        # Collect member names, but omit '' (from an entry like "PLATLIB/"
+        if newname:
+            members.append(newname)
+        # Remember egg-info name for the egg2dist call below
+        if not egginfo_name:
+            if newname.endswith('.egg-info'):
+                egginfo_name = newname
+            elif '.egg-info/' in newname:
+                egginfo_name, sep, _ = newname.rpartition('/')
+    dir = tempfile.mkdtemp(suffix="_b2w")
+    bdw.extractall(dir, members)
+
+    # egg2wheel
+    abi = 'none'
+    pyver = info['pyver']
+    arch = (info['arch'] or 'any').replace('.', '_').replace('-', '_')
+    # Wininst installers always have arch even if they are not
+    # architecture-specific (because the format itself is).
+    # So, assume the content is architecture-neutral if root is purelib.
+    if root_is_purelib:
+        arch = 'any'
+    # If the installer is architecture-specific, it's almost certainly also
+    # CPython-specific.
+    if arch != 'any':
+        pyver = pyver.replace('py', 'cp')
+    wheel_name = '-'.join((
+                          dist_info,
+                          pyver,
+                          abi,
+                          arch
+                          ))
+    bw = wheel.bdist_wheel.bdist_wheel(distutils.dist.Distribution())
+    bw.root_is_purelib = root_is_purelib
+    dist_info_dir = os.path.join(dir, '%s.dist-info' % dist_info)
+    bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir)
+    bw.write_wheelfile(dist_info_dir, generator='wininst2wheel')
+    bw.write_record(dir, dist_info_dir)
+    
+    archive_wheelfile(os.path.join(dest_dir, wheel_name), dir)
+    rmtree(dir)
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument('installers', nargs='*', help="Installers to convert")
+    parser.add_argument('--dest-dir', '-d', default=os.path.curdir,
+            help="Directory to store wheels (default %(default)s)")
+    parser.add_argument('--verbose', '-v', action='store_true')
+    args = parser.parse_args()
+    for pat in args.installers:
+        for installer in iglob(pat):
+            if args.verbose:
+                sys.stdout.write("{0}... ".format(installer))
+            bdist_wininst2wheel(installer, args.dest_dir)
+            if args.verbose:
+                sys.stdout.write("OK\n")
+
+if __name__ == "__main__":
+    main()

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/wheel.git



More information about the Reproducible-commits mailing list