Skip to content

Commit 6fbadf1

Browse files
authored
Add wheel with a C extension to test mounting (#229)
This recreates the source to the `tests/minimext*.whl` wheels with a couple of differences: * The C extension is placed in the minimext package rather than being a top level package. * A Python implementation of the C extension is provided to make it easier for pure Python programmers to understand / modify. This also adds a script `build_wheels.sh` that will install Python interpreters and build the minimext wheel for different Python versions with and without adding the `EXTENSIONS` metadata file. Related to #222
1 parent a16423f commit 6fbadf1

File tree

8 files changed

+304
-0
lines changed

8 files changed

+304
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ distlib-vcsid
1616
tests/passwords
1717
tests/keys
1818
tests/pypi-server-standalone.py
19+
tests/minimext/*.egg-info
20+
tests/minimext/.venv*
21+
tests/minimext/minimext/*.pyd
22+
tests/minimext/minimext/*.so
23+
tests/minimext/wheels
1924
dist
2025
htmlcov
2126
build

tests/minimext/LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
BSD-licensed.

tests/minimext/README.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Distribution with a simple C extension that calculates Fibonacci numbers.

tests/minimext/build_wheels.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/bash -e
2+
# Copyright (C) 2024 Stewart Miles
3+
# Licensed to the Python Software Foundation under a contributor agreement.
4+
# See LICENSE.txt and CONTRIBUTORS.txt.
5+
6+
readonly DEFAULT_PYTHON_VERSION="$(python --version |
7+
cut -d ' ' -f 2 |
8+
cut -d. -f 1,2)"
9+
readonly DEFAULT_PYTHON_VERSIONS="2.7 3.5 ${DEFAULT_PYTHON_VERSION}"
10+
11+
12+
help() {
13+
echo "\
14+
Builds Linux wheels for this package using a range of Python distributions.
15+
16+
This script requires a Ubuntu distribution and will leave the deadsnakes PPA
17+
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa
18+
and Python packages used by this script installed.
19+
20+
$(basename "$0") [-d] [-I] [-S] [-p versions] [-h]
21+
22+
-d: Enable dry run mode, simply display rather than execute commands.
23+
-S: Disable Python PPA installation.
24+
-I: Disable Python apt package installation
25+
-p: Space separated list of Python versions to install and build wheels for.
26+
This defaults to \"${DEFAULT_PYTHON_VERSIONS}\".
27+
-h: Display this help.
28+
"
29+
exit 1
30+
}
31+
32+
main() {
33+
readonly THIS_DIRECTORY="$(cd "$(dirname "${0}")"; pwd)"
34+
local dryrun=
35+
local install_python=1
36+
local install_python_ppa=1
37+
local selected_python_versions="${DEFAULT_PYTHON_VERSIONS}"
38+
39+
while getopts "dhISp:" OPTION; do
40+
# shellcheck disable=SC2209
41+
case "${OPTION}" in
42+
d) dryrun=echo
43+
;;
44+
I) install_python=0
45+
;;
46+
S) install_python_ppa=0
47+
;;
48+
p) selected_python_versions="${OPTARG}"
49+
;;
50+
h|*) help
51+
;;
52+
esac
53+
done
54+
55+
IFS=' ' read -r -a python_versions <<< "${selected_python_versions}"
56+
57+
if [[ $((install_python_ppa)) -eq 1 ]]; then
58+
set -x
59+
${dryrun} sudo add-apt-repository ppa:deadsnakes/ppa
60+
set +x
61+
fi
62+
63+
if [[ $((install_python)) -eq 1 ]]; then
64+
# shellcheck disable=SC2207
65+
readonly -a PYTHON_APT_PACKAGES=(
66+
$(for version in "${python_versions[@]}"; do
67+
echo "python${version}-dev";
68+
done))
69+
set -x
70+
${dryrun} sudo apt install "${PYTHON_APT_PACKAGES[@]}"
71+
set +x
72+
fi
73+
74+
local wheels_directory="${THIS_DIRECTORY}/wheels"
75+
mkdir -p "${wheels_directory}"
76+
77+
local venv_directory
78+
local versioned_python
79+
local version
80+
for version in "${python_versions[@]}"; do
81+
versioned_python="python${version}"
82+
venv_directory="${THIS_DIRECTORY}/.venv${version}"
83+
84+
# Try to bootstrap pip if it isn't found.
85+
if ! ${dryrun} "${versioned_python}" -c "import pip" 2> /dev/null; then
86+
# shellcheck disable=SC2155
87+
local temporary_directory="$(mktemp -d)"
88+
local get_pip="${temporary_directory}/get-pip-${version}.py"
89+
${dryrun} curl --output "${get_pip}" \
90+
"https://bootstrap.pypa.io/pip/${version}/get-pip.py"
91+
${dryrun} "${versioned_python}" "${get_pip}"
92+
rm -rf "${temporary_directory}"
93+
fi
94+
95+
# Install virtualenv as venv isn't available in all Python versions.
96+
${dryrun} "${versioned_python}" -m pip install virtualenv
97+
${dryrun} "${versioned_python}" -m virtualenv "${venv_directory}"
98+
(
99+
cd "${THIS_DIRECTORY}"
100+
${dryrun} source "${venv_directory}/bin/activate"
101+
# Upgrade pip and setuptools.
102+
${dryrun} pip install -U pip
103+
${dryrun} pip install -U setuptools
104+
# Build wheels to the wheels subdirectory.
105+
for embed_extension_metadata in 0 1; do
106+
set -x
107+
MINIMEXT_EMBED_EXTENSIONS_METADATA=${embed_extension_metadata} \
108+
${dryrun} pip wheel . -w "${wheels_directory}"
109+
set +x
110+
done
111+
)
112+
done
113+
114+
cp "${wheels_directory}"/*.whl "${THIS_DIRECTORY}/.."
115+
}
116+
117+
main "$@"

tests/minimext/calculate.c

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (C) 2024 Stewart Miles
2+
// Licensed to the Python Software Foundation under a contributor agreement.
3+
// See LICENSE.txt and CONTRIBUTORS.txt.
4+
5+
// Use the limited API to ensure ABI compatibility across all major Python
6+
// versions starting from 3.2
7+
// https://docs.python.org/3/c-api/stable.html#limited-c-api
8+
#if !defined(Py_LIMITED_API)
9+
#define Py_LIMITED_API 3
10+
#endif // !defined(Py_LIMITED_API)
11+
#define PY_SSIZE_T_CLEAN
12+
#include <Python.h>
13+
14+
// Name and doc string for this module.
15+
#define MODULE_NAME calculate
16+
#define MODULE_DOCS "Calculates Fibonacci numbers."
17+
18+
// Convert the argument into a string.
19+
#define _STRINGIFY(x) #x
20+
#define STRINGIFY(x) _STRINGIFY(x)
21+
22+
// Calculate a Fibonacci number at the specified index of the sequence.
23+
static PyObject *fib(PyObject *self, PyObject *args)
24+
{
25+
long int index;
26+
if (!PyArg_ParseTuple(args, "l", &index)) {
27+
PyErr_SetString(PyExc_ValueError, "An index must be specified.");
28+
}
29+
30+
long int current_value = 1;
31+
long int previous_value = 0;
32+
index--;
33+
for ( ; index > 0 ; --index) {
34+
long int next_value = current_value + previous_value;
35+
previous_value = current_value;
36+
current_value = next_value;
37+
}
38+
return PyLong_FromLong(current_value);
39+
}
40+
41+
// Exposes methods in this module.
42+
static PyMethodDef methods[] =
43+
{
44+
{
45+
"fib",
46+
fib,
47+
METH_VARARGS,
48+
PyDoc_STR("Calculate a Fibonacci number.\n"
49+
"\n"
50+
":param index: Index of the number in the Fibonacci sequence\n"
51+
" to calculate.\n"
52+
"\n"
53+
":returns: Fibonacci number at the specified index.\n"
54+
" For example an index of 7 will return 13\n"),
55+
},
56+
};
57+
58+
#if PY_MAJOR_VERSION >= 3
59+
// Defines the module.
60+
static struct PyModuleDef module =
61+
{
62+
PyModuleDef_HEAD_INIT,
63+
STRINGIFY(MODULE_NAME),
64+
PyDoc_STR(MODULE_DOCS),
65+
-1,
66+
methods,
67+
};
68+
#endif // PY_MAJOR_VERSION >= 3
69+
70+
// Expands to the init function name.
71+
#define _PYINIT_FUNCTION_NAME(prefix, name) prefix ## name
72+
#define PYINIT_FUNCTION_NAME(prefix, name) _PYINIT_FUNCTION_NAME(prefix, name)
73+
74+
// Initialize this module.
75+
#if PY_MAJOR_VERSION >= 3
76+
PyMODINIT_FUNC
77+
PYINIT_FUNCTION_NAME(PyInit_, MODULE_NAME)(void)
78+
{
79+
return PyModule_Create(&module);
80+
}
81+
#else
82+
PyMODINIT_FUNC
83+
PYINIT_FUNCTION_NAME(init, MODULE_NAME)(void)
84+
{
85+
// Ignore the returned module object.
86+
(void)Py_InitModule(STRINGIFY(MODULE_NAME), methods);
87+
}
88+
#endif // PY_MAJOR_VERSION >= 3

tests/minimext/minimext/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (C) 2024 Stewart Miles
2+
# Licensed to the Python Software Foundation under a contributor agreement.
3+
# See LICENSE.txt and CONTRIBUTORS.txt.
4+
5+
"""Python implementation of the calculate extension module."""
6+
7+
def fib(index):
8+
"""Calculate a Fibonacci number.
9+
10+
:param index: Index of the number in the Fibonacci sequence
11+
to calculate.
12+
13+
:returns: Fibonacci number at the specified index.
14+
For example an index of 7 will return 13
15+
"""
16+
current_value = 1
17+
previous_value = 0
18+
index -= 1
19+
while index > 0:
20+
next_value = current_value + previous_value
21+
previous_value = current_value
22+
current_value = next_value
23+
index -= 1
24+
return current_value
25+

tests/minimext/setup.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (C) 2024 Stewart Miles
2+
# Licensed to the Python Software Foundation under a contributor agreement.
3+
# See LICENSE.txt and CONTRIBUTORS.txt.
4+
import codecs
5+
import os
6+
import json
7+
from setuptools import Extension, setup
8+
from setuptools.command import egg_info
9+
import sys
10+
11+
12+
EMBED_EXTENSIONS_METADATA = (
13+
int(os.getenv('MINIMEXT_EMBED_EXTENSIONS_METADATA', '0')))
14+
15+
16+
class EggInfo(egg_info.egg_info):
17+
"""egg_info command that optionally writes extensions metadata.
18+
19+
distlib.wheel.Wheel attempts to read the list of extensions from the
20+
undocumented JSON EXTENSIONS metadata file.
21+
22+
This command will add the special file JSON EXTENSIONS metadata file to the
23+
*.dist-info directory in the wheel if the
24+
MINIMEXT_EMBED_EXTENSIONS_METADATA environment variable is set to 1.
25+
"""
26+
27+
def run(self):
28+
egg_info.egg_info.run(self)
29+
if EMBED_EXTENSIONS_METADATA:
30+
build_ext = self.get_finalized_command('build_ext')
31+
extensions_dict = {
32+
ext_module.name: build_ext.get_ext_filename(ext_module.name)
33+
for ext_module in self.distribution.ext_modules
34+
}
35+
with open(os.path.join(self.egg_info, 'EXTENSIONS'), 'wb') as (
36+
extensions_file):
37+
json.dump(extensions_dict,
38+
codecs.getwriter('utf-8')(extensions_file),
39+
indent=2)
40+
41+
42+
setup(
43+
name='minimext' + ('_metadata' if EMBED_EXTENSIONS_METADATA else ''),
44+
version='0.1',
45+
description='Calculates Fibonacci numbers.',
46+
long_description=(
47+
'Distribution that provides calculate.fib() and calculate_py.fib() '
48+
'which calculate Fibonacci numbers. minimext.calculate is implemented '
49+
'as a C extension to test distlib.wheel.Wheel.mount().'),
50+
packages=['minimext'],
51+
ext_modules=[
52+
Extension(name='minimext.calculate',
53+
sources=['calculate.c'],
54+
py_limited_api=True,
55+
define_macros=[
56+
('Py_LIMITED_API', str(sys.version_info.major)),
57+
]),
58+
],
59+
# The extension uses the limited API so tag the wheel as compatible with
60+
# Python 3.2 and later.
61+
#
62+
# Unfortunately the py_limited_api argument to Extension does not mark the
63+
# wheel as supporting the limited API, so set the see compatibility
64+
# manually.
65+
options={'bdist_wheel': {'py_limited_api': 'cp32'}},
66+
cmdclass={'egg_info': EggInfo},
67+
)

0 commit comments

Comments
 (0)