diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4adce5b19..f4f2b5a04 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,8 +48,12 @@ jobs: pip install flake8 - name: Build cython modules in-place + env: + TA_INCLUDE_PATH: /usr/include + TA_LIBRARY_PATH: /usr/lib run: | python setup.py build_ext --inplace + ldd talib/_ta_lib*.so | grep -E 'libta-lib\.so[^ ]* => /' - name: Lint with flake8 run: | diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7a05287d5..66ed4d4f5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -26,11 +26,9 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: - CIBW_BEFORE_ALL: | - bash ./tools/build_talib_linux.sh CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_MUSLLINUX_X86_64_IMAGE: musllinux_1_2 - CIBW_ENVIRONMENT_PASS_LINUX: TALIB_C_VER CFLAGS + CIBW_ENVIRONMENT_PASS_LINUX: CFLAGS CFLAGS: -D_GNU_SOURCE CIBW_TEST_SKIP: "cp*-musllinux*" CIBW_TEST_COMMAND: > @@ -39,12 +37,7 @@ jobs: CIBW_ARCHS_LINUX: x86_64 CIBW_BUILD_FRONTEND: build CIBW_ENVIRONMENT_LINUX : > - TA_LIBRARY_PATH="ta-lib-install/lib" - TA_INCLUDE_PATH="ta-lib-install/include" PIP_NO_BUILD_ISOLATION=false - CIBW_REPAIR_WHEEL_COMMAND_LINUX: | - export LD_LIBRARY_PATH="/project/ta-lib-install/lib:$LD_LIBRARY_PATH" - auditwheel repair -w {dest_dir} {wheel} - name: Upload wheels uses: actions/upload-artifact@v7 with: @@ -62,11 +55,9 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: - CIBW_BEFORE_ALL: | - bash ./tools/build_talib_linux.sh CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 CIBW_MUSLLINUX_AARCH64_IMAGE: musllinux_1_2 - CIBW_ENVIRONMENT_PASS_LINUX: TALIB_C_VER CFLAGS + CIBW_ENVIRONMENT_PASS_LINUX: CFLAGS CFLAGS: -D_GNU_SOURCE CIBW_TEST_SKIP: "cp*-musllinux*" CIBW_TEST_COMMAND: > @@ -75,12 +66,7 @@ jobs: CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD_FRONTEND: build CIBW_ENVIRONMENT_LINUX : > - TA_LIBRARY_PATH="ta-lib-install/lib" - TA_INCLUDE_PATH="ta-lib-install/include" PIP_NO_BUILD_ISOLATION=false - CIBW_REPAIR_WHEEL_COMMAND_LINUX: | - export LD_LIBRARY_PATH="/project/ta-lib-install/lib:$LD_LIBRARY_PATH" - auditwheel repair -w {dest_dir} {wheel} - name: Upload wheels uses: actions/upload-artifact@v7 with: @@ -99,9 +85,6 @@ jobs: uses: ilammy/msvc-dev-cmd@v1 with: arch: amd64 - - name: Build TA-Lib C Library - run: tools/build_talib_windows.cmd - shell: cmd - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: @@ -112,8 +95,6 @@ jobs: MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT: 1 CIBW_ARCHS_WINDOWS: AMD64 CIBW_ENVIRONMENT_WINDOWS: > - TA_LIBRARY_PATH="ta-lib-$TALIB_C_VER\\_build;$LIB" - TA_INCLUDE_PATH="ta-lib-$TALIB_C_VER\\include;$INCLUDE" PIP_NO_BUILD_ISOLATION=false - name: Upload wheel uses: actions/upload-artifact@v7 @@ -133,9 +114,6 @@ jobs: uses: ilammy/msvc-dev-cmd@v1 with: arch: amd64_x86 - - name: Build TA-Lib C library - run: tools/build_talib_windows.cmd - shell: cmd - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: @@ -146,8 +124,6 @@ jobs: MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT: 1 CIBW_ARCHS_WINDOWS: x86 CIBW_ENVIRONMENT_WINDOWS: > - TA_LIBRARY_PATH="ta-lib-$TALIB_C_VER\\_build;$LIB" - TA_INCLUDE_PATH="ta-lib-$TALIB_C_VER\\include;$INCLUDE" PIP_NO_BUILD_ISOLATION=false - name: Upload wheel uses: actions/upload-artifact@v7 @@ -167,9 +143,6 @@ jobs: uses: ilammy/msvc-dev-cmd@v1 with: arch: arm64 - - name: Build TA-Lib C Library - run: tools/build_talib_windows.cmd - shell: cmd - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: @@ -180,8 +153,6 @@ jobs: MSBUILDTREATHIGHERTOOLSVERSIONASCURRENT: 1 CIBW_ARCHS_WINDOWS: ARM64 CIBW_ENVIRONMENT_WINDOWS: > - TA_LIBRARY_PATH="ta-lib-$TALIB_C_VER\\_build;$LIB" - TA_INCLUDE_PATH="ta-lib-$TALIB_C_VER\\include;$INCLUDE" PIP_NO_BUILD_ISOLATION=false - name: Upload wheel uses: actions/upload-artifact@v7 @@ -200,20 +171,13 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: - CIBW_BEFORE_ALL: | - bash ./tools/build_talib_macos.sh CIBW_TEST_COMMAND: > cd .. && pytest -k "not RSI and not threading" {project}/tests CIBW_ARCHS_MACOS: x86_64 CIBW_ENVIRONMENT_MACOS : > - TA_LIBRARY_PATH="ta-lib-install/lib" - TA_INCLUDE_PATH="ta-lib-install/include" + MACOSX_DEPLOYMENT_TARGET=13.7 PIP_NO_BUILD_ISOLATION=false - CIBW_REPAIR_WHEEL_COMMAND_MACOS: | - export MACOSX_DEPLOYMENT_TARGET=13.7 - export DYLD_LIBRARY_PATH="/Users/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/ta-lib-install/lib:$DYLD_LIBRARY_PATH" - delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} - name: Upload wheels uses: actions/upload-artifact@v7 with: @@ -231,20 +195,13 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v4.2.1 env: - CIBW_BEFORE_ALL: | - bash ./tools/build_talib_macos.sh CIBW_TEST_COMMAND: > cd .. && pytest -k "not RSI and not threading" {project}/tests CIBW_ARCHS_MACOS: arm64 CIBW_ENVIRONMENT_MACOS : > - TA_LIBRARY_PATH="ta-lib-install/lib" - TA_INCLUDE_PATH="ta-lib-install/include" + MACOSX_DEPLOYMENT_TARGET=14.0 PIP_NO_BUILD_ISOLATION=false - CIBW_REPAIR_WHEEL_COMMAND_MACOS: | - export MACOSX_DEPLOYMENT_TARGET=14.0 - export DYLD_LIBRARY_PATH="/Users/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/ta-lib-install/lib:$DYLD_LIBRARY_PATH" - delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} - name: Upload wheels uses: actions/upload-artifact@v7 with: diff --git a/CHANGELOG b/CHANGELOG index a7a46958d..8969f7540 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,17 @@ +0.8.3 +===== + +- [NEW]: Source builds download and verify TA-Lib C 0.8.1, build it as a + static library and link it into the extension. A compiler, CMake and network + access are required; a separate TA-Lib C installation is no longer needed. + Generated C files remain included in source distributions. + +- [CHANGE]: Setting ``TA_INCLUDE_PATH`` or ``TA_LIBRARY_PATH`` uses an existing + TA-Lib C installation instead, without downloading or building the C library. + If only one is set, the other uses the standard search paths. + +- [CHANGE]: Wheels statically link TA-Lib C through the same build process. + 0.8.2 ===== diff --git a/README.md b/README.md index b80888459..6c6da4ea9 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,24 @@ conda install -c conda-forge ta-lib ### Dependencies 🧩 -To use TA-Lib for python, you need to have the [TA-Lib](http://ta-lib.org) -already installed. You should probably follow their [installation -directions](https://ta-lib.org/install/) for your platform, but some -suggestions are included below for reference. +Binary wheels include the [TA-Lib](http://ta-lib.org) C library and do not +require a compiler or a separate TA-Lib C installation. + +Starting with version 0.8.3, source builds download the pinned TA-Lib C 0.8.1 +release, verify its checksum, and build and statically link it into the Python +extension. This requires network access, a C compiler and CMake. On Windows, +source builds require the Microsoft C++ Build Tools. CMake is installed +automatically when using pip's default build isolation. + +To use an existing TA-Lib C installation instead, set ``TA_INCLUDE_PATH`` or +``TA_LIBRARY_PATH`` before building. Setting either variable disables the +bundled C build, including its download; if only one is set, the other uses +the standard search paths. These variables accept multiple directories +separated by ``os.pathsep`` (``:`` on Unix, ``;`` on Windows). They apply to +source builds, not prebuilt wheels. + +The following instructions are for installing your own TA-Lib C library. +You can also follow its [installation directions](https://ta-lib.org/install/). > Some Conda Forge users have reported success installing the underlying TA-Lib C > library using [the libta-lib package](https://anaconda.org/conda-forge/libta-lib): @@ -91,7 +105,7 @@ for your architecture: arch -arm64 brew install ta-lib ``` -And perhaps you can set these before installing with `pip`: +To link a source build against the Homebrew installation, set: ```shell export TA_INCLUDE_PATH="$(brew --prefix ta-lib)/include" @@ -143,7 +157,8 @@ fail with ``No such file or directory`` errors. For convenience, and starting with version 0.6.5, we now build binary wheels for different operating systems, architectures, and Python versions using GitHub Actions which include the underlying TA-Lib C library and are easy to -install. +install. Starting with version 0.8.3, this library is statically linked into +the extension. Supported platforms: @@ -173,28 +188,15 @@ using the instructions above. ### Troubleshooting 🛠️ -If you get a warning that looks like this: - -```shell -setup.py:79: UserWarning: Cannot find ta-lib library, installation may fail. -warnings.warn('Cannot find ta-lib library, installation may fail.') -``` - -This typically means `setup.py` can't find the underlying `TA-Lib` -library, a dependency which needs to be installed. - ---- - If you installed the underlying `TA-Lib` library with a custom prefix (e.g., with `./configure --prefix=$PREFIX`), then when you go to install -this python wrapper you can specify additional search paths to find the -library and include files for the underlying `TA-Lib` library using the -`TA_LIBRARY_PATH` and `TA_INCLUDE_PATH` environment variables: +this Python wrapper you can select that installation instead of the bundled +build using the `TA_LIBRARY_PATH` and `TA_INCLUDE_PATH` environment variables: ```shell export TA_LIBRARY_PATH=$PREFIX/lib export TA_INCLUDE_PATH=$PREFIX/include -python setup.py install # or pip install ta-lib +python -m pip install --no-binary=TA-Lib TA-Lib ``` --- @@ -218,10 +220,10 @@ common.obj : error LNK2001: unresolved external symbol TA_GetUnstablePeriod common.obj : error LNK2001: unresolved external symbol TA_GetVersionString ``` -This typically means that it can't find the underlying `TA-Lib` library, a -dependency which needs to be installed. On Windows, this could be caused by -installing the 32-bit binary distribution of the underlying `TA-Lib` library, -but trying to use it with 64-bit Python. +When using ``TA_INCLUDE_PATH`` or ``TA_LIBRARY_PATH``, check that they point +to the headers and libraries of a compatible TA-Lib C installation. On Windows, +the library architecture must match Python (for example, a 64-bit Python needs +a 64-bit TA-Lib C library). --- diff --git a/pyproject.toml b/pyproject.toml index 904070d89..6e24a9f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools", "Cython", "numpy"] +requires = ["setuptools", "Cython", "numpy", "cmake"] build-backend = "setuptools.build_meta" [project] name = "TA-Lib" -version = "0.8.2" +version = "0.8.3" description = "Python wrapper for TA-Lib" readme = "README.md" license-files = ["LICENSE"] diff --git a/setup.py b/setup.py index 8500fc582..9c89950c9 100644 --- a/setup.py +++ b/setup.py @@ -3,70 +3,61 @@ import sys import os import os.path -import warnings +import hashlib +import shlex +import subprocess +import tarfile +import tempfile +import urllib.request +from pathlib import Path import sysconfig from setuptools import setup, Extension import numpy -platform_supported = False - -lib_talib_name = 'ta-lib' # the name as of TA-Lib 0.6.1 - -if any(s in sys.platform for s in ['darwin', 'linux', 'bsd', 'sunos']): - platform_supported = True - include_dirs = [ - '/usr/include', - '/usr/local/include', - '/opt/include', - '/opt/local/include', - '/opt/homebrew/include', - '/opt/homebrew/opt/ta-lib/include', - ] - library_dirs = [ - '/usr/lib', - '/usr/local/lib', - '/usr/lib64', - '/usr/local/lib64', - '/opt/lib', - '/opt/local/lib', - '/opt/homebrew/lib', - '/opt/homebrew/opt/ta-lib/lib', - ] - -elif sys.platform == "win32": - platform_supported = True - lib_talib_name = 'ta-lib-static' - include_dirs = [ - r"c:\ta-lib\c\include", - r"c:\Program Files\TA-Lib\include", - r"c:\Program Files (x86)\TA-Lib\include", +TALIB_C_VERSION = '0.8.1' +TALIB_C_SHA256 = 'ec59ccd88c0c77f618587d858787c8f9d06c40460a09d66751926f6fd670f985' + + +def build_talib(directory, plat_name): + """Build the pinned C release locally, without installing it system-wide.""" + archive = directory / 'ta-lib.tar.gz' + urllib.request.urlretrieve( + f'https://github.com/TA-Lib/ta-lib/releases/download/v{TALIB_C_VERSION}/' + f'ta-lib-{TALIB_C_VERSION}-src.tar.gz', archive) + if hashlib.sha256(archive.read_bytes()).hexdigest() != TALIB_C_SHA256: + raise RuntimeError('TA-Lib C source checksum mismatch') + with tarfile.open(archive) as source: + source.extractall(directory) + + prefix = directory / 'install' + build = directory / 'build' + args = [ + 'cmake', '-S', str(directory / f'ta-lib-{TALIB_C_VERSION}'), + '-B', str(build), '-DCMAKE_BUILD_TYPE=Release', + f'-DCMAKE_INSTALL_PREFIX={prefix}', + '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_STATIC_LIBS=ON', + '-DBUILD_DEV_TOOLS=OFF', '-DCMAKE_POSITION_INDEPENDENT_CODE=ON', ] - library_dirs = [ - r"c:\ta-lib\c\lib", - r"c:\Program Files\TA-Lib\lib", - r"c:\Program Files (x86)\TA-Lib\lib", - ] - -if 'TA_INCLUDE_PATH' in os.environ: - include_dirs = os.environ['TA_INCLUDE_PATH'].split(os.pathsep) - -if 'TA_LIBRARY_PATH' in os.environ: - library_dirs = os.environ['TA_LIBRARY_PATH'].split(os.pathsep) + if sys.platform == 'win32': + args += ['-A', {'win32': 'Win32', 'win-amd64': 'x64', + 'win-arm64': 'ARM64'}[plat_name], + '-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL'] + elif sys.platform == 'darwin': + flags = shlex.split(os.environ.get('ARCHFLAGS', '')) + archs = [flags[i + 1] for i, flag in enumerate(flags) if flag == '-arch'] + if archs: + args.append('-DCMAKE_OSX_ARCHITECTURES=' + ';'.join(archs)) + target = (os.environ.get('MACOSX_DEPLOYMENT_TARGET') or + sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')) + if target: + args.append('-DCMAKE_OSX_DEPLOYMENT_TARGET=' + target) + subprocess.check_call(args) + subprocess.check_call(['cmake', '--build', str(build), '--config', 'Release']) + subprocess.check_call(['cmake', '--install', str(build), '--config', 'Release']) + return prefix -if not platform_supported: - raise NotImplementedError(sys.platform) - -for path in library_dirs: - try: - files = os.listdir(path) - if any(lib_talib_name in f for f in files): - break - except OSError: - pass -else: - warnings.warn('Cannot find ta-lib library, installation may fail.') # Get the Cython build_ext or fall back to setuptools build_ext try: @@ -78,18 +69,49 @@ class NumpyBuildExt(build_ext): """ - Custom build_ext command that adds numpy's include_dir to extensions. + Build TA-Lib C unless explicit library or header paths are supplied. """ def build_extensions(self): """ - Add numpy's include directory to Extension includes. + Respect explicit paths, otherwise build and statically link TA-Lib C. """ - numpy_incl = numpy.get_include() - for ext in self.extensions: - ext.include_dirs.append(numpy_incl) - - super().build_extensions() + if 'TA_LIBRARY_PATH' in os.environ or 'TA_INCLUDE_PATH' in os.environ: + if sys.platform == 'win32': + include_dirs = [r'c:\ta-lib\c\include', + r'c:\Program Files\TA-Lib\include', + r'c:\Program Files (x86)\TA-Lib\include'] + library_dirs = [r'c:\ta-lib\c\lib', + r'c:\Program Files\TA-Lib\lib', + r'c:\Program Files (x86)\TA-Lib\lib'] + library = 'ta-lib-static' + else: + include_dirs = ['/usr/include', '/usr/local/include', '/opt/include', + '/opt/local/include', '/opt/homebrew/include', + '/opt/homebrew/opt/ta-lib/include'] + library_dirs = ['/usr/lib', '/usr/local/lib', '/usr/lib64', + '/usr/local/lib64', '/opt/lib', '/opt/local/lib', + '/opt/homebrew/lib', '/opt/homebrew/opt/ta-lib/lib'] + library = 'ta-lib' + if 'TA_INCLUDE_PATH' in os.environ: + include_dirs = os.environ['TA_INCLUDE_PATH'].split(os.pathsep) + if 'TA_LIBRARY_PATH' in os.environ: + library_dirs = os.environ['TA_LIBRARY_PATH'].split(os.pathsep) + for ext in self.extensions: + ext.include_dirs += include_dirs + [numpy.get_include()] + ext.library_dirs = library_dirs + ext.libraries.append(library) + ext.runtime_library_dirs = [] if sys.platform == 'win32' else library_dirs + super().build_extensions() + return + + with tempfile.TemporaryDirectory(prefix='ta-lib-') as temporary: + prefix = build_talib(Path(temporary), self.plat_name) + library = 'ta-lib-static.lib' if sys.platform == 'win32' else 'libta-lib.a' + for ext in self.extensions: + ext.include_dirs += [str(prefix / 'include'), numpy.get_include()] + ext.extra_objects = [str(prefix / 'lib' / library)] + super().build_extensions() cmdclass = {'build_ext': NumpyBuildExt} @@ -99,10 +121,7 @@ def build_extensions(self): ['talib/_ta_lib.pyx' if has_cython else 'talib/_ta_lib.c'], define_macros=[('Py_GIL_DISABLED', '1')] if sys.platform == 'win32' and sysconfig.get_config_var('Py_GIL_DISABLED') else [], - include_dirs=include_dirs, - library_dirs=library_dirs, - libraries=[lib_talib_name], - runtime_library_dirs=[] if sys.platform == 'win32' else library_dirs) + libraries=[] if sys.platform == 'win32' else ['m']) ] setup( diff --git a/talib/__init__.py b/talib/__init__.py index 97750c0f2..72893bbdf 100644 --- a/talib/__init__.py +++ b/talib/__init__.py @@ -138,7 +138,7 @@ def wrapper(*args, **kwds): from . import stream -__version__ = '0.8.2' +__version__ = '0.8.3' # In order to use this python library, talib (i.e. this __file__) will be # imported at some point, either explicitly or indirectly via talib.func