Skip to content
This repository has been archived by the owner on May 25, 2022. It is now read-only.

Commit

Permalink
Merge 2f7f8e8 into 7f8d595
Browse files Browse the repository at this point in the history
  • Loading branch information
jamienoss committed Apr 20, 2018
2 parents 7f8d595 + 2f7f8e8 commit 24d18d6
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 36 deletions.
28 changes: 28 additions & 0 deletions .travis.yml
Expand Up @@ -36,6 +36,24 @@ matrix:
env: PYTHON_VERSION=3.6 PIP_DEPENDENCIES='git+https://github.com/sphinx-doc/sphinx.git#egg=sphinx coveralls'
CONDA_DEPENDENCIES="setuptools cython numpy pytest-cov"

# Test conda's clang
- os: osx
env:
- PYTHON_VERSION=3.5
- CONDA_DEPENDENCIES="setuptools sphinx cython numpy pytest-cov clang llvm-openmp"
before_install:
- export TEST_OPENMP="True"
- export CC=/Users/travis/miniconda/envs/test/bin/clang

# Test gcc on OSX
- os: osx
env:
- PYTHON_VERSION=3.5
- CONDA_DEPENDENCIES="setuptools sphinx cython numpy pytest-cov gcc"
before_install:
- export TEST_OPENMP="True"
- export CC=/Users/travis/miniconda/envs/test/bin/gcc

# Uncomment the following if there are issues in setuptools that we
# can't work around quickly - otherwise leave uncommented so that
# we notice when things go wrong.
Expand All @@ -45,6 +63,16 @@ matrix:
# CONDA_DEPENDENCIES='sphinx cython numpy pytest-cov'
# EVENT_TYPE='push pull_request cron'

before_install:

# Test OSX without OpenMP support
# Since the matrix OSX tests use the OS shipped version of clang, they double up
# as exploritory tests for when the shipped version has automatic OpenMP support.
# These tests will then fail and at such a time a new one should be added
# to explicitly remove OpenMP support.
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then export TEST_OPENMP="False";
else export TEST_OPENMP="True"; fi

install:

