387 lines
13 KiB
Python
387 lines
13 KiB
Python
"""
|
|
Wheel command-line utility.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
from glob import iglob
|
|
|
|
from .. import signatures
|
|
from ..install import WheelFile, VerifyingZipFile
|
|
from ..paths import get_install_command
|
|
from ..util import urlsafe_b64decode, urlsafe_b64encode, native, binary, matches_requirement
|
|
|
|
|
|
def require_pkgresources(name):
|
|
try:
|
|
import pkg_resources # noqa: F401
|
|
except ImportError:
|
|
raise RuntimeError("'{0}' needs pkg_resources (part of setuptools).".format(name))
|
|
|
|
|
|
class WheelError(Exception):
|
|
pass
|
|
|
|
|
|
# For testability
|
|
def get_keyring():
|
|
try:
|
|
from ..signatures import keys
|
|
import keyring
|
|
assert keyring.get_keyring().priority
|
|
except (ImportError, AssertionError):
|
|
raise WheelError(
|
|
"Install wheel[signatures] (requires keyring, keyrings.alt, pyxdg) for signatures.")
|
|
|
|
return keys.WheelKeys, keyring
|
|
|
|
|
|
def warn_signatures():
|
|
print('WARNING: The wheel signing and signature verification commands have been deprecated '
|
|
'and will be removed before the v1.0.0 release.', file=sys.stderr)
|
|
|
|
|
|
def keygen(get_keyring=get_keyring):
|
|
"""Generate a public/private key pair."""
|
|
warn_signatures()
|
|
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)
|
|
print("Created Ed25519 keypair with vk={}".format(vk))
|
|
print("in {!r}".format(kr))
|
|
|
|
sk2 = kr.get_password('wheel', vk)
|
|
if sk2 != sk:
|
|
raise WheelError("Keyring is broken. Could not retrieve secret key.")
|
|
|
|
print("Trusting {} to sign and verify all packages.".format(vk))
|
|
wk.add_signer('+', vk)
|
|
wk.trust('+', vk)
|
|
wk.save()
|
|
|
|
|
|
def sign(wheelfile, replace=False, get_keyring=get_keyring):
|
|
"""Sign a wheel"""
|
|
warn_signatures()
|
|
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]
|
|
print("Signing {} with {}".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.
|
|
"""
|
|
warn_signatures()
|
|
vzf = VerifyingZipFile(wheelfile, "a")
|
|
info = vzf.infolist()
|
|
if not (len(info) and info[-1].filename.endswith('/RECORD.jws')):
|
|
raise WheelError('The wheel is not signed (RECORD.jws not found at end of the 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.
|
|
"""
|
|
warn_signatures()
|
|
wf = WheelFile(wheelfile)
|
|
sig_name = wf.distinfo_name + '/RECORD.jws'
|
|
try:
|
|
sig = json.loads(native(wf.zipfile.open(sig_name).read()))
|
|
except KeyError:
|
|
raise WheelError('The wheel is not signed (RECORD.jws not found at end of the archive).')
|
|
|
|
verified = signatures.verify(sig)
|
|
print("Signatures are internally consistent.", file=sys.stderr)
|
|
print(json.dumps(verified, indent=2))
|
|
|
|
|
|
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)
|
|
print("Unpacking to: %s" % (destination), file=sys.stderr)
|
|
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:
|
|
print("Installing:")
|
|
|
|
if dry_run:
|
|
return
|
|
|
|
for wf in to_install:
|
|
if list_files:
|
|
print(" {}".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 = 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:
|
|
print("{}... ".format(installer))
|
|
sys.stdout.flush()
|
|
conv(installer, dest_dir)
|
|
if verbose:
|
|
print("OK")
|
|
|
|
|
|
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__
|
|
print("wheel %s" % __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:
|
|
print(e, file=sys.stderr)
|
|
|
|
return 1
|