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)