- git clone git://github.com/astropy/ci-helpers.git
Expand Down
11 changes: 10 additions & 1 deletion CHANGES.rst
Expand Up @@ -12,11 +12,20 @@ astropy-helpers Changelog
by the ``sphinx-astropy`` package in conjunction with the ``astropy-theme-sphinx``,
``sphinx-automodapi``, and ``numpydoc`` packages. [#368]

- openmp_helpers.py: Make add_openmp_flags_if_available() work for clang.
The small openmp C test code, used to determine if openmp works, fails to
build with clang due to missing include and library paths for openmp.
When run, it then fails due to rpath (runtime path) issues.
Adds the necessary include, library, and runtime paths to the build.
Autogenerator utility added to gen openmp_enabled.py::is_openmp_enabled()
which can be called post build to determine state of OpenMP support.
[#382]


3.0.2 (unreleased)
------------------

- Nothing changed yet.
- Nothing changed.


3.0.1 (2018-02-22)
Expand Down
219 changes: 187 additions & 32 deletions astropy_helpers/openmp_helpers.py
Expand Up @@ -13,15 +13,17 @@

from __future__ import absolute_import, print_function

import datetime
import glob
import os
import subprocess
import sys
import glob
import tempfile
import subprocess
import time

from distutils import log
from distutils.ccompiler import new_compiler
from distutils.sysconfig import customize_compiler
from distutils.sysconfig import customize_compiler, get_config_var
from distutils.errors import CompileError, LinkError

from .setup_helpers import get_compiler_option
Expand All @@ -39,69 +41,222 @@
}
"""

def _get_flag_value_from_var(flag, var, delim=' '):
"""
Utility to extract `flag` value from `os.environ[`var`]` or, if not present,
from `distutils.sysconfig.get_config_var(`var`)`.
E.g. to get include path: _get_flag_value_from_var('-I', 'CFLAGS')
might return "/usr/local/include".
def add_openmp_flags_if_available(extension):
Notes
-----
Not yet tested and therefore not yet supported on Windows
"""
Add OpenMP compilation flags, if available (if not a warning will be
printed to the console and no flags will be added)

Returns `True` if the flags were added, `False` otherwise.
if sys.platform.startswith('win'):
return None

# Simple input validation
if not var or not flag:
return None
l = len(flag)
if not l:
return None

# Look for var in os.eviron
try:
flags = os.environ[var]
except KeyError:
flags = None

# If os.environ was unsuccesful try in sysconfig
if not flags:
try:
flags = get_config_var(var)
except KeyError:
return None

# Extract flag from {var:value}
if flags:
for item in flags.split(delim):
if flag in item[:l]:
return item[l:]

return None

def _get_include_path():
return _get_flag_value_from_var('-I', 'CFLAGS')

def _get_library_path():
return _get_flag_value_from_var('-L', 'LDFLAGS')

def get_openmp_flags():
"""
Utility for returning compiler and linker flags possibly needed for
OpenMP support.
ccompiler = new_compiler()
customize_compiler(ccompiler)
Returns
-------
result : `{'compiler_flags':<flags>, 'linker_flags':<flags>}`
tmp_dir = tempfile.mkdtemp()
Notes
-----
The flags returned are not tested for validity, use
`test_openmp_support(openmp_flags=get_openmp_flags())` to do so.
"""

start_dir = os.path.abspath('.')
compile_flags = []
include_path = _get_include_path()
if include_path:
compile_flags.append('-I' + include_path)

link_flags = []
lib_path = _get_library_path()
if lib_path:
link_flags.append('-L' + lib_path)

if get_compiler_option() == 'msvc':
compile_flag = '-openmp'
link_flag = ''
compile_flags.append('-openmp')
else:
compile_flag = '-fopenmp'
link_flag = '-fopenmp'
compile_flags.append('-fopenmp')
link_flags.append('-fopenmp')
if lib_path:
link_flags.append('-Wl,-rpath,' + lib_path)

try:
return {'compiler_flags':compile_flags, 'linker_flags':link_flags}

def test_openmp_support(openmp_flags=None, silent=False):
"""
Compile and run OpenMP test code to determine viable support.
Parameters
----------
openmp_flags : dictionary, optional
Expecting `{'compiler_flags':<flags>, 'linker_flags':<flags>}`.
These are passed as `extra_postargs` to `compile()` and
`link_executable()` respectively.
silent : bool, optional
silence log warnings
Returns
-------
result : bool
`True` if the test passed, `False` otherwise.
"""

ccompiler = new_compiler()
customize_compiler(ccompiler)

if not openmp_flags:
# customize_compiler() extracts info from os.environ. If certain keys
# exist it uses these plus those from sysconfig.get_config_vars().
# If the key is missing in os.environ it is not extracted from
# sysconfig.get_config_var(). E.g. 'LDFLAGS' get left out, preventing
# clang from finding libomp.dylib because -L<path> is not passed to linker.
# Call get_openmp_flags() to get flags missed by customize_compiler().
openmp_flags = get_openmp_flags()
compile_flags = openmp_flags['compiler_flags'] if 'compiler_flags' in openmp_flags else None
link_flags = openmp_flags['linker_flags'] if 'linker_flags' in openmp_flags else None

tmp_dir = tempfile.mkdtemp()
start_dir = os.path.abspath('.')

try:
os.chdir(tmp_dir)

# Write test program
with open('test_openmp.c', 'w') as f:
f.write(CCODE)

os.mkdir('objects')

# Compile, link, and run test program
ccompiler.compile(['test_openmp.c'], output_dir='objects', extra_postargs=[compile_flag])
ccompiler.link_executable(glob.glob(os.path.join('objects', '*' + ccompiler.obj_extension)), 'test_openmp', extra_postargs=[link_flag])
ccompiler.compile(['test_openmp.c'], output_dir='objects', extra_postargs=compile_flags)
ccompiler.link_executable(glob.glob(os.path.join('objects', '*' + ccompiler.obj_extension)), 'test_openmp', extra_postargs=link_flags)
output = subprocess.check_output('./test_openmp').decode(sys.stdout.encoding or 'utf-8').splitlines()

if 'nthreads=' in output[0]:
nthreads = int(output[0].strip().split('=')[1])
if len(output) == nthreads:
using_openmp = True
is_openmp_supported = True
else:
log.warn("Unexpected number of lines from output of test OpenMP "
"program (output was {0})".format(output))
using_openmp = False
if not silent:
log.warn("Unexpected number of lines from output of test OpenMP "
"program (output was {0})".format(output))
is_openmp_supported = False
else:
log.warn("Unexpected output from test OpenMP "
"program (output was {0})".format(output))
using_openmp = False

if not silent:
log.warn("Unexpected output from test OpenMP "
"program (output was {0})".format(output))
is_openmp_supported = False
except (CompileError, LinkError):

using_openmp = False
is_openmp_supported = False

finally:

os.chdir(start_dir)

return is_openmp_supported

def is_openmp_supported():
"""
Utility to determine whether the build compiler
has OpenMP support.
"""
return test_openmp_support(silent=True)

def add_openmp_flags_if_available(extension):
"""
Add OpenMP compilation flags, if supported (if not a warning will be
printed to the console and no flags will be added.)
Returns `True` if the flags were added, `False` otherwise.
"""

openmp_flags = get_openmp_flags()
using_openmp = test_openmp_support(openmp_flags=openmp_flags, silent=False)

if using_openmp:
log.info("Compiling Cython extension with OpenMP support")
extension.extra_compile_args.append(compile_flag)
extension.extra_link_args.append(link_flag)
compile_flags = openmp_flags['compiler_flags'] if 'compiler_flags' in openmp_flags else None
link_flags = openmp_flags['linker_flags'] if 'linker_flags' in openmp_flags else None
log.info("Compiling Cython/C/C++ extension with OpenMP support")
extension.extra_compile_args.extend(compile_flags)
extension.extra_link_args.extend(link_flags)
else:
log.warn("Cannot compile Cython extension with OpenMP, reverting to non-parallel code")

return using_openmp

_IS_OPENMP_ENABLED_SRC = """
# Autogenerated by {packagetitle}'s setup.py on {timestamp!s}
def is_openmp_enabled():
\'\'\'
Autogenerated utility to determine, post build, whether the package
was built with or without OpenMP support.
\'\'\'
return {return_bool}
"""[1:]

def generate_openmp_enabled_py(packagename, srcdir='.'):
"""
Utility for creating openmp_enabled.py::is_openmp_enabled()
used to determine, post build, whether the package was built
with or without OpenMP support.
"""

if packagename.lower() == 'astropy':
packagetitle = 'Astropy'
else:
packagetitle = packagename

epoch = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
timestamp = datetime.datetime.utcfromtimestamp(epoch)

src = _IS_OPENMP_ENABLED_SRC.format(packagetitle=packagetitle,
timestamp=timestamp,
return_bool=is_openmp_supported())

package_srcdir = os.path.join(srcdir, *packagename.split('.'))
is_openmp_enabled_py = os.path.join(package_srcdir, 'openmp_enabled.py')
with open(is_openmp_enabled_py, 'w') as f:
f.write(src)

0 comments on commit 24d18d6

Please sign in to comment.