Files
ports/math/py-cbcbox/files/patch-setup.py
T
2026-10-04 04:07:15 +00:00

1128 lines
53 KiB
Python

-- 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 <amd.h>
- # inside extern "C" {} in ClpCholeskyUfl.cpp, and SuiteSparse v7's
- # amd.h transitively includes C++ headers (<complex> 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)