diff --git a/tests/integration/nonpy_rpath/README.md b/tests/integration/nonpy_rpath/README.md new file mode 100644 index 00000000..e01b25e4 --- /dev/null +++ b/tests/integration/nonpy_rpath/README.md @@ -0,0 +1,9 @@ +# Python 3 extension with non-Python library dependency + +This example was inspired from https://gist.github.com/physacco/2e1b52415f3a964ad2a542a99bebed8f + +This test extension builds two libraries: `_nonpy_rpath.*.so` and `lib_cryptexample.*.so`, where the `*` is a string composed of Python ABI versions and platform tags. + +The extension `lib_cryptexample.*.so` should be repaired by auditwheel because it is a needed library, even though it is not a Python extension. + +[Issue #136](https://github.com/pypa/auditwheel/issues/136) documents the underlying problem that this test case is designed to solve. diff --git a/tests/integration/nonpy_rpath/extensions/testcrypt.cpp b/tests/integration/nonpy_rpath/extensions/testcrypt.cpp new file mode 100644 index 00000000..5a3c1e39 --- /dev/null +++ b/tests/integration/nonpy_rpath/extensions/testcrypt.cpp @@ -0,0 +1,6 @@ +#include "testcrypt.h" +#include + +std::string crypt_something() { + return std::string(crypt("will error out", NULL)); +} diff --git a/tests/integration/nonpy_rpath/extensions/testcrypt.h b/tests/integration/nonpy_rpath/extensions/testcrypt.h new file mode 100644 index 00000000..1fff29d2 --- /dev/null +++ b/tests/integration/nonpy_rpath/extensions/testcrypt.h @@ -0,0 +1,4 @@ +#pragma once +#include + +std::string crypt_something(void); diff --git a/tests/integration/nonpy_rpath/nonpy_rpath.cpp b/tests/integration/nonpy_rpath/nonpy_rpath.cpp new file mode 100644 index 00000000..49bcbf42 --- /dev/null +++ b/tests/integration/nonpy_rpath/nonpy_rpath.cpp @@ -0,0 +1,25 @@ +#define PY_SSIZE_T_CLEAN +#include +#include "extensions/testcrypt.h" + +// Module method definitions +static PyObject* crypt_something(PyObject *self, PyObject *args) { + return PyUnicode_FromString(crypt_something().c_str()); +} + +/* Module initialization */ +PyMODINIT_FUNC PyInit__nonpy_rpath(void) +{ + static PyMethodDef module_methods[] = { + {"crypt_something", (PyCFunction)crypt_something, METH_NOARGS, "crypt_something."}, + {NULL} /* Sentinel */ + }; + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_nonpy_rpath", + "_nonpy_rpath module", + -1, + module_methods, + }; + return PyModule_Create(&moduledef); +} diff --git a/tests/integration/nonpy_rpath/nonpy_rpath/__init__.py b/tests/integration/nonpy_rpath/nonpy_rpath/__init__.py new file mode 100644 index 00000000..1779ebb1 --- /dev/null +++ b/tests/integration/nonpy_rpath/nonpy_rpath/__init__.py @@ -0,0 +1,3 @@ +from ._nonpy_rpath import crypt_something + +__all__ = ["crypt_something"] diff --git a/tests/integration/nonpy_rpath/setup.py b/tests/integration/nonpy_rpath/setup.py new file mode 100644 index 00000000..e4f35458 --- /dev/null +++ b/tests/integration/nonpy_rpath/setup.py @@ -0,0 +1,98 @@ +import setuptools.command.build_ext +from setuptools import setup, find_packages, Distribution +from setuptools.extension import Extension, Library +import os + +# despite its name, setuptools.command.build_ext.link_shared_object won't +# link a shared object on Linux, but a static library and patches distutils +# for this ... We're patching this back now. + + +def always_link_shared_object( + self, + objects, + output_libname, + output_dir=None, + libraries=None, + library_dirs=None, + runtime_library_dirs=None, + export_symbols=None, + debug=0, + extra_preargs=None, + extra_postargs=None, + build_temp=None, + target_lang=None, +): + self.link( + self.SHARED_LIBRARY, + objects, + output_libname, + output_dir, + libraries, + library_dirs, + runtime_library_dirs, + export_symbols, + debug, + extra_preargs, + extra_postargs, + build_temp, + target_lang, + ) + + +setuptools.command.build_ext.libtype = "shared" +setuptools.command.build_ext.link_shared_object = always_link_shared_object + +libtype = setuptools.command.build_ext.libtype +build_ext_cmd = Distribution().get_command_obj("build_ext") +build_ext_cmd.initialize_options() +build_ext_cmd.setup_shlib_compiler() + + +def libname(name): + """ gets 'name' and returns something like libname.cpython-37m-darwin.so""" + filename = build_ext_cmd.get_ext_filename(name) + fn, ext = os.path.splitext(filename) + return build_ext_cmd.shlib_compiler.library_filename(fn, libtype) + + +pkg_name = "nonpy_rpath" +crypt_name = "_cryptexample" +crypt_soname = libname(crypt_name) + +build_cmd = Distribution().get_command_obj("build") +build_cmd.finalize_options() +build_platlib = build_cmd.build_platlib + + +def link_args(soname=None): + args = [] + if soname: + args += ["-Wl,-soname," + soname] + loader_path = "$ORIGIN" + args += ["-Wl,-rpath," + loader_path] + return args + + +nonpy_rpath_module = Extension( + pkg_name + "._nonpy_rpath", + language="c++", + sources=["nonpy_rpath.cpp"], + extra_link_args=link_args(), + extra_objects=[build_platlib + "/nonpy_rpath/" + crypt_soname], +) +crypt_example = Library( + pkg_name + "." + crypt_name, + language="c++", + extra_compile_args=["-lcrypt"], + extra_link_args=link_args(crypt_soname) + ["-lcrypt"], + sources=["extensions/testcrypt.cpp"], +) + +setup( + name="nonpy_rpath", + version="0.1.0", + packages=find_packages(), + description="Test package for nonpy_rpath", + ext_modules=[crypt_example, nonpy_rpath_module], +) diff --git a/tests/integration/test_manylinux.py b/tests/integration/test_manylinux.py index a14b6430..40e25d22 100644 --- a/tests/integration/test_manylinux.py +++ b/tests/integration/test_manylinux.py @@ -219,10 +219,10 @@ def assert_show_output(manylinux_ctr, wheel, expected_tag, strict): assert match['tag'] == expected_tag else: expected_match = TAG_RE.match(expected_tag) - assert expected_match + assert expected_match, f"No match for tag {expected_tag}" expected_glibc = (int(expected_match['major']), int(expected_match['minor'])) actual_match = TAG_RE.match(match['tag']) - assert actual_match + assert actual_match, f"No match for tag {match['tag']}" actual_glibc = (int(actual_match['major']), int(actual_match['minor'])) assert expected_match['arch'] == actual_match['arch'] assert actual_glibc <= expected_glibc @@ -705,3 +705,35 @@ def test_build_wheel_compat(target_policy, only_plat, any_manylinux_container, ['python', '-c', 'from sys import exit; from testsimple import run; exit(run())'] ) + + +def test_nonpy_rpath(any_manylinux_container, docker_python, io_folder): + # Tests https://github.com/pypa/auditwheel/issues/136 + policy, tag, manylinux_ctr = any_manylinux_container + docker_exec( + manylinux_ctr, + ['bash', '-c', 'cd /auditwheel_src/tests/integration/nonpy_rpath ' + '&& python -m pip wheel --no-deps -w /io .'] + ) + + orig_wheel, *_ = os.listdir(io_folder) + assert orig_wheel.startswith("nonpy_rpath-0.1.0") + assert 'manylinux' not in orig_wheel + + # Repair the wheel using the appropriate manylinux container + repair_command = \ + f'auditwheel repair --plat {policy} --only-plat -w /io /io/{orig_wheel}' + docker_exec(manylinux_ctr, repair_command) + filenames = os.listdir(io_folder) + assert len(filenames) == 2 + repaired_wheel = f'nonpy_rpath-0.1.0-{PYTHON_ABI}-{tag}.whl' + assert repaired_wheel in filenames + assert_show_output(manylinux_ctr, repaired_wheel, policy, False) + + # Test the resulting wheel outside the manylinux container + docker_exec(docker_python, "pip install /io/" + repaired_wheel) + docker_exec( + docker_python, + ["python", "-c", + "import nonpy_rpath; assert nonpy_rpath.crypt_something().startswith('*')"] + )