summaryrefslogtreecommitdiff
path: root/wheel/tool/__init__.py
blob: 4c0187b54092fa8e1860da35b7ff8f37f95e88b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
"""
Wheel command-line utility.
"""

import os
import hashlib
import sys
import json

from glob import iglob
from .. import signatures
from ..util import (urlsafe_b64decode, urlsafe_b64encode, native, binary,
                    matches_requirement)
from ..install import WheelFile, VerifyingZipFile
from ..paths import get_install_command

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
        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 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))
    sys.stdout.write("in {0!r}\n".format(kr))

    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.
    """
    vzf = 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 = 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