blob: f996713bf4974df8189a1a01846d04d3c542b1e0 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import ConfigParser
import hashlib
import os
import shutil
import sys
import textwrap
from glucose import util
from glucose import zipfix
def grab_wheel(src, dst, build_num=0):
"""Move a single wheel file and fix its name.
Args:
src (str): directory containing one wheel file.
dst (str): directory where to put the renamed wheel file.
Returns:
wheel_filename (str): path to the generated file, in its final location.
"""
items = os.listdir(src)
assert len(items) == 1, (
'Wrong number of files (%r) in wheel directory: %r' % (items, src))
wheelfile = items[0]
wheel_info = util.WHEEL_FILE_RE.match(wheelfile)
assert wheel_info is not None, (
'Not a wheel file? %r' % os.path.join(src, wheelfile))
src_path = os.path.join(src, wheelfile)
zipfix.reset_all_timestamps_in_zip(src_path)
plat_tag = ''
if not wheelfile.endswith('none-any.whl'):
plat_tag = util.platform_tag()
with open(src_path, 'rb') as f:
digest = hashlib.sha1(f.read())
wheel_sha = digest.hexdigest()
dest_path = os.path.join(dst, '{}-{}_{}{}{}'.format(
wheel_info.group('namever'),
build_num,
wheel_sha,
plat_tag,
wheel_info.group(4),
))
shutil.copyfile(src_path, dest_path)
return dest_path
def get_packing_list(source_dirs):
"""Consolidate the list of things to pack.
Args:
source_dirs (list of str): local source packages locations.
"""
packing_list = []
for source_dir in source_dirs:
location = util.path2fileurl(os.path.abspath(source_dir))
if os.path.isfile(os.path.join(source_dir, 'setup.py')):
package_type = 'standard'
elif (os.path.isfile(os.path.join(source_dir, 'setup.cfg')) and
os.path.isfile(os.path.join(source_dir, '__init__.py'))):
package_type = 'bare'
else:
package_type = 'unhandled'
if not os.path.exists(source_dir):
package_type = 'missing'
packing_list.append({'location': location, 'package_type': package_type})
return packing_list
def get_setup_py_content(cfg_path):
"""Generate a setup.py file based on a setup.cfg file.
The setup.cfg file is supposed to live within the package itself: the
name of the directory containing setup.cfg is treated as the package name.
Args:
cfg_path (str): path to a setup.cfg file.
Returns:
content (str): content of setup.py.
"""
parser = ConfigParser.ConfigParser()
parser.read(cfg_path)
if not parser.has_section('metadata'):
raise util.SetupError('Config file must have a [metadata] section: %s' %
cfg_path)
package_name = os.path.split(os.path.dirname(cfg_path))[-1]
setup_kwargs = {'name': package_name, 'packages': [package_name]}
section = 'metadata'
for option in ('version', 'author', 'author_email', 'description', 'url'):
if parser.has_option(section, option):
setup_kwargs[option] = parser.get(section, option)
if parser.has_option('metadata', 'package_data'):
setup_kwargs['package_data'] = parser.get('metadata',
'package_data').splitlines()
setup_py_template = textwrap.dedent("""
from setuptools import setup
setup(
{keywords}
)
""")
keywords = ['%s=%r' % (k, v) for k, v in sorted(setup_kwargs.iteritems())]
return setup_py_template.format(keywords=',\n '.join(keywords))
def pack_local_package(venv, path, wheelhouse, build_num=0, build_options=()):
"""Create a wheel file from package source available locally.
Args:
path (str): directory containing the package to pack.
wheelhouse (str): directory where to place the generated wheel file.
build_num (int): rank of the build, only added to the filename.
build_options (list of str): values passed as --global-option to pip.
Returns:
wheel_path (str): path to the generated wheel file.
"""
print 'Packing %s' % path
with util.temporary_directory() as tempdir:
args = ['pip', 'wheel', '--no-index', '--no-deps', '--wheel-dir', tempdir]
for op in build_options:
args += ['--global-option', op]
args += [path]
venv.check_call(args)
wheel_path = grab_wheel(tempdir, wheelhouse, build_num)
return wheel_path
def pack_bare_package(venv, path, wheelhouse, build_num=0, build_options=(),
keep_directory=False):
"""Create a wheel file from an importable package containing a setup.cfg file.
Args:
path (str): directory containing the package to pack.
wheelhouse (str): directory where to place the generated wheel file.
build_num (int): rank of the build, only added to the filename.
build_options (list of str): values passed as --global-option to pip.
Returns:
wheel_path (str): path to the generated wheel file.
"""
path = os.path.abspath(path)
print 'Packing %s' % path
# Generate a "standard" package with a setup.py file, and call
# pack_local_package()
cfg_file = os.path.join(path, 'setup.cfg')
setup_py_content = get_setup_py_content(cfg_file)
package_name = os.path.split(path)[-1]
with util.temporary_directory(prefix="glyco-pack-bare-",
keep_directory=keep_directory) as tempdir:
if keep_directory:
print ' Using temporary dir: %s' % tempdir
with open(os.path.join(tempdir, 'setup.py'), 'w') as f:
f.write(setup_py_content)
shutil.copytree(path, os.path.join(tempdir, package_name), symlinks=True)
wheel_path = pack_local_package(venv, tempdir,
wheelhouse,
build_num=build_num,
build_options=build_options)
return wheel_path
def pack(args):
"""Pack wheel files.
Returns 0 or None if all went well, a non-zero int otherwise.
"""
if not args.packages:
print 'No packages have been provided on the command-line, doing nothing.'
return 0
packing_list = get_packing_list(args.packages)
unhandled = [util.fileurl2path(d['location'])
for d in packing_list
if d['package_type'] in ('unhandled', 'missing')]
if unhandled:
print >> sys.stderr, ('These directories do not seem to be packable '
'because they do not exist or\n'
'have neither setup.cfg or setup.py inside them:')
print >> sys.stderr, ('\n'.join(unhandled))
return 1
if not os.path.isdir(args.output_dir):
os.mkdir(args.output_dir)
wheel_paths = []
with util.Virtualenv(
prefix="glyco-pack-",
keep_directory=args.keep_tmp_directories) as venv:
for element in packing_list:
if element['location'].startswith('file://'):
pathname = util.fileurl2path(element['location'])
# Standard Python source package: contains a setup.py
if element['package_type'] == 'standard':
wheel_path = pack_local_package(venv, pathname, args.output_dir)
# The Glyco special case: importable package with a setup.cfg file.
elif element['package_type'] == 'bare':
wheel_path = pack_bare_package(
venv,
pathname,
args.output_dir,
keep_directory=args.keep_tmp_directories)
wheel_paths.append(wheel_path)
if args.verbose:
print '\nGenerated %d packages:' % len(wheel_paths)
for wheel_path in wheel_paths:
print wheel_path
def add_subparser(subparsers):
"""Add the 'pack' command.
Also add the 'gen' command as a synonym.
Args:
subparsers: output of argparse.ArgumentParser.add_subparsers()
"""
pack_parser = subparsers.add_parser('pack',
help='Compile wheel files from Python '
'packages (synonym of gen).')
pack_parser.set_defaults(command=pack)
# Add synonym, just for the pun
gen_parser = subparsers.add_parser('gen',
help='Compile wheel files from Python '
'packages (synonym of pack).')
gen_parser.set_defaults(command=pack)
for parser in (pack_parser, gen_parser):
parser.add_argument('--output-dir', '-o',
help='Directory where to write generated wheel files. '
'Default: %(default)s',
default='glyco_wheels')
parser.add_argument('packages', metavar='PKG_PATH', nargs='*',
help='Local directory containing Python packages'
' to process. These directories are supposed to contain'
' a setup.py or a setup.cfg file.')