This PEP describes the second version of a built-package format for Pythoncalled “wheel”. Wheel provides a Python-specific, relocatable package formatthat allows people to install software more quickly and predictably thanre-building from source each time.
A wheel is a ZIP-format archive with a specially formatted file name andthe.whl extension. It contains a single distribution nearly as itwould be installed according toPEP 376 with a particular installationscheme. Simple wheels can be unpacked ontosys.path and used directlybut wheels are usually installed with a specialized installer.
This version of the wheel specification adds support for installingdistributions into many different directories, and adds a way to findthose files after they have been installed.
This PEP is not currently being actively pursued, with Python packagingimprovements currently focusing on the package build process rather thanexpanding the binary archive format to cover additional use cases.
Some specific elements to be addressed when work on this PEP is resumed in thefuture:
Wheel 1.0 is best at installing files intosite-packages and a fewother locations specified by distutils, but users would like to installfiles from single distribution into many directories – perhaps separatelocations for docs, data, and code. Unfortunately not everyone agreeson where these install locations should be relative to the root directory.This version of the format adds many more categories, each of which can beinstalled to a different destination based on policy. Since it mightalso be important to locate the installed files at runtime, this versionof the format also adds a way to record the installed paths in a way thatcan be read by the installed software.
Wheel installation notionally consists of two phases:
distribution-1.0.dist-info/WHEEL.distribution-1.0.dist-info/ and (ifthere is data)distribution-1.0.data/.distribution-1.0.data/ onto itsdestination path. Each subdirectory ofdistribution-1.0.data/is a key into a dict of destination directories, such asdistribution-1.0.data/(purelib|platlib|headers|scripts|data).#!python to point to the correctinterpreter. (Note: Python scripts are usually handled by packagemetadata, and not included verbatim in wheel.)distribution-1.0.dist.info/RECORD with the installedpaths.distribution-1.0.data directory.In practice, installers will usually extract files directly from the archiveto their destinations without writing a temporarydistribution-1.0.data/directory.
#!python.{distribution}-{version}.data/scripts/. If the first line ofa file inscripts/ starts with exactlyb'#!python', rewrite topoint to the correct interpreter. Unix installers may need to addthe +x bit to these files if the archive was created on Windows.Theb'#!pythonw' convention is allowed.b'#!pythonw' indicatesa GUI script instead of a console script.
module:callablestring in package metadata, and are not included verbatim in the wheelarchive’sscripts directory. This kind of script gives the installeran opportunity to generate platform specific wrappers..dist-info at the end of the archive..dist-info files physicallyat the end of the archive. This enables some potentially interestingZIP tricks including the ability to amend the metadata withoutrewriting the entire archive.The wheel filename is{distribution}-{version}(-{buildtag})?-{pythontag}-{abitag}-{platformtag}.whl.
For example,distribution-1.0-1-py27-none-any.whl is the firstbuild of a package called ‘distribution’, and is compatible withPython 2.7 (any Python 2.7 implementation), with no ABI (pure Python),on any CPU architecture.
The last three components of the filename before the extension arecalled “compatibility tags.” The compatibility tags express thepackage’s basic interpreter requirements and are detailed inPEP 425.
Each component of the filename is escaped by replacing runs ofnon-alphanumeric characters with an underscore_:
re.sub("[^\w\d.]+","_",distribution,re.UNICODE)
The archive filename is Unicode. The packaging tools may only supportASCII package names, but Unicode filenames are supported in thisspecification.
The filenamesinside the archive are encoded as UTF-8. Although someZIP clients in common use do not properly display UTF-8 filenames,the encoding is supported by both the ZIP specification and Python’szipfile.
The contents of a wheel file, where {distribution} is replaced with thename of the package, e.g.beaglevote and {version} is replaced withits version, e.g.1.0.0, consist of:
/, the root of the archive, contains all files to be installed inpurelib orplatlib as specified inWHEEL.purelib andplatlib are usually bothsite-packages.{distribution}-{version}.dist-info/ contains metadata.{distribution}-{version}.data/ contains one subdirectoryfor each non-empty install scheme key not already covered, wherethe subdirectory name is an index into a dictionary of install paths(e.g.data,scripts,include,purelib,platlib).scripts and begin with exactlyb'#!python' in order to enjoy script wrapper generation and#!python rewriting at install time. They may have any or noextension.{distribution}-{version}.dist-info/METADATA is Metadata version 1.1or greater format metadata.{distribution}-{version}.dist-info/WHEEL is metadata about the archiveitself in the same basic key: value format:Wheel-Version:1.9Generator:bdist_wheel1.9Root-Is-Purelib:trueTag:py2-none-anyTag:py3-none-anyBuild:1Install-Paths-To:wheel/_paths.pyInstall-Paths-To:wheel/_paths.json
Wheel-Version is the version number of the Wheel specification.Generator is the name and optionally the version of the softwarethat produced the archive.Root-Is-Purelib is true if the top level directory of the archiveshould be installed into purelib; otherwise the root should be installedinto platlib.Tag is the wheel’s expanded compatibility tags; in the example thefilename would containpy2.py3-none-any.Build is the build number and is omitted if there is no build number.Install-Paths-To is a locationrelative to the archive that will beoverwritten with the install-time paths of each category in the installscheme. See the install paths section. May appear 0 or more times.Any file that is not normally installed inside site-packages goes intothe .data directory, named as the .dist-info directory but with the.data/ extension:
distribution-1.0.dist-info/distribution-1.0.data/
The .data directory contains subdirectories with the scripts, headers,documentation and so forth from the distribution. During installation thecontents of these subdirectories are moved onto their destination paths.
If a subdirectory is not found in the install scheme, the installer shouldemit a warning, and it should be installed atdistribution-1.0.data/...as if the package was unpacked by a standard unzip tool.
In addition to the distutils install paths, wheel now includes the listedcategories based on GNU autotools. This expanded scheme should help installersto implement system policy, but installers may root each category at anylocation.
A UNIX install scheme might map the categories to their installation pathslike this:
{'bindir':'$eprefix/bin','sbindir':'$eprefix/sbin','libexecdir':'$eprefix/libexec','sysconfdir':'$prefix/etc','sharedstatedir':'$prefix/com','localstatedir':'$prefix/var','libdir':'$eprefix/lib','static_libdir':r'$prefix/lib','includedir':'$prefix/include','datarootdir':'$prefix/share','datadir':'$datarootdir','mandir':'$datarootdir/man','infodir':'$datarootdir/info','localedir':'$datarootdir/locale','docdir':'$datarootdir/doc/$dist_name','htmldir':'$docdir','dvidir':'$docdir','psdir':'$docdir','pdfdir':'$docdir','pkgdatadir':'$datadir/$dist_name'}
If a package needs to find its files at runtime, it can requestthey be written to a specified file or files by the installerandincluded in those same files inside the archive itself, relativeto their location within the archive (so a wheel is still installedcorrectly if unpacked with a standard unzip tool, or perhaps notunpacked at all).
If theWHEEL metadata contains these fields:
Install-Paths-To:wheel/_paths.pyInstall-Paths-To:wheel/_paths.json
Then the wheel installer, when it is about to unpackwheel/_paths.py fromthe archive, replaces it with the actual paths used at install time. Thepaths may be absolute or relative to the generated file.
If the filename ends with.py then a Python script is written. Thescript MUST be executed to get the paths, but it will probably look likethis:
data='../wheel-0.26.0.dev1.data/data'headers='../wheel-0.26.0.dev1.data/headers'platlib='../wheel-0.26.0.dev1.data/platlib'purelib='../wheel-0.26.0.dev1.data/purelib'scripts='../wheel-0.26.0.dev1.data/scripts'# ...
If the filename ends with.json then a JSON document is written:
{"data":"../wheel-0.26.0.dev1.data/data",...}
Only the categories actually used by a particular wheel must be written tothis file.
These files are designed to be written to a location that can be found by theinstalled package without introducing any dependency on a packaging library.
Wheel files include an extended RECORD that enables digitalsignatures.PEP 376’s RECORD is altered to include a secure hashdigestname=urlsafe_b64encode_nopad(digest) (urlsafe base64encoding with no trailing = characters) as the second column insteadof an md5sum. All possible entries are hashed, including anygenerated files such as .pyc files, but not RECORD which cannot contain itsown hash. For example:
file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\_pNh2yI,3144distribution-1.0.dist-info/RECORD,,
The signature file(s) RECORD.jws and RECORD.p7s are not mentioned inRECORD at all since they can only be added after RECORD is generated.Every other file in the archive must have a correct hash in RECORDor the installation will fail.
If JSON web signatures are used, one or more JSON Web Signature JSONSerialization (JWS-JS) signatures is stored in a file RECORD.jws adjacentto RECORD. JWS is used to sign RECORD by including the SHA-256 hash ofRECORD as the signature’s JSON payload:
{"hash":"sha256=ADD-r2urObZHcxBW3Cr-vDCu5RJwT4CaRTHiFmbcIYY"}
(The hash value is the same format used in RECORD.)
If RECORD.p7s is used, it must contain a detached S/MIME format signatureof RECORD.
A wheel installer is not required to understand digital signatures butMUST verify the hashes in RECORD against the extracted file contents.When the installer checks file hashes against RECORD, a separate signaturechecker only needs to establish that RECORD matches the signature.
See
This specification does not have an opinion on how you should organizeyour code. The .data directory is just a place for any files that arenot normally installed insidesite-packagesor on the PYTHONPATH.In other words, you may continue to usepkgutil.get_data(package,resource)even thoughthose files will usually not be distributedinwheel’s.datadirectory.
Attached signatures are more convenient than detached signaturesbecause they travel with the archive. Since only the individual filesare signed, the archive can be recompressed without invalidatingthe signature or individual files can be verified without havingto download the whole archive.
The JOSE specifications of which JWS is a part are designed to be easyto implement, a feature that is also one of wheel’s primary designgoals. JWS yields a useful, concise pure-Python implementation.
S/MIME signatures are allowed for users who need or want to useexisting public key infrastructure with wheel.Signed packages are only a basic building block in a secure packageupdate system. Wheel only provides the building block.
Wheel preserves the “purelib” vs. “platlib” distinction, which issignificant on some platforms. For example, Fedora installs purePython packages to ‘/usr/lib/pythonX.Y/site-packages’ and platformdependent packages to ‘/usr/lib64/pythonX.Y/site-packages’.A wheel with “Root-Is-Purelib: false” with all its filesin
{name}-{version}.data/purelibis equivalent to a wheel with“Root-Is-Purelib: true” with those same files in the root, and itis legal to have files in both the “purelib” and “platlib” categories.In practice a wheel should have only one of “purelib” or “platlib”depending on whether it is pure Python or not and those files shouldbe at the root with the appropriate setting given for “Root-is-purelib”.
Technically, due to the combination of supporting installation viasimple extraction and using an archive format that is compatible withzipimport, a subset of wheel filesdo support being placed directlyonsys.path. However, while this behaviour is a natural consequenceof the format design, actually relying on it is generally discouraged.Firstly, wheelis designed primarily as a distribution format, soskipping the installation step also means deliberately avoiding anyreliance on features that assume full installation (such as being ableto use standard tools like
pipandvirtualenvto capture andmanage dependencies in a way that can be properly tracked for auditingand security update purposes, or integrating fully with the standardbuild machinery for C extensions by publishing header files in theappropriate place).Secondly, while some Python software is written to support runningdirectly from a zip archive, it is still common for code to be writtenassuming it has been fully installed. When that assumption is brokenby trying to run the software from a zip archive, the failures can oftenbe obscure and hard to diagnose (especially when they occur in thirdparty libraries). The two most common sources of problems with thisare the fact that importing C extensions from a zip archive isnotsupported by CPython (since doing so is not supported directly by thedynamic loading machinery on any platform) and that when running froma zip archive the
__file__attribute no longer refers to anordinary filesystem path, but to a combination path that includesboth the location of the zip archive on the filesystem and therelative path to the module inside the archive. Even when softwarecorrectly uses the abstract resource APIs internally, interfacing withexternal components may still require the availability of an actualon-disk file.Like metaclasses, monkeypatching and metapath importers, if you’re notalready sure you need to take advantage of this feature, you almostcertainly don’t need it. If youdo decide to use it anyway, beaware that many projects will require a failure to be reproduced witha fully installed package before accepting it as a genuine bug.
Example urlsafe-base64-nopad implementation:
# urlsafe-base64-nopad for Python 3importbase64defurlsafe_b64encode_nopad(data):returnbase64.urlsafe_b64encode(data).rstrip(b'=')defurlsafe_b64decode_nopad(data):pad=b'='*(4-(len(data)&3))returnbase64.urlsafe_b64decode(data+pad)
This document has been placed into the public domain.
Source:https://github.com/python/peps/blob/main/peps/pep-0491.rst
Last modified:2025-02-01 08:55:40 GMT