-- Replace the upstream setup.py with a minimal setuptools configuration. -- Upstream setup.py rebuilds OpenBLAS, SuiteSparse AMD and the full COIN-OR -- stack from git sources and bundles the resulting binaries into a wheel. -- FreeBSD already provides math/cbc, so this port installs only the Python -- wrapper and points it at the system CBC binary. --- setup.py.orig 2026-09-08 23:28:23 UTC +++ setup.py @@ -1,1109 +1,15 @@ -import glob as _glob -import multiprocessing import os -import platform -import re -import shlex -import shutil -import subprocess -import sys - from setuptools import setup -from wheel.bdist_wheel import bdist_wheel as _bdist_wheel +THIS_DIR = os.path.abspath(os.path.dirname(__file__)) -# ── Wheel customisation ─────────────────────────────────────────────────────── +with open(os.path.join(THIS_DIR, "README.md"), "r", encoding="utf-8") as f: + long_description = f.read() -class genericpy_bdist_wheel(_bdist_wheel): - def finalize_options(self): - _bdist_wheel.finalize_options(self) - self.root_is_pure = False - - def get_tag(self): - python, abi, plat = _bdist_wheel.get_tag(self) - python, abi = "py3", "none" - if os.environ.get("CIBUILDWHEEL", "0") == "1": - if plat == "linux_x86_64": - plat = "manylinux2014_x86_64" - elif plat == "linux_aarch64": - plat = "manylinux2014_aarch64" - return python, abi, plat - - -cmdclass = {"bdist_wheel": genericpy_bdist_wheel} - - -# ── Constants ───────────────────────────────────────────────────────────────── - -THIS_DIR = os.path.abspath(os.path.dirname(__file__)) -DIST_DIR = os.path.join(THIS_DIR, "cbc_dist") -DIST_DIR_AVX2 = os.path.join(THIS_DIR, "cbc_dist_avx2") -DIST_DIR_DEBUG = os.path.join(THIS_DIR, "cbc_dist_debug") -DIST_DIR_DEBUG_AVX2 = os.path.join(THIS_DIR, "cbc_dist_debug_avx2") -LIB_DIR = os.path.join(DIST_DIR, "lib") -NPROC = str(max(1, multiprocessing.cpu_count())) - -SUITESPARSE_TAG = "v7.12.2" -OPENBLAS_TAG = "v0.3.31" - -# Development of the COIN-OR stack has moved to the "next" branch of each -# repository (the "master" branch is now the last stable release line). -COIN_OR_BRANCH = "next" - -# -ffp-contract=off disables the compiler's automatic fusion of separate -# multiply/add operations into a single FMA instruction. FMA computes the -# product with full (unrounded) intermediate precision before the final -# rounding, so results can differ slightly (in the last bit) from the -# strictly-IEEE separate multiply + add. This tiny discrepancy is enough to -# make CBC's numerical routines (which rely on exact reproducibility across -# the stack) occasionally behave inconsistently, so this flag is applied to -# the COIN-OR stack (CoinUtils, Osi, Clp, Cgl, Cbc) only — third-party -# dependencies built from source (OpenBLAS, SuiteSparse AMD) keep their -# default FMA-contraction behaviour. -FP_CONTRACT_OFF = "-ffp-contract=off" - -# Build order matters: each project depends on the ones before it. -COIN_REPOS = [ - ("CoinUtils", "https://github.com/coin-or/CoinUtils.git"), - ("Osi", "https://github.com/coin-or/Osi.git"), - ("Clp", "https://github.com/coin-or/Clp.git"), - ("Cgl", "https://github.com/coin-or/Cgl.git"), - ("Cbc", "https://github.com/coin-or/Cbc.git"), -] - -# Shared libraries allowed by the manylinux2014 policy (PEP 599). -# Anything NOT matching this pattern must be either linked statically -# or bundled inside the wheel. -_MANYLINUX_ALLOWED = re.compile( - r"^lib(gcc_s|stdc\+\+|m|dl|rt|pthread|c|nsl|util|z|gomp|crypt|resolv)\." - r"|^(linux-vdso|linux-gate|ld-linux)" +setup( + long_description=long_description, + long_description_content_type="text/markdown", + packages=["cbcbox"], + package_dir={"cbcbox": "src"}, + zip_safe=False, ) - -# ── Windows / MSYS2-MinGW64 constants ──────────────────────────────────────── - -# MSYS2 is pre-installed on windows-latest GitHub Actions runners. -_MSYS2_BASH = r"C:\msys64\usr\bin\bash.exe" - -# DLLs that ship with Windows itself and must NOT be bundled. -_WIN_SYS_DLL = re.compile( - r"^(kernel32|user32|ntdll|msvcrt|api-ms-win|ext-ms-win|advapi32|" - r"shell32|ole32|oleaut32|ws2_32|mswsock|bcrypt|crypt32|secur32|" - r"ucrtbase|vcruntime|hid|setupapi|cfgmgr32|imm32|version|winmm|" - r"shlwapi|rpcrt4|comctl32|comdlg32|gdi32|netapi32|psapi|dbghelp)" - r"\.dll$", - re.IGNORECASE, -) - - -def _win_to_msys2(s: str) -> str: - """Convert Windows absolute path references within *s* to MSYS2 format. - - 'C:\\foo\\bar' → '/c/foo/bar' - '--prefix=C:/foo/bar' → '--prefix=/c/foo/bar' - '-LC:/foo/bar' → '-L/c/foo/bar' (even when preceded by a flag letter) - 'https://example.com' → unchanged - Strings without drive letters pass through unchanged. - """ - s = str(s) - # Protect URL schemes (e.g. "https://") so they aren't misidentified as - # Windows drive letters. Use a lambda so Python doesn't interpret \x in - # the replacement string (re.sub string replacement doesn't allow \x). - placeholder = "\x00\x00" - s = re.sub(r'([A-Za-z]+)://', lambda m: m.group(1) + placeholder, s) - # Convert Windows drive letters to MSYS2 /x/ format. - s = re.sub(r'([A-Za-z]):[/\\]', lambda m: f"/{m.group(1).lower()}/", s) - # Restore URL schemes and normalise remaining back-slashes. - return s.replace('\x00\x00', '://').replace('\\', '/') - - -# ── Generic helpers ─────────────────────────────────────────────────────────── - -def run(*cmd, cwd=None, env=None): - print(f">>> {' '.join(str(c) for c in cmd)}", flush=True) - if platform.system() == "Windows": - # Route every build command through MSYS2/MinGW64 so that autotools, - # make, pkg-config, gfortran etc. are all available. - parts = ["export PATH=/mingw64/bin:/usr/bin:$PATH"] - if cwd: - parts.append(f"cd {shlex.quote(_win_to_msys2(str(cwd)))}") - parts.append(" ".join(shlex.quote(_win_to_msys2(str(c))) for c in cmd)) - subprocess.run([_MSYS2_BASH, "-lc", " && ".join(parts)], - check=True, env=env) - else: - subprocess.run(list(cmd), check=True, cwd=cwd, env=env) - - -def _is_x86_64() -> bool: - return platform.machine().lower() in ("x86_64", "amd64") - - -def clone_if_missing(name, url, branch="master"): - dest = os.path.join(THIS_DIR, name) - if not os.path.exists(dest): - # Use native git directly — avoids routing through MSYS2 where git - # may not be on PATH, and avoids any path-conversion of the URL. - print(f">>> git clone --depth 1 --branch {branch} {url} {dest}", flush=True) - subprocess.run( - ["git", "clone", "--depth", "1", "--branch", branch, url, dest], - check=True, - ) - else: - # Pull the latest commits so that a stale shallow clone never causes - # API-mismatch build failures (e.g. a new method added to Clp that - # Cbc already calls, but the local clone predates the push). - print(f">>> git -C {dest} pull --ff-only", flush=True) - subprocess.run( - ["git", "-C", dest, "pull", "--ff-only"], - check=True, - ) - return dest - - - -# ── Build OpenBLAS (static, with Fortran/LAPACK) ───────────────────────────── - -def build_openblas(dest_dir, *, dynamic_arch=False, target=None, extra_cflags="", - dynamic_list=None): - src = clone_if_missing( - "OpenBLAS", - "https://github.com/xianyi/OpenBLAS.git", - OPENBLAS_TAG, - ) - # OpenBLAS builds in-tree; always clean before each build so that a - # second (AVX2) pass doesn't inherit object files from the first. - run("make", "clean", cwd=src) - - make_vars = [] - if platform.system() == "Windows": - make_vars.append("BINARY=64") - if target: - # Explicit CPU target (e.g. TARGET=HASWELL for AVX2 build). - make_vars.append(f"TARGET={target}") - elif dynamic_arch: - # DYNAMIC_ARCH=1 compiles multiple kernels and dispatches at runtime. - # NO_AVX512=1: AVX-512 kernels use very large stack frames (ZMM spills) - # that overflow the 512 KB default pthread stack on macOS, causing - # SIGSEGV inside dgetrf_single on CI runners with Ice Lake Xeons. - # Our wheel targets Haswell (AVX2) as the high-water mark, so AVX-512 - # is never needed. - # This must ALSO be applied on Windows: without DYNAMIC_ARCH/NO_AVX512, - # OpenBLAS falls back to build-time CPUID auto-detection of the CI - # runner's host CPU, and some GitHub-hosted Windows runners land on - # AVX-512-capable hosts, which then intermittently fail to compile - # OpenBLAS's SkylakeX kernel under MinGW GCC 16.2 ("inlining failed - # in call to always_inline ...: target specific option mismatch"). - # That is the root cause of the flaky "Compile Windows *" job failures - # seen across many CI runs — it depends on which physical host the - # runner happens to land on, not on any code change. - make_vars.append("DYNAMIC_ARCH=1") - make_vars.append("NO_AVX512=1") - # Limit the set of compiled kernels to modern CPUs; pre-2010 - # architectures (BARCELONA, CORE2, …) are dropped. CPUs not in the - # list fall back to the generic kernel — correctness is unaffected. - if dynamic_list: - make_vars.append(f"DYNAMIC_LIST={dynamic_list}") - - # Never build the CBLAS C interface — COIN-OR uses Fortran-style BLAS - # (dgemm_, dtrsm_, …) so the cblas_* wrappers are dead weight. - make_vars.append("NO_CBLAS=1") - - if extra_cflags: - make_vars.append(f"CFLAGS={extra_cflags}") - make_vars.append(f"FFLAGS={extra_cflags}") - - # Build static + shared libs (libs target skips test suite). - # Building shared is required on all platforms so configure scripts - # (CoinUtils LAPACK test) can link against the shared library — static - # requires explicit Fortran runtime flags which is fragile. - run("make", f"-j{NPROC}", "libs", "shared", *make_vars, cwd=src) - run("make", *make_vars, f"PREFIX={dest_dir}", "install", cwd=src) - - -# ── Build SuiteSparse AMD (static only, direct compilation) ────────────────── - -def build_amd(extra_cflags=""): - src = clone_if_missing( - "SuiteSparse", - "https://github.com/DrTimothyAldenDavis/SuiteSparse.git", - SUITESPARSE_TAG, - ) - ss_dir = os.path.join(src, "SuiteSparse_config") - amd_src = os.path.join(src, "AMD", "Source") - amd_inc = os.path.join(src, "AMD", "Include") - - os.makedirs(LIB_DIR, exist_ok=True) - inc_out = os.path.join(DIST_DIR, "include", "suitesparse") - os.makedirs(inc_out, exist_ok=True) - - # AMD is a pure combinatorial/integer library — it doesn't use BLAS or - # LAPACK at all. Compiling directly from C source avoids the SuiteSparse - # cmake build system which unconditionally runs FindBLAS (and fails when - # only a static OpenBLAS is present because the link test needs the - # Fortran runtime libraries). - cc = os.environ.get("CC", "gcc") - cflags = ["-O2", "-fPIC", f"-I{ss_dir}", f"-I{amd_inc}"] - if extra_cflags: - cflags.extend(extra_cflags.split()) - - # SuiteSparse_config.c → libsuitesparseconfig.a - ss_obj = os.path.join(THIS_DIR, "_ss_config.o") - run(cc, *cflags, "-c", - os.path.join(ss_dir, "SuiteSparse_config.c"), "-o", ss_obj) - run("ar", "rcs", os.path.join(LIB_DIR, "libsuitesparseconfig.a"), ss_obj) - - # AMD/Source/*.c → libamd.a - amd_objs = [] - for c_file in sorted(_glob.glob(os.path.join(amd_src, "*.c"))): - obj = c_file[:-2] + ".o" - run(cc, *cflags, "-c", c_file, "-o", obj) - amd_objs.append(obj) - run("ar", "rcs", os.path.join(LIB_DIR, "libamd.a"), *amd_objs) - - # Install headers - for h in _glob.glob(os.path.join(amd_inc, "*.h")): - shutil.copy2(h, inc_out) - shutil.copy2(os.path.join(ss_dir, "SuiteSparse_config.h"), inc_out) - - -# ── Build COIN-OR projects ──────────────────────────────────────────────────── - -def build_coin_or(dest_dir=None, extra_cxxflags="", extra_ldflags="", is_debug=False): - """Build the full COIN-OR stack and install into *dest_dir*. - - *extra_cxxflags* is appended to CXXFLAGS and can be used to enable - architecture-specific optimisations (e.g. "-O3 -march=haswell -DCOIN_AVX2=4" - for the Haswell-optimised build) or debug flags. - - *extra_ldflags* is appended to LDFLAGS for all configure calls. On macOS, - the Clp project already gets "-L{lib_dir} -lopenblas"; extra_ldflags is - merged into that rather than passed separately. - - *is_debug* controls whether C assert()s are compiled in. COIN-OR's own - AC_COIN_PROG_CXX/AC_COIN_PROG_CC autoconf macros normally default - CXXFLAGS/CFLAGS to "-O2 -DNDEBUG" (release) or "-g" (--enable-debug), but - only via ": ${CXXFLAGS:=...}" -- a shell default that never fires because - we always pass CXXFLAGS/CFLAGS explicitly on the configure command line - (to control -march, -ffp-contract, etc.). That silently opted every - release build (generic and AVX2, on every platform) out of -DNDEBUG, - leaving internal consistency assert()s compiled into the SHIPPED wheels: - an assert failing in the field aborts (SIGABRT/access violation) the - caller's process instead of leaving debugging to an explicit debug build. - So: pass is_debug=True for the debug/debug_avx2 variants (which - deliberately want assertions active, see scripts/build_mip_debug_cuts.sh) - and leave it False (the default) for the generic/avx2 release variants, - which now get -DNDEBUG explicitly. - """ - if dest_dir is None: - dest_dir = DIST_DIR - lib_dir = os.path.join(dest_dir, "lib") - - # AMD is a pure combinatorial/integer library that does not benefit from - # AVX2 and is only built once (into the base cbc_dist/). Both COIN-OR - # variants can safely link against the same static archive. - amd_inc = os.path.join(DIST_DIR, "include", "suitesparse") - - env = os.environ.copy() - pkg_config_dir = os.path.join(lib_dir, "pkgconfig") - # MSYS2 bash uses ':' as separator and MSYS2-format paths. - if platform.system() == "Windows": - env["PKG_CONFIG_PATH"] = ( - _win_to_msys2(pkg_config_dir) + ":" + env.get("PKG_CONFIG_PATH", "") - ) - else: - env["PKG_CONFIG_PATH"] = ( - pkg_config_dir + os.pathsep + env.get("PKG_CONFIG_PATH", "") - ) - - # Flags common to every project. - # zlib is intentionally kept enabled (it is manylinux2014-allowed and - # lets CBC read compressed MPS/LP files). - common = [ - f"--prefix={dest_dir}", - f"--libdir={lib_dir}", - "--enable-static" if platform.system() != "Windows" else "--disable-static", - "--enable-shared", # produce .so/.dylib/.dll for cffi use - "--disable-readline", # libreadline not manylinux-allowed - "--disable-bzlib", # libbz2 not manylinux-allowed - "--without-cholmod", # use AMD instead - "--without-glpk", # not needed - "--without-asl", # AMPL solver library not needed - ] - if platform.system() == "Windows": - # Declare both build and host as MinGW64 so that autoconf sets - # cross_compiling=no (build==host) and uses the plain gcc/g++ from - # /mingw64/bin rather than looking for x86_64-w64-mingw32-prefixed - # cross-compiler tools. With host_os=mingw32, libtool names DLLs as - # lib*.dll (MinGW convention) rather than cyg*.dll (Cygwin convention). - common += ["--build=x86_64-w64-mingw32", "--host=x86_64-w64-mingw32"] - - # Use a distinct build sub-directory per variant so that generic, - # AVX2, and debug builds can coexist in the same cloned source tree. - if dest_dir == DIST_DIR: - bld_suffix = "_build" - elif dest_dir == DIST_DIR_AVX2: - bld_suffix = "_build_avx2" - elif dest_dir == DIST_DIR_DEBUG_AVX2: - bld_suffix = "_build_debug_avx2" - else: - bld_suffix = "_build_debug" - - for name, url in COIN_REPOS: - src = clone_if_missing(name, url, COIN_OR_BRANCH) - bld = os.path.join(src, bld_suffix) - os.makedirs(bld, exist_ok=True) - - extra = [] - ldflags_in_extra = False - if name == "CoinUtils": - # OpenBLAS provides both BLAS and LAPACK in one archive. - # AMD provides fill-reducing ordering for sparse systems. - # - # On macOS, COINUTILS_HAS_LAPACK causes CoinDenseFactorization to - # call OpenBLAS's dgetrf_single, which uses aligned AVX2 loads - # (vmovdqa). CoinDenseFactorization's elements_ buffer is allocated - # with plain new[] (8/16-byte aligned), causing SIGSEGV on the - # unaligned 32-byte AVX2 access. Disabling LAPACK for CoinUtils - # makes it fall back to the built-in pure-C pivot factorization, - # which is correct and fast enough for the small dense bases - # encountered in practice (e.g. 36-row LP). Clp still gets LAPACK - # via its own --with-lapack-lflags. - if platform.system() == "Darwin": - extra += ["--without-lapack"] - else: - extra += [f"--with-lapack-lflags=-L{lib_dir} -lopenblas"] - extra += [ - f"--with-amd-cflags=-I{amd_inc}", - f"--with-amd-lflags=-L{LIB_DIR} -lamd -lsuitesparseconfig", - ] - elif name == "Clp": - # Do NOT pass --with-amd-cflags here: Clp wraps #include - # inside extern "C" {} in ClpCholeskyUfl.cpp, and SuiteSparse v7's - # amd.h transitively includes C++ headers ( etc.) which - # clang rejects inside extern "C". AMD ordering is still available - # to CBC through CoinUtils which doesn't have this wrapping issue. - # --without-amd is required (not just omitting the flag): Clp's - # configure auto-probes default system paths (e.g. - # /usr/include/suitesparse) for AMD and, if found, compiles - # ClpCholeskyUfl.cpp against it — but without --with-amd-lflags - # the resulting libClp fails to link with undefined references to - # SuiteSparse_malloc/SuiteSparse_free on any host that happens to - # have a system SuiteSparse package installed. - extra += [ - "--without-amd", - f"--with-lapack-lflags=-L{lib_dir} -lopenblas", - ] - if platform.system() == "Darwin": - # ClpMain.cpp calls openblas_set_num_threads() when CLP_USE_OPENBLAS=1. - # On macOS two-level namespace, the clp executable must link -lopenblas - # explicitly — transitive propagation via libClp.dylib is not enough. - # --with-lapack-lflags covers shared library dependencies but the - # executable link step needs LDFLAGS for direct symbol resolution. - darwin_ldflags = f"-L{lib_dir} -lopenblas" - if extra_ldflags: - darwin_ldflags += f" {extra_ldflags}" - extra += [f"LDFLAGS={darwin_ldflags}"] - ldflags_in_extra = True - elif platform.system() == "Linux": - # ClpRacingSolver.cpp resolves openblas_set_num_threads() via - # dlsym(RTLD_DEFAULT, ...) when CLP_USE_OPENBLAS is defined - # (which it always is here). glibc < 2.34 (e.g. manylinux2014, - # manylinux_2_28) keeps dlopen/dlsym in libdl rather than libc, - # and it is not linked automatically, causing "undefined - # reference to `dlsym'" when linking libClp.so. Newer glibc - # (>= 2.34) folds libdl into libc, so -ldl is a harmless no-op - # there. - linux_ldflags = "-ldl" - if extra_ldflags: - linux_ldflags += f" {extra_ldflags}" - extra += [f"LDFLAGS={linux_ldflags}"] - ldflags_in_extra = True - elif name == "Cbc": - extra += [ - "--without-nauty", # symmetry detection via nauty disabled - "--without-lapack", - # Enable multi-threaded MIP search (parallel branch-and-bound). - # Requires pthreads — available on all supported platforms. - "--enable-cbc-parallel", - ] - if platform.system() == "Linux": - # Same dlsym/libdl requirement as Clp above: CbcModel.cpp and - # Cbc_C_Interface.cpp also resolve openblas_set_num_threads() - # and omp_set_num_threads() via dlsym(RTLD_DEFAULT, ...). - linux_ldflags = "-ldl" - if extra_ldflags: - linux_ldflags += f" {extra_ldflags}" - extra += [f"LDFLAGS={linux_ldflags}"] - ldflags_in_extra = True - else: # Osi, Cgl — do not use LAPACK directly - extra += ["--without-lapack"] - - configure = os.path.join(src, "configure") - # clang (macOS) requires C++17 mode to accept aggregate assignment from - # braced initializer lists (e.g. CoinDynamicConflictGraph.cpp). - # -std=c++17 is harmless on GCC as well. - # Note: -no-undefined is NOT passed here via LDFLAGS because it is a - # libtool-specific flag, not a raw linker flag. Passing it via LDFLAGS - # causes configure's own link tests ("C compiler cannot create - # executables") to fail with exit code 77 on MinGW. COIN-OR's - # AC_COIN_PROG_LIBTOOL macro already appends -no-undefined to - # LT_LDFLAGS internally (aclocal.m4), which is the correct path — - # it reaches libtool only when building shared libraries. - # Clp and Cbc both check CLP_USE_OPENBLAS to conditionally compile - # the OpenBLAS thread-count management (openblas_set_num_threads). - # This enables CbcModel to cap BLAS threads to 1 during parallel B&B, - # preventing stack overflow crashes on macOS whose secondary threads - # have a 512 KB default stack (vs 8 MB on Linux). - openblas_flag = "-DCLP_USE_OPENBLAS=1" if name in ("Clp", "Cbc") else "" - # -DNDEBUG strips assert()s for release builds (never added by - # autoconf's own defaults here -- see the is_debug docstring above); - # debug builds must NOT get it so assertions stay active for gdb/ASan. - ndebug_flag = "" if is_debug else "-DNDEBUG" - cxxflags_parts = ["-std=c++17", FP_CONTRACT_OFF] - if ndebug_flag: - cxxflags_parts.append(ndebug_flag) - if extra_cxxflags: - cxxflags_parts.append(extra_cxxflags) - if openblas_flag: - cxxflags_parts.append(openblas_flag) - cxxflags = " ".join(cxxflags_parts) - # Also set CFLAGS: the COIN-OR stack is pure C++ (no .c sources as of - # this writing), so this is mostly a defensive no-op, but configure's - # own link/feature-detection tests invoke $CC directly and future .c - # files would silently pick up FMA contraction otherwise. - cflags_parts = [FP_CONTRACT_OFF] - if ndebug_flag: - cflags_parts.append(ndebug_flag) - configure_args = [ - configure, *common, *extra, - f"CXXFLAGS={cxxflags}", f"CFLAGS={' '.join(cflags_parts)}", - ] - if extra_ldflags and not ldflags_in_extra: - configure_args.append(f"LDFLAGS={extra_ldflags}") - run(*configure_args, cwd=bld, env=env) - run("make", "-j", NPROC, cwd=bld) - run("make", "install", cwd=bld) - - -# ── Bundle dynamic dependencies ─────────────────────────────────────────────── - -def _is_system_lib(path: str) -> bool: - """Return True for libs that should NOT be bundled into the wheel.""" - name = os.path.basename(path) - if platform.system() == "Linux": - return bool(_MANYLINUX_ALLOWED.match(name)) - # macOS: only the standard system library trees are truly "system". - # A loader-relative (@rpath/@loader_path/@executable_path) install name - # does NOT necessarily mean the lib has already been bundled: modern - # Homebrew GCC reports its own runtime libs (libgcc_s, libstdc++, ...) - # with an @rpath install name from the start, before cbcbox's own - # bundling pass ever touches them. Those still need to be resolved to - # a real file on disk and copied in; see _resolve_macos_dep(). - return path.startswith("/usr/lib/") or path.startswith("/System/") - - -def _resolve_macos_dep(binary: str, dep: str) -> str: - """Resolve a loader-relative (@rpath/@loader_path/@executable_path) - dependency reference *dep* found in *binary* to an absolute file path. - - Returns "" if it cannot be resolved. - """ - if not dep.startswith("@"): - return dep - name = os.path.basename(dep) - base_dir = os.path.dirname(os.path.realpath(binary)) - - # 0) @loader_path/@executable_path are directly relative to the - # binary's own directory -- no need to consult LC_RPATH. - if dep.startswith("@loader_path/") or dep.startswith("@executable_path/"): - candidate = os.path.join(base_dir, name) - if os.path.exists(candidate): - return candidate - - # 1) Look at the binary's own LC_RPATH entries (this is how the - # dynamic linker would actually resolve an @rpath/ reference). - if dep.startswith("@rpath/"): - out = subprocess.run( - ["otool", "-l", binary], capture_output=True, text=True - ).stdout - lines = out.splitlines() - rpaths = [] - for i, line in enumerate(lines): - if line.strip() == "cmd LC_RPATH": - for j in range(i, min(i + 4, len(lines))): - m = re.search(r"path (.+) \(offset", lines[j]) - if m: - rpaths.append(m.group(1).strip()) - break - for rp in rpaths: - rp = rp.replace("@loader_path", base_dir).replace( - "@executable_path", base_dir - ) - candidate = os.path.join(rp, name) - if os.path.exists(candidate): - return candidate - - # 2) Fall back to the compiler toolchain -- this covers GCC runtime - # libs (libgcc_s, libstdc++, libquadmath, libgfortran) that Homebrew - # GCC reports with an @rpath install name but which live in the - # toolchain's own lib directory, not next to *binary*. - for cc in ("gfortran", "gcc", "g++"): - try: - r = subprocess.run( - [cc, "-print-file-name=" + name], - capture_output=True, text=True, - ) - except FileNotFoundError: - continue - candidate = r.stdout.strip() - if candidate and candidate != name and os.path.exists(candidate): - return candidate - - return "" - - -def _soname_linux(lib_path: str) -> str: - """Return the ELF SONAME of *lib_path*, falling back to its basename.""" - try: - r = subprocess.run( - ["patchelf", "--print-soname", lib_path], - capture_output=True, text=True, - ) - soname = r.stdout.strip() - if soname: - return soname - except Exception: - pass - return os.path.basename(lib_path) - - -def _dynamic_deps_linux(path: str) -> dict: - """Return {soname: resolved_path} for non-system deps on Linux.""" - out = subprocess.run(["ldd", path], capture_output=True, text=True).stdout - deps = {} - for line in out.splitlines(): - m = re.search(r"\s=>\s(/\S+)", line) - if m and not _is_system_lib(m.group(1)): - resolved = m.group(1) - # Use the ELF soname as the destination filename so the dynamic - # linker finds it even when ldd reports the versioned real file. - deps[_soname_linux(resolved)] = resolved - return deps - - -def _dynamic_deps_macos(path: str) -> list: - """Return [install_name, ...] for non-system deps on macOS.""" - out = subprocess.run( - ["otool", "-L", path], capture_output=True, text=True - ).stdout - deps = [] - for line in out.splitlines()[1:]: - parts = line.strip().split() - if parts and not _is_system_lib(parts[0]): - deps.append(parts[0]) - return deps - - -def _dynamic_deps_windows(path: str) -> dict: - """Return {dll_name: source_path} for non-system MinGW DLLs needed by *path*. - - Uses objdump (from MinGW64) to list DLL imports, then resolves each name - against C:\\msys64\\mingw64\\bin\\ and DIST_DIR\\bin\\ (for DLLs installed - by our own build, such as libopenblas.dll). - """ - search_dirs = [r"C:\msys64\mingw64\bin", os.path.join(DIST_DIR, "bin")] - r = subprocess.run( - [_MSYS2_BASH, "-lc", - "export PATH=/mingw64/bin:/usr/bin:$PATH && " - f"objdump -p {shlex.quote(_win_to_msys2(path))} | grep 'DLL Name:'"], - capture_output=True, text=True, - ) - deps = {} - for line in r.stdout.splitlines(): - m = re.search(r"DLL Name:\s+(\S+\.dll)", line, re.IGNORECASE) - if m: - name = m.group(1) - if not _WIN_SYS_DLL.match(name): - for search_dir in search_dirs: - candidate = os.path.join(search_dir, name) - if os.path.exists(candidate): - deps[name] = candidate - break - return deps - - -def bundle_dynamic_deps(binary: str, lib_dir: str, _visited: set = None): - """ - Inspect *binary* for non-system dynamic dependencies and handle them so - the wheel is self-contained: - - Linux — copies each .so to *lib_dir* and uses patchelf to set an - $ORIGIN-relative RPATH on both the binary and every copied lib. - macOS — copies each .dylib to *lib_dir*, rewrites the install-name - reference inside *binary* to @rpath/name, sets the copied - lib's own id to @rpath/name, and adds an @loader_path rpath - to the binary. - Windows — copies each non-system MinGW DLL next to the binary (in its - own directory); no rpath patching needed since Windows searches - the executable's directory first. - - Recurses into copied libraries so transitive deps are also covered. - If everything was statically linked this function is a no-op. - """ - if _visited is None: - _visited = set() - os.makedirs(lib_dir, exist_ok=True) - - in_lib_dir = ( - os.path.dirname(os.path.realpath(binary)) == os.path.realpath(lib_dir) - ) - - if platform.system() == "Linux": - rpath = "$ORIGIN" if in_lib_dir else "$ORIGIN/../lib" - subprocess.run(["patchelf", "--set-rpath", rpath, binary], check=True) - - for name, src_path in _dynamic_deps_linux(binary).items(): - if name in _visited: - continue - _visited.add(name) - dst = os.path.join(lib_dir, name) - if not os.path.exists(dst): - shutil.copy2(src_path, dst) - os.chmod(dst, 0o755) - bundle_dynamic_deps(dst, lib_dir, _visited) - - elif platform.system() == "Darwin": - rpath = "@loader_path" if in_lib_dir else "@loader_path/../lib" - # Silently ignore "already exists" errors from install_name_tool. - subprocess.run( - ["install_name_tool", "-add_rpath", rpath, binary], - capture_output=True, - ) - - for install_name in _dynamic_deps_macos(binary): - name = os.path.basename(install_name) - - # Always resolve a real on-disk source for this dependency (needed - # to copy it into lib_dir even when `binary`'s reference is - # already @rpath/name and needs no rewrite -- e.g. Homebrew GCC's - # own libgcc_s reported natively via @rpath). - src_path = install_name - if install_name.startswith("@"): - resolved = _resolve_macos_dep(binary, install_name) - if not resolved: - print( - f"warning: could not resolve {install_name} " - f"referenced by {binary}; not bundled, may " - f"fail to load at runtime", - file=sys.stderr, - ) - src_path = resolved or None - - # Always fix up *this* binary's own reference, regardless of - # whether the target dependency file has already been bundled - # ("visited") via some other referencing binary. The previous - # code skipped this whenever `name in _visited`, which meant - # only the *first* binary reaching a given dependency (in DFS - # traversal order) got its reference rewritten to @rpath -- - # every other binary kept the absolute build-machine path. - if install_name != f"@rpath/{name}": - subprocess.run( - ["install_name_tool", "-change", install_name, f"@rpath/{name}", binary], - check=True, - ) - - if name in _visited: - continue - _visited.add(name) - - dst = os.path.join(lib_dir, name) - if not os.path.exists(dst) and src_path: - shutil.copy2(src_path, dst) - os.chmod(dst, 0o755) - if os.path.exists(dst): - # Normalize the lib's own id to @rpath/name. This also - # covers libraries that were already sitting in lib_dir - # from the start (e.g. sibling COIN-OR libs built straight - # there) and never went through the "freshly copied" path - # above, which previously left their LC_ID_DYLIB as an - # absolute build-machine path. - subprocess.run( - ["install_name_tool", "-id", f"@rpath/{name}", dst], - capture_output=True, - ) - bundle_dynamic_deps(dst, lib_dir, _visited) - - elif platform.system() == "Windows": - # On Windows the loader finds DLLs in the same directory as the - # executable. Copy all non-system MinGW DLLs there and recurse for - # transitive dependencies (libgfortran → libquadmath, etc.). - bin_dir = os.path.dirname(os.path.realpath(binary)) - for name, src_path in _dynamic_deps_windows(binary).items(): - if name in _visited: - continue - _visited.add(name) - dst = os.path.join(bin_dir, name) - if not os.path.exists(dst): - shutil.copy2(src_path, dst) - os.chmod(dst, 0o755) - bundle_dynamic_deps(dst, lib_dir, _visited) - - -# ── Windows: mirror DLLs into lib/ ─────────────────────────────────────────── - -def copy_win_dlls_to_lib(dist_dir=None): - """Copy every DLL from bin/ into lib/ as well. - - On Windows, libtool installs DLLs to bindir (bin/) and import libs - (.dll.a) to libdir (lib/). python-mip and other ctypes/cffi consumers - locate libraries via cbcbox.cbc_lib_dir() which points to lib/, so all - DLLs must also be present there — both the COIN-OR ones (libCbc.dll, …) - and the bundled MinGW runtime DLLs they depend on. - """ - if dist_dir is None: - dist_dir = DIST_DIR - bin_dir = os.path.join(dist_dir, "bin") - lib_dir = os.path.join(dist_dir, "lib") - os.makedirs(lib_dir, exist_ok=True) - for dll in _glob.glob(os.path.join(bin_dir, "*.dll")): - dst = os.path.join(lib_dir, os.path.basename(dll)) - if not os.path.exists(dst): - shutil.copy2(dll, dst) - - - -_cbc_exe = "cbc.exe" if platform.system() == "Windows" else "cbc" - -# CBCBOX_BUILD_VARIANT controls which variants are compiled (used by CI to run -# builds in parallel jobs): -# unset / "all" — build generic, AVX2 (x86_64), and debug: -# x86_64: generic + avx2 + debug_avx2 -# other arch: generic + debug -# "generic" — build only the generic variant (no debug) -# "avx2" — build only the AVX2 variant (x86_64 only, no debug) -# "debug" — build only the non-AVX2 debug variant (all platforms) -# COIN-OR flags: -O1 -g -fno-omit-frame-pointer -# AddressSanitizer automatically enabled on Linux and macOS. -# "debug_avx2" — build only the debug+AVX2 variant (x86_64 only) -# COIN-OR flags: -O1 -g -march=haswell -fno-omit-frame-pointer -# AddressSanitizer automatically enabled on Linux and macOS. -# Use this to debug AVX2-specific issues or to run a debuggable -# binary that exercises the same AVX2 code paths as the release. -# "release_symbols" — build only the generic *release* variant (same CXXFLAGS, -# same optimisation level, same -DNDEBUG) with -g added on -# top, so the resulting binary is bit-for-bit behaviourally -# identical to what actually ships, just with debug symbols. -# Use this (under gdb) to get a real backtrace for bugs that -# only reproduce with release codegen/optimisation and do -# NOT reproduce in the "debug" (-O1) variant. -# In "avx2" mode AMD is still compiled (as a link-time static dep for the -# COIN-OR AVX2 build) but with Haswell-optimised flags. -# OpenBLAS and AMD are always built without debug flags; only the COIN-OR stack -# (CoinUtils, Osi, Clp, Cgl, Cbc) carries debug symbols in debug builds. -# -# CBCBOX_BUILD_ONLY=1 — skip the wheel-packaging stage (used by CI compile -# jobs that only need the binaries, not the final .whl). -_build_variant = os.environ.get("CBCBOX_BUILD_VARIANT", "") -_build_release_symbols = _build_variant == "release_symbols" -_build_generic = _build_variant not in ("avx2", "debug", "debug_avx2", "release_symbols") -_build_avx2 = _is_x86_64() and _build_variant not in ("generic", "debug", "debug_avx2", "release_symbols") -# Debug (non-AVX2): built by default on non-x86_64; on x86_64 only when -# explicitly requested so we avoid a redundant generic-debug wheel alongside -# the haswell-debug one. -_build_debug = _build_variant == "debug" or (_build_variant == "" and not _is_x86_64()) -# Debug+AVX2: built by default on x86_64 (the only debug variant for that arch). -_build_debug_avx2 = _is_x86_64() and (_build_variant == "debug_avx2" or _build_variant == "") - -if _build_variant == "debug_avx2" and not _is_x86_64(): - print( - f"[cbcbox] WARNING: CBCBOX_BUILD_VARIANT=debug_avx2 is only supported on x86_64 " - f"(current arch: {platform.machine()}). No build will be performed.", - flush=True, - ) - -# Flags applied to all C/C++ code in the AVX2 variant, including the static -# AMD library that is ultimately linked into COIN-OR .so/.dylib. -_AVX2_CFLAGS = "-O3 -march=haswell" - -# OpenBLAS DYNAMIC_ARCH kernel lists for x86_64 (reduces library size by -# dropping pre-2010 architectures that are unlikely to be encountered). -# CPUs not covered by the list fall back to the generic kernel automatically. -# Generic: SSE4.2 baseline (Nehalem 2008+) through current Zen. -# AVX2: only AVX2-capable targets (Haswell 2013+). -# ZEN2, ZEN3, SKYLAKE are excluded: OpenBLAS 0.3.31 compiles their generic C -# dot/nrm2 kernels with AVX2+FMA intrinsics but omits -mfma from the compiler -# invocation, causing an "always_inline target mismatch" build error. -# ZEN2/ZEN3 users fall back to the ZEN kernel (still AVX2); SKYLAKE falls back -# to HASWELL — both negligible in practice. -_OPENBLAS_DYNLIST_X86_GENERIC = "NEHALEM SANDYBRIDGE HASWELL SKYLAKEX ZEN" -_OPENBLAS_DYNLIST_X86_AVX2 = "HASWELL SKYLAKEX" - -# AddressSanitizer is automatically enabled for debug builds on Linux and macOS. -# Windows/MinGW does not support ASan, so it is skipped there. -# OpenBLAS is always built without ASan to avoid false positives from its -# hand-optimised BLAS kernels; only the COIN-OR stack is instrumented. -# Debug build flags: -O1 -g (no ASan — ASan proved too fragile across -# the different CI container toolchains; debug info alone is sufficient -# for stack traces and debugger use). -_DEBUG_CFLAGS = "-O1 -g -fno-omit-frame-pointer" -_DEBUG_LDFLAGS = "" -_DEBUG_AVX2_CFLAGS = "-O1 -g -march=haswell -fno-omit-frame-pointer" -_DEBUG_AVX2_LDFLAGS = "" - -if _build_generic and not os.path.exists(os.path.join(DIST_DIR, "bin", _cbc_exe)): - build_openblas(DIST_DIR, dynamic_arch=True, - dynamic_list=_OPENBLAS_DYNLIST_X86_GENERIC if _is_x86_64() else None) - build_amd() - build_coin_or(DIST_DIR) - -# Release-with-symbols build: identical CXXFLAGS/optimisation/-DNDEBUG to the -# generic release build above, with only "-g -fno-omit-frame-pointer" added. -# Used to get real backtraces (via gdb) for bugs that reproduce only with -# release codegen and do NOT reproduce in the (differently-optimised) "debug" -# variant. Built into DIST_DIR itself (mutually exclusive with _build_generic) -# since it's meant to stand in for the release binary, not ship alongside it. -if _build_release_symbols and not os.path.exists(os.path.join(DIST_DIR, "bin", _cbc_exe)): - build_openblas(DIST_DIR, dynamic_arch=True, - dynamic_list=_OPENBLAS_DYNLIST_X86_GENERIC if _is_x86_64() else None) - build_amd() - build_coin_or(DIST_DIR, extra_cxxflags="-g -fno-omit-frame-pointer") - -# AVX2-optimised build: all x86_64 platforms (Linux, macOS, Windows). -# In avx2-only mode AMD is still needed as a link-time static dep for the -# COIN-OR AVX2 build; compile it with Haswell flags so it is fully -# optimised and ends up embedded in the AVX2 COIN-OR shared libraries. -if (not _build_generic and not _build_debug and not _build_debug_avx2 - and not _build_release_symbols - and not os.path.exists(os.path.join(LIB_DIR, "libamd.a"))): - build_amd(extra_cflags=_AVX2_CFLAGS) - -if _build_avx2 and not os.path.exists(os.path.join(DIST_DIR_AVX2, "bin", _cbc_exe)): - # Use DYNAMIC_ARCH=1 rather than TARGET=HASWELL for OpenBLAS: TARGET=HASWELL - # mandates aligned AVX2 loads in dgetrf_single; CoinDenseFactorization may - # pass unaligned data which causes SIGSEGV on macOS Intel. DYNAMIC_ARCH - # dispatches to the best available kernel at runtime without that assumption. - # The Haswell advantage comes from -march=haswell on the COIN-OR stack. - build_openblas(DIST_DIR_AVX2, dynamic_arch=True, - dynamic_list=_OPENBLAS_DYNLIST_X86_AVX2) - build_coin_or(DIST_DIR_AVX2, extra_cxxflags=f"{_AVX2_CFLAGS} -DCOIN_AVX2=4") - -# Debug build: OpenBLAS is built WITHOUT debug flags (no debug info for -# third-party code); only the COIN-OR stack gets full debug flags + optional -# ASan. AMD is a static link-time dep shared with the base dist. -if _build_debug and not os.path.exists(os.path.join(DIST_DIR_DEBUG, "bin", _cbc_exe)): - build_openblas(DIST_DIR_DEBUG, dynamic_arch=True, - dynamic_list=_OPENBLAS_DYNLIST_X86_GENERIC if _is_x86_64() else None) - if not os.path.exists(os.path.join(LIB_DIR, "libamd.a")): - build_amd() - build_coin_or(DIST_DIR_DEBUG, - extra_cxxflags=_DEBUG_CFLAGS, - extra_ldflags=_DEBUG_LDFLAGS, - is_debug=True) - -# Debug+AVX2 build (x86_64 only): like the debug build but with -march=haswell -# and -DCOIN_AVX2=4 so the binary exercises the same AVX2 code paths as the -# release. OpenBLAS is built WITHOUT debug flags (no debug info for third-party -# code); only the COIN-OR stack gets debug+AVX2 flags. -# AMD is shared with the base dist (a pure integer lib, no SIMD). -if _build_debug_avx2 and not os.path.exists(os.path.join(DIST_DIR_DEBUG_AVX2, "bin", _cbc_exe)): - build_openblas(DIST_DIR_DEBUG_AVX2, dynamic_arch=True, - dynamic_list=_OPENBLAS_DYNLIST_X86_AVX2) - if not os.path.exists(os.path.join(LIB_DIR, "libamd.a")): - build_amd() - build_coin_or(DIST_DIR_DEBUG_AVX2, - extra_cxxflags=f"{_DEBUG_AVX2_CFLAGS} -DCOIN_AVX2=4", - extra_ldflags=_DEBUG_AVX2_LDFLAGS, - is_debug=True) - - -def _bundle_dist(dist_dir): - """Patch rpaths / bundle DLLs for all binaries and shared libs in *dist_dir*.""" - lib_dir = os.path.join(dist_dir, "lib") - bundle_dir = os.path.join(dist_dir, "bin") if platform.system() == "Windows" else lib_dir - - # Share one _visited set across every top-level call below so a - # dependency file bundled/recursed-into while processing one binary is - # recognized as already-copied when reached again from a different - # top-level binary -- while each binary's own load-command references - # are still always fixed up regardless (see bundle_dynamic_deps). - _visited = set() - - for bin_name in [_cbc_exe, "clp.exe" if platform.system() == "Windows" else "clp"]: - bin_path = os.path.join(dist_dir, "bin", bin_name) - if os.path.exists(bin_path): - bundle_dynamic_deps(bin_path, bundle_dir, _visited) - - if platform.system() == "Windows": - shared_pattern = os.path.join(dist_dir, "bin", "*.dll") - elif platform.system() == "Darwin": - shared_pattern = os.path.join(lib_dir, "*.dylib") - else: - shared_pattern = os.path.join(lib_dir, "*.so*") - - for lib_path in _glob.glob(shared_pattern): - if not os.path.islink(lib_path): - bundle_dynamic_deps(lib_path, bundle_dir, _visited) - if platform.system() == "Darwin": - # Unconditionally normalize this dylib's own LC_ID_DYLIB to - # @rpath/name. bundle_dynamic_deps only does this when the - # file is reached as some *other* binary's dependency; libs - # that happen to never appear in anyone's dependency list - # (or are reached only here, first) would otherwise keep - # their absolute build-machine install name forever. - subprocess.run( - ["install_name_tool", "-id", f"@rpath/{os.path.basename(lib_path)}", lib_path], - capture_output=True, - ) - - if platform.system() == "Windows": - copy_win_dlls_to_lib(dist_dir) - - -_bundle_dist(DIST_DIR) -if _build_avx2 and os.path.isdir(DIST_DIR_AVX2): - _bundle_dist(DIST_DIR_AVX2) -if _build_debug and os.path.isdir(DIST_DIR_DEBUG): - _bundle_dist(DIST_DIR_DEBUG) -if _build_debug_avx2 and os.path.isdir(DIST_DIR_DEBUG_AVX2): - _bundle_dist(DIST_DIR_DEBUG_AVX2) - - -def _remove_static_libs(dist_dir: str) -> None: - """Remove static (.a) and libtool (.la) files — not needed at runtime.""" - lib_dir = os.path.join(dist_dir, "lib") - removed = [] - for pattern in ("*.a", "*.la"): - for path in _glob.glob(os.path.join(lib_dir, pattern)): - os.remove(path) - removed.append(os.path.basename(path)) - if removed: - print(f"[cbcbox] removed static libs from {lib_dir}: {', '.join(sorted(removed))}") - - -def _strip_binaries(dist_dir: str) -> None: - """Strip debug/unneeded symbols from shared libraries and executables. - - Linux: ``strip --strip-unneeded`` removes unreferenced symbols while - keeping all exported symbols needed for dynamic linking. - macOS: ``strip -x`` removes local/private symbols, preserving exports. - Windows: skipped — strip on MinGW DLLs is unreliable. - Callers must NOT invoke this on debug builds (symbols are intentional). - """ - system = platform.system() - if system == "Windows": - return - - if system == "Darwin": - strip_args = ["strip", "-x"] - lib_glob = os.path.join(dist_dir, "lib", "*.dylib") - else: - strip_args = ["strip", "--strip-unneeded"] - lib_glob = os.path.join(dist_dir, "lib", "*.so*") - - targets = [ - p for p in - _glob.glob(lib_glob) + _glob.glob(os.path.join(dist_dir, "bin", "*")) - if os.path.isfile(p) and not os.path.islink(p) - ] - - stripped, skipped = [], [] - for path in targets: - r = subprocess.run([*strip_args, path], capture_output=True) - (stripped if r.returncode == 0 else skipped).append(os.path.basename(path)) - - if stripped: - print(f"[cbcbox] stripped symbols: {', '.join(sorted(stripped))}", flush=True) - if skipped: - print(f"[cbcbox] strip skipped (already stripped?): {', '.join(sorted(skipped))}", flush=True) - - -# On Windows, libCbc.dll.a is the MinGW import library needed to link -# against the Cbc DLL (e.g. by scripts/build_mip_debug_cuts.sh, which runs -# as a separate CI step *after* build_ext / CBCBOX_BUILD_ONLY=1 completes). -# Keep static libs around for the debug variants in that case so the -# mip-debug-cuts diagnostic tool can still be linked; they're stripped from -# the final wheel by the "Package ..." jobs' own build (CBCBOX_BUILD_ONLY -# unset), which is a separate setup.py invocation. -_keep_debug_static_libs = ( - platform.system() == "Windows" and os.environ.get("CBCBOX_BUILD_ONLY") -) - -_remove_static_libs(DIST_DIR) -# Skip stripping for release_symbols builds: stripping would defeat the -# entire point (removing the -g symbols we just added for gdb backtraces). -if not _build_release_symbols: - _strip_binaries(DIST_DIR) -if _build_avx2 and os.path.isdir(DIST_DIR_AVX2): - _remove_static_libs(DIST_DIR_AVX2) - _strip_binaries(DIST_DIR_AVX2) -if _build_debug and os.path.isdir(DIST_DIR_DEBUG) and not _keep_debug_static_libs: - _remove_static_libs(DIST_DIR_DEBUG) -if _build_debug_avx2 and os.path.isdir(DIST_DIR_DEBUG_AVX2) and not _keep_debug_static_libs: - _remove_static_libs(DIST_DIR_DEBUG_AVX2) - - -# ── Package ─────────────────────────────────────────────────────────────────── -# Skip the wheel-packaging stage when CBCBOX_BUILD_ONLY=1 (CI compile jobs -# that only need the pre-built binaries, not the final .whl). -if not os.environ.get("CBCBOX_BUILD_ONLY"): - long_description = """\ -**cbcbox** ships pre-built binaries of the -[CBC](https://github.com/coin-or/Cbc) MILP solver (COIN-OR Branch and Cut), -built from the latest next branch of the COIN-OR repositories. - -Built with: -- OpenBLAS for optimised BLAS/LAPACK routines -- AMD reordering (SuiteSparse) for improved numerical performance -- zlib for reading compressed MPS/LP files -""" - - # setuptools requires package_dir values to be relative paths (not absolute). - # Use a staging dir inside THIS_DIR so we can pass a simple relative name. - _PKG_STAGING = "_cbcbox_pkg" - _pkg_dir = os.path.join(THIS_DIR, _PKG_STAGING) - if os.path.exists(_pkg_dir): - shutil.rmtree(_pkg_dir) - os.makedirs(_pkg_dir) - - try: - dist_name = "cbc_dist" - shutil.copytree(DIST_DIR, os.path.join(_pkg_dir, dist_name), dirs_exist_ok=True) - - package_data_patterns = [f"{dist_name}/**"] - - # Include the AVX2-optimised build when present (x86_64 Linux/macOS/Windows). - dist_name_avx2 = "cbc_dist_avx2" - if os.path.isdir(DIST_DIR_AVX2): - shutil.copytree(DIST_DIR_AVX2, os.path.join(_pkg_dir, dist_name_avx2), - dirs_exist_ok=True) - package_data_patterns.append(f"{dist_name_avx2}/**") - - # Include the debug build when present (non-x86_64: generic debug; - # x86_64: debug+AVX2 only, shipped as cbc_dist_debug_avx2). - dist_name_debug = "cbc_dist_debug" - if os.path.isdir(DIST_DIR_DEBUG): - shutil.copytree(DIST_DIR_DEBUG, os.path.join(_pkg_dir, dist_name_debug), - dirs_exist_ok=True) - package_data_patterns.append(f"{dist_name_debug}/**") - - dist_name_debug_avx2 = "cbc_dist_debug_avx2" - if os.path.isdir(DIST_DIR_DEBUG_AVX2): - shutil.copytree(DIST_DIR_DEBUG_AVX2, os.path.join(_pkg_dir, dist_name_debug_avx2), - dirs_exist_ok=True) - package_data_patterns.append(f"{dist_name_debug_avx2}/**") - - for fname in ["__init__.py", "__main__.py"]: - shutil.copy2(os.path.join(THIS_DIR, "src", fname), os.path.join(_pkg_dir, fname)) - - setup( - cmdclass=cmdclass, - long_description=long_description, - long_description_content_type="text/markdown", - packages=["cbcbox"], - zip_safe=False, - package_dir={"cbcbox": _PKG_STAGING}, - package_data={ - "cbcbox": package_data_patterns, - }, - ) - finally: - shutil.rmtree(_pkg_dir, ignore_errors=True)