From 2719bf126d2c1a7eeafa7bd7476da2300a63df9a Mon Sep 17 00:00:00 2001 From: Diego Ferigo Date: Fri, 28 May 2021 10:06:14 +0200 Subject: [PATCH 1/4] Expose CMake binaries --- src/cmake_build_extension/build_extension.py | 46 ++++++++++++++++++++ src/cmake_build_extension/cmake_extension.py | 3 ++ 2 files changed, 49 insertions(+) diff --git a/src/cmake_build_extension/build_extension.py b/src/cmake_build_extension/build_extension.py index 4eba0fb..8e6a6cd 100644 --- a/src/cmake_build_extension/build_extension.py +++ b/src/cmake_build_extension/build_extension.py @@ -3,6 +3,7 @@ import platform import shutil import subprocess +import sys from pathlib import Path from setuptools.command.build_ext import build_ext @@ -181,6 +182,51 @@ def build_extension(self, ext: CMakeExtension) -> None: with open(file=cmake_install_prefix / "__init__.py", mode="w") as f: f.write(ext.write_top_level_init) + # Write content to the bin/__main__.py magic file to expose binaries + if len(ext.expose_binaries) > 0: + bin_dirs = {str(Path(d).parents[0]) for d in ext.expose_binaries} + + import inspect + + main_py = inspect.cleandoc( + f""" + from pathlib import Path + import subprocess + import sys + + def main(): + + binary_name = Path(sys.argv[0]).name + prefix = Path(__file__).parent.parent + bin_dirs = {str(bin_dirs)} + + binary_path = "" + + for dir in bin_dirs: + path = prefix / Path(dir) / binary_name + if path.is_file(): + binary_path = str(path) + break + + if not Path(binary_path).is_file(): + name = binary_path if binary_path != "" else binary_name + raise RuntimeError(f"Failed to find binary: {{ name }}") + + sys.argv[0] = binary_path + + result = subprocess.run(args=sys.argv, capture_output=False) + exit(result.returncode) + + if __name__ == "__main__" and len(sys.argv) > 1: + sys.argv = sys.argv[1:] + main()""" + ) + + bin_folder = cmake_install_prefix / "bin" + Path(bin_folder).mkdir(exist_ok=True, parents=True) + with open(file=bin_folder / "__main__.py", mode="w") as f: + f.write(main_py) + @staticmethod def extend_cmake_prefix_path(path: str) -> None: diff --git a/src/cmake_build_extension/cmake_extension.py b/src/cmake_build_extension/cmake_extension.py index 26cc7ce..83980ed 100644 --- a/src/cmake_build_extension/cmake_extension.py +++ b/src/cmake_build_extension/cmake_extension.py @@ -20,6 +20,7 @@ class CMakeExtension(Extension): cmake_build_type: The default build type of the CMake project. cmake_component: The name of component to install. Defaults to all components. cmake_depends_on: List of dependency packages containing required CMake projects. + expose_binaries: List of binary paths to expose, relative to top-level directory. """ def __init__( @@ -33,6 +34,7 @@ def __init__( cmake_build_type: str = "Release", cmake_component: str = None, cmake_depends_on: List[str] = (), + expose_binaries: List[str] = (), ): super().__init__(name=name, sources=[]) @@ -51,3 +53,4 @@ def __init__( self.source_dir = str(Path(source_dir).absolute()) self.cmake_configure_options = cmake_configure_options self.cmake_component = cmake_component + self.expose_binaries = expose_binaries From d9ce9b0612f39049d9c639df1c177b678d043cab Mon Sep 17 00:00:00 2001 From: Diego Ferigo Date: Fri, 28 May 2021 10:18:14 +0200 Subject: [PATCH 2/4] Check also .exe binaries extension --- src/cmake_build_extension/build_extension.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cmake_build_extension/build_extension.py b/src/cmake_build_extension/build_extension.py index 8e6a6cd..a380f26 100644 --- a/src/cmake_build_extension/build_extension.py +++ b/src/cmake_build_extension/build_extension.py @@ -208,6 +208,11 @@ def main(): binary_path = str(path) break + path = Path(str(path) + ".exe") + if path.is_file(): + binary_path = str(path) + break + if not Path(binary_path).is_file(): name = binary_path if binary_path != "" else binary_name raise RuntimeError(f"Failed to find binary: {{ name }}") From 09d67c856f0e2639033afa6957e3476967c10cfb Mon Sep 17 00:00:00 2001 From: Diego Ferigo Date: Fri, 28 May 2021 00:56:25 +0200 Subject: [PATCH 3/4] Test example against the correct commit, without downloading from PyPI --- .github/workflows/python.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 0184eb0..bf1b161 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -59,9 +59,9 @@ jobs: shell: bash continue-on-error: ${{ matrix.experimental }} run: | - cd examples/swig - pip install -v . - pytest -sv tests + pip install numpy + pip install --no-build-isolation -v ./examples/swig + pytest -sv examples/swig/tests publish: name: 'Publish to PyPI' From a42742b44b15b79aa7b9f0195b14f7d43a7af7cb Mon Sep 17 00:00:00 2001 From: Diego Ferigo Date: Fri, 28 May 2021 08:58:51 +0200 Subject: [PATCH 4/4] Add executable to swig example --- examples/swig/CMakeLists.txt | 8 ++++++- examples/swig/setup.cfg | 4 ++++ examples/swig/setup.py | 1 + examples/swig/src/print_answer.cpp | 9 ++++++++ examples/swig/tests/test_mymath.py | 34 +++++++++++++++++++++++------- 5 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 examples/swig/src/print_answer.cpp diff --git a/examples/swig/CMakeLists.txt b/examples/swig/CMakeLists.txt index f2f338a..483f968 100644 --- a/examples/swig/CMakeLists.txt +++ b/examples/swig/CMakeLists.txt @@ -26,6 +26,12 @@ target_include_directories(mymath PUBLIC $ $) +# ======================= +# print_answer executable +# ======================= + +add_executable(print_answer src/print_answer.cpp) + # ============= # SWIG bindings # ============= @@ -77,7 +83,7 @@ set_property( # Install the target with C++ code install( - TARGETS mymath + TARGETS mymath print_answer LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} diff --git a/examples/swig/setup.cfg b/examples/swig/setup.cfg index 427bba7..186feee 100644 --- a/examples/swig/setup.cfg +++ b/examples/swig/setup.cfg @@ -33,3 +33,7 @@ testing = pytest-icdiff all = %(testing)s + +[options.entry_points] +console_scripts = + print_answer = mymath.bin.__main__:main diff --git a/examples/swig/setup.py b/examples/swig/setup.py index 1ae5531..4a6e4ef 100644 --- a/examples/swig/setup.py +++ b/examples/swig/setup.py @@ -22,6 +22,7 @@ CMakeExtension( name="mymath", install_prefix="mymath", + expose_binaries=["bin/print_answer"], write_top_level_init=init_py, source_dir=str(Path(__file__).parent.absolute()), cmake_configure_options=[ diff --git a/examples/swig/src/print_answer.cpp b/examples/swig/src/print_answer.cpp new file mode 100644 index 0000000..7ad16ed --- /dev/null +++ b/examples/swig/src/print_answer.cpp @@ -0,0 +1,9 @@ +#include +#include + +int main() +{ + // Answer to the Ultimate Question of Life, the Universe, and Everything + std::cout << int('*') << std::endl; + return EXIT_SUCCESS; +} diff --git a/examples/swig/tests/test_mymath.py b/examples/swig/tests/test_mymath.py index 1e7ee62..c4f1597 100644 --- a/examples/swig/tests/test_mymath.py +++ b/examples/swig/tests/test_mymath.py @@ -1,11 +1,13 @@ -import pytest -import numpy as np +import sys + import mymath.bindings +import numpy as np +import pytest def test_dot(): - v1 = [1., 2, 3, -5.5, 42] + v1 = [1.0, 2, 3, -5.5, 42] v2 = [-3.2, 0, 13, 6, -3.14] result = mymath.bindings.dot(vector1=v1, vector2=v2) @@ -14,7 +16,7 @@ def test_dot(): def test_normalize(): - v = [1., 2, 3, -5.5, 42] + v = [1.0, 2, 3, -5.5, 42] result = mymath.bindings.normalize(input=v) assert pytest.approx(result) == np.array(v) / np.linalg.norm(v) @@ -22,7 +24,7 @@ def test_normalize(): def test_dot_numpy(): - v1 = np.array([1., 2, 3, -5.5, 42]) + v1 = np.array([1.0, 2, 3, -5.5, 42]) v2 = np.array([-3.2, 0, 13, 6, -3.14]) result = mymath.bindings.dot_numpy(in_1=v1, in_2=v2) @@ -31,7 +33,7 @@ def test_dot_numpy(): def test_normalize_numpy(): - v = np.array([1., 2, 3, -5.5, 42]) + v = np.array([1.0, 2, 3, -5.5, 42]) result = mymath.bindings.normalize_numpy(in_1=v) @@ -41,8 +43,24 @@ def test_normalize_numpy(): def test_assertion(): - v1 = np.array([1., 2, 3, -5.5]) - v2 = np.array([42.]) + v1 = np.array([1.0, 2, 3, -5.5]) + v2 = np.array([42.0]) with pytest.raises(RuntimeError): _ = mymath.bindings.dot_numpy(in_1=v1, in_2=v2) + + +@pytest.mark.skipif( + sys.version_info < (3, 7), reason="capture_output and text require Python 3.7" +) +def test_executable(): + + import subprocess + + result = subprocess.run("print_answer", capture_output=True, text=True) + assert result.stdout.strip() == "42" + + result = subprocess.run( + "python -m mymath.bin print_answer".split(), capture_output=True, text=True + ) + assert result.stdout.strip() == "42"