Newer
Older
#!/usr/bin/env python
# coding: utf-8
import collections
import configparser
import glob
import itertools
import os
import platform
import re
import sys
import subprocess
from unittest import mock
import setuptools
from distutils import log
from distutils.command.clean import clean as _clean
from distutils.errors import CompileError

Martin Larralde
committed
from setuptools.command.build_clib import build_clib as _build_clib
from setuptools.command.build_ext import build_ext as _build_ext
from setuptools.command.sdist import sdist as _sdist

Martin Larralde
committed
from setuptools.extension import Extension, Library as _Library
try:
from Cython.Build import cythonize
except ImportError as err:
cythonize = err
# --- Constants --------------------------------------------------------------

Martin Larralde
committed
PLATFORM_MACHINE = platform.machine()
PLATFORM_SYSTEM = platform.system()
UNIX = not sys.platform.startswith("win")
PROCESSOR_IS_MIPS = PLATFORM_MACHINE.startswith("mips")
PROCESSOR_IS_ARM = PLATFORM_MACHINE.startswith("arm")
PROCESSOR_IS_AARCH64 = PLATFORM_MACHINE.startswith("aarch64")
PROCESSOR_IS_X86 = PLATFORM_MACHINE.startswith(("x86_64", "AMD64", "amd64", "i386", "i486"))
PROCESSOR_IS_POWER = PLATFORM_MACHINE.startswith(("powerpc", "ppc"))
# --- Utils ------------------------------------------------------------------
def _split_multiline(value):
value = value.strip()
sep = max('\n,;', key=value.count)
return list(filter(None, map(lambda x: x.strip(), value.split(sep))))

Martin Larralde
committed
def _patch_osx_compiler(compiler):
# On newer OSX, Python has been compiled as a universal binary, so
# it will attempt to pass universal binary flags when building the
# extension. This will not work because the code makes use of SSE2.
for tool in ("compiler", "compiler_so", "linker_so"):
flags = getattr(compiler, tool)
i = next((i for i in range(1, len(flags)) if flags[i-1] == "-arch" and flags[i] != platform.machine()), None)
if i is not None:
flags.pop(i)
flags.pop(i-1)

Martin Larralde
committed
# --- `setuptools` classes ---------------------------------------------------
class PlatformCode:
def __init__(self, platform, sources, extra_compile_args=None):
self.platform = platform
self.sources = sources
self.extra_compile_args = extra_compile_args or []
class Library(_Library):
def __init__(self, name, sources, *args, **kwargs):
self.platform_code = kwargs.pop("platform_code", None) or []
super().__init__(name, sources, *args, **kwargs)
for platform_code in self.platform_code:
self.depends.extend(platform_code.sources)
# --- `setup.py` commands ----------------------------------------------------
class sdist(_sdist):
"""A `sdist` that generates a `pyproject.toml` on the fly.
"""
def run(self):
# build `pyproject.toml` from `setup.cfg`
c = configparser.ConfigParser()
c.add_section("build-system")
c.set("build-system", "requires", str(self.distribution.setup_requires))
c.set("build-system", 'build-backend', '"setuptools.build_meta"')
with open("pyproject.toml", "w") as pyproject:
c.write(pyproject)
# run the rest of the packaging
_sdist.run(self)
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class publicize_headers(_build_ext):
"""A custom command to make everything in C++ headers public.
"""
user_options = _build_ext.user_options + [
("sources=", "s", "the folder with the source headers to patch")
]
def initialize_options(self):
_build_ext.initialize_options(self)
self.sources = None
def finalize_options(self):
_build_ext.finalize_options(self)
if self.sources is not None:
self.sources = _split_multiline(self.sources)
def run(self):
# patch sources
for source_dir in self.sources:
for dirpath, dirnames, filenames in os.walk(source_dir):
base_dirpath = os.path.relpath(dirpath, start=source_dir)
new_dirpath = os.path.join( self.build_temp, base_dirpath )
for filename in filenames:
if filename.endswith((".h", ".hpp")):
filepath = os.path.join(dirpath, filename)
new_filepath = os.path.join( new_dirpath, filename )
self.make_file([filepath], new_filepath, self.patch_header, (filepath, new_filepath))
# update the include dirs
for ext in self.extensions:
ext.include_dirs.append(self.build_temp)
def patch_header(self, old_path, new_path):
self.mkpath(os.path.dirname(new_path))
with open(old_path, "r") as src:
source = src.read()

Martin Larralde
committed
# make everything public!
source = re.sub("private:", "public:", source)

Martin Larralde
committed
# patch some classes with private members at the
# beginning of their declaration (skch::Sketch)
source = re.sub(r"class (.+\s*)\{(\s*)", r"class \1 {\npublic:\n\2", source)

