python/setup.py (130 lines of code) (raw):
# Copyright 2022-2023 Spotify AB
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import platform
import sys
from pathlib import Path
import numpy as np
import pybind11
import setuptools
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
# Find the "cpp" folder depending on where this script is run from:
for search_path in ["./cpp/src/", "../cpp/src/", "../../cpp/src/"]:
path = os.path.abspath(os.path.join(os.path.dirname(__file__), search_path))
if os.path.exists(path):
VOYAGER_HEADERS_PATH = path
break
else:
dir_contents = os.listdir(os.getcwd())
raise OSError(
"Unable to find the 'cpp' folder to build voyager. "
f"Current working directory is: {os.getcwd()}, directory contains: "
f"{', '.join([repr(x) for x in dir_contents[:5]])} and {len(dir_contents) - 5} more files."
)
ext_modules = [
Extension(
"voyager",
["./bindings.cpp"],
include_dirs=[pybind11.get_include(), np.get_include(), VOYAGER_HEADERS_PATH],
libraries=[],
language="c++",
extra_objects=[],
),
]
# As of Python 3.6, CCompiler has a `has_flag` method.
# cf http://bugs.python.org/issue26689
def has_flag(compiler, flagname):
"""Return a boolean indicating whether a flag name is supported on
the specified compiler.
"""
import tempfile
with tempfile.NamedTemporaryFile("w", suffix=".cpp") as f:
f.write("int main (int argc, char **argv) { return 0; }")
try:
compiler.compile([f.name], extra_postargs=[flagname])
except setuptools.distutils.errors.CompileError:
return False
return True
DEBUG = int(os.environ.get("DEBUG", "0")) == 1
USE_ASAN = int(os.environ.get("USE_ASAN", "0")) == 1
class BuildExt(build_ext):
"""A custom build extension for adding compiler-specific options."""
compiler_flags = {
"msvc": ["/EHsc", "/O2"],
"unix": ["-O0" if DEBUG else "-O3"] + (["-g"] if DEBUG else []),
}
linker_flags = {"unix": [], "msvc": []}
if sys.platform == "darwin":
compiler_flags["unix"] += ["-stdlib=libc++", "-mmacosx-version-min=10.13"]
linker_flags["unix"] += ["-stdlib=libc++", "-mmacosx-version-min=10.13"]
if USE_ASAN:
compiler_flags["unix"].append("-fsanitize=address")
compiler_flags["unix"].append("-fno-omit-frame-pointer")
linker_flags["unix"].append("-fsanitize=address")
if platform.system() == "Linux":
linker_flags["unix"].append("-shared-libasan")
def build_extensions(self):
ct = self.compiler.compiler_type
opts = self.compiler_flags.get(ct, [])
if ct == "unix":
opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version())
opts.append("-std=c++17")
# Allow reordering floating-point operations for
# better automatic vectorization, even without -ffast-math
# See: https://simonbyrne.github.io/notes/fastmath/#flushing_subnormals_to_zero
# for why -ffast-math is not included:
opts.extend(
[
"-fassociative-math",
"-fno-signaling-nans",
"-fno-trapping-math",
"-fno-signed-zeros",
"-freciprocal-math",
"-fno-math-errno",
]
)
if has_flag(self.compiler, "-fvisibility=hidden"):
opts.append("-fvisibility=hidden")
elif ct == "msvc":
opts.append('/DVERSION_INFO=\\"%s\\"' % self.distribution.get_version())
opts.append("/std:c++17")
for ext in self.extensions:
ext.extra_compile_args.extend(opts)
ext.extra_link_args.extend(self.linker_flags.get(ct, []))
build_ext.build_extensions(self)
current_directory = Path(__file__).resolve().parent
search_directory = current_directory
search_paths = []
for _ in range(10):
search_paths.append(str(search_directory))
readme = search_directory / "README.md"
if readme.exists():
break
search_directory = search_directory.parent
else:
raise ValueError(f"Unable to find README.md. Searched: {search_paths}")
long_description = readme.read_text("utf-8")
# read the contents of the version.py
version = {}
exec((current_directory / "voyager" / "version.py").read_text(), version)
setup(
name="voyager",
version=version["__version__"],
description=(
"Easy-to-use, fast, simple multi-platform approximate nearest-neighbor search library."
),
author="Peter Sobot",
url="https://github.com/spotify/voyager",
long_description=long_description,
long_description_content_type="text/markdown",
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: C++",
"Programming Language :: Python",
"Topic :: Database :: Database Engines/Servers",
"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",
],
ext_modules=ext_modules,
install_requires=["numpy"],
cmdclass={"build_ext": BuildExt},
zip_safe=False,
package_data={
"voyager": ["py.typed", "*.pyi", "**/*.pyi"],
},
)