Include wsgi_scripts in generated wheels

Downstream consumers, such as OpenStack Ansible, generate wheels for all
packages (including services). More and more services are moving to use
the wsgi_scripts entry-points provided and handled by pbr. Unfortunately,
these scripts are not generated during wheel creation unless we force
them to be generated because Setuptools and Distutils will only generate
console_scripts entry-points.

This also fixes the C extension on Python 3 because it was previously
broken.

Change-Id: Icecc8474028436e8b2fb752d576204d9439fb0e7
Closes-bug: #1542383
This commit is contained in:
Ian Cordasco 2016-02-12 17:12:16 -06:00
parent 64699d79be
commit 139110c89b
3 changed files with 161 additions and 17 deletions

View File

@ -308,22 +308,39 @@ ENTRY_POINTS_MAP = {
}
def generate_script(group, entry_point, header, template):
"""Generate the script based on the template.
:param str group:
The entry-point group name, e.g., "console_scripts".
:param str header:
The first line of the script, e.g., "!#/usr/bin/env python".
:param str template:
The script template.
:returns:
The templated script content
:rtype:
str
"""
if not entry_point.attrs or len(entry_point.attrs) > 2:
raise ValueError("Script targets must be of the form "
"'func' or 'Class.class_method'.")
script_text = template % dict(
group=group,
module_name=entry_point.module_name,
import_target=entry_point.attrs[0],
invoke_target='.'.join(entry_point.attrs),
)
return header + script_text
def override_get_script_args(
dist, executable=os.path.normpath(sys.executable), is_wininst=False):
"""Override entrypoints console_script."""
header = easy_install.get_script_header("", executable, is_wininst)
for group, template in ENTRY_POINTS_MAP.items():
for name, ep in dist.get_entry_map(group).items():
if not ep.attrs or len(ep.attrs) > 2:
raise ValueError("Script targets must be of the form "
"'func' or 'Class.class_method'.")
script_text = template % dict(
group=group,
module_name=ep.module_name,
import_target=ep.attrs[0],
invoke_target='.'.join(ep.attrs),
)
yield (name, header + script_text)
yield (name, generate_script(group, ep, header, template))
class LocalDevelop(develop.develop):
@ -342,6 +359,14 @@ class LocalInstallScripts(install_scripts.install_scripts):
"""Intercepts console scripts entry_points."""
command_name = 'install_scripts'
def _make_wsgi_scripts_only(self, dist, executable, is_wininst):
header = easy_install.get_script_header("", executable, is_wininst)
wsgi_script_template = ENTRY_POINTS_MAP['wsgi_scripts']
for name, ep in dist.get_entry_map('wsgi_scripts').items():
content = generate_script(
'wsgi_scripts', ep, header, wsgi_script_template)
self.write_script(name, content)
def run(self):
import distutils.command.install_scripts
@ -351,9 +376,6 @@ class LocalInstallScripts(install_scripts.install_scripts):
distutils.command.install_scripts.install_scripts.run(self)
else:
self.outfiles = []
if self.no_ep:
# don't install entry point scripts into .egg file!
return
ei_cmd = self.get_finalized_command("egg_info")
dist = pkg_resources.Distribution(
@ -368,6 +390,19 @@ class LocalInstallScripts(install_scripts.install_scripts):
self.get_finalized_command("bdist_wininst"), '_is_running', False
)
if 'bdist_wheel' in self.distribution.have_run:
# We're building a wheel which has no way of generating mod_wsgi
# scripts for us. Let's build them.
# NOTE(sigmavirus24): This needs to happen here because, as the
# comment below indicates, no_ep is True when building a wheel.
self._make_wsgi_scripts_only(dist, executable, is_wininst)
if self.no_ep:
# no_ep is True if we're installing into an .egg file or building
# a .whl file, in those cases, we do not want to build all of the
# entry-points listed for this package.
return
if os.name != 'nt':
get_script_args = override_get_script_args
else:

View File