Martin Larralde
committed
# patch the contructors of skch::Map and skch::Seq so that they don't
# do anything
source = re.sub(r"(Sketch|Map)\(([^)]*)\)([^}]+)\{[^}]*}", r"\1(\2)\3 {}", source)
with open(new_path, "w") as dst:
dst.write(source)

Martin Larralde
committed
class build_clib(_build_clib):
"""A custom `build_clib` that compiles out of source.

Martin Larralde
committed
# --- Silent invocation of the compiler ---
def _silent_spawn(self, cmd):
try:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as err:
raise CompileError(err.stderr)
def _silent_compile(self, *args, **kwargs):
with mock.patch.object(self.compiler, "spawn", new=self._silent_spawn):
return self.compiler.compile(*args, **kwargs)

Martin Larralde
committed
# --- Compatibility with base `build_clib` command ---

Martin Larralde
committed
def check_library_list(self, libraries):
pass
def get_library_names(self):
return [ lib.name for lib in self.libraries ]
def get_source_files(self):
return [ source for lib in self.libraries for source in lib.sources ]
# --- Build code ---

Martin Larralde
committed
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def _check_function(self, funcname, header, args="()"):
print('checking whether function', repr(funcname), 'is available', end="... ", file=sys.stderr)
base = "have_{}".format(funcname)
testfile = os.path.join(self.build_temp, "{}.c".format(base))
binfile = self.compiler.executable_filename(base, output_dir=self.build_temp)
objects = []
with open(testfile, "w") as f:
f.write("""
#include <{}>
int main() {{
{}{};
return 0;
}}
""".format(header, funcname, args))
try:
objects = self.compiler.compile([testfile], debug=self.debug)
self.compiler.link_executable(objects, base, output_dir=self.build_temp)
except CompileError:
print("no", file=sys.stderr)
return False
else:
print("yes", file=sys.stderr)
return True
finally:
os.remove(testfile)
for obj in filter(os.path.isfile, objects):
os.remove(obj)
if os.path.isfile(binfile):
os.remove(binfile)
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def _has_dlopen(self):
self.mkpath(self.build_temp)
filename = os.path.join(self.build_temp, "test_dlopen.c")
with open(filename, "w") as f:
f.write("""
#include <dlfcn.h>
int main() { dlopen("", 0); }
""")
try:
self.compiler.compile([filename], debug=True)
except CompileError:
log.warn("could not find `dlopen` function from <dlfcn.h>")
return False
else:
log.info("found `dlopen` function from <dlfcn.h>")
return True
def _has_getauxval(self):
self.mkpath(self.build_temp)
filename = os.path.join(self.build_temp, "test_dlopen.c")
with open(filename, "w") as f:
f.write("""
#include <sys/getauxval.h>
int main() { getauxval(0); }
""")
try:
self._silent_compile([filename], debug=True)
except CompileError:
log.warn("could not find `getauxval` function from <sys/getauxval.h>")
return False
else:
log.info("found `getauxval` function from <sys/getauxval.h>")
return True
def _add_platform_defines(self, library):
# add some compatibility defines from `cpu_features`
if PROCESSOR_IS_X86 and sys.platform == "darwin":
library.define_macros.append(("HAVE_SYSCTLBYNAME", 1))
if UNIX:
hardware_detect = False
if self._has_dlopen():
library.define_macros.append(("HAVE_DLFCN_H", 1))
hardware_detect = True
if self._has_getauxval():
library.define_macros.append(("HAVE_STRONG_GETAUXVAL", 1))
hardware_detect = True
if hardware_detect:
library.sources.append(os.path.join("vendor", "cpu_features", "src", "hwcaps.c"))

Martin Larralde
committed
def build_libraries(self, libraries):

Martin Larralde
committed
# check for functions required for libcpu_features on OSX
if PLATFORM_SYSTEM == "Darwin":
_patch_osx_compiler(self.compiler)
if self._check_function("sysctlbyname", "sys/sysctl.h", args="(NULL, NULL, 0, NULL, 0)"):
self.compiler.define_macro("HAVE_SYSCTLBYNAME", 1)
# build each library only if the sources are outdated

Martin Larralde
committed
self.mkpath(self.build_clib)
for library in libraries:
sources = library.sources.copy()
for platform_code in library.platform_code:
sources.extend(platform_code.sources)
self.make_file(
sources,
self.compiler.library_filename(library.name, output_dir=self.build_clib),
self.build_library,
(library,)
)
def build_library(self, library):
# add specific code for `cpu_features`
if library.name == "cpu_features":
self._add_platform_defines(library)

Martin Larralde
committed
# update compile flags if compiling in debug or release mode
if self.debug:
if self.compiler.compiler_type in {"unix", "cygwin", "mingw32"}:
library.extra_compile_args.append("-Og")
library.extra_compile_args.append("--coverage")
library.extra_link_args.append("--coverage")
elif self.compiler.compiler_type == "msvc":
library.extra_compile_args.append("/Od")
# attempt to build the platform specific code
extra_objects = []
for platform_code in library.platform_code:
extra_preargs = library.extra_compile_args + platform_code.extra_compile_args
try:
extra_objects.extend(self._silent_compile(

Martin Larralde
committed
platform_code.sources,
output_dir=self.build_temp,
include_dirs=library.include_dirs + [self.build_clib],
macros=library.define_macros,
debug=self.debug,
depends=library.depends,
extra_preargs=extra_preargs,
))
except CompileError:
log.warn(f"failed to compile platform-specific {platform_code.platform} code")
else:
log.info(f"successfully built platform-specific {platform_code.platform} code")

Martin Larralde
committed
self.compiler.define_macro(f"{platform_code.platform}_BUILD_SUPPORTED")
# build objects and create a static library
objects = self.compiler.compile(
library.sources,
output_dir=self.build_temp,
include_dirs=library.include_dirs + [self.build_clib],
macros=library.define_macros,
debug=self.debug,
depends=library.depends,
extra_preargs=library.extra_compile_args,
)
self.compiler.create_static_lib(
objects + extra_objects,
library.name,
output_dir=self.build_clib,
debug=self.debug,
)
class build_ext(_build_ext):
"""A `build_ext` that disables optimizations if compiled in debug mode.
"""
# --- Autotools-like helpers ---
def finalize_options(self):
_build_ext.finalize_options(self)
self._clib_cmd = self.get_finalized_command("build_clib")

Martin Larralde
committed
self._pub_cmd = self.get_finalized_command("publicize_headers")
self._clib_cmd.force = self._pub_cmd.force = self.force
self._clib_cmd.debug = self.debug

Martin Larralde
committed
self.build_clib = self._clib_cmd.build_clib
def run(self):
# make sure sources have been patched to expose private fields
if not self.distribution.have_run.get("publicize_headers", False):

Martin Larralde
committed
self._pub_cmd.run()
if not self.distribution.have_run.get("build_clib", False):
self._clib_cmd.run()
# check `cythonize` is available
if isinstance(cythonize, ImportError):
raise RuntimeError("Cython is required to run `build_ext` command") from cythonize
# use debug directives with Cython if building in debug mode

Martin Larralde
committed
cython_args = {
"include_path": ["include", "pyfastani"],
"compiler_directives": {},
"compile_time_env": {
"FASTANI_PRIVATE_ACCESS": 1,
"SYS_IMPLEMENTATION_NAME": sys.implementation.name,
"SYS_VERSION_INFO_MAJOR": sys.version_info.major,
"SYS_VERSION_INFO_MINOR": sys.version_info.minor,
"SYS_VERSION_INFO_MICRO": sys.version_info.micro,
}
}
if self.force:
cython_args["force"] = True
if self.debug:
cython_args["annotate"] = True
cython_args["compiler_directives"]["warn.undeclared"] = True
cython_args["compiler_directives"]["warn.unreachable"] = True
cython_args["compiler_directives"]["warn.maybe_uninitialized"] = True
cython_args["compiler_directives"]["warn.unused"] = True
cython_args["compiler_directives"]["warn.unused_arg"] = True
cython_args["compiler_directives"]["warn.unused_result"] = True
cython_args["compiler_directives"]["warn.multiple_declarators"] = True
cython_args["compiler_directives"]["profile"] = True
else:
cython_args["compiler_directives"]["boundscheck"] = False
cython_args["compiler_directives"]["wraparound"] = False
cython_args["compiler_directives"]["cdivision"] = True
# cythonize and patch the extensions
self.extensions = cythonize(self.extensions, **cython_args)
for ext in self.extensions:
ext._needs_stub = False
# build the extensions as normal
_build_ext.run(self)

Martin Larralde
committed
def build_extensions(self):
# remove universal compilation flags for OSX
if platform.system() == "Darwin":
_patch_osx_compiler(self.compiler)
# build the extensions as normal
_build_ext.build_extensions(self)
def build_extension(self, ext):
# update compile flags if compiling in debug mode
if self.debug:
if self.compiler.compiler_type in {"unix", "cygwin", "mingw32"}:
ext.extra_compile_args.append("-Og")
ext.extra_compile_args.append("--coverage")
ext.extra_link_args.append("--coverage")
elif self.compiler.compiler_type == "msvc":
ext.extra_compile_args.append("/Od")
if sys.implementation.name == "cpython":
ext.define_macros.append(("CYTHON_TRACE_NOGIL", 1))
else:
ext.define_macros.append(("CYTHON_WITHOUT_ASSERTIONS", 1))
# C++ OS-specific options
if ext.language == "c++":
# make sure to build with C++11
if self.compiler.compiler_type == "msvc":
ext.extra_compile_args.append("/std:c++11")
ext.extra_link_args.append("/std:c++11")
else:
ext.extra_compile_args.append("-std=c++11")
ext.extra_link_args.append("-std=c++11")
# in case we are compiling with clang, make sure to use libstdc++
if self.compiler.compiler_type == "unix" and sys.platform == "darwin":
ext.extra_compile_args.append("-stdlib=libc++")
ext.extra_link_args.append("-stdlib=libc++")

Martin Larralde
committed
# add extra static objects
for lib in ext.libraries:
ext.extra_objects.append(self.compiler.library_filename(
lib,
output_dir=self.build_clib
))
# build the rest of the extension as normal
_build_ext.build_extension(self, ext)
class clean(_clean):

Martin Larralde
committed
def remove_file(self, filename):
if os.path.exists(filename):
log.info("removing {!r}".format(filename))
os.remove(filename)
else:
log.info("{!r} does not exist -- can't clean it".format(filename))

Martin Larralde
committed
def run(self):
_clean.run(self)

Martin Larralde
committed
_build_cmd = self.get_finalized_command("build_ext")
_build_cmd.inplace = True

Martin Larralde
committed
for ext in self.distribution.ext_modules:
filename = _build_cmd.get_ext_filename(ext.name)
if self.all:
self.remove_file(filename)
basename = _build_cmd.get_ext_fullname(ext.name).replace(".", os.path.sep)
for ext in ["c", "cpp", "html"]:
filename = os.path.extsep.join([basename, ext])
self.remove_file(filename)
# --- Cython extensions ------------------------------------------------------

Martin Larralde
committed
libraries = [

Martin Larralde
committed
Library(
"cpu_features",
include_dirs=[os.path.join("vendor", "cpu_features", "include")],
define_macros=[("STACK_LINE_READER_BUFFER_SIZE", 1024)],
sources=[
os.path.join("vendor", "cpu_features", "src", x)
for x in ("filesystem.c", "stack_line_reader.c", "string_view.c")
],
platform_code=[
PlatformCode(
platform="AARCH64",
sources=[os.path.join("vendor", "cpu_features", "src", "cpuinfo_aarch64.c")]
),
PlatformCode(
platform="ARM",
sources=[os.path.join("vendor", "cpu_features", "src", "cpuinfo_arm.c")]
),
PlatformCode(
platform="X86",
sources=[os.path.join("vendor", "cpu_features", "src", "cpuinfo_x86.c")]
)
]
),

Martin Larralde
committed
Library(
"sequtils",
include_dirs=[
os.path.join("pyfastani", "_sequtils"),
os.path.join("vendor", "cpu_features", "include")
],
sources=[os.path.join("pyfastani", "_sequtils", "sequtils.cpp")],

Martin Larralde
committed
libraries=["cpu_features"],
language="c++",

Martin Larralde
committed
platform_code=[
PlatformCode(
platform="NEON",
sources=[os.path.join("pyfastani", "_sequtils", "neon.c")],
extra_compile_args=["-mneon"],
),
PlatformCode(
platform="SSE2",
sources=[os.path.join("pyfastani", "_sequtils", "sse2.c")],
extra_compile_args=["-msse2"],
),
PlatformCode(
platform="SSSE3",
sources=[os.path.join("pyfastani", "_sequtils", "ssse3.c")],
extra_compile_args=["-mssse3"],
)
]
),
]
extensions = [
Extension(
"pyfastani._fastani",

Martin Larralde
committed
[os.path.join("pyfastani", x) for x in ("_utils.cpp", "omp.cpp", "_fastani.pyx")],
language="c++",

Martin Larralde
committed
include_dirs=["include", "pyfastani", os.path.join("pyfastani", "_sequtils")],
libraries=["sequtils"],
define_macros=[
("USE_BOOST", 1), # used to compile FastANI without GSL
("BOOST_NO_EXCEPTIONS", 1), # don't raise overflow errors
],
),
Extension(
"pyfastani._fasta",

Martin Larralde
committed
[os.path.join("pyfastani", "_fasta.pyx")],
include_dirs=["include", "pyfastani", os.path.join("pyfastani", "simd")],
language="c",

Martin Larralde
committed
libraries=["sequtils"],
)
]
# --- Setup ------------------------------------------------------------------
setuptools.setup(
ext_modules=extensions,

Martin Larralde
committed
libraries=libraries,

Martin Larralde
committed
build_clib=build_clib,
build_ext=build_ext,
publicize_headers=publicize_headers,
clean=clean,
sdist=sdist,
),
)