diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 137bc19e..bce3f754 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -128,6 +128,7 @@ def main() -> None: build_config, skip_config = os.environ.get('CIBW_BUILD', '*'), os.environ.get('CIBW_SKIP', '') environment_config = get_option_from_environment('CIBW_ENVIRONMENT', platform=platform, default='') + before_all = get_option_from_environment('CIBW_BEFORE_ALL', platform=platform) before_build = get_option_from_environment('CIBW_BEFORE_BUILD', platform=platform) repair_command = get_option_from_environment('CIBW_REPAIR_WHEEL_COMMAND', platform=platform, default=repair_command_default) dependency_versions = get_option_from_environment('CIBW_DEPENDENCY_VERSIONS', platform=platform, default='pinned') @@ -212,6 +213,7 @@ def main() -> None: test_extras=test_extras, before_test=before_test, before_build=before_build, + before_all=before_all, build_verbosity=build_verbosity, build_selector=build_selector, repair_command=repair_command, diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 99e779c4..8bbda24d 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -133,6 +133,10 @@ def build(options: BuildOptions) -> None: call(['docker', 'start', container_name]) + if options.before_all: + before_all_prepared = prepare_command(options.before_all, project='/project', package=options.package_dir) + call(['docker', 'exec', '-i', container_name] + shell_cmd, universal_newlines=True, input=before_all_prepared) + for config in platform_configs: if options.dependency_constraints: constraints_file = options.dependency_constraints.get_for_python_version(config.version) diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index 2fd28d20..3e67b534 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -176,6 +176,10 @@ def build(options: BuildOptions) -> None: built_wheel_dir = temp_dir / 'built_wheel' repaired_wheel_dir = temp_dir / 'repaired_wheel' + if options.before_all: + before_all_prepared = prepare_command(options.before_all, project='.', package=options.package_dir) + call([before_all_prepared], shell=True) + python_configurations = get_python_configurations(options.build_selector) for config in python_configurations: diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index feef6185..f7201462 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -112,6 +112,7 @@ class BuildOptions(NamedTuple): output_dir: Path build_selector: BuildSelector environment: ParsedEnvironment + before_all: str before_build: Optional[str] repair_command: str manylinux_images: Optional[Dict[str, str]] @@ -120,7 +121,7 @@ class BuildOptions(NamedTuple): before_test: Optional[str] test_requires: List[str] test_extras: str - build_verbosity: int +x build_verbosity: int resources_dir = Path(__file__).resolve().parent / 'resources' diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index e81fe301..73628c7f 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -162,6 +162,10 @@ def build(options: BuildOptions) -> None: nuget = Path('C:\\cibw\\nuget.exe') download('https://dist.nuget.org/win-x86-commandline/latest/nuget.exe', nuget) + if options.before_all: + before_all_prepared = prepare_command(options.before_all, project='.', package=options.package_dir) + shell([before_all_prepared]) + python_configurations = get_python_configurations(options.build_selector) for config in python_configurations: dependency_constraint_flags = [] diff --git a/docs/options.md b/docs/options.md index 18d5417b..0d61060d 100644 --- a/docs/options.md +++ b/docs/options.md @@ -190,6 +190,25 @@ CIBW_ENVIRONMENT: "BUILD_TIME=$(date) SAMPLE_TEXT=\"sample text\"" !!! note `cibuildwheel` always defines the environment variable `CIBUILDWHEEL=1`. This can be useful for [building wheels with optional extensions](faq.md#building-packages-with-optional-c-extensions). +### `CIBW_BEFORE_ALL` {: #before-all} +> Execute a shell command preparing common part for each wheel. + +Shell command to prepare common part of project (ex. build libraries which does not depend on python). +This option is added mainly for linux build, because linux wheels are build in isolated from host docker containers. + +The placeholder `{package}` can be used here; it will be replaced by the path to the package being built by `cibuildwheel`. + +Platform-specific variants also available:
+ `CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX` + +#### Examples +```yaml +# build third party library +CIBW_BEFORE_ALL: make -C third_party_lib + +# install system library +CIBW_BEFORE_ALL: yum install -y libffi-dev +``` ### `CIBW_BEFORE_BUILD` {: #before-build} > Execute a shell command preparing each wheel's build diff --git a/test/14_before_all/cibuildwheel_test.py b/test/14_before_all/cibuildwheel_test.py new file mode 100644 index 00000000..e5340e35 --- /dev/null +++ b/test/14_before_all/cibuildwheel_test.py @@ -0,0 +1,23 @@ +import os + +import utils + + +def test(): + project_dir = os.path.dirname(__file__) + + with open(os.path.join(project_dir, "text_info.txt"), mode='w') as ff: + print("dummy text", file=ff) + + # 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_ALL': '''python -c "open('{project}/text_info.txt', 'w').write('sample text')"''', + }) + + + # also check that we got the right wheels + os.remove(os.path.join(project_dir, "text_info.txt")) + expected_wheels = utils.expected_wheels('spam', '0.1.0') + assert set(actual_wheels) == set(expected_wheels) diff --git a/test/14_before_all/setup.py b/test/14_before_all/setup.py new file mode 100644 index 00000000..7b68808b --- /dev/null +++ b/test/14_before_all/setup.py @@ -0,0 +1,23 @@ +import os +import sys + +from setuptools import ( + Extension, + setup, +) + +# assert that the Python version as written to text_info.txt in the CIBW_BEFORE_ALL step +# is the same one as is currently running. +with open("text_info.txt") as f: + stored_text = f.read() + +print("## stored text: "+stored_text) +assert stored_text == "sample text" + + + +setup( + name="spam", + ext_modules=[Extension('spam', sources=['spam.c'])], + version="0.1.0", +) diff --git a/test/14_before_all/spam.c b/test/14_before_all/spam.c new file mode 100644 index 00000000..d1ab0f22 --- /dev/null +++ b/test/14_before_all/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) +}