diff --git a/README.md b/README.md index 021fcdc3..748ae19e 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Options | | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each (non-pure Python) built wheel | | | [`CIBW_MANYLINUX_X86_64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) [`CIBW_MANYLINUX_I686_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) [`CIBW_MANYLINUX_PYPY_X86_64_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#manylinux-image) | Specify alternative manylinux docker images | | **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel | +| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-test) | Execute shell command to prepare test environment | | | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests | | | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require | | **Other** | [`CIBW_BUILD_VERBOSITY`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-verbosity) | Increase/decrease the output of pip wheel | diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index aa7f6587..e20028d8 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -113,6 +113,7 @@ def main(): repair_command_default = '' repair_command = get_option_from_environment('CIBW_REPAIR_WHEEL_COMMAND', platform=platform, default=repair_command_default) environment_config = get_option_from_environment('CIBW_ENVIRONMENT', platform=platform, default='') + before_test = get_option_from_environment('CIBW_BEFORE_TEST', platform=platform, default='') if test_extras: test_extras = '[{0}]'.format(test_extras) @@ -154,6 +155,7 @@ def main(): build_selector=build_selector, repair_command=repair_command, environment=environment, + before_test=before_test ) if platform == 'linux': diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 20dae7e6..3113f309 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -68,7 +68,7 @@ def get_python_configurations(build_selector): return [c for c in python_configurations if matches_platform(c.identifier) and build_selector(c.identifier)] -def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment, manylinux_images): +def build(project_dir, output_dir, test_command, before_test, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment, manylinux_images): try: subprocess.check_call(['docker', '--version']) except Exception: @@ -154,6 +154,10 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef echo "Running tests using `which python`" + if [ ! -z {before_test} ]; then + sh -c {before_test} + fi + # Install the wheel we just built # Note: If auditwheel produced two wheels, it's because the earlier produced wheel # conforms to multiple manylinux standards. These multiple versions of the wheel are @@ -202,6 +206,9 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef environment_exports='\n'.join(environment.as_shell_commands()), uid=os.getuid(), gid=os.getgid(), + before_test=shlex.quote( + prepare_command(before_test, project='/project') if before_test else '' + ), ) container_name = 'cibuildwheel-{}'.format(uuid.uuid4()) diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index bcc87d83..59106262 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -105,7 +105,7 @@ def install_pypy(version, url): return installation_bin_path -def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment): +def build(project_dir, output_dir, test_command, before_test, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment): abs_project_dir = os.path.abspath(project_dir) temp_dir = tempfile.mkdtemp(prefix='cibuildwheel') built_wheel_dir = os.path.join(temp_dir, 'built_wheel') @@ -214,6 +214,10 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef # check that we are using the Python from the virtual environment call(['which', 'python'], env=virtualenv_env) + if before_test: + before_test_prepared = prepare_command(before_test, project=abs_project_dir) + call(before_test_prepared, env=virtualenv_env, shell=True) + # install the wheel call(['pip', 'install', repaired_wheel + test_extras], env=virtualenv_env) diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 6450d9f6..8030cf31 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -86,7 +86,7 @@ def install_pypy(version, arch, url): return installation_path -def build(project_dir, output_dir, test_command, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment): +def build(project_dir, output_dir, test_command, before_test, test_requires, test_extras, before_build, build_verbosity, build_selector, repair_command, environment): abs_project_dir = os.path.abspath(project_dir) temp_dir = tempfile.mkdtemp(prefix='cibuildwheel') built_wheel_dir = os.path.join(temp_dir, 'built_wheel') @@ -192,6 +192,10 @@ def build(project_dir, output_dir, test_command, test_requires, test_extras, bef # check that we are using the Python from the virtual environment shell(['which', 'python'], env=virtualenv_env) + if before_test: + before_test_prepared = prepare_command(before_test, project=abs_project_dir) + shell([before_test_prepared], env=virtualenv_env) + # install the wheel shell(['pip', 'install', repaired_wheel + test_extras], env=virtualenv_env) diff --git a/docs/options.md b/docs/options.md index 1ae87f04..81ed03c3 100644 --- a/docs/options.md +++ b/docs/options.md @@ -343,6 +343,34 @@ Platform-specific variants also available:
CIBW_TEST_EXTRAS: test,qt ``` +### `CIBW_BEFORE_TEST` {: #before-test} +> Execute a shell command before testing each wheel + +A shell command to run in **each** test virtual environment, before your wheel is installed and tested. This is useful if you need to install a non pip package, change values of environment variables +or perform multi step pip installation (e.g. installing `scikit-build` or `cython` before install test package) + + +The active Python binary can be accessed using `python`, and pip with `pip`; `cibuildwheel` makes sure the right version of Python and pip will be executed. `{project}` can be used as a placeholder for the absolute path to the project's root and will be replaced by `cibuildwheel`. + +On Linux and macOS, the command is run in a shell, so you can write things like `cmd1 && cmd2`. + +Platform-specific variants also available:
+ `CIBW_BEFORE_TEST_MACOS` | `CIBW_BEFORE_TEST_WINDOWS` | `CIBW_BEFORE_TEST_LINUX` + +#### Examples +```yaml +# install test dependencies with overwritten environment variables. +CIBW_BEFORE_TEST: CC=gcc CXX=g++ pip install -r requirements.txt + +# chain commands using && +CIBW_BEFORE_TEST : rm -rf ./data/cache && mkdir -p ./data/cache + +# install non pip python package +CIBW_BEFORE_TEST: cd some_dir; ./configure; make; make install + +# install python packages that are required to install test dependencies +CIBW_BEFORE_TEST: pip install cmake scikit-build +``` ## Other diff --git a/test/11_before_test/cibuildwheel_test.py b/test/11_before_test/cibuildwheel_test.py new file mode 100644 index 00000000..5aa1c4ca --- /dev/null +++ b/test/11_before_test/cibuildwheel_test.py @@ -0,0 +1,23 @@ +import os +import utils + + +def test(): + project_dir = os.path.dirname(__file__) + + # build the wheels + actual_wheels = utils.cibuildwheel_run(project_dir, add_env={ + # write python version information to a temporary file, this is + # checked in setup.py + 'CIBW_BEFORE_TEST': '''python -c "import sys; open('/tmp/pythonversion.txt', 'w').write(sys.version)" && python -c "import sys; open('/tmp/pythonprefix.txt', 'w').write(sys.prefix)"''', + 'CIBW_BEFORE_TEST_WINDOWS': '''python -c "import sys; open('c:\\pythonversion.txt', 'w').write(sys.version)" && python -c "import sys; open('c:\\pythonprefix.txt', 'w').write(sys.prefix)"''', + 'CIBW_TEST_REQUIRES': 'nose', + # the 'false ||' bit is to ensure this command runs in a shell on + # mac/linux. + 'CIBW_TEST_COMMAND': 'false || nosetests {project}/test', + 'CIBW_TEST_COMMAND_WINDOWS': 'nosetests {project}/test', + }) + + # also check that we got the right wheels + expected_wheels = utils.expected_wheels('spam', '0.1.0') + assert set(actual_wheels) == set(expected_wheels) diff --git a/test/11_before_test/setup.py b/test/11_before_test/setup.py new file mode 100644 index 00000000..e3ea2938 --- /dev/null +++ b/test/11_before_test/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, Extension + + +setup( + name="spam", + ext_modules=[Extension('spam', sources=['spam.c'])], + version="0.1.0", +) diff --git a/test/11_before_test/spam.c b/test/11_before_test/spam.c new file mode 100644 index 00000000..d1ab0f22 --- /dev/null +++ b/test/11_before_test/spam.c @@ -0,0 +1,48 @@ +#include + +static PyObject * +spam_system(PyObject *self, PyObject *args) +{ + const char *command; + int sts; + + if (!PyArg_ParseTuple(args, "s", &command)) + return NULL; + sts = system(command); + return PyLong_FromLong(sts); +} + +/* Module initialization */ + +#if PY_MAJOR_VERSION >= 3 + #define MOD_INIT(name) PyMODINIT_FUNC PyInit_##name(void) + #define MOD_DEF(m, name, doc, methods, module_state_size) \ + static struct PyModuleDef moduledef = { \ + PyModuleDef_HEAD_INIT, name, doc, module_state_size, methods, }; \ + m = PyModule_Create(&moduledef); + #define MOD_RETURN(m) return m; +#else + #define MOD_INIT(name) PyMODINIT_FUNC init##name(void) + #define MOD_DEF(m, name, doc, methods, module_state_size) \ + m = Py_InitModule3(name, methods, doc); + #define MOD_RETURN(m) return; +#endif + +static PyMethodDef module_methods[] = { + {"system", (PyCFunction)spam_system, METH_VARARGS, + "Execute a shell command."}, + {NULL} /* Sentinel */ +}; + +MOD_INIT(spam) +{ + PyObject* m; + + MOD_DEF(m, + "spam", + "Example module", + module_methods, + -1) + + MOD_RETURN(m) +} diff --git a/test/11_before_test/test/spam_test.py b/test/11_before_test/test/spam_test.py new file mode 100644 index 00000000..21bad8c2 --- /dev/null +++ b/test/11_before_test/test/spam_test.py @@ -0,0 +1,28 @@ +import sys +import os +from unittest import TestCase + + +class TestBeforeTest(TestCase): + def test_version(self): + # assert that the Python version as written to pythonversion.txt in the CIBW_BEFORE_TEST step + # is the same one as is currently running. + # because of use symlinks in MacOS run this test is also need + version_file = 'c:\\pythonversion.txt' if sys.platform == 'win32' else '/tmp/pythonversion.txt' + with open(version_file) as f: + stored_version = f.read() + print('stored_version', stored_version) + print('sys.version', sys.version) + assert stored_version == sys.version + + def test_prefix(self): + # check that the prefix also was written + prefix_file = 'c:\\pythonprefix.txt' if sys.platform == 'win32' else '/tmp/pythonprefix.txt' + with open(prefix_file) as f: + stored_prefix = f.read() + print('stored_prefix', stored_prefix) + print('sys.prefix', sys.prefix) + # Works around path-comparison bugs caused by short-paths on Windows e.g. + # vssadm~1 instead of vssadministrator + + assert os.stat(stored_prefix) == os.stat(sys.prefix)