diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b86dc0b30..34f9e0636 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && scripts/build_igraph.sh" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" # Skip tests for Python 3.10 onwards because SciPy does not have # 32-bit wheels for Linux @@ -41,7 +41,7 @@ jobs: - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && scripts/build_igraph.sh" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" @@ -66,7 +66,7 @@ jobs: - name: Build wheels (manylinux) uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && scripts/build_igraph.sh" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" @@ -91,7 +91,7 @@ jobs: - name: Build wheels (musllinux) uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && scripts/build_igraph.sh" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-musllinux_aarch64" CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" @@ -159,7 +159,7 @@ jobs: uses: pypa/cibuildwheel@v2.19.2 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" - CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install -U setuptools && scripts/build_igraph.sh" CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local @@ -258,7 +258,7 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.19.2 env: - CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install -U setuptools && scripts/build_igraph.bat" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" CIBW_TEST_COMMAND: "cd /d {project} && pip install --prefer-binary \".[test]\" && python -m pytest tests" # Skip tests for Python 3.10 onwards because SciPy does not have @@ -303,7 +303,7 @@ jobs: - name: Build sdist run: | - python setup.py build_c_core + scripts/build_igraph.sh python setup.py sdist python setup.py install @@ -356,7 +356,7 @@ jobs: env: IGRAPH_USE_SANITIZERS: 1 run: | - python setup.py build_c_core + scripts/build_igraph.sh - name: Build and install Python extension env: diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index bb3f72ea4..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "vendor/source/igraph"] - path = vendor/source/igraph - url = https://github.com/igraph/igraph diff --git a/scripts/build_igraph.bat b/scripts/build_igraph.bat new file mode 100644 index 000000000..68140917c --- /dev/null +++ b/scripts/build_igraph.bat @@ -0,0 +1,63 @@ +@echo off + +set IGRAPH_VERSION=0.10.13 + +set ROOT_DIR=%cd% +echo Using root dir %ROOT_DIR% + +if not exist "%ROOT_DIR%\build-deps\src\" ( + md %ROOT_DIR%\build-deps\src +) + +cd "%ROOT_DIR%\build-deps\src" +if not exist "igraph\" ( + echo. + echo Cloning igraph into %ROOT_DIR%\build-deps\src\igraph + REM Clone repository if it does not exist yet + git clone --depth 1 --branch %IGRAPH_VERSION% https://github.com/igraph/igraph.git +) + +REM Make sure the git repository points to the correct version +echo. +echo Checking out %IGRAPH_VERSION} in ${ROOT_DIR%\build-deps\src\igraph +cd "%ROOT_DIR%\build-deps\src\igraph" +git fetch origin tag %IGRAPH_VERSION% --no-tags +git checkout %IGRAPH_VERSION% + +REM Make build directory +if not exist "%ROOT_DIR%\build-deps\build\igraph\" ( + echo. + echo Make directory %ROOT_DIR%\build-deps\build\igraph + md %ROOT_DIR%\build-deps\build\igraph +) + +REM Configure, build and install +cd "%ROOT_DIR%\build-deps\build\igraph" + +echo. +echo Configure igraph build +cmake %ROOT_DIR%\build-deps\src\igraph ^ + -DCMAKE_INSTALL_PREFIX=%ROOT_DIR%\build-deps\install\ ^ + -DBUILD_SHARED_LIBS=ON ^ + -DIGRAPH_GLPK_SUPPORT=ON ^ + -DIGRAPH_GRAPHML_SUPPORT=ON ^ + -DIGRAPH_OPENMP_SUPPORT=ON ^ + -DIGRAPH_USE_INTERNAL_BLAS=ON ^ + -DIGRAPH_USE_INTERNAL_LAPACK=ON ^ + -DIGRAPH_USE_INTERNAL_ARPACK=ON ^ + -DIGRAPH_USE_INTERNAL_GLPK=ON ^ + -DIGRAPH_USE_INTERNAL_GMP=ON ^ + -DIGRAPH_WARNINGS_AS_ERRORS=OFF ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DBUILD_TESTING=OFF ^ + %EXTRA_CMAKE_ARGS% + +echo. +echo Build igraph +cmake --build . --config Release + +echo. +echo Install igraph to %ROOT_DIR%\build-deps\install\ +cmake --build . --target install --config Release + +cd "%ROOT_DIR%" \ No newline at end of file diff --git a/scripts/build_igraph.sh b/scripts/build_igraph.sh new file mode 100644 index 000000000..9d082a40f --- /dev/null +++ b/scripts/build_igraph.sh @@ -0,0 +1,61 @@ +IGRAPH_VERSION=0.10.13 + +ROOT_DIR=`pwd` +echo "Using root dir ${ROOT_DIR}" + +# Create source directory +if [ ! -d "${ROOT_DIR}/build-deps/src" ]; then + echo "" + echo "Make directory ${ROOT_DIR}/build-deps/src" + mkdir -p ${ROOT_DIR}/build-deps/src +fi + +cd ${ROOT_DIR}/build-deps/src +if [ ! -d "igraph" ]; then + echo "" + echo "Cloning igraph into ${ROOT_DIR}/build-deps/src/igraph" + # Clone repository if it does not exist yet + git clone --branch ${IGRAPH_VERSION} https://github.com/igraph/igraph.git --single-branch +fi + +# Make sure the git repository points to the correct version +echo "" +echo "Checking out ${IGRAPH_VERSION} in ${ROOT_DIR}/build-deps/src/igraph" +cd ${ROOT_DIR}/build-deps/src/igraph +git fetch origin tag ${IGRAPH_VERSION} --no-tags +git checkout ${IGRAPH_VERSION} + +# Make build directory +if [ ! -d "${ROOT_DIR}/build-deps/build/igraph" ]; then + echo "" + echo "Make directory ${ROOT_DIR}/build-deps/build/igraph" + mkdir -p ${ROOT_DIR}/build-deps/build/igraph +fi + +# Configure, build and install +cd ${ROOT_DIR}/build-deps/build/igraph + +echo "" +echo "Configure igraph build" +cmake ${ROOT_DIR}/build-deps/src/igraph \ + -DCMAKE_INSTALL_PREFIX=${ROOT_DIR}/build-deps/install/ \ + -DBUILD_SHARED_LIBS=ON \ + -DIGRAPH_GLPK_SUPPORT=ON \ + -DIGRAPH_GRAPHML_SUPPORT=ON \ + -DIGRAPH_OPENMP_SUPPORT=ON \ + -DIGRAPH_USE_INTERNAL_BLAS=ON \ + -DIGRAPH_USE_INTERNAL_LAPACK=ON \ + -DIGRAPH_USE_INTERNAL_ARPACK=ON \ + -DIGRAPH_USE_INTERNAL_GLPK=ON \ + -DIGRAPH_USE_INTERNAL_GMP=ON \ + -DIGRAPH_WARNINGS_AS_ERRORS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + ${EXTRA_CMAKE_ARGS} + +echo "" +echo "Build igraph" +cmake --build . + +echo "" +echo "Install igraph to ${ROOT_DIR}/build-deps/install/" +cmake --build . --target install diff --git a/setup.py b/setup.py index 1610a5f1d..d38dc5f7b 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import os import platform import sys +import glob ########################################################################### @@ -13,31 +14,16 @@ ########################################################################### -from setuptools import find_packages, setup, Command, Extension +from setuptools import find_packages, setup, Extension try: from wheel.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None -import glob -import shlex -import shutil -import subprocess -import sysconfig - -from contextlib import contextmanager -from pathlib import Path -from select import select -from shutil import which -from time import sleep -from typing import List, Iterable, Iterator, Optional, Tuple, TypeVar, Union -########################################################################### +import sysconfig -LIBIGRAPH_FALLBACK_INCLUDE_DIRS = ["/usr/include/igraph", "/usr/local/include/igraph"] -LIBIGRAPH_FALLBACK_LIBRARIES = ["igraph"] -LIBIGRAPH_FALLBACK_LIBRARY_DIRS = [] # Check whether we are compiling for PyPy or wasm with emscripten. Headers will # not be installed in these cases, or when the SKIP_HEADER_INSTALL envvar is @@ -48,859 +34,6 @@ or "SKIP_HEADER_INSTALL" in os.environ ) -########################################################################### - - -T = TypeVar("T") - - -def is_envvar_on(name: str) -> bool: - """Returns whether the given environment variable is set to a truthy value - such as '1', 'on' or 'true'. - """ - value = os.environ.get(name, "") - return value and str(value).lower() in ("1", "on", "true") - - -def building_on_windows_msvc() -> bool: - """Returns True when using the non-MinGW CPython interpreter on Windows""" - return platform.system() == "Windows" and sysconfig.get_platform() != "mingw" - - -def building_with_emscripten() -> bool: - """Returns True when building with Emscripten to WebAssembly""" - return (sysconfig.get_config_var("HOST_GNU_TYPE") or "").endswith("emscripten") - - -def building_with_sanitizers() -> bool: - """Returns True when the IGRAPH_USE_SANITIZERS envvar is set, indicating that - we want to build the Python interface with AddressSanitizer and LeakSanitizer - enabled. Currently works on Linux only and the primary use-case is to be able - to test igraph with sanitizers in CI. - """ - return platform.system() == "Linux" and is_envvar_on("IGRAPH_USE_SANITIZERS") - - -def exclude_from_list(items: Iterable[T], items_to_exclude: Iterable[T]) -> List[T]: - """Excludes certain items from a list, keeping the original order of - the remaining items.""" - itemset = set(items_to_exclude) - return [item for item in items if item not in itemset] - - -def fail(message: str, code: int = 1) -> None: - """Fails the build with the given error message and exit code.""" - print(message) - sys.exit(code) - - -def find_static_library(library_name: str, library_path: List[str]) -> Optional[str]: - """Given the raw name of a library in `library_name`, tries to find a - static library with this name in the given `library_path`. `library_path` - is automatically extended with common library directories on Linux and Mac - OS X.""" - - variants = ["lib{0}.a", "{0}.a", "{0}.lib", "lib{0}.lib"] - if is_unix_like(): - extra_libdirs = [ - "/opt/homebrew/lib", # for newer Homebrew installations on macOS - "/usr/local/lib64", - "/usr/local/lib", - "/usr/lib/x86_64-linux-gnu", - "/usr/lib64", - "/usr/lib", - "/lib64", - "/lib", - ] - else: - extra_libdirs = [] - - for path in extra_libdirs: - if path not in library_path and os.path.isdir(path): - library_path.append(path) - - for path in library_path: - for variant in variants: - full_path = os.path.join(path, variant.format(library_name)) - if os.path.isfile(full_path): - return full_path - - -def first(iterable: Iterable[T]) -> T: - """Returns the first element from the given iterable.""" - for item in iterable: - return item - raise ValueError("iterable is empty") - - -def get_output(args, encoding: str = "utf-8") -> Tuple[str, int]: - """Returns the output of a command returning a single line of output, and - the exit code of the command. - """ - PIPE = subprocess.PIPE - try: - p = subprocess.Popen(args, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE) - stdout, stderr = p.communicate() - returncode = p.returncode - except OSError: - stdout, stderr = None, None - returncode = 77 - if isinstance(stdout, bytes): - stdout = str(stdout, encoding=encoding) - if isinstance(stderr, bytes): - stderr = str(stderr, encoding=encoding) - return (stdout or ""), returncode - - -def get_output_single_line(args, encoding: str = "utf-8") -> Tuple[str, int]: - """Returns the first line of the output of a command, stripped from any - trailing newlines, and the exit code of the command. - """ - stdout, returncode = get_output(args, encoding=encoding) - line, _, _ = stdout.partition("\n") - return line, returncode - - -def is_unix_like(platform: str = sys.platform) -> bool: - """Returns whether the given platform is a Unix-like platform with the usual - Unix filesystem. When the parameter is omitted, it defaults to ``sys.platform`` - """ - platform = platform or sys.platform - platform = platform.lower() - return ( - platform.startswith("linux") - or platform.startswith("darwin") - or platform.startswith("cygwin") - ) - - -def wait_for_keypress(seconds: float) -> None: - """Wait for a keypress or until the given number of seconds have passed, - whichever happens first. - """ - while seconds > 0: - if seconds > 1: - plural = "s" - else: - plural = "" - - sys.stdout.write( - "\rContinuing in %2d second%s; press Enter to continue " - "immediately. " % (seconds, plural) - ) - sys.stdout.flush() - - if platform.system() == "Windows": - from msvcrt import kbhit # type: ignore - - for _ in range(10): - if kbhit(): - seconds = 0 - break - sleep(0.1) - else: - rlist, _, _ = select([sys.stdin], [], [], 1) - if rlist: - sys.stdin.readline() - seconds = 0 - break - - seconds -= 1 - - sys.stdout.write("\r" + " " * 65 + "\r") - - -@contextmanager -def working_directory(dir: Union[str, Path]) -> Iterator[None]: - cwd = os.getcwd() - os.chdir(dir) - try: - yield - finally: - os.chdir(cwd) - - -########################################################################### - - -class IgraphCCoreCMakeBuilder: - """Class responsible for downloading and building the C core of igraph - if it is not installed yet, assuming that the C core uses CMake as the - build tool. This is the case from igraph 0.9. - """ - - def compile_in( - self, source_folder: Path, build_folder: Path, install_folder: Path - ) -> Union[bool, List[str]]: - """Compiles igraph from its source code in the given folder. - - Parameters: - source_folder: absolute path to the folder that contains igraph's - source files - build_folder: absolute path to the folder where the build should be - executed - install_folder: absolute path to the folder where the built library - should be installed - - Returns: - False if the build failed or the list of libraries to link to when - linking the Python interface to igraph - """ - with working_directory(build_folder): - return self._compile_in(source_folder, build_folder, install_folder) - - def _compile_in( - self, source_folder: Path, build_folder: Path, install_folder: Path - ) -> Union[bool, List[str]]: - cmake = which("cmake") - if not cmake: - print( - "igraph uses CMake as the build system. You need to install CMake " - "before compiling igraph." - ) - return False - - build_to_source_folder = os.path.relpath(source_folder, build_folder) - - print("Configuring build...") - args = [cmake] - cmake_build_mode = "Release" - - # Build to wasm requires invocation of the Emscripten SDK - if building_with_emscripten(): - emcmake = which("emcmake") - if not emcmake: - print( - "You need to install emcmake from the Emscripten SDK before " - "compiling igraph." - ) - return False - args.insert(0, emcmake) - args.append("-DIGRAPH_WARNINGS_AS_ERRORS:BOOL=OFF") - args.append("-DIGRAPH_GRAPHML_SUPPORT:BOOL=OFF") - - # Build the Python interface with vendored libraries - for deps in "ARPACK BLAS GLPK GMP LAPACK".split(): - args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") - - # Use link-time optinization if available - args.append("-DIGRAPH_ENABLE_LTO=AUTO") - - # -fPIC is needed on Linux so we can link to a static igraph lib from a - # Python shared library - args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") - - # No need to build tests - args.append("-DBUILD_TESTING=OFF") - - # Do not treat compilation warnings as errors in case someone is trying - # to "pip install" igraph in an environment for which we don't provide - # wheels and the compiler complains about harmless things - args.append("-DIGRAPH_WARNINGS_AS_ERRORS=OFF") - - # Set install directory during config step instead of install step in order - # to avoid having the architecture name in the LIBPATH (e.g. lib/x86_64-linux-gnu) - args.append("-DCMAKE_INSTALL_PREFIX=" + str(install_folder)) - - # On macOS, compile the C core with the same macOS deployment target as - # the one that was used to compile Python itself - if sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET"): - args.append( - "-DCMAKE_OSX_DEPLOYMENT_TARGET=" - + sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET") - ) - - # Compile the C core with sanitizers if needed - if building_with_sanitizers(): - args.append("-DUSE_SANITIZER=Address;Undefined") - args.append("-DFLEX_KEEP_LINE_NUMBERS=ON") - cmake_build_mode = "Debug" - - # Add any extra CMake args from environment variables - if "IGRAPH_CMAKE_EXTRA_ARGS" in os.environ: - args.extend( - shlex.split( - os.environ["IGRAPH_CMAKE_EXTRA_ARGS"], - posix=not building_on_windows_msvc(), - ) - ) - - # Finally, add the source folder path - args.append(str(build_to_source_folder)) - - retcode = subprocess.call(args) - if retcode: - return False - - print("Running build...") - # We are _not_ using a parallel build; this is intentional, see igraph/igraph#1755 - retcode = subprocess.call([cmake, "--build", ".", "--config", cmake_build_mode]) - if retcode: - return False - - print("Installing build...") - retcode = subprocess.call( - [ - cmake, - "--install", - ".", - "--config", - cmake_build_mode, - ] - ) - if retcode: - return False - - for candidate in install_folder.rglob("igraph.pc"): - return self._parse_pkgconfig_file(candidate) - - raise RuntimeError( - "no igraph.pc was found in the installation folder of igraph" - ) - - def create_build_config_file( - self, install_folder: Path, libraries: List[str] - ) -> None: - with (install_folder / "build.cfg").open("w") as fp: - fp.write(repr(libraries)) - - def _parse_pkgconfig_file(self, filename: Path) -> List[str]: - building_on_windows = building_on_windows_msvc() - - if building_on_windows: - libraries = ["igraph"] - else: - libraries = [] - with filename.open("r") as fp: - for line in fp: - if line.startswith("Libs: ") or line.startswith("Libs.private: "): - words = line.strip().split() - libraries.extend( - word[2:] for word in words if word.startswith("-l") - ) - # Remap known library names in Requires and Requires.private with - # prior knowledge -- we don't want to rebuild pkg-config in Python - if line.startswith("Requires: ") or line.startswith( - "Requires.private: " - ): - for word in line.strip().split(): - if word.startswith("libxml-"): - libraries.append("xml2") - if not libraries: - # Educated guess - libraries = ["igraph"] - - return libraries - - -########################################################################### - - -class BuildConfiguration: - def __init__(self): - self.include_dirs = [] - self.library_dirs = [] - self.runtime_library_dirs = [] - self.libraries = [] - self.extra_compile_args = [] - self.extra_link_args = [] - self.define_macros = [] - self.extra_objects = [] - self.static_extension = False - self.use_pkgconfig = False - self.use_sanitizers = False - self.c_core_built = False - self.allow_educated_guess = True - self._has_pkgconfig = None - self.excluded_include_dirs = [] - self.excluded_library_dirs = [] - self.wait = platform.system() != "Windows" - - @property - def has_pkgconfig(self) -> bool: - """Returns whether ``pkg-config`` is available on the current system - and it knows about igraph or not.""" - if self._has_pkgconfig is None: - if self.use_pkgconfig: - _, exit_code = get_output_single_line(["pkg-config", "igraph"]) - self._has_pkgconfig = exit_code == 0 - else: - self._has_pkgconfig = False - return self._has_pkgconfig - - @property - def build_c_core(self) -> Command: - """Returns a class representing a custom setup.py command that builds - the C core of igraph. - - This is used in CI environments where we want to build the C core of - igraph once and then build the Python interface for various Python - versions without having to recompile the C core all the time. - - If is also used as a custom building block of `build_ext`. - """ - - buildcfg = self - - class build_c_core(Command): - description = "Compile the C core of igraph only" - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - buildcfg.c_core_built = buildcfg.compile_igraph_from_vendor_source() - - return build_c_core - - @property - def build_ext(self) -> Command: - """Returns a class that can be used as a replacement for the - ``build_ext`` command in ``setuptools`` and that will compile the C core - of igraph before compiling the Python extension. - """ - from setuptools.command.build_ext import build_ext - - buildcfg = self - - class custom_build_ext(build_ext): - def run(self): - # Bail out if we don't have the Python include files - include_dir = sysconfig.get_path("include") - if not os.path.isfile(os.path.join(include_dir, "Python.h")): - fail("You will need the Python headers to compile this extension.") - - # Check whether the user asked us to discover a pre-built igraph - # with pkg-config - detected = False - if buildcfg.use_pkgconfig: - detected = buildcfg.detect_from_pkgconfig() - if not detected: - fail( - "Cannot find the C core of igraph on this system using pkg-config." - ) - else: - # Build the C core from the vendored igraph source - self.run_command("build_c_core") - if not buildcfg.c_core_built: - # Fall back to an educated guess if everything else failed - if not detected: - if buildcfg.allow_educated_guess: - buildcfg.use_educated_guess() - else: - fail("Cannot build the C core of igraph.") - - # Add any extra library paths if needed; this is needed for the - # Appveyor CI build - if "IGRAPH_EXTRA_LIBRARY_PATH" in os.environ: - buildcfg.library_dirs = ( - list(os.environ["IGRAPH_EXTRA_LIBRARY_PATH"].split(os.pathsep)) - + buildcfg.library_dirs - ) - - # Add extra libraries that may have been specified - if "IGRAPH_EXTRA_LIBRARIES" in os.environ: - extra_libraries = os.environ["IGRAPH_EXTRA_LIBRARIES"].split(",") - buildcfg.libraries.extend(extra_libraries) - - # Override build configuration based on environment variables - if "IGRAPH_STATIC_EXTENSION" in os.environ: - buildcfg.static_extension = is_envvar_on("IGRAPH_STATIC_EXTENSION") - buildcfg.use_sanitizers = building_with_sanitizers() - - # Replaces library names with full paths to static libraries - # where possible. libm.a is excluded because it caused problems - # on Sabayon Linux where libm.a is probably not compiled with - # -fPIC - if buildcfg.static_extension: - if buildcfg.static_extension == "only_igraph": - buildcfg.replace_static_libraries(only=["igraph"]) - else: - buildcfg.replace_static_libraries(exclusions=["m"]) - - # Add sanitizer flags - if buildcfg.use_sanitizers: - buildcfg.extra_link_args += [ - "-fsanitize=address", - "-fsanitize=undefined", - ] - buildcfg.extra_compile_args += [ - "-g", - "-Og", - "-fno-omit-frame-pointer", - "-fdiagnostics-color", - ] - - # Add extra libraries that may have been specified - if "IGRAPH_EXTRA_DYNAMIC_LIBRARIES" in os.environ: - extra_libraries = os.environ[ - "IGRAPH_EXTRA_DYNAMIC_LIBRARIES" - ].split(",") - buildcfg.libraries.extend(extra_libraries) - - # Remove C++ standard library as we will use the C++ linker - for lib in ("c++", "stdc++"): - if lib in buildcfg.libraries: - buildcfg.libraries.remove(lib) - - # Prints basic build information - buildcfg.print_build_info() - - # Find the igraph extension and configure it with the settings - # of this build configuration - ext = first( - extension - for extension in self.extensions - if extension.name == "igraph._igraph" - ) - buildcfg.configure(ext) - - # Run the original build_ext command - build_ext.run(self) - - return custom_build_ext - - @property - def sdist(self): - """Returns a class that can be used as a replacement for the - ``sdist`` command in ``setuptools`` and that will clean up - ``vendor/source/igraph`` before running the original ``sdist`` - command. - """ - from setuptools.command.sdist import sdist - - def is_git_repo(folder) -> bool: - return (Path(folder) / ".git").exists() - - def cleanup_git_repo(folder) -> None: - with working_directory(folder): - if os.path.exists(".git"): - retcode = subprocess.call("git clean -dfx", shell=True) - if retcode: - raise RuntimeError(f"Failed to clean {folder} with git") - - class custom_sdist(sdist): - def run(self): - igraph_source_repo = Path("vendor", "source", "igraph") - igraph_build_dir = Path("vendor", "build", "igraph") - version_file = igraph_source_repo / "IGRAPH_VERSION" - version = None - - # Check whether the source repo contains an IGRAPH_VERSION file, - # and extract the version number from that - if version_file.exists(): - version = version_file.read_text().strip().split("\n")[0] - - # If no IGRAPH_VERSION file exists, but we have a git repo, try - # git describe - if not version and is_git_repo(igraph_source_repo): - with working_directory(igraph_source_repo): - version = ( - subprocess.check_output("git describe", shell=True) - .decode("utf-8") - .strip() - ) - - # If we still don't have a version number, try to parse it from - # include/igraph_version.h - if not version: - version_header = igraph_build_dir / "include" / "igraph_version.h" - if not version_header.exists(): - raise RuntimeError( - "You need to build the C core of igraph first before generating a source tarball of the Python interface of igraph" - ) - - with version_header.open("r") as fp: - lines = [ - line.strip() - for line in fp - if line.startswith("#define IGRAPH_VERSION ") - ] - if len(lines) == 1: - version = lines[0].split('"')[1] - - if not isinstance(version, str) or len(version) < 5: - raise RuntimeError( - f"Cannot determine the version number of the C core in {igraph_source_repo}" - ) - - if not is_git_repo(igraph_source_repo): - # The Python interface was extracted from an official - # tarball so there is no need to tweak anything - return sdist.run(self) - else: - # Clean up vendor/source/igraph with git - cleanup_git_repo(igraph_source_repo) - - # Copy the generated parser sources from the build folder - parser_dir = igraph_build_dir / "src" / "io" / "parsers" - if parser_dir.is_dir(): - shutil.copytree( - parser_dir, igraph_source_repo / "src" / "io" / "parsers" - ) - else: - raise RuntimeError( - "You need to build the C core of igraph first before " - "generating a source tarball of the Python interface" - ) - - # Add a version file to the tarball - version_file.write_text(version) - - # Run the original sdist command - retval = sdist.run(self) - - # Clean up vendor/source/igraph with git again - cleanup_git_repo(igraph_source_repo) - - return retval - - return custom_sdist - - def compile_igraph_from_vendor_source(self) -> bool: - """Compiles igraph from the vendored source code inside `vendor/source/igraph`. - This folder typically comes from a git submodule. - """ - vendor_folder = Path("vendor") - source_folder = vendor_folder / "source" / "igraph" - build_folder = vendor_folder / "build" / "igraph" - install_folder = vendor_folder / "install" / "igraph" - - if install_folder.exists(): - # Vendored igraph already compiled and installed, just use it - self.use_vendored_igraph() - return True - - if (source_folder / "CMakeLists.txt").exists(): - igraph_builder = IgraphCCoreCMakeBuilder() - else: - print("Cannot find vendored igraph source in {0}".format(source_folder)) - print("") - return False - - print("We are going to build the C core of igraph.") - print(" Source folder: {0}".format(source_folder)) - print(" Build folder: {0}".format(build_folder)) - print(" Install folder: {0}".format(install_folder)) - print("") - - source_folder = source_folder.resolve() - build_folder = build_folder.resolve() - install_folder = install_folder.resolve() - - Path(build_folder).mkdir(parents=True, exist_ok=True) - - libraries = igraph_builder.compile_in( - source_folder=source_folder, - build_folder=build_folder, - install_folder=install_folder, - ) - - if libraries is False: - fail("Build failed for the C core of igraph.") - - assert not isinstance(libraries, bool) - - igraph_builder.create_build_config_file(install_folder, libraries) - - self.use_vendored_igraph() - return True - - def configure(self, ext) -> None: - """Configures the given Extension object using this build configuration.""" - ext.include_dirs = exclude_from_list( - self.include_dirs, self.excluded_include_dirs - ) - ext.library_dirs = exclude_from_list( - self.library_dirs, self.excluded_library_dirs - ) - ext.runtime_library_dirs = self.runtime_library_dirs - ext.libraries = self.libraries - ext.extra_compile_args = self.extra_compile_args - ext.extra_link_args = self.extra_link_args - ext.extra_objects = self.extra_objects - ext.define_macros = self.define_macros - - def detect_from_pkgconfig(self) -> bool: - """Detects the igraph include directory, library directory and the - list of libraries to link to using ``pkg-config``.""" - if not buildcfg.has_pkgconfig: - return False - - cmd = ["pkg-config", "igraph", "--cflags", "--libs"] - if self.static_extension: - cmd += ["--static"] - line, exit_code = get_output_single_line(cmd) - if exit_code > 0 or len(line) == 0: - return False - - opts = line.strip().split() - self.libraries = [opt[2:] for opt in opts if opt.startswith("-l")] - self.library_dirs = [opt[2:] for opt in opts if opt.startswith("-L")] - self.include_dirs = [opt[2:] for opt in opts if opt.startswith("-I")] - return True - - def print_build_info(self) -> None: - """Prints the include and library path being used for debugging purposes.""" - if self.static_extension == "only_igraph": - build_type = "dynamic extension with vendored igraph source" - elif self.static_extension: - build_type = "static extension" - else: - build_type = "dynamic extension" - print("Build type: %s" % build_type) - print("Include path: %s" % " ".join(self.include_dirs)) - if self.excluded_include_dirs: - print(" - excluding: %s" % " ".join(self.excluded_include_dirs)) - print("Library path: %s" % " ".join(self.library_dirs)) - if self.excluded_library_dirs: - print(" - excluding: %s" % " ".join(self.excluded_library_dirs)) - print("Runtime library path: %s" % " ".join(self.runtime_library_dirs)) - print("Linked dynamic libraries: %s" % " ".join(self.libraries)) - print("Linked static libraries: %s" % " ".join(self.extra_objects)) - print("Extra compiler options: %s" % " ".join(self.extra_compile_args)) - print("Extra linker options: %s" % " ".join(self.extra_link_args)) - - def process_args_from_command_line(self): - """Preprocesses the command line options before they are passed to - setup.py and sets up the build configuration. - """ - # Yes, this is ugly, but we don't want to interfere with setup.py's own - # option handling - opts_to_remove = [] - for idx, option in enumerate(sys.argv): - if not option.startswith("--"): - continue - if option == "--static": - opts_to_remove.append(idx) - self.static_extension = True - elif option == "--no-pkg-config": - opts_to_remove.append(idx) - self.use_pkgconfig = False - elif option == "--no-wait": - opts_to_remove.append(idx) - self.wait = False - elif option == "--use-pkg-config": - opts_to_remove.append(idx) - self.use_pkgconfig = True - - for idx in reversed(opts_to_remove): - sys.argv[idx : (idx + 1)] = [] - - def process_environment_variables(self): - """Processes environment variables that serve as replacements for the - command line options. This is typically useful in CI environments where - it is easier to set up a few environment variables permanently than to - pass the same options to ``setup.py build`` and ``setup.py install`` - at the same time. - """ - - def process_envvar(name, attr, value): - name = "IGRAPH_" + name.upper() - if name in os.environ: - value = str(os.environ[name]).lower() - if value in ("on", "true", "yes"): - value = True - elif value in ("off", "false", "no"): - value = False - else: - try: - value = bool(int(value)) - except Exception: - return - - setattr(self, attr, value) - - process_envvar("static", "static_extension", True) - process_envvar("no_pkg_config", "use_pkgconfig", False) - process_envvar("no_wait", "wait", False) - process_envvar("use_pkg_config", "use_pkgconfig", True) - process_envvar("use_sanitizers", "use_sanitizers", False) - - def replace_static_libraries(self, only=None, exclusions=None): - """Replaces references to libraries with full paths to their static - versions if the static version is to be found on the library path.""" - if exclusions is None: - exclusions = [] - - for library_name in set(self.libraries) - set(exclusions): - if only is not None and library_name not in only: - continue - - static_lib = find_static_library(library_name, self.library_dirs) - if static_lib: - print(f"Found {library_name} as static library in {static_lib}.") - self.libraries.remove(library_name) - self.extra_objects.append(static_lib) - else: - print(f"Warning: could not find static library of {library_name}.") - - def use_vendored_igraph(self) -> None: - """Assumes that igraph is installed already in ``vendor/install/igraph`` and sets up - the include and library paths and the library names accordingly.""" - building_on_windows = building_on_windows_msvc() - - vendor_dir = Path("vendor") / "install" / "igraph" - - buildcfg.include_dirs = [str(vendor_dir / "include" / "igraph")] - buildcfg.library_dirs = [] - - for candidate in ("lib", "lib64"): - candidate = vendor_dir / candidate - if candidate.exists(): - buildcfg.library_dirs.append(str(candidate)) - break - else: - raise RuntimeError( - "cannot detect igraph library dir within " + str(vendor_dir) - ) - - if not buildcfg.static_extension: - buildcfg.static_extension = "only_igraph" - if building_on_windows: - buildcfg.define_macros.append(("IGRAPH_STATIC", "1")) - - buildcfg_file = vendor_dir / "build.cfg" - if buildcfg_file.exists(): - buildcfg.libraries = eval(buildcfg_file.open("r").read()) - - def use_educated_guess(self) -> None: - """Tries to guess the proper library names, include and library paths - if everything else failed.""" - - global LIBIGRAPH_FALLBACK_LIBRARIES - global LIBIGRAPH_FALLBACK_INCLUDE_DIRS - global LIBIGRAPH_FALLBACK_LIBRARY_DIRS - - print("WARNING: we were not able to detect where igraph is installed on") - print("your machine (if it is installed at all). We will use the fallback") - print("library and include paths hardcoded in setup.py and hope that the") - print("C core of igraph is installed there.") - print("") - print("If the compilation fails and you are sure that igraph is installed") - print("on your machine, adjust the following two variables in setup.py") - print("accordingly and try again:") - print("") - print("- LIBIGRAPH_FALLBACK_INCLUDE_DIRS") - print("- LIBIGRAPH_FALLBACK_LIBRARY_DIRS") - print("") - - if self.wait: - wait_for_keypress(seconds=10) - - self.libraries = LIBIGRAPH_FALLBACK_LIBRARIES[:] - if self.static_extension: - self.libraries.extend(["xml2", "z", "m", "stdc++"]) - self.include_dirs = LIBIGRAPH_FALLBACK_INCLUDE_DIRS[:] - self.library_dirs = LIBIGRAPH_FALLBACK_LIBRARY_DIRS[:] - - -########################################################################### - if bdist_wheel is not None: class bdist_wheel_abi3(bdist_wheel): @@ -931,11 +64,6 @@ def get_tag(self): __version__: str = "" exec(open("src/igraph/version.py").read()) -# Process command line options -buildcfg = BuildConfiguration() -buildcfg.process_environment_variables() -buildcfg.process_args_from_command_line() - # Define the extension sources = glob.glob(os.path.join("src", "_igraph", "*.c")) sources.append(os.path.join("src", "_igraph", "force_cpp_linker.cpp")) @@ -944,6 +72,9 @@ def get_tag(self): macros.append(("Py_LIMITED_API", "0x03090000")) igraph_extension = Extension( "igraph._igraph", + libraries=["igraph"], + include_dirs=['build-deps/install/include'], + library_dirs=['build-deps/install/lib'], sources=sources, py_limited_api=should_build_abi3_wheel, define_macros=macros, @@ -963,11 +94,7 @@ def get_tag(self): headers = ["src/_igraph/igraphmodule_api.h"] if not SKIP_HEADER_INSTALL else [] -cmdclass = { - "build_c_core": buildcfg.build_c_core, # used by CI - "build_ext": buildcfg.build_ext, - "sdist": buildcfg.sdist, -} +cmdclass = {} if should_build_abi3_wheel: cmdclass["bdist_wheel"] = bdist_wheel_abi3 diff --git a/src/_igraph/arpackobject.h b/src/_igraph/arpackobject.h index 92b685112..e3406f240 100644 --- a/src/_igraph/arpackobject.h +++ b/src/_igraph/arpackobject.h @@ -25,7 +25,7 @@ #include "preamble.h" -#include +#include #include "graphobject.h" /** diff --git a/src/_igraph/attributes.h b/src/_igraph/attributes.h index 6d3da9c90..801da6a56 100644 --- a/src/_igraph/attributes.h +++ b/src/_igraph/attributes.h @@ -25,11 +25,11 @@ #include "preamble.h" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #define ATTRHASH_IDX_GRAPH 0 #define ATTRHASH_IDX_VERTEX 1 diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 54094684c..a70276d55 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -30,8 +30,8 @@ #include "preamble.h" -#include -#include +#include +#include #include "graphobject.h" typedef enum { diff --git a/src/_igraph/error.c b/src/_igraph/error.c index 41b2b879e..9a63b67bf 100644 --- a/src/_igraph/error.c +++ b/src/_igraph/error.c @@ -23,7 +23,7 @@ #include "error.h" #include "pyhelpers.h" -#include +#include /** \ingroup python_interface_errors * \brief Exception type to be returned when an internal \c igraph error occurs. diff --git a/src/_igraph/error.h b/src/_igraph/error.h index 1265f2918..039280c1f 100644 --- a/src/_igraph/error.h +++ b/src/_igraph/error.h @@ -25,7 +25,7 @@ #include "preamble.h" -#include +#include /** \defgroup python_interface_errors Error handling * \ingroup python_interface */ diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index f6c775c88..0366f6a00 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -25,7 +25,7 @@ #include "preamble.h" -#include +#include #include "structmember.h" #include "common.h" diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 97864f61e..b01e0ec6c 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -22,7 +22,7 @@ #include "preamble.h" -#include +#include #include "arpackobject.h" #include "attributes.h" #include "bfsiter.h" diff --git a/src/_igraph/indexing.h b/src/_igraph/indexing.h index d87ccdc54..2b831d2e8 100644 --- a/src/_igraph/indexing.h +++ b/src/_igraph/indexing.h @@ -26,7 +26,7 @@ #include "preamble.h" -#include +#include PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, PyObject* row_index, PyObject* column_index, PyObject* attr_name); diff --git a/src/_igraph/random.c b/src/_igraph/random.c index aef52c379..9ed1859dd 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -24,7 +24,7 @@ #include "random.h" #include "pyhelpers.h" #include -#include +#include /** * \ingroup python_interface_rng diff --git a/src/_igraph/utils.h b/src/_igraph/utils.h index bc39b84b9..017ad5061 100644 --- a/src/_igraph/utils.h +++ b/src/_igraph/utils.h @@ -25,8 +25,8 @@ #include "preamble.h" -#include -#include +#include +#include #include "convert.h" #include "graphobject.h" diff --git a/vendor/source/igraph b/vendor/source/igraph deleted file mode 160000 index ac22a920d..000000000 --- a/vendor/source/igraph +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ac22a920d882ec9e2bd433c55e2da2fce0ebe529