diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml index ed35a2897..25ef39636 100644 --- a/.github/workflows/publish_to_pypi.yml +++ b/.github/workflows/publish_to_pypi.yml @@ -1,10 +1,13 @@ -name: Publish source package to TestPyPI -on: [push] +name: Build and publish to TestPyPI or PyPI +on: [push, pull_request] jobs: build-n-publish: - name: Build and publish source package to TestPyPI and PyPI - runs-on: ubuntu-latest + name: Build wheels on ${{ matrix.os }} and publish to (Test)PyPI + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-20.04, windows-2019, macOS-10.15 ] steps: - uses: actions/checkout@v2 with: @@ -13,20 +16,38 @@ jobs: uses: actions/setup-python@v2 with: python-version: '3.x' - - name: Install dependencies + - name: Build source tarball (only on Linux) run: | python -m pip install --upgrade pip - pip install setuptools cython - - name: Build source tarball - run: python setup.py sdist --formats=gztar --with-cython --fail-on-error - - name: Publish distribution 📦 to Test PyPI - if: ${{ ! startsWith(github.ref, 'refs/tags') }} - uses: pypa/gh-action-pypi-publish@master + python -m pip install "cython>=0.29" numpy>=1.15 setuptools + python setup.py sdist --formats=gztar --with-cython --fail-on-error + if: ${{ startsWith(matrix.os, 'ubuntu-') }} + - name: Build wheels + uses: joerick/cibuildwheel@v1.9.0 with: - password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ + output-dir: dist + env: + CIBW_BEFORE_BUILD: pip install --only-binary numpy "cython>=0.29" numpy>=1.15 setuptools + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.7" + CIBW_ARCHS: auto64 + CIBW_ARCHS_MACOS: x86_64 universal2 + CIBW_TEST_SKIP: '*_arm64 *_universal2:arm64' + CIBW_SKIP: pp* + CIBW_TEST_COMMAND: python {project}/dev/continuous-integration/run_simple_test.py + CIBW_TEST_REQUIRES: pytest + - name: Publish distribution 📦 to Test PyPI + if: github.ref == 'refs/heads/master' + run: | + pip install twine + twine upload -r testpypi dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.test_pypi_password }} - name: Publish distribution release 📦 to PyPI - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.pypi_password }} + if: ${{ startsWith(github.ref, 'refs/tags') }} + run: | + pip install twine + twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.pypi_password }} diff --git a/brian2/codegen/cpp_prefs.py b/brian2/codegen/cpp_prefs.py index 88ff23a44..c22bf1a0c 100644 --- a/brian2/codegen/cpp_prefs.py +++ b/brian2/codegen/cpp_prefs.py @@ -19,7 +19,7 @@ import tempfile from brian2.core.preferences import prefs, BrianPreference -from brian2.utils.logger import get_logger +from brian2.utils.logger import get_logger, std_silent __all__ = ['get_compiler_and_args', 'get_msvc_env', 'compiler_supports_c99', 'C99Check'] @@ -188,6 +188,38 @@ '''), ) +# check whether compiler supports a flag +# Adapted from https://github.com/pybind/pybind11/ +def _determine_flag_compatibility(compiler, flagname): + import tempfile + from distutils.errors import CompileError + with tempfile.NamedTemporaryFile('w', suffix='.cpp') as f, std_silent(): + f.write('int main (int argc, char **argv) { return 0; }') + try: + compiler.compile([f.name], extra_postargs=[flagname]) + except CompileError: + logger.warn(f'Removing unsupported flag \'{flagname}\' from ' + f'compiler flags.') + return False + return True + +_compiler_flag_compatibility = {} +def has_flag(compiler, flagname): + if compiler.compiler_type == 'msvc': + # MSVC does not raise an error for illegal flags, so determining + # whether it accepts a flag would mean parsing the output for warnings + # This is non-trivial so we don't do it (the main reason to check + # flags in the first place are differences between gcc and clang) + return True + else: + compiler_exe = ' '.join(compiler.executables['compiler_cxx']) + + if (compiler_exe, flagname) not in _compiler_flag_compatibility: + compatibility = _determine_flag_compatibility(compiler, flagname) + _compiler_flag_compatibility[(compiler_exe, flagname)] = compatibility + + return _compiler_flag_compatibility[(compiler_exe, flagname)] + def get_compiler_and_args(): ''' @@ -200,8 +232,20 @@ def get_compiler_and_args(): if extra_compile_args is None: if compiler in ('gcc', 'unix'): extra_compile_args = prefs['codegen.cpp.extra_compile_args_gcc'] - if compiler == 'msvc': + elif compiler == 'msvc': extra_compile_args = prefs['codegen.cpp.extra_compile_args_msvc'] + else: + extra_compile_args = [] + logger.warn(f'Unsupported compiler \'{compiler}\'.') + + from distutils.ccompiler import new_compiler + from distutils.sysconfig import customize_compiler + compiler_obj = new_compiler(compiler=compiler, verbose=0) + customize_compiler(compiler_obj) + extra_compile_args = [flag + for flag in extra_compile_args + if has_flag(compiler_obj, flag)] + return compiler, extra_compile_args @@ -222,7 +266,7 @@ def get_msvc_env(): arch_name=arch_name) return None, vcvars_cmd - # Search for MSVC environemtn if not already cached + # Search for MSVC environment if not already cached if _msvc_env is None: try: _msvc_env = msvc.msvc14_get_vc_env(arch_name) diff --git a/brian2/tests/test_codegen.py b/brian2/tests/test_codegen.py index f728f8b82..ee169965f 100644 --- a/brian2/tests/test_codegen.py +++ b/brian2/tests/test_codegen.py @@ -6,7 +6,7 @@ import pytest from brian2 import prefs, clear_cache, _cache_dirs_and_extensions -from brian2.codegen.cpp_prefs import compiler_supports_c99 +from brian2.codegen.cpp_prefs import compiler_supports_c99, get_compiler_and_args from brian2.codegen.optimisation import optimise_statements from brian2.codegen.translation import (analyse_identifiers, get_identifiers_recursively, @@ -21,6 +21,7 @@ from brian2.devices.device import auto_target, device from brian2.units.fundamentalunits import Unit from brian2.units import second, ms +from brian2.utils.logger import catch_logs FakeGroup = namedtuple('FakeGroup', ['variables']) @@ -452,6 +453,32 @@ def test_compiler_c99(): assert c99_support +def test_cpp_flags_support(): + from distutils.ccompiler import get_default_compiler + compiler = get_default_compiler() + old_prefs = prefs['codegen.cpp.extra_compile_args'] + + # Should always be supported + if compiler in ('gcc', 'unix'): + prefs['codegen.cpp.extra_compile_args'] = ['-w'] + else: + prefs['codegen.cpp.extra_compile_args'] = ['/w'] + _, compile_args = get_compiler_and_args() + assert compile_args == prefs['codegen.cpp.extra_compile_args'] + + # Should never be supported and raise a warning + if compiler in ('gcc', 'unix'): + prefs['codegen.cpp.extra_compile_args'] = ['-invalidxyz'] + else: + prefs['codegen.cpp.extra_compile_args'] = ['/invalidxyz'] + with catch_logs() as l: + _, compile_args = get_compiler_and_args() + assert len(l) == 1 and l[0][0] == 'WARNING' + assert compile_args == [] + + prefs['codegen.cpp.extra_compile_args'] = old_prefs + + if __name__ == '__main__': test_auto_target() test_analyse_identifiers() diff --git a/dev/continuous-integration/run_simple_test.py b/dev/continuous-integration/run_simple_test.py new file mode 100644 index 000000000..42836a84f --- /dev/null +++ b/dev/continuous-integration/run_simple_test.py @@ -0,0 +1,8 @@ +# Run a simple test that uses the main simulation elements and force code +# generation to use Cython +from brian2 import prefs +from brian2.tests.test_synapses import test_transmission_simple + +prefs.codegen.target = 'cython' + +test_transmission_simple()