git: 9351c1129192 - main - math/py-cbcbox: New port: Python access to the CBC MILP solver
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Mon, 14 Sep 2026 18:52:13 UTC
The branch main has been updated by yuri:
URL: https://cgit.FreeBSD.org/ports/commit/?id=9351c1129192deae3ee165f6b127edfc923c651c
commit 9351c1129192deae3ee165f6b127edfc923c651c
Author: Yuri Victorovich <yuri@FreeBSD.org>
AuthorDate: 2026-09-14 03:19:50 +0000
Commit: Yuri Victorovich <yuri@FreeBSD.org>
CommitDate: 2026-09-14 18:52:04 +0000
math/py-cbcbox: New port: Python access to the CBC MILP solver
PR: 298331
Approved by: Baptiste Daroussin <bapt@FreeBSD.org> (on behalf of portmgr@)
---
math/Makefile | 1 +
math/py-cbcbox/Makefile | 33 +
math/py-cbcbox/distinfo | 3 +
math/py-cbcbox/files/patch-pyproject.toml | 14 +
math/py-cbcbox/files/patch-setup.py | 1127 +++++++++++++++++++++++
math/py-cbcbox/files/patch-src_____init____.py | 55 ++
math/py-cbcbox/files/patch-tests_conftest.py | 29 +
math/py-cbcbox/files/patch-tests_test__solve.py | 52 ++
math/py-cbcbox/pkg-descr | 9 +
9 files changed, 1323 insertions(+)
diff --git a/math/Makefile b/math/Makefile
index 94cbe6fcf223..553bc278ca84 100644
--- a/math/Makefile
+++ b/math/Makefile
@@ -974,6 +974,7 @@
SUBDIR += py-blis
SUBDIR += py-bottleneck
SUBDIR += py-brial
+ SUBDIR += py-cbcbox
SUBDIR += py-chaospy
SUBDIR += py-clarabel
SUBDIR += py-claripy
diff --git a/math/py-cbcbox/Makefile b/math/py-cbcbox/Makefile
new file mode 100644
index 000000000000..ffedfc5518a8
--- /dev/null
+++ b/math/py-cbcbox/Makefile
@@ -0,0 +1,33 @@
+PORTNAME= cbcbox
+DISTVERSION= 2.935 # not tagged on GH
+CATEGORIES= math python
+PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX}
+
+MAINTAINER= yuri@FreeBSD.org
+COMMENT= Python access to the CBC MILP solver
+WWW= https://github.com/h-g-s/cbcbox
+
+LICENSE= EPL
+
+BUILD_DEPENDS= ${PYTHON_PKGNAMEPREFIX}setuptools>0:devel/py-setuptools@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}wheel>0:devel/py-wheel@${PY_FLAVOR}
+RUN_DEPENDS= cbc:math/cbc
+
+USES= python
+USE_PYTHON= pep517 concurrent autoplist pytest
+
+USE_GITHUB= yes
+GH_ACCOUNT= h-g-s
+GH_TAGNAME= adf09a5ea6f3d697b9a0b38831d7ad711b0cfe7a # adequate commit for 2.935 release
+
+NO_ARCH= yes
+
+TEST_ENV= PYTHONPATH=${WRKSRC}/build/lib
+TEST_ARGS= --disable-plugin-autoload
+
+post-patch:
+ @${REINPLACE_CMD} -e 's|@LOCALBASE@|${LOCALBASE}|g' ${WRKSRC}/src/__init__.py
+
+# tests as of 2.935: 11 passed in 107.00s
+
+.include <bsd.port.mk>
diff --git a/math/py-cbcbox/distinfo b/math/py-cbcbox/distinfo
new file mode 100644
index 000000000000..96bd05948fbe
--- /dev/null
+++ b/math/py-cbcbox/distinfo
@@ -0,0 +1,3 @@
+TIMESTAMP = 1788910090
+SHA256 (h-g-s-cbcbox-2.935-adf09a5ea6f3d697b9a0b38831d7ad711b0cfe7a_GH0.tar.gz) = d0ccf7902cdc09550d6c8f01ac501051008905cefe53b988bc2360c6721f6c16
+SIZE (h-g-s-cbcbox-2.935-adf09a5ea6f3d697b9a0b38831d7ad711b0cfe7a_GH0.tar.gz) = 13095448
diff --git a/math/py-cbcbox/files/patch-pyproject.toml b/math/py-cbcbox/files/patch-pyproject.toml
new file mode 100644
index 000000000000..849279cfa7a1
--- /dev/null
+++ b/math/py-cbcbox/files/patch-pyproject.toml
@@ -0,0 +1,14 @@
+-- Remove the "cbc" console script so this port does not conflict with the
+-- bin/cbc executable already provided by math/cbc.
+
+--- pyproject.toml.orig 2026-09-08 23:28:23 UTC
++++ pyproject.toml
+@@ -35,8 +35,5 @@ Homepage = "https://github.com/h-g-s/cbcbox"
+ Homepage = "https://github.com/h-g-s/cbcbox"
+ "Bug Tracker" = "https://github.com/h-g-s/cbcbox/issues"
+
+-[project.scripts]
+-cbc = "cbcbox.__main__:main"
+-
+ [tool.cibuildwheel]
+ build-verbosity = 1
diff --git a/math/py-cbcbox/files/patch-setup.py b/math/py-cbcbox/files/patch-setup.py
new file mode 100644
index 000000000000..5d79daa3b495
--- /dev/null
+++ b/math/py-cbcbox/files/patch-setup.py
@@ -0,0 +1,1127 @@
+-- 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)
+-
*** 421 LINES SKIPPED ***