__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. """
  2. Wheel command-line utility.
  3. """
  4. import os
  5. import hashlib
  6. import sys
  7. import json
  8. import wheel.paths
  9. from glob import iglob
  10. from .. import signatures
  11. from ..util import (urlsafe_b64decode, urlsafe_b64encode, native, binary,
  12. matches_requirement)
  13. from ..install import WheelFile
  14. def require_pkgresources(name):
  15. try:
  16. import pkg_resources
  17. except ImportError:
  18. raise RuntimeError("'{0}' needs pkg_resources (part of setuptools).".format(name))
  19. import argparse
  20. class WheelError(Exception): pass
  21. # For testability
  22. def get_keyring():
  23. try:
  24. from ..signatures import keys
  25. import keyring
  26. assert keyring.get_keyring().priority
  27. except (ImportError, AssertionError):
  28. raise WheelError("Install wheel[signatures] (requires keyring, keyrings.alt, pyxdg) for signatures.")
  29. return keys.WheelKeys, keyring
  30. def keygen(get_keyring=get_keyring):
  31. """Generate a public/private key pair."""
  32. WheelKeys, keyring = get_keyring()
  33. ed25519ll = signatures.get_ed25519ll()
  34. wk = WheelKeys().load()
  35. keypair = ed25519ll.crypto_sign_keypair()
  36. vk = native(urlsafe_b64encode(keypair.vk))
  37. sk = native(urlsafe_b64encode(keypair.sk))
  38. kr = keyring.get_keyring()
  39. kr.set_password("wheel", vk, sk)
  40. sys.stdout.write("Created Ed25519 keypair with vk={0}\n".format(vk))
  41. sys.stdout.write("in {0!r}\n".format(kr))
  42. sk2 = kr.get_password('wheel', vk)
  43. if sk2 != sk:
  44. raise WheelError("Keyring is broken. Could not retrieve secret key.")
  45. sys.stdout.write("Trusting {0} to sign and verify all packages.\n".format(vk))
  46. wk.add_signer('+', vk)
  47. wk.trust('+', vk)
  48. wk.save()
  49. def sign(wheelfile, replace=False, get_keyring=get_keyring):
  50. """Sign a wheel"""
  51. WheelKeys, keyring = get_keyring()
  52. ed25519ll = signatures.get_ed25519ll()
  53. wf = WheelFile(wheelfile, append=True)
  54. wk = WheelKeys().load()
  55. name = wf.parsed_filename.group('name')
  56. sign_with = wk.signers(name)[0]
  57. sys.stdout.write("Signing {0} with {1}\n".format(name, sign_with[1]))
  58. vk = sign_with[1]
  59. kr = keyring.get_keyring()
  60. sk = kr.get_password('wheel', vk)
  61. keypair = ed25519ll.Keypair(urlsafe_b64decode(binary(vk)),
  62. urlsafe_b64decode(binary(sk)))
  63. record_name = wf.distinfo_name + '/RECORD'
  64. sig_name = wf.distinfo_name + '/RECORD.jws'
  65. if sig_name in wf.zipfile.namelist():
  66. raise WheelError("Wheel is already signed.")
  67. record_data = wf.zipfile.read(record_name)
  68. payload = {"hash":"sha256=" + native(urlsafe_b64encode(hashlib.sha256(record_data).digest()))}
  69. sig = signatures.sign(payload, keypair)
  70. wf.zipfile.writestr(sig_name, json.dumps(sig, sort_keys=True))
  71. wf.zipfile.close()
  72. def unsign(wheelfile):
  73. """
  74. Remove RECORD.jws from a wheel by truncating the zip file.
  75. RECORD.jws must be at the end of the archive. The zip file must be an
  76. ordinary archive, with the compressed files and the directory in the same
  77. order, and without any non-zip content after the truncation point.
  78. """
  79. import wheel.install
  80. vzf = wheel.install.VerifyingZipFile(wheelfile, "a")
  81. info = vzf.infolist()
  82. if not (len(info) and info[-1].filename.endswith('/RECORD.jws')):
  83. raise WheelError("RECORD.jws not found at end of archive.")
  84. vzf.pop()
  85. vzf.close()
  86. def verify(wheelfile):
  87. """Verify a wheel.
  88. The signature will be verified for internal consistency ONLY and printed.
  89. Wheel's own unpack/install commands verify the manifest against the
  90. signature and file contents.
  91. """
  92. wf = WheelFile(wheelfile)
  93. sig_name = wf.distinfo_name + '/RECORD.jws'
  94. sig = json.loads(native(wf.zipfile.open(sig_name).read()))
  95. verified = signatures.verify(sig)
  96. sys.stderr.write("Signatures are internally consistent.\n")
  97. sys.stdout.write(json.dumps(verified, indent=2))
  98. sys.stdout.write('\n')
  99. def unpack(wheelfile, dest='.'):
  100. """Unpack a wheel.
  101. Wheel content will be unpacked to {dest}/{name}-{ver}, where {name}
  102. is the package name and {ver} its version.
  103. :param wheelfile: The path to the wheel.
  104. :param dest: Destination directory (default to current directory).
  105. """
  106. wf = WheelFile(wheelfile)
  107. namever = wf.parsed_filename.group('namever')
  108. destination = os.path.join(dest, namever)
  109. sys.stderr.write("Unpacking to: %s\n" % (destination))
  110. wf.zipfile.extractall(destination)
  111. wf.zipfile.close()
  112. def install(requirements, requirements_file=None,
  113. wheel_dirs=None, force=False, list_files=False,
  114. dry_run=False):
  115. """Install wheels.
  116. :param requirements: A list of requirements or wheel files to install.
  117. :param requirements_file: A file containing requirements to install.
  118. :param wheel_dirs: A list of directories to search for wheels.
  119. :param force: Install a wheel file even if it is not compatible.
  120. :param list_files: Only list the files to install, don't install them.
  121. :param dry_run: Do everything but the actual install.
  122. """
  123. # If no wheel directories specified, use the WHEELPATH environment
  124. # variable, or the current directory if that is not set.
  125. if not wheel_dirs:
  126. wheelpath = os.getenv("WHEELPATH")
  127. if wheelpath:
  128. wheel_dirs = wheelpath.split(os.pathsep)
  129. else:
  130. wheel_dirs = [ os.path.curdir ]
  131. # Get a list of all valid wheels in wheel_dirs
  132. all_wheels = []
  133. for d in wheel_dirs:
  134. for w in os.listdir(d):
  135. if w.endswith('.whl'):
  136. wf = WheelFile(os.path.join(d, w))
  137. if wf.compatible:
  138. all_wheels.append(wf)
  139. # If there is a requirements file, add it to the list of requirements
  140. if requirements_file:
  141. # If the file doesn't exist, search for it in wheel_dirs
  142. # This allows standard requirements files to be stored with the
  143. # wheels.
  144. if not os.path.exists(requirements_file):
  145. for d in wheel_dirs:
  146. name = os.path.join(d, requirements_file)
  147. if os.path.exists(name):
  148. requirements_file = name
  149. break
  150. with open(requirements_file) as fd:
  151. requirements.extend(fd)
  152. to_install = []
  153. for req in requirements:
  154. if req.endswith('.whl'):
  155. # Explicitly specified wheel filename
  156. if os.path.exists(req):
  157. wf = WheelFile(req)
  158. if wf.compatible or force:
  159. to_install.append(wf)
  160. else:
  161. msg = ("{0} is not compatible with this Python. "
  162. "--force to install anyway.".format(req))
  163. raise WheelError(msg)
  164. else:
  165. # We could search on wheel_dirs, but it's probably OK to
  166. # assume the user has made an error.
  167. raise WheelError("No such wheel file: {}".format(req))
  168. continue
  169. # We have a requirement spec
  170. # If we don't have pkg_resources, this will raise an exception
  171. matches = matches_requirement(req, all_wheels)
  172. if not matches:
  173. raise WheelError("No match for requirement {}".format(req))
  174. to_install.append(max(matches))
  175. # We now have a list of wheels to install
  176. if list_files:
  177. sys.stdout.write("Installing:\n")
  178. if dry_run:
  179. return
  180. for wf in to_install:
  181. if list_files:
  182. sys.stdout.write(" {0}\n".format(wf.filename))
  183. continue
  184. wf.install(force=force)
  185. wf.zipfile.close()
  186. def install_scripts(distributions):
  187. """
  188. Regenerate the entry_points console_scripts for the named distribution.
  189. """
  190. try:
  191. from setuptools.command import easy_install
  192. import pkg_resources
  193. except ImportError:
  194. raise RuntimeError("'wheel install_scripts' needs setuptools.")
  195. for dist in distributions:
  196. pkg_resources_dist = pkg_resources.get_distribution(dist)
  197. install = wheel.paths.get_install_command(dist)
  198. command = easy_install.easy_install(install.distribution)
  199. command.args = ['wheel'] # dummy argument
  200. command.finalize_options()
  201. command.install_egg_scripts(pkg_resources_dist)
  202. def convert(installers, dest_dir, verbose):
  203. require_pkgresources('wheel convert')
  204. # Only support wheel convert if pkg_resources is present
  205. from ..wininst2wheel import bdist_wininst2wheel
  206. from ..egg2wheel import egg2wheel
  207. for pat in installers:
  208. for installer in iglob(pat):
  209. if os.path.splitext(installer)[1] == '.egg':
  210. conv = egg2wheel
  211. else:
  212. conv = bdist_wininst2wheel
  213. if verbose:
  214. sys.stdout.write("{0}... ".format(installer))
  215. sys.stdout.flush()
  216. conv(installer, dest_dir)
  217. if verbose:
  218. sys.stdout.write("OK\n")
  219. def parser():
  220. p = argparse.ArgumentParser()
  221. s = p.add_subparsers(help="commands")
  222. def keygen_f(args):
  223. keygen()
  224. keygen_parser = s.add_parser('keygen', help='Generate signing key')
  225. keygen_parser.set_defaults(func=keygen_f)
  226. def sign_f(args):
  227. sign(args.wheelfile)
  228. sign_parser = s.add_parser('sign', help='Sign wheel')
  229. sign_parser.add_argument('wheelfile', help='Wheel file')
  230. sign_parser.set_defaults(func=sign_f)
  231. def unsign_f(args):
  232. unsign(args.wheelfile)
  233. unsign_parser = s.add_parser('unsign', help=unsign.__doc__)
  234. unsign_parser.add_argument('wheelfile', help='Wheel file')
  235. unsign_parser.set_defaults(func=unsign_f)
  236. def verify_f(args):
  237. verify(args.wheelfile)
  238. verify_parser = s.add_parser('verify', help=verify.__doc__)
  239. verify_parser.add_argument('wheelfile', help='Wheel file')
  240. verify_parser.set_defaults(func=verify_f)
  241. def unpack_f(args):
  242. unpack(args.wheelfile, args.dest)
  243. unpack_parser = s.add_parser('unpack', help='Unpack wheel')
  244. unpack_parser.add_argument('--dest', '-d', help='Destination directory',
  245. default='.')
  246. unpack_parser.add_argument('wheelfile', help='Wheel file')
  247. unpack_parser.set_defaults(func=unpack_f)
  248. def install_f(args):
  249. install(args.requirements, args.requirements_file,
  250. args.wheel_dirs, args.force, args.list_files)
  251. install_parser = s.add_parser('install', help='Install wheels')
  252. install_parser.add_argument('requirements', nargs='*',
  253. help='Requirements to install.')
  254. install_parser.add_argument('--force', default=False,
  255. action='store_true',
  256. help='Install incompatible wheel files.')
  257. install_parser.add_argument('--wheel-dir', '-d', action='append',
  258. dest='wheel_dirs',
  259. help='Directories containing wheels.')
  260. install_parser.add_argument('--requirements-file', '-r',
  261. help="A file containing requirements to "
  262. "install.")
  263. install_parser.add_argument('--list', '-l', default=False,
  264. dest='list_files',
  265. action='store_true',
  266. help="List wheels which would be installed, "
  267. "but don't actually install anything.")
  268. install_parser.set_defaults(func=install_f)
  269. def install_scripts_f(args):
  270. install_scripts(args.distributions)
  271. install_scripts_parser = s.add_parser('install-scripts', help='Install console_scripts')
  272. install_scripts_parser.add_argument('distributions', nargs='*',
  273. help='Regenerate console_scripts for these distributions')
  274. install_scripts_parser.set_defaults(func=install_scripts_f)
  275. def convert_f(args):
  276. convert(args.installers, args.dest_dir, args.verbose)
  277. convert_parser = s.add_parser('convert', help='Convert egg or wininst to wheel')
  278. convert_parser.add_argument('installers', nargs='*', help='Installers to convert')
  279. convert_parser.add_argument('--dest-dir', '-d', default=os.path.curdir,
  280. help="Directory to store wheels (default %(default)s)")
  281. convert_parser.add_argument('--verbose', '-v', action='store_true')
  282. convert_parser.set_defaults(func=convert_f)
  283. def version_f(args):
  284. from .. import __version__
  285. sys.stdout.write("wheel %s\n" % __version__)
  286. version_parser = s.add_parser('version', help='Print version and exit')
  287. version_parser.set_defaults(func=version_f)
  288. def help_f(args):
  289. p.print_help()
  290. help_parser = s.add_parser('help', help='Show this help')
  291. help_parser.set_defaults(func=help_f)
  292. return p
  293. def main():
  294. p = parser()
  295. args = p.parse_args()
  296. if not hasattr(args, 'func'):
  297. p.print_help()
  298. else:
  299. # XXX on Python 3.3 we get 'args has no func' rather than short help.
  300. try:
  301. args.func(args)
  302. return 0
  303. except WheelError as e:
  304. sys.stderr.write(e.message + "\n")
  305. return 1