diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ef0341..121e603 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,6 +78,29 @@ jobs: name: ${{ matrix.py }}-${{ matrix.platform.wheel_tag }} path: ./wheelhouse/*.whl + build_pyodide: + name: Build Pyodide wheel + needs: build_sdist + runs-on: ubuntu-latest + steps: + - name: Download source distribution + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: source + - name: Unpack source distribution + run: tar --strip-components 1 -xvf *.tar.gz + - name: Build and test wheel + uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0 + env: + CIBW_PLATFORM: pyodide + CIBW_BUILD: cp314-pyodide_wasm32 + CIBW_TEST_COMMAND: >- + python -c "import pypcode; assert pypcode.Context('x86:LE:64:default').translate(b'\xc3').ops" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cp314-pyodide_wasm32 + path: ./wheelhouse/*.whl + build_docs: name: Build docs runs-on: ubuntu-latest @@ -152,7 +175,7 @@ jobs: upload_pypi: name: Upload wheels to PyPI - needs: [lint, build_docs, build_sdist, build_wheels, upload_coverage] + needs: [lint, build_docs, build_sdist, build_wheels, build_pyodide, upload_coverage] environment: name: pypi url: https://pypi.org/p/pypcode diff --git a/CMakeLists.txt b/CMakeLists.txt index f20f00e..ab679d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,27 @@ cmake_minimum_required(VERSION 3.18...3.22) project(pypcode) -find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) set(CMAKE_CXX_STANDARD 17) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.14) +option(PYPCODE_BUILD_EXTENSION "Build the Python extension" ON) +option(PYPCODE_BUILD_SLEIGH "Build the SLEIGH compiler" ON) + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() -# Detect the installed nanobind package and import it into CMake -execute_process( - COMMAND "${Python_EXECUTABLE}" -c "import nanobind; print(nanobind.cmake_dir())" - OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR) -list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") -find_package(nanobind CONFIG REQUIRED) +if(PYPCODE_BUILD_EXTENSION) + find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) + + # Detect the installed nanobind package and import it into CMake + execute_process( + COMMAND "${Python_EXECUTABLE}" -c "import nanobind; print(nanobind.cmake_dir())" + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR) + list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") + find_package(nanobind CONFIG REQUIRED) +endif() if(MSVC) add_compile_options(/O2 /D_HAS_STD_BYTE=0 /DLOCAL_ZLIB=1 /DNO_GZIP=1) @@ -58,27 +64,31 @@ set(SLEIGH_COMMON pypcode/sleigh/xml.cc ) -add_executable(sleigh - pypcode/sleigh/filemanage.cc - pypcode/sleigh/slgh_compile.cc - pypcode/sleigh/slghparse.cc - pypcode/sleigh/slghscan.cc - ${SLEIGH_COMMON} - ${ZLIB} - ) -install(TARGETS sleigh DESTINATION bin) +if(PYPCODE_BUILD_SLEIGH) + add_executable(sleigh + pypcode/sleigh/filemanage.cc + pypcode/sleigh/slgh_compile.cc + pypcode/sleigh/slghparse.cc + pypcode/sleigh/slghscan.cc + ${SLEIGH_COMMON} + ${ZLIB} + ) + install(TARGETS sleigh DESTINATION bin) +endif() -nanobind_add_module(pypcode_native - pypcode/pypcode_native.cpp - ${SLEIGH_COMMON} - ${ZLIB} - ) +if(PYPCODE_BUILD_EXTENSION) + nanobind_add_module(pypcode_native + pypcode/pypcode_native.cpp + ${SLEIGH_COMMON} + ${ZLIB} + ) -if(DEFINED ENV{COVERAGE} AND NOT "$ENV{COVERAGE}" STREQUAL "") - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(pypcode_native PRIVATE --coverage -O0 -g) - target_link_options(pypcode_native PRIVATE --coverage -O0 -g) + if(DEFINED ENV{COVERAGE} AND NOT "$ENV{COVERAGE}" STREQUAL "") + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(pypcode_native PRIVATE --coverage -O0 -g) + target_link_options(pypcode_native PRIVATE --coverage -O0 -g) + endif() endif() -endif() -install(TARGETS pypcode_native DESTINATION .) + install(TARGETS pypcode_native DESTINATION .) +endif() diff --git a/setup.py b/setup.py index 9348861..a945e6d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import json import os from pathlib import Path import platform @@ -6,6 +7,7 @@ import struct import subprocess import sys + from setuptools import setup from setuptools.command.build_ext import build_ext @@ -27,7 +29,10 @@ def run(self): cross_compiling_for_macos_amd64 = ( platform.system() == "Darwin" and platform.machine() != "x86_64" and "x86_64" in os.getenv("ARCHFLAGS", "") ) - cross_compiling = cross_compiling_for_macos_arm64 or cross_compiling_for_macos_amd64 + cross_compiling_for_emscripten = os.getenv("_PYTHON_HOST_PLATFORM", "").startswith("emscripten") + cross_compiling = ( + cross_compiling_for_macos_arm64 or cross_compiling_for_macos_amd64 or cross_compiling_for_emscripten + ) root_dir = Path(__file__).parent.absolute() target_build_dir = root_dir / "build" / "native" @@ -51,7 +56,16 @@ def run(self): cmake_build_args += ["--config", "Release"] target_cmake_config_args = cmake_config_args[::] - if cross_compiling: + if cross_compiling_for_emscripten: + import nanobind # pylint: disable=import-error,import-outside-toplevel + + pywasmcross_args = json.loads(os.environ["PYWASMCROSS_ARGS"]) + target_cmake_config_args += [ + f"-DPython_INCLUDE_DIR={pywasmcross_args['pythoninclude']}", + f"-Dnanobind_DIR={nanobind.cmake_dir()}", + "-DPYPCODE_BUILD_SLEIGH=OFF", + ] + if cross_compiling_for_macos_arm64 or cross_compiling_for_macos_amd64: target_cmake_config_args += [ "-DCMAKE_OSX_DEPLOYMENT_TARGET=10.14", "-DCMAKE_OSX_ARCHITECTURES=" + os.getenv("ARCHFLAGS"), @@ -64,20 +78,51 @@ def run(self): if cross_compiling: # Also build a host version of sleigh to process .sla files - host_cmake_config_args = cmake_config_args - subprocess.check_call(["cmake", "-S", ".", "-B", host_build_dir] + host_cmake_config_args, cwd=root_dir) + host_cmake_config_args = ["-DPYPCODE_BUILD_EXTENSION=OFF"] + host_cmake = "cmake" + host_env = None + if cross_compiling_for_emscripten: + wrapper_dir = os.environ["COMPILER_WRAPPER_DIR"] + host_path = os.pathsep.join( + path for path in os.environ["PATH"].split(os.pathsep) if path != wrapper_dir + ) + host_cmake = shutil.which("cmake", path=host_path) + if host_cmake is None: + raise RuntimeError("Could not find the host CMake executable") + host_env = os.environ.copy() + host_env["PATH"] = host_path + for name in ( + "AR", + "CC", + "CFLAGS", + "CMAKE_CROSSCOMPILING_EMULATOR", + "CMAKE_TOOLCHAIN_FILE", + "CXX", + "CXXFLAGS", + "LD", + "LDFLAGS", + "RANLIB", + ): + host_env.pop(name, None) + subprocess.check_call( + [host_cmake, "-S", ".", "-B", host_build_dir] + host_cmake_config_args, + cwd=root_dir, + env=host_env, + ) subprocess.check_call( - ["cmake", "--build", host_build_dir, "--parallel", "--verbose", "--target", "sleigh"] + [host_cmake, "--build", host_build_dir, "--parallel", "--verbose", "--target", "sleigh"] + cmake_build_args, cwd=root_dir, + env=host_env, ) # Install extension and sleigh binary into target package if cross_compiling: # Note: Manually install because cmake install step may refuse to install binaries for foreign architectures - install_pkg_bin_dir.mkdir(exist_ok=True) ext_path = next(target_build_dir.glob("pypcode_native.*")) - shutil.copy(target_build_dir / sleigh_filename, install_pkg_bin_dir / sleigh_filename) + if not cross_compiling_for_emscripten: + install_pkg_bin_dir.mkdir(exist_ok=True) + shutil.copy(target_build_dir / sleigh_filename, install_pkg_bin_dir / sleigh_filename) shutil.copy(ext_path, install_pkg_root_dir / ext_path.name) else: subprocess.check_call(["cmake", "--install", target_build_dir], cwd=root_dir)