diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml deleted file mode 100644 index 2be08911..00000000 --- a/.github/workflows/ci-sage.yml +++ /dev/null @@ -1,223 +0,0 @@ -name: Run Sage CI for Linux/Cygwin/macOS - -## This GitHub Actions workflow provides: -## -## - portability testing, by building and testing this project on many platforms -## (Linux variants and Cygwin), each with two configurations (installed packages), -## -## - continuous integration, by building and testing other software -## that depends on this project. -## -## It runs on every pull request and push of a tag to the GitHub repository. -## -## The testing can be monitored in the "Actions" tab of the GitHub repository. -## -## After all jobs have finished (or are canceled) and a short delay, -## tar files of all logs are made available as "build artifacts". -## -## This GitHub Actions workflow uses the portability testing framework -## of SageMath (https://www.sagemath.org/). For more information, see -## https://doc.sagemath.org/html/en/developer/portability_testing.html - -## The workflow consists of two jobs: -## -## - First, it builds a source distribution of the project -## and generates a script "update-pkgs.sh". It uploads them -## as a build artifact named upstream. -## -## - Second, it checks out a copy of the SageMath source tree. -## It downloads the upstream artifact and replaces the project's -## package in the SageMath distribution by the newly packaged one -## from the upstream artifact, by running the script "update-pkgs.sh". -## Then it builds a small portion of the Sage distribution. -## -## Many copies of the second step are run in parallel for each of the tested -## systems/configurations. - -#on: [push, pull_request] - -on: - pull_request: - types: [opened, synchronize] - push: - tags: - - '*' - workflow_dispatch: - # Allow to run manually - -concurrency: - # Cancel previous runs of this workflow for the same branch - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # Ubuntu packages to install so that the project's "setup.py sdist" can succeed - DIST_PREREQ: python3-setuptools autoconf - # Name of this project in the Sage distribution - SPKG: cysignals - # Sage distribution packages to build - TARGETS_PRE: build/make/Makefile - TARGETS: SAGE_CHECK=no SAGE_CHECK_PACKAGES="cysignals,cypari" cysignals cypari - TARGETS_OPTIONAL: build/make/Makefile - # Standard setting: Test the current beta release of Sage - SAGE_REPO: sagemath/sage - SAGE_REF: develop - REMOVE_PATCHES: "*" - -jobs: - - dist: - runs-on: ubuntu-latest - steps: - - name: Check out ${{ env.SPKG }} - uses: actions/checkout@v4 - with: - path: build/pkgs/${{ env.SPKG }}/src - - name: Install prerequisites - run: | - sudo DEBIAN_FRONTEND=noninteractive apt-get update - sudo DEBIAN_FRONTEND=noninteractive apt-get install $DIST_PREREQ - - name: Run make dist, prepare upstream artifact - run: | - (cd build/pkgs/${{ env.SPKG }}/src && autoreconf -fi && python3 setup.py sdist) \ - && mkdir -p upstream && cp build/pkgs/${{ env.SPKG }}/src/dist/*.tar.gz upstream/${{ env.SPKG }}-git.tar.gz \ - && echo "sage-package create ${{ env.SPKG }} --version git --tarball ${{ env.SPKG }}-git.tar.gz --type=standard" > upstream/update-pkgs.sh \ - && if [ -n "${{ env.REMOVE_PATCHES }}" ]; then echo "(cd ../build/pkgs/${{ env.SPKG }}/patches && rm -f ${{ env.REMOVE_PATCHES }}; :)" >> upstream/update-pkgs.sh; fi \ - && ls -l upstream/ - - uses: actions/upload-artifact@v2 - with: - path: upstream - name: upstream - - cygwin-without-sage: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-version-start: [python-3] - python-version: [9] - steps: - - run: | - git config --global core.autocrlf false - git config --global core.symlinks true - - name: Set up the repository - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Install cygwin and minimal prerequisites with choco - shell: bash {0} - run: | - choco --version - choco install make autoconf gcc-core gcc-g++ python3${{ matrix.python-version }}-devel --source cygwin - - name: Install dependencies - run: | - C:\\tools\\cygwin\\bin\\bash -l -x -c 'export PATH=/usr/local/bin:/usr/bin && cd $(cygpath -u "$GITHUB_WORKSPACE") && python3.${{ matrix.python-version }} -m pip install --upgrade pip' - - name: Build and check - run: | - C:\\tools\\cygwin\\bin\\bash -l -x -c 'export PATH=/usr/local/bin:/usr/bin && cd $(cygpath -u "$GITHUB_WORKSPACE") && make check PYTHON=python3.${{ matrix.python-version }}' - - ubuntu-without-sage: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13-dev'] - steps: - - name: Set up the repository - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - - name: Build and check - run: | - make -j4 check - - linux: - uses: sagemath/sage/.github/workflows/docker.yml@develop - with: - # FIXME: duplicated from env.TARGETS - targets_pre: build/make/Makefile - targets: SAGE_CHECK=no SAGE_CHECK_PACKAGES="cysignals,cypari" cysignals cypari - targets_optional: build/make/Makefile - sage_repo: sagemath/sage - sage_ref: develop - upstream_artifact: upstream - # We prefix the image name with the SPKG name ("cysignals-") to avoid the error - # 'Package "sage-docker-..." is already associated with another repository.' - docker_push_repository: ghcr.io/${{ github.repository }}/cysignals- - needs: [dist, ubuntu-without-sage] - - linux-sage-incremental: - uses: sagemath/sage/.github/workflows/docker.yml@develop - with: - # Build incrementally from published Docker image - incremental: true - free_disk_space: true - from_docker_repository: ghcr.io/sagemath/sage/ - from_docker_target: "with-targets" - from_docker_tag: "dev" - docker_targets: "with-targets" - targets_pre: build/make/Makefile - targets: "cysignals-uninstall cypari-uninstall build doc-html ptest" - targets_optional: build/make/Makefile - sage_repo: sagemath/sage - sage_ref: develop - upstream_artifact: upstream - # We prefix the image name with the SPKG name ("cysignals-") to avoid the error - # 'Package "sage-docker-..." is already associated with another repository.' - docker_push_repository: ghcr.io/${{ github.repository }}/cysignals- - needs: [dist, linux] - - macos-without-sage: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: ['macos-13', 'macos-latest'] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13-dev'] - steps: - - name: Set up the repository - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - brew install autoconf - python -m pip install --upgrade pip - - name: Build and check - # Work around https://github.com/sagemath/cysignals/issues/179 - run: | - case $RUNNER_ARCH in - X*) export ARCHFLAGS="-arch x86_64";; - esac - make -j4 check - - macos: - uses: sagemath/sage/.github/workflows/macos.yml@develop - with: - osversion_xcodeversion_toxenv_tuples: >- - [["latest", "", "homebrew-macos-usrlocal-minimal"], - ["latest", "", "homebrew-macos-usrlocal-standard"], - ["13", "xcode_15.0", "homebrew-macos-usrlocal-standard"], - ["latest", "", "conda-forge-macos-standard"]] - # FIXME: duplicated from env.TARGETS - targets_pre: build/make/Makefile - targets: SAGE_CHECK=no SAGE_CHECK_PACKAGES="cysignals,cypari" cysignals cypari - targets_optional: build/make/Makefile - sage_repo: sagemath/sage - sage_ref: develop - upstream_artifact: upstream - needs: [dist, macos-without-sage] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fa0b9657 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: Run Sage CI for Linux/Cygwin/macOS + +## This GitHub Actions workflow provides: +## +## - portability testing, by building and testing this project on many platforms +## (Linux variants and Cygwin), each with two configurations (installed packages), +## +## - continuous integration, by building and testing other software +## that depends on this project. +## +## It runs on every pull request and push of a tag to the GitHub repository. +## +## The testing can be monitored in the "Actions" tab of the GitHub repository. +## +## After all jobs have finished (or are canceled) and a short delay, +## tar files of all logs are made available as "build artifacts". +## +## This GitHub Actions workflow uses the portability testing framework +## of SageMath (https://www.sagemath.org/). For more information, see +## https://doc.sagemath.org/html/en/developer/portability_testing.html + +## The workflow consists of two jobs: +## +## - First, it builds a source distribution of the project +## and generates a script "update-pkgs.sh". It uploads them +## as a build artifact named upstream. +## +## - Second, it checks out a copy of the SageMath source tree. +## It downloads the upstream artifact and replaces the project's +## package in the SageMath distribution by the newly packaged one +## from the upstream artifact, by running the script "update-pkgs.sh". +## Then it builds a small portion of the Sage distribution. +## +## Many copies of the second step are run in parallel for each of the tested +## systems/configurations. + +#on: [push, pull_request] + +on: + pull_request: + types: [opened, synchronize] + push: + tags: + - '*' + workflow_dispatch: + # Allow to run manually + +concurrency: + # Cancel previous runs of this workflow for the same branch + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cygwin-without-sage: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + python-version-start: [python-3] + python-version: [9] + steps: + - run: | + git config --global core.autocrlf false + git config --global core.symlinks true + - name: Set up the repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Set up Cygwin + uses: egor-tensin/setup-cygwin@v4 + with: + packages: gcc-core gcc-g++ python3${{ matrix.python-version }}-devel ninja pkgconf + - name: Install dependencies + shell: C:\tools\cygwin\bin\bash.exe --norc -eo pipefail -o igncr '{0}' + run: | + python3.${{ matrix.python-version }} -m pip install --upgrade pip + python3.${{ matrix.python-version }} -m pip install --upgrade -r ./requirements.txt + - name: Build and check + shell: C:\tools\cygwin\bin\bash.exe --norc -eo pipefail -o igncr '{0}' + run: | + pip install --no-build-isolation --config-settings=builddir=builddir . + meson test --print-errorlogs -C builddir + + ci: + name: CI (${{ matrix.os }} with Python ${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ['macos-13', 'macos-latest', 'ubuntu-latest'] + python-version: ['3.10', '3.11', '3.12', '3.13-dev'] + steps: + - name: Set up the repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install --upgrade pip + pip install --upgrade -r requirements.txt + pip install --upgrade ninja + - name: Build + run: pip install --no-build-isolation --config-settings=builddir=builddir . + - name: Test + run: meson test --print-errorlogs -C builddir diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml index 6fd737aa..78fe82fa 100644 --- a/.github/workflows/dist.yml +++ b/.github/workflows/dist.yml @@ -20,12 +20,15 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 - - name: make dist + - name: Install dependencies run: | - make dist + pip install --upgrade pip + pip install --upgrade -r requirements.txt + - name: Build sdist + run: python -m build --sdist . - uses: actions/upload-artifact@v4 with: - path: "dist/*.tar.gz" + path: "dist/*.tar.*" name: dist - uses: pypa/gh-action-pypi-publish@release/v1 with: @@ -82,7 +85,7 @@ jobs: # Installing pipx follows the approach of https://github.com/pypa/cibuildwheel/pull/1743 id: python with: - python-version: "3.8 - 3.12" + python-version: "3.9 - 3.12" update-environment: false - name: Build platform wheels diff --git a/.gitignore b/.gitignore index 65359fcd..5541fc3e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,9 +62,6 @@ target/ # Auto-generated files example/cysignals_example.cpp -src/config.h -src/cysignals/signals.pxd -src/cysignals/cysignals_config.h # C files generated by Cython src/cysignals/alarm.c diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c8ea784e..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,13 +0,0 @@ -global-include README README.rst VERSION LICENSE -global-include Makefile configure configure.ac -global-include setup.py rundoctests.py testgdb.py *.pyx -graft src -graft docs/source -prune build -prune dist -prune tmp -prune .* -prune example/.* -exclude src/config.h -exclude src/cysignals/signals.pxd -exclude src/cysignals/cysignals_config.h diff --git a/Makefile b/Makefile deleted file mode 100644 index 5ffb2143..00000000 --- a/Makefile +++ /dev/null @@ -1,90 +0,0 @@ -# Optional Makefile for easier development - -VERSION = $(shell cat VERSION) - -PYTHON = python3 -PIP = $(PYTHON) -m pip -v -LS_R = ls -Ra1 - -DOCTEST = $(PYTHON) -B rundoctests.py - - -##################### -# Build -##################### - -all: build doc - -build: configure - $(PIP) install build - $(PYTHON) -m build - -install: configure - $(PIP) install . - -dist: configure - $(PIP) install build - chmod -R go+rX-w . - umask 0022 && $(PYTHON) -m build --sdist - -doc: install - cd docs && $(MAKE) html - - -##################### -# Clean -##################### - -clean: clean-doc clean-build - rm -rf tmp - -clean-build: - rm -rf build example/build example/*.cpp - -clean-doc: - rm -rf docs/build - -distclean: clean - rm -rf .eggs example/.eggs - rm -rf autom4te.cache - rm -f config.log config.status - rm -f src/config.h src/cysignals/signals.pxd src/cysignals/cysignals_config.h - - -##################### -# Check -##################### - -test: check - -check: check-all - -check-all: - $(MAKE) check-install - -# Install and check -check-install: check-doctest check-example - -check-doctest: install - $(DOCTEST) src/cysignals/*.pyx - -check-example: install - $(PYTHON) -m pip install -U build setuptools wheel Cython - cd example && $(PYTHON) -m build --no-isolation . - -check-gdb: install - $(PYTHON) testgdb.py - - -##################### -# Maintain -##################### - -configure: configure.ac - autoconf - autoheader - @rm -f src/config.h.in~ - -.PHONY: all build doc install dist doc clean clean-build clean-doc \ - distclean test check check-all check-install \ - check-doctest check-example diff --git a/VERSION b/VERSION deleted file mode 100644 index f4cd8cf1..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.12.0rc2 diff --git a/configure.ac b/configure.ac deleted file mode 100644 index 6a3dd6c3..00000000 --- a/configure.ac +++ /dev/null @@ -1,205 +0,0 @@ -dnl Not sure which autoconf version we need, but 2.68 (from 2010) -dnl is widely available. -AC_PREREQ([2.68]) - -AC_DEFUN([VERSION], m4_esyscmd_s(cat VERSION)) - -AC_INIT([cysignals], VERSION, [https://github.com/sagemath/cysignals/issues]) -AC_COPYRIGHT([GNU Lesser General Public License version 3 or later]) -AC_CONFIG_SRCDIR([configure.ac]) -AC_CONFIG_HEADERS([src/config.h src/cysignals/cysignals_config.h]) -AC_CONFIG_FILES([src/cysignals/signals.pxd]) - -AC_ARG_ENABLE(debug, - AS_HELP_STRING([--enable-debug], [enable debug output])) - -if test "$enable_debug" = yes; then - AC_DEFINE([ENABLE_DEBUG_CYSIGNALS], 1, [Enable debug output]) -fi - - -AC_PROG_CC() -dnl We use the C compiler for C++ to emulate the Python bug -dnl https://bugs.python.org/issue1222585 -CXX="$CC" -CXXFLAGS="$CFLAGS" -AC_PROG_CXX() - -AC_CHECK_HEADERS([execinfo.h sys/mman.h sys/prctl.h time.h sys/wait.h windows.h]) -AC_CHECK_FUNCS([fork kill sigprocmask sigaltstack backtrace]) - -AC_MSG_CHECKING([for emms instruction]) -# We add the "leal" instruction to reduce false positives in case some -# non-x86 architecture also has an "emms" instruction. -AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[]], [[asm("leal (%eax), %eax; emms");]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(HAVE_EMMS, 1, [Define to 1 if your processor understands the "emms" instruction.]) - , - dnl NO - [AC_MSG_RESULT([no])] -) - -AC_MSG_CHECKING([whether setjmp() saves the signal mask]) -AC_RUN_IFELSE([AC_LANG_PROGRAM( - [[ - #include - #include - #include - ]], - [[ - jmp_buf env; - sigset_t set; - sigemptyset(&set); - if (sigprocmask(SIG_SETMASK, &set, NULL)) return 2; - if (setjmp(env) == 0) - { - sigaddset(&set, SIGFPE); - if (sigprocmask(SIG_SETMASK, &set, NULL)) return 3; - longjmp(env, 1); - } - if (sigprocmask(SIG_SETMASK, NULL, &set)) return 4; - return sigismember(&set, SIGFPE); - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - sigsetjmp=yes - , - dnl NO - [AC_MSG_RESULT([no])] - , - [AC_MSG_RESULT([cross, assume yes])] - sigsetjmp=yes -) - -AC_MSG_CHECKING([for GNU libc]) -AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - #include - #ifndef __GLIBC__ - syntax error! - #endif - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - # GNU libc implements setjmp(...) as a wrapper of sigsetjmp(..., 0) - # so we might as well call the latter directly - sigsetjmp=yes - , - dnl NO - [AC_MSG_RESULT([no])] -) -if test x$sigsetjmp = xyes; then - AC_DEFINE(CYSIGNALS_USE_SIGSETJMP, 1, [Define to 1 to use sigsetjmp() in sig_on(), as opposed to setjmp().]) -fi - -dnl Check for atomic operations -AC_MSG_CHECKING([for _Atomic in C code]) -AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - static _Atomic int x; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(CYSIGNALS_C_ATOMIC, 1, [Define to 1 if your C compiler supports _Atomic.]) - - AC_MSG_CHECKING([for _Atomic with OpenMP in C code]) - saved_CFLAGS="$CFLAGS" - CFLAGS="$CFLAGS -fopenmp" - AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - static _Atomic int x; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(CYSIGNALS_C_ATOMIC_WITH_OPENMP, 1, [Define to 1 if your C compiler supports _Atomic with OpenMP]) - , - dnl NO - [AC_MSG_RESULT([no])] - ) - CFLAGS="$saved_CFLAGS" - , - dnl NO - [AC_MSG_RESULT([no])] -) - -AC_LANG(C++) -AC_MSG_CHECKING([for _Atomic in C++ code]) -AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - static _Atomic int x; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(CYSIGNALS_CXX_ATOMIC, 1, [Define to 1 if your C++ compiler supports _Atomic.]) - - AC_MSG_CHECKING([for _Atomic with OpenMP in C++ code]) - saved_CXXFLAGS="$CXXFLAGS" - CXXFLAGS="$CXXFLAGS -fopenmp" - AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - static _Atomic int x; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(CYSIGNALS_CXX_ATOMIC_WITH_OPENMP, 1, [Define to 1 if your C++ compiler supports _Atomic with OpenMP]) - , - dnl NO - [AC_MSG_RESULT([no])] - ) - CXXFLAGS="$saved_CXXFLAGS" - , - dnl NO - [AC_MSG_RESULT([no])] -) - -AC_MSG_CHECKING([for std::atomic]) -AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - #include - static std::atomic x; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(CYSIGNALS_STD_ATOMIC, 1, [Define to 1 if your C++ compiler supports std::atomic.]) - - AC_MSG_CHECKING([for std::atomic with OpenMP in C++ code]) - saved_CXXFLAGS="$CXXFLAGS" - CXXFLAGS="$CXXFLAGS -fopenmp" - AC_COMPILE_IFELSE([AC_LANG_SOURCE( - [[ - #include - static std::atomic x; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(CYSIGNALS_STD_ATOMIC_WITH_OPENMP, 1, [Define to 1 if your C++ compiler supports std::atomic with OpenMP]) - , - dnl NO - [AC_MSG_RESULT([no])] - ) - CXXFLAGS="$saved_CXXFLAGS" - , - dnl NO - [AC_MSG_RESULT([no])] -) - -AC_MSG_CHECKING([whether MINSIGSTKSZ is constant]) -AC_COMPILE_IFELSE([AC_LANG_PROGRAM( - [[ - #include - ]], - [[ - static char alt_stack[MINSIGSTKSZ]; - ]])], - dnl YES - [AC_MSG_RESULT([yes])] - AC_DEFINE(MINSIGSTKSZ_IS_CONSTANT, 1, [Define to 1 if MINSIGSTKSZ defined in signal.h is constant.]) - , - dnl NO - [AC_MSG_RESULT([no])] -) - -AC_OUTPUT() - -dnl vim:syntax=m4 diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..627a7ff5 --- /dev/null +++ b/environment.yml @@ -0,0 +1,6 @@ +name: cysignals-dev +channels: + - conda-forge +dependencies: + - meson-python + - cython diff --git a/example/cysignals_example.pyx b/example/cysignals_example.pyx index 321c0063..afda5597 100644 --- a/example/cysignals_example.pyx +++ b/example/cysignals_example.pyx @@ -1,6 +1,3 @@ -# distutils: language = c++ -# cython: language_level = 3 - from cysignals.signals cimport sig_check from cysignals.memory cimport check_allocarray diff --git a/example/meson.build b/example/meson.build new file mode 100644 index 00000000..e40c614c --- /dev/null +++ b/example/meson.build @@ -0,0 +1,31 @@ +project('cysignals_example', 'cython', 'cpp') + +py = import('python').find_installation() + +inc_cysignals = run_command( + py, + [ + '-c', + ''' +import cysignals +print(cysignals.__file__.replace('__init__.py', '')) + '''.strip(), + ], + check: true, +).stdout().strip() +cysignals = declare_dependency(include_directories: inc_cysignals) + +# Make declarations in Cython code available to C include files +add_project_arguments( + '-X preliminary_late_includes_cy28=True', + language: 'cython', +) + + +py.extension_module('cysignals_example', + sources: ['cysignals_example.pyx'], + install: true, + dependencies: [cysignals], + override_options: ['cython_language=cpp'], + subdir: 'cysignals_example' +) diff --git a/example/pyproject.toml b/example/pyproject.toml index 5d7cd719..6922a84a 100644 --- a/example/pyproject.toml +++ b/example/pyproject.toml @@ -1,3 +1,7 @@ [build-system] -requires = ['setuptools', 'Cython>=0.28', 'cysignals'] -build-backend = "setuptools.build_meta" +requires = ["meson-python", "cython>=0.28"] +build-backend = "mesonpy" + +[project] +name = "cysignals-example" +version = "0.1.0" diff --git a/example/setup.py b/example/setup.py deleted file mode 100755 index 9b53c8a8..00000000 --- a/example/setup.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import setup -from setuptools.extension import Extension -from distutils.command.build import build as _build - - -class build(_build): - def run(self): - dist = self.distribution - ext_modules = dist.ext_modules - if ext_modules: - dist.ext_modules[:] = self.cythonize(ext_modules) - super().run() - - def cythonize(self, extensions): - from Cython.Build.Dependencies import cythonize - return cythonize(extensions) - - -setup( - name="cysignals_example", - version='1.0', - license='Public Domain', - ext_modules=[Extension("*", ["cysignals_example.pyx"])], - cmdclass=dict(build=build), -) diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..7784fce6 --- /dev/null +++ b/meson.build @@ -0,0 +1,121 @@ +project('cysignals', 'c', 'cpp', 'cython', + default_options: ['warning_level=3', 'cpp_std=c++17'] +) + +# Python +py_module = import('python') +py = py_module.find_installation(pure: false) +py_dep = py.dependency() + +# Compilers +cc = meson.get_compiler('c') +cxx = meson.get_compiler('cpp') + +is_windows = host_machine.system() == 'windows' +is_cygwin = host_machine.system() == 'cygwin' +is_msvc = cc.get_id() == 'msvc' + +# Set preprocessor macros +# Disable .c line numbers in exception tracebacks +add_project_arguments('-DCYTHON_CLINE_IN_TRACEBACK=0', language: 'c') +# Disable sanity checking in GNU libc +# This is required because of false positives in the longjmp() check +add_project_arguments('-U_FORTIFY_SOURCE', language: 'c') +# Make declarations in Cython code available to C include files +add_project_arguments( + '-X preliminary_late_includes_cy28=True', + language: 'cython', +) + +# Platform-specific settings +if is_cygwin + # On Cygwin FD_SETSIZE defaults to a rather low 64; we set it higher for use with PSelecter + # See https://github.com/sagemath/cysignals/pull/57 + add_project_arguments('-DFD_SETSIZE=512', language: 'c') +endif + +config = configuration_data() +# Toggle debug output +config.set('ENABLE_DEBUG_CYSIGNALS', get_option('debug') ? 1 : 0) + +config.set('HAVE_EXECINFO_H', cc.has_header('execinfo.h') ? 1 : 0) +config.set('HAVE_SYS_MMAN_H', cc.has_header('sys/mman.h') ? 1 : 0) +config.set('HAVE_SYS_PRCTL_H', cc.has_header('sys/prctl.h') ? 1 : 0) +config.set('HAVE_TIME_H', cc.has_header('time.h') ? 1 : 0) +config.set('HAVE_SYS_WAIT_H', cc.has_header('sys/wait.h') ? 1 : 0) +config.set('HAVE_WINDOWS_H', cc.has_header('windows.h') ? 1 : 0) + +config.set('HAVE_FORK', cc.has_function('fork') ? 1 : 0) +config.set('HAVE_KILL', cc.has_function('kill') ? 1 : 0) +config.set('HAVE_SIGPROCMASK', cc.has_function('sigprocmask') ? 1 : 0) +config.set('HAVE_SIGALTSTACK', cc.has_function('sigaltstack') ? 1 : 0) +config.set('HAVE_BACKTRACE', cc.has_function('backtrace') ? 1 : 0) + +# We add the "leal" instruction to reduce false positives in case some +# non-x86 architecture also has an "emms" instruction. +config.set('HAVE_EMMS', cc.links('int main() { asm("leal (%eax), %eax; emms"); return 0; }') ? 1 : 0) + +# Whether setjmp() saves the signal mask +setjmp_saves_mask = cc.links(''' +#include +#include +#include +jmp_buf env; +sigset_t set; +int main() { + sigemptyset(&set); + if (sigprocmask(SIG_SETMASK, &set, NULL)) return 2; + if (setjmp(env) == 0) { + sigaddset(&set, SIGFPE); + if (sigprocmask(SIG_SETMASK, &set, NULL)) return 3; + longjmp(env, 1); + } + if (sigprocmask(SIG_SETMASK, NULL, &set)) return 4; + return sigismember(&set, SIGFPE); +} +''') +gnulibc = cc.links(''' + #include + #ifndef __GLIBC__ + syntax error! + #endif + int main() { return 0; } +''') +# Define to 1 to use sigsetjmp() in sig_on(), as opposed to setjmp(). +config.set('CYSIGNALS_USE_SIGSETJMP', (setjmp_saves_mask or gnulibc) ? 1 : 0) + +# Check for atomic operations +# for _Atomic in C code +config.set('CYSIGNALS_C_ATOMIC', cc.links('static _Atomic int x;') ? 1 : 0) +# for _Atomic with OpenMP in C code +config.set('CYSIGNALS_C_ATOMIC_WITH_OPENMP', cc.links('static _Atomic int x;', args: ['-fopenmp']) ? 1 : 0) +# for _Atomic in C++ code +config.set('CYSIGNALS_CXX_ATOMIC', cxx.links('static _Atomic int x;') ? 1 : 0) +# for _Atomic with OpenMP in C++ code +config.set('CYSIGNALS_CXX_ATOMIC_WITH_OPENMP', cxx.links('static _Atomic int x;', args: ['-fopenmp']) ? 1 : 0) +# for std::atomic in C++ code +config.set('CYSIGNALS_STD_ATOMIC', cxx.links('#include \nstatic std::atomic x;') ? 1 : 0) +# for std::atomic with OpenMP in C++ code +config.set('CYSIGNALS_STD_ATOMIC_WITH_OPENMP', cxx.links('#include \nstatic std::atomic x;', args: ['-fopenmp']) ? 1 : 0) + +if is_windows + threads_dep = [] +else + threads_dep = dependency('threads') +endif + +subdir('src') + +pytest = py_module.find_installation(modules: ['pytest'], required: false) +if pytest.found() + test('pytest', pytest, args: ['-m', 'pytest'], workdir: meson.current_source_dir(), timeout: 300) +else + message('pytest not found, skipping tests') +endif + +build = py_module.find_installation(modules: ['build'], required: false) +if build.found() + test('example', py, args: ['-m', 'build', '--no-isolation', 'example'], workdir: meson.current_source_dir()) +else + message('build not found, skipping example') +endif diff --git a/pyproject.toml b/pyproject.toml index 162900f5..79d1a216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,37 @@ [build-system] -requires = ['setuptools', 'Cython>=0.28'] -build-backend = "setuptools.build_meta" +requires = ["meson-python", "cython>=0.28"] +build-backend = "mesonpy" + +[project] +name = "cysignals" +version = "1.12.0rc2" +description = "Interrupt and signal handling for Cython" +license = { file = "LICENSE" } +readme = { file = "README.rst", content-type = "text/x-rst" } +authors = [ + { name = "The Sage Developers", email = "sage-support@googlegroups.com" }, +] +classifiers = [ + "Development Status :: 6 - Mature", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: C", + "Programming Language :: Cython", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: System", + "Topic :: Software Development :: Debuggers", +] +urls = { Homepage = "https://github.com/sagemath/cysignals" } +requires-python = ">=3.9" + +[tool.pytest.ini_options] +addopts = "--doctest-modules --import-mode importlib" +norecursedirs = "builddir docs example" diff --git a/requirements.txt b/requirements.txt index 19132679..f7e33c72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -setuptools +meson-python +build wheel Cython Sphinx flake8 +pytest>=8.0.0 diff --git a/rundoctests.py b/rundoctests.py deleted file mode 100755 index 8db318ae..00000000 --- a/rundoctests.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -# Run doctests for cysignals -# -# We add the ELLIPSIS flag by default and we run all tests even if -# one fails. -# -import os -import sys -import doctest -from doctest import DocTestParser, Example, SKIP -from multiprocessing import Process - -flags = doctest.ELLIPSIS -timeout = 600 - -filenames = sys.argv[1:] -if os.name == 'nt': - notinwindows = set(['src/cysignals/pysignals.pyx', - 'src/cysignals/alarm.pyx', - 'src/cysignals/pselect.pyx']) - - filenames = [f for f in filenames if f not in notinwindows] - - -# Add an option to flag doctests which should be skipped depending on -# the platform -SKIP_WINDOWS = doctest.register_optionflag("SKIP_WINDOWS") -SKIP_CYGWIN = doctest.register_optionflag("SKIP_CYGWIN") -SKIP_POSIX = doctest.register_optionflag("SKIP_POSIX") - -skipflags = set() - -if os.name == 'posix': - skipflags.add(SKIP_POSIX) -elif os.name == 'nt': - skipflags.add(SKIP_WINDOWS) -if sys.platform == 'cygwin': - skipflags.add(SKIP_CYGWIN) - - -class CysignalsDocTestParser(DocTestParser): - def parse(self, *args, **kwargs): - examples = DocTestParser.parse(self, *args, **kwargs) - for example in examples: - if not isinstance(example, Example): - continue - if any(flag in example.options for flag in skipflags): - example.options[SKIP] = True - - return examples - - -parser = CysignalsDocTestParser() - - -print(f"Doctesting {len(filenames)} files.") - - -if os.name != 'nt': - import resource - # Limit stack size to avoid errors in stack overflow doctest - stacksize = 1 << 20 - if sys.platform != 'darwin': - # Work around a very strange OS X. - # This was discovered at https://github.com/sagemath/cysignals/issues/71. - # The original solution did not last very long and the issue reappeared. - resource.setrlimit(resource.RLIMIT_STACK, (stacksize, stacksize)) - - # Disable core dumps - resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) - - -def testfile(file): - # Child process - try: - if sys.platform == 'darwin': - from cysignals.signals import _setup_alt_stack - _setup_alt_stack() - failures, _ = doctest.testfile(file, module_relative=False, - optionflags=flags, parser=parser) - if not failures: - os._exit(0) - except BaseException as E: - print(E) - finally: - os._exit(23) - - -if __name__ == "__main__": - success = True - for f in filenames: - print(f) - sys.stdout.flush() - - # Test every file in a separate process (like in SageMath) to avoid - # side effects from doctests. - p = Process(target=testfile, args=(f,)) - p.start() - p.join(timeout) - - status = p.exitcode - - if p.is_alive(): - p.terminate() - print(f"Doctest {f} terminated. Timeout limit exceeded " - f"(>{timeout}s)", file=sys.stderr) - success = False - elif status != 0: - success = False - if status < 0: - print(f"killed by signal: {abs(status)}", file=sys.stderr) - elif status != 23: - print(f"bad exit: {status}", file=sys.stderr) - - sys.exit(0 if success else 1) diff --git a/setup.py b/setup.py deleted file mode 100755 index 79531705..00000000 --- a/setup.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import shlex -import subprocess -import sys -import sysconfig - -# When building with readthedocs, install the requirements too -if "READTHEDOCS" in os.environ: - reqs = "requirements.txt" - if os.path.isfile(reqs): - subprocess.check_call( - [sys.executable, "-m", "pip", "install", "-r", reqs]) - -from setuptools import setup, Command -from setuptools.command.build_py import build_py as _build_py -# Explicitly use the build_ext from distutils for now so we don't get the -# old one that tries to wrap Cython for us. This shold be fixed with newer -# versions of setuptools. -from distutils.command.build_ext import build_ext as _build_ext -from setuptools.command.bdist_egg import bdist_egg as _bdist_egg -from setuptools.extension import Extension - -import warnings -warnings.simplefilter("always") - -from glob import glob - -opj = os.path.join - - -cythonize_dir = "build" - -macros = [ - # Disable .c line numbers in exception tracebacks - ("CYTHON_CLINE_IN_TRACEBACK", 0), -] - -depends = glob(opj("src", "cysignals", "*.h")) - -if sys.platform == 'cygwin': - # On Cygwin FD_SETSIZE defaults to a rather low 64; we set it higher - # for use with PSelecter - # See https://github.com/sagemath/cysignals/pull/57 - macros.append(('FD_SETSIZE', 512)) - depends.append(opj("src", "cysignals", "implementation_cygwin.c")) - -# Disable sanity checking in GNU libc. This is required because of -# false positives in the longjmp() check. -undef_macros = ["_FORTIFY_SOURCE"] - -kwds = dict(include_dirs=[opj("src"), - opj("src", "cysignals")], - depends=depends, - define_macros=macros, - undef_macros=undef_macros, - extra_compile_args=["-pthread", "-Wp,-U_FORTIFY_SOURCE"], - extra_link_args=["-pthread"],) - -extensions = [ - Extension("cysignals.signals", ["src/cysignals/signals.pyx"], **kwds), - Extension("cysignals.pysignals", ["src/cysignals/pysignals.pyx"], **kwds), - Extension("cysignals.alarm", ["src/cysignals/alarm.pyx"], **kwds), - Extension("cysignals.pselect", ["src/cysignals/pselect.pyx"], **kwds), - Extension("cysignals.tests", ["src/cysignals/tests.pyx"], **kwds), -] - - -classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - ('License :: OSI Approved :: ' - 'GNU Lesser General Public License v3 or later (LGPLv3+)'), - 'Operating System :: POSIX', - 'Programming Language :: C', - 'Programming Language :: Cython', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: System', - 'Topic :: Software Development :: Debuggers', -] - - -def is_newer_than(file1, file2): - """ - Return True if file1 is newer than file2. - - Also returns True if file2 does not exist (file1 *must* exist). - """ - - if not os.path.isfile(file2): - return True - - return os.stat(file1).st_mtime > os.stat(file2).st_mtime - - -class configure(Command): - description = "generate and run the configure script" - user_options = [ - ('configure-flags=', 'c', 'additional flags to pass to --configure') - ] - - # Hard-coded list of outputs generated by ./configure; this should be - # kept in sync with configure.ac. We could also perhaps search the - # source tree for .in files, but this list does not change often. - configure_outputs = [ - opj('src', 'config.h'), - opj('src', 'cysignals', 'cysignals_config.h'), - opj('src', 'cysignals', 'signals.pxd') - ] - - # NOTE: Perhaps --prefix and other configure flags can be determined - # based on other setup.py arguments, but so far cysignals has not really - # needed this (it always just ran ./configure without arguments by default) - # so I think we can live without this for now. - def initialize_options(self): - self.configure_flags = None - - def finalize_options(self): - if self.configure_flags is None: - self.configure_flags = [] - else: - self.configure_flags = shlex.split(self.configure_flags) - - def run(self): - # Re-make the configure script if necessary - if is_newer_than('configure.ac', 'configure'): - subprocess.check_call(['make', 'configure']) - - def needs_reconfigure(filename): - # If the configure script or the file's input template are - # newer, configure should be re-run - return (is_newer_than('configure', filename) or - is_newer_than(filename + '.in', filename)) - - if any(needs_reconfigure(f) for f in self.configure_outputs): - subprocess.check_call(['sh', 'configure', - f'CC={sysconfig.get_config_var("CC")}', - f'CXX={sysconfig.get_config_var("CXX")}'] - + self.configure_flags) - - -class build_ext(_build_ext): - def run(self): - # Make sure configure has been run - self.run_command('configure') - dist = self.distribution - ext_modules = dist.ext_modules - if ext_modules: - dist.ext_modules[:] = self.cythonize(ext_modules) - - super().run() - - def cythonize(self, extensions): - # Run Cython with -Werror on continuous integration services - # with Python 3.6 or later - from Cython.Compiler import Options - Options.warning_errors = False - - from Cython.Build.Dependencies import cythonize - return cythonize(extensions, - build_dir=cythonize_dir, - include_path=["src", os.path.join(cythonize_dir, "src")], - compiler_directives=dict( - binding=True, - language_level=2, - )) - - -class build_py(_build_py): - # Override the build_py command to run configure as a prerequisite - def run(self): - self.run_command('configure') - super().run() - - -class no_egg(_bdist_egg): - def run(self): - from distutils.errors import DistutilsOptionError - raise DistutilsOptionError( - "The package cysignals will not function correctly when built as " - "egg. Therefore, it cannot be installed using " - "'python setup.py install' or 'easy_install'. Instead, use " - "'pip install' to install cysignals.") - - -with open("VERSION") as f: - VERSION = f.read().strip() - -with open('README.rst') as f: - README = f.read() - - -setup( - name="cysignals", - author=u"Martin R. Albrecht, François Bissey, Volker Braun, Jeroen Demeyer", - author_email="sage-devel@googlegroups.com", - version=VERSION, - url="https://github.com/sagemath/cysignals", - license="GNU Lesser General Public License, version 3 or later", - description="Interrupt and signal handling for Cython", - long_description=README, - long_description_content_type='text/x-rst', - classifiers=classifiers, - ext_modules=extensions, - packages=["cysignals"], - package_dir={"": "src"}, - package_data={"cysignals": ["*.pxd", "*.h"]}, - data_files=[(opj("share", "cysignals"), [opj("src", "scripts", "cysignals-CSI-helper.py")])], - scripts=glob(opj("src", "scripts", "cysignals-CSI")), - cmdclass=dict( - configure=configure, - build_py=build_py, - build_ext=build_ext, - bdist_egg=no_egg - ), -) diff --git a/src/conftest.py b/src/conftest.py new file mode 100755 index 00000000..693071c3 --- /dev/null +++ b/src/conftest.py @@ -0,0 +1,41 @@ +import pathlib + +from _pytest.nodes import Collector +from _pytest.doctest import DoctestModule + + +def pytest_collect_file( + file_path: pathlib.Path, + parent: Collector, +): + """Collect doctests in cython files and run them as test modules.""" + config = parent.config + if file_path.suffix == ".pyx": + if config.option.doctestmodules: + return DoctestModule.from_parent(parent, path=file_path) + return None + + +# Need to import cysignals to initialize it +import cysignals # noqa: E402 + +try: + import cysignals.alarm +except ImportError: + pass +try: + import cysignals.signals +except ImportError: + pass +try: + import cysignals.pselect +except ImportError: + pass +try: + import cysignals.pysignals +except ImportError: + pass +try: + import cysignals.tests # noqa: F401 +except ImportError: + pass diff --git a/src/cysignals/implementation.c b/src/cysignals/implementation.c index c3358a64..b940b350 100644 --- a/src/cysignals/implementation.c +++ b/src/cysignals/implementation.c @@ -253,11 +253,82 @@ static inline void sigdie_for_sig(int sig, int inside) } } +/* Cygwin-specific implementation details */ +#if defined(__CYGWIN__) && defined(__x86_64__) +#include +LONG WINAPI win32_altstack_handler(EXCEPTION_POINTERS *exc) +{ + int sig = 0; + /* If we're not handling a signal there is no reason to execute the + * following code; otherwise it can be run in inappropriate contexts + * such as when a STATUS_ACCESS_VIOLATION is raised when accessing + * uncommitted memory in an mmap created with MAP_NORESERVE. See + * discussion at https://trac.sagemath.org/ticket/27214#comment:11 + * + * Unfortunately, when handling an exception that occurred while + * handling another signal, there is currently no way (through Cygwin) + * to distinguish this case from a legitimate segfault. + */ + if (!cysigs.inside_signal_handler) { + return ExceptionContinueExecution; + } -/* Additional platform-specific implementation code */ -#if defined(__CYGWIN__) -#include "implementation_cygwin.c" -#endif + /* Logic cribbed from Cygwin for mapping common Windows exception + * codes to the relevant signal numbers: + * https://cygwin.com/git/gitweb.cgi?p=newlib-cygwin.git;a=blob;f=winsup/cygwin/exceptions.cc;h=77eff05707f95f7277974fadbccf0e74223d8d1c;hb=HEAD#l650 + * Unfortunately there is no external API by which to access this + * mapping (a la cygwin_internal(CW_GET_ERRNO_FROM_WINERROR, ...)) */ + switch (exc->ExceptionRecord->ExceptionCode) { + case STATUS_FLOAT_DENORMAL_OPERAND: + case STATUS_FLOAT_DIVIDE_BY_ZERO: + case STATUS_FLOAT_INVALID_OPERATION: + case STATUS_FLOAT_STACK_CHECK: + case STATUS_FLOAT_INEXACT_RESULT: + case STATUS_FLOAT_OVERFLOW: + case STATUS_FLOAT_UNDERFLOW: + case STATUS_INTEGER_DIVIDE_BY_ZERO: + case STATUS_INTEGER_OVERFLOW: + sig = SIGFPE; + break; + case STATUS_ILLEGAL_INSTRUCTION: + case STATUS_PRIVILEGED_INSTRUCTION: + case STATUS_NONCONTINUABLE_EXCEPTION: + sig = SIGILL; + break; + case STATUS_TIMEOUT: + sig = SIGALRM; + break; + case STATUS_GUARD_PAGE_VIOLATION: + case STATUS_DATATYPE_MISALIGNMENT: + sig = SIGBUS; + break; + case STATUS_ACCESS_VIOLATION: + /* In the case of this last resort exception handling we can + * probably safely assume this should be a SIGSEGV; + * other access violations would have already been handled by + * Cygwin before we wound up on the alternate stack */ + case STATUS_STACK_OVERFLOW: + case STATUS_ARRAY_BOUNDS_EXCEEDED: + case STATUS_IN_PAGE_ERROR: + case STATUS_NO_MEMORY: + case STATUS_INVALID_DISPOSITION: + sig = SIGSEGV; + break; + case STATUS_CONTROL_C_EXIT: + sig = SIGINT; + break; + } + + sigdie_for_sig(sig, 1); + return ExceptionContinueExecution; +} + + +static void cygwin_setup_alt_stack() { + AddVectoredContinueHandler(0, win32_altstack_handler); +} + +#endif /* CYGWIN && __x86_64__ */ /* Handler for SIGHUP, SIGINT, SIGALRM @@ -520,18 +591,11 @@ static void setup_alt_stack(void) #if HAVE_SIGALTSTACK /* Space for the alternate signal stack. The size should be * of the form MINSIGSTKSZ + constant. The constant is chosen rather - * ad hoc but sufficiently large. - * MINSIGSTKSZ is no longer a constant starting with glibc 2.34. */ + * ad hoc but sufficiently large. */ stack_t ss; -#if MINSIGSTKSZ_IS_CONSTANT - static char alt_stack[MINSIGSTKSZ + 5120 + BACKTRACELEN * sizeof(void*)]; - ss.ss_sp = alt_stack; - ss.ss_size = sizeof(alt_stack); -#else size_t stack_size = MINSIGSTKSZ + 5120 + BACKTRACELEN * sizeof(void*); ss.ss_sp = malloc(stack_size); ss.ss_size = stack_size; -#endif if (ss.ss_sp == NULL) {perror("cysignals malloc alt signal stack"); exit(1);} ss.ss_flags = 0; if (sigaltstack(&ss, NULL) == -1) {perror("cysignals sigaltstack"); exit(1);} @@ -702,7 +766,6 @@ static void sigdie(int sig, const char* s) exit(128 + sig); } - /* Finally include the macros and inline functions for use in * signals.pyx. These require some of the above functions, therefore * this include must come at the end of this file. */ diff --git a/src/cysignals/implementation_cygwin.c b/src/cysignals/implementation_cygwin.c deleted file mode 100644 index 3b181143..00000000 --- a/src/cysignals/implementation_cygwin.c +++ /dev/null @@ -1,77 +0,0 @@ -/* Cygwin-specific implementation details */ - -#if defined(__CYGWIN__) && defined(__x86_64__) -#include -LONG WINAPI win32_altstack_handler(EXCEPTION_POINTERS *exc) -{ - int sig = 0; - /* If we're not handling a signal there is no reason to execute the - * following code; otherwise it can be run in inappropriate contexts - * such as when a STATUS_ACCESS_VIOLATION is raised when accessing - * uncommitted memory in an mmap created with MAP_NORESERVE. See - * discussion at https://trac.sagemath.org/ticket/27214#comment:11 - * - * Unfortunately, when handling an exception that occurred while - * handling another signal, there is currently no way (through Cygwin) - * to distinguish this case from a legitimate segfault. - */ - if (!cysigs.inside_signal_handler) { - return ExceptionContinueExecution; - } - - /* Logic cribbed from Cygwin for mapping common Windows exception - * codes to the relevant signal numbers: - * https://cygwin.com/git/gitweb.cgi?p=newlib-cygwin.git;a=blob;f=winsup/cygwin/exceptions.cc;h=77eff05707f95f7277974fadbccf0e74223d8d1c;hb=HEAD#l650 - * Unfortunately there is no external API by which to access this - * mapping (a la cygwin_internal(CW_GET_ERRNO_FROM_WINERROR, ...)) */ - switch (exc->ExceptionRecord->ExceptionCode) { - case STATUS_FLOAT_DENORMAL_OPERAND: - case STATUS_FLOAT_DIVIDE_BY_ZERO: - case STATUS_FLOAT_INVALID_OPERATION: - case STATUS_FLOAT_STACK_CHECK: - case STATUS_FLOAT_INEXACT_RESULT: - case STATUS_FLOAT_OVERFLOW: - case STATUS_FLOAT_UNDERFLOW: - case STATUS_INTEGER_DIVIDE_BY_ZERO: - case STATUS_INTEGER_OVERFLOW: - sig = SIGFPE; - break; - case STATUS_ILLEGAL_INSTRUCTION: - case STATUS_PRIVILEGED_INSTRUCTION: - case STATUS_NONCONTINUABLE_EXCEPTION: - sig = SIGILL; - break; - case STATUS_TIMEOUT: - sig = SIGALRM; - break; - case STATUS_GUARD_PAGE_VIOLATION: - case STATUS_DATATYPE_MISALIGNMENT: - sig = SIGBUS; - break; - case STATUS_ACCESS_VIOLATION: - /* In the case of this last resort exception handling we can - * probably safely assume this should be a SIGSEGV; - * other access violations would have already been handled by - * Cygwin before we wound up on the alternate stack */ - case STATUS_STACK_OVERFLOW: - case STATUS_ARRAY_BOUNDS_EXCEEDED: - case STATUS_IN_PAGE_ERROR: - case STATUS_NO_MEMORY: - case STATUS_INVALID_DISPOSITION: - sig = SIGSEGV; - break; - case STATUS_CONTROL_C_EXIT: - sig = SIGINT; - break; - } - - sigdie_for_sig(sig, 1); - return ExceptionContinueExecution; -} - - -static void cygwin_setup_alt_stack() { - AddVectoredContinueHandler(0, win32_altstack_handler); -} - -#endif /* CYGWIN && __x86_64__ */ diff --git a/src/cysignals/meson.build b/src/cysignals/meson.build new file mode 100644 index 00000000..a053d22f --- /dev/null +++ b/src/cysignals/meson.build @@ -0,0 +1,34 @@ +configure_file(output: 'cysignals_config.h', configuration: config, install_dir: py.get_install_dir(pure: false) / 'cysignals', install: true) + +py.install_sources( + '__init__.py', + 'memory.pxd', + 'pysignals.pxd', + 'signals.pxd', + 'struct_signals.h', + 'macros.h', + subdir: 'cysignals' +) + +extensions = { + 'alarm': files('alarm.pyx'), + 'pselect': files('pselect.pyx'), + 'pysignals': files('pysignals.pyx'), + 'signals': files('signals.pyx'), + 'tests': files('tests.pyx'), +} + +foreach name, pyx : extensions + if name != 'signals' and is_windows + # These modules are not supported on Windows + continue + endif + + py.extension_module(name, + pyx, + include_directories: [include_directories('.'), src], + dependencies: [py_dep, threads_dep], + install: true, + subdir: 'cysignals' + ) +endforeach diff --git a/src/cysignals/pselect.pyx b/src/cysignals/pselect.pyx index 04fcf31d..07d6b14a 100644 --- a/src/cysignals/pselect.pyx +++ b/src/cysignals/pselect.pyx @@ -88,13 +88,14 @@ def interruptible_sleep(double seconds): >>> import signal, time >>> def alarm_handler(sig, frame): ... pass - >>> _ = signal.signal(signal.SIGALRM, alarm_handler) + >>> old_handler = signal.signal(signal.SIGALRM, alarm_handler) >>> t0 = time.time() >>> _ = signal.alarm(1) >>> interruptible_sleep(2) >>> t = time.time() - t0 >>> (0.9 <= t <= 1.9) or t True + >>> _ = signal.signal(signal.SIGALRM, old_handler) TESTS:: @@ -346,6 +347,7 @@ cdef class PSelecter: The file ``/dev/null`` should always be available for reading and writing:: + >>> import os >>> from cysignals.pselect import PSelecter >>> f = open(os.devnull, "r+") >>> sel = PSelecter() @@ -389,6 +391,7 @@ cdef class PSelecter: Open a file and close it, but save the (invalid) file descriptor:: + >>> import os >>> f = open(os.devnull, "r") >>> n = f.fileno() >>> f.close() diff --git a/src/cysignals/pysignals.pyx b/src/cysignals/pysignals.pyx index b0c3f4fa..4a273264 100644 --- a/src/cysignals/pysignals.pyx +++ b/src/cysignals/pysignals.pyx @@ -168,7 +168,7 @@ def getossignal(int sig): >>> getossignal(signal.SIGUSR1) >>> def handler(*args): pass - >>> _ = signal.signal(signal.SIGUSR1, handler) + >>> old_handler = signal.signal(signal.SIGUSR1, handler) >>> getossignal(signal.SIGUSR1) @@ -182,6 +182,7 @@ def getossignal(int sig): False >>> getossignal(signal.SIGABRT) == python_os_handler False + >>> _ = signal.signal(signal.SIGUSR1, old_handler) TESTS:: @@ -274,11 +275,13 @@ def setsignal(int sig, action, osaction=None): got signal >>> setsignal(signal.SIGILL, signal.SIG_DFL) - >>> _ = setsignal(signal.SIGALRM, signal.SIG_DFL, signal.SIG_IGN) + >>> old_handler = getossignal(signal.SIGALRM) + >>> old_py_handler = setsignal(signal.SIGALRM, signal.SIG_DFL, signal.SIG_IGN) >>> os.kill(os.getpid(), signal.SIGALRM) >>> _ = setsignal(signal.SIGALRM, handler, getossignal(signal.SIGSEGV)) >>> os.kill(os.getpid(), signal.SIGALRM) got signal + >>> _ = setsignal(signal.SIGALRM, old_py_handler, old_handler) TESTS:: diff --git a/src/cysignals/signals.pxd.in b/src/cysignals/signals.pxd similarity index 97% rename from src/cysignals/signals.pxd.in rename to src/cysignals/signals.pxd index aefb1a30..76320f74 100644 --- a/src/cysignals/signals.pxd.in +++ b/src/cysignals/signals.pxd @@ -1,5 +1,3 @@ -# cython: preliminary_late_includes_cy28=True -# distutils: extra_link_args = @LIBS@ #***************************************************************************** # cysignals is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -54,6 +52,8 @@ cdef int add_custom_signals(int (*custom_signal_is_blocked)() noexcept, void (*custom_signal_unblock)() noexcept, void (*custom_set_pending_signal)(int) noexcept) except -1 +cdef int sig_raise_exception "sig_raise_exception"(int sig, const char* msg) except 0 with gil + # This function does nothing, but it is declared cdef except *, so it # can be used to make Cython check whether there is a pending exception # (PyErr_Occurred() is non-NULL). To Cython, it will look like diff --git a/src/cysignals/signals.pyx b/src/cysignals/signals.pyx index 5b841364..6e7376e2 100644 --- a/src/cysignals/signals.pyx +++ b/src/cysignals/signals.pyx @@ -1,4 +1,3 @@ -# cython: preliminary_late_includes_cy28=True r""" Interrupt and signal handling @@ -105,10 +104,10 @@ class AlarmInterrupt(KeyboardInterrupt): >>> from cysignals import AlarmInterrupt >>> from signal import alarm + >>> from time import sleep >>> try: ... _ = alarm(1) - ... while True: - ... pass + ... sleep(2) ... except AlarmInterrupt: ... print("alarm!") alarm! diff --git a/src/cysignals/tests.pyx b/src/cysignals/tests.pyx index 34629152..547095f8 100644 --- a/src/cysignals/tests.pyx +++ b/src/cysignals/tests.pyx @@ -1,4 +1,3 @@ -# cython: preliminary_late_includes_cy28=True """ Test interrupt and signal handling @@ -8,15 +7,6 @@ We disable crash debugging for this test run:: >>> import os >>> os.environ["CYSIGNALS_CRASH_NDEBUG"] = "" - -Verify that the doctester was set up correctly:: - - >>> import os - >>> os.name == "posix" # doctest: +SKIP_POSIX - False - >>> os.name == "nt" # doctest: +SKIP_WINDOWS - False - """ #***************************************************************************** @@ -787,8 +777,11 @@ def test_interrupt_bomb(long n=100, long p=10): TESTS:: + >>> import sys, pytest + >>> if sys.platform == 'cygwin': + ... pytest.skip('this doctest does not work on Windows') >>> from cysignals.tests import * - >>> test_interrupt_bomb() # doctest: +SKIP_CYGWIN + >>> test_interrupt_bomb() Received ... interrupts """ @@ -1297,11 +1290,12 @@ def test_thread_sig_block(long delay=DEFAULT_DELAY): sig_off() -cdef void* func_thread_sig_block(void* ignored) noexcept nogil: +cdef void* func_thread_sig_block(void* ignored) noexcept with gil: # This is executed by the two threads spawned by test_thread_sig_block() cdef int n for n in range(1000000): sig_block() if not (1 <= cysigs.block_sigint <= 2): + PyErr_SetString(RuntimeError, "sig_block() is not thread-safe") sig_error() sig_unblock() diff --git a/src/cysignals/tests_helper.c b/src/cysignals/tests_helper.c index 1ce600e2..09b8645f 100644 --- a/src/cysignals/tests_helper.c +++ b/src/cysignals/tests_helper.c @@ -63,7 +63,7 @@ static void ms_sleep(long ms) #if HAVE_UNISTD_H usleep(1000 * ms); #else - Sleep(ms); + sleep(ms); #endif } diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 00000000..39013a51 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,4 @@ +configure_file(output: 'config.h', configuration: config) +src = include_directories('.') +subdir('cysignals') +subdir('scripts') diff --git a/src/scripts/conftest.py b/src/scripts/conftest.py new file mode 100755 index 00000000..dc724f44 --- /dev/null +++ b/src/scripts/conftest.py @@ -0,0 +1 @@ +collect_ignore = ["cysignals-CSI-helper.py"] diff --git a/src/scripts/meson.build b/src/scripts/meson.build new file mode 100644 index 00000000..6cacd68c --- /dev/null +++ b/src/scripts/meson.build @@ -0,0 +1 @@ +install_data('cysignals-CSI-helper.py', install_dir: py.get_install_dir(pure: false) / 'cysignals' / 'data')