@ -38,8 +38,10 @@
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
import imp
import os
import re
import sysconfig
import tempfile
import textwrap
@ -47,8 +49,10 @@ import fixtures
import mock
import pkg_resources
import six
import testtools
from testtools import matchers
import virtualenv
import wheel.install
from pbr import git
from pbr import packaging
@ -318,6 +322,94 @@ class TestPackagingInGitRepoWithoutCommit(base.BaseTestCase):
self.assertEqual(body, 'CHANGES\n=======\n\n')
class TestPackagingWheels(base.BaseTestCase):
def setUp(self):
super(TestPackagingWheels, self).setUp()
self.useFixture(TestRepo(self.package_dir))
# Build the wheel
self.run_setup('bdist_wheel', allow_fail=False)
# Slowly construct the path to the generated whl
dist_dir = os.path.join(self.package_dir, 'dist')
relative_wheel_filename = os.listdir(dist_dir)[0]
absolute_wheel_filename = os.path.join(
dist_dir, relative_wheel_filename)
wheel_file = wheel.install.WheelFile(absolute_wheel_filename)
wheel_name = wheel_file.parsed_filename.group('namever')
# Create a directory path to unpack the wheel to
self.extracted_wheel_dir = os.path.join(dist_dir, wheel_name)
# Extract the wheel contents to the directory we just created
wheel_file.zipfile.extractall(self.extracted_wheel_dir)
wheel_file.zipfile.close()
def test_data_directory_has_wsgi_scripts(self):
# Build the path to the scripts directory
scripts_dir = os.path.join(
self.extracted_wheel_dir, 'pbr_testpackage-0.0.data/scripts')
self.assertTrue(os.path.exists(scripts_dir))
scripts = os.listdir(scripts_dir)
self.assertIn('pbr_test_wsgi', scripts)
self.assertIn('pbr_test_wsgi_with_class', scripts)
self.assertNotIn('pbr_test_cmd', scripts)
self.assertNotIn('pbr_test_cmd_with_class', scripts)
def test_generates_c_extensions(self):
built_package_dir = os.path.join(
self.extracted_wheel_dir, 'pbr_testpackage')
static_object_filename = 'testext.so'
soabi = get_soabi()
if soabi:
static_object_filename = 'testext.{0}.so'.format(soabi)
static_object_path = os.path.join(
built_package_dir, static_object_filename)
self.assertTrue(os.path.exists(built_package_dir))
self.assertTrue(os.path.exists(static_object_path))
class TestPackagingHelpers(testtools.TestCase):
def test_generate_script(self):
group = 'console_scripts'
entry_point = pkg_resources.EntryPoint(
name='test-ep',
module_name='pbr.packaging',
attrs=('LocalInstallScripts',))
header = '#!/usr/bin/env fake-header\n'
template = ('%(group)s %(module_name)s %(import_target)s '
'%(invoke_target)s')
generated_script = packaging.generate_script(
group, entry_point, header, template)
expected_script = (
'#!/usr/bin/env fake-header\nconsole_scripts pbr.packaging '
'LocalInstallScripts LocalInstallScripts'
)
self.assertEqual(expected_script, generated_script)
def test_generate_script_validates_expectations(self):
group = 'console_scripts'
entry_point = pkg_resources.EntryPoint(
name='test-ep',
module_name='pbr.packaging')
header = '#!/usr/bin/env fake-header\n'
template = ('%(group)s %(module_name)s %(import_target)s '
'%(invoke_target)s')
self.assertRaises(
ValueError, packaging.generate_script, group, entry_point, header,
template)
entry_point = pkg_resources.EntryPoint(
name='test-ep',
module_name='pbr.packaging',
attrs=('attr1', 'attr2', 'attr3'))
self.assertRaises(
ValueError, packaging.generate_script, group, entry_point, header,
template)
class TestPackagingInPlainDirectory(base.BaseTestCase):
def setUp(self):
@ -618,3 +710,19 @@ class TestRequirementParsing(base.BaseTestCase):
pkg_resources.split_sections(requires))
self.assertEqual(expected_requirements, generated_requirements)
def get_soabi():
try:
return sysconfig.get_config_var('SOABI')
except IOError:
pass
if 'pypy' in sysconfig.get_scheme_names():
# NOTE(sigmavirus24): PyPy only added support for the SOABI config var
# to sysconfig in 2015. That was well after 2.2.1 was published in the
# Ubuntu 14.04 archive.
for suffix, _, _ in imp.get_suffixes():
if suffix.startswith('.pypy') and suffix.endswith('.so'):
return suffix.split('.')[1]
return None

View File

@ -8,10 +8,11 @@ static PyMethodDef TestextMethods[] = {
#if PY_MAJOR_VERSION >=3
static struct PyModuleDef testextmodule = {
PyModuleDef_HEAD_INIT,
"testext",
-1,
TestextMethods
PyModuleDef_HEAD_INIT, /* This should correspond to a PyModuleDef_Base type */
"testext", /* This is the module name */
"Test extension module", /* This is the module docstring */
-1, /* This defines the size of the module and says everything is global */
TestextMethods /* This is the method definition */
};
PyObject*