Skip to content

Commit 972029d

Browse files
committed
Integrate COPT (Cardinal Optimizer) v8.0.2 into Docker image
- Add COPT v8.0.2 binary installation with patchelf for Nix compatibility - Pre-install coptpy Python package via uv pip (fallback to package extraction) - Configure COPT environment variables (COPT_HOME, LD_LIBRARY_PATH, PYTHONPATH) - Add COPT verification script (verify-copt.sh) - Update build workflow and documentation - Simplify CI workflow configuration
1 parent 47c2cfb commit 972029d

File tree

4 files changed

+163
-44
lines changed

4 files changed

+163
-44
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,14 @@ jobs:
4747
with:
4848
lfs: true # 必须开启 LFS 以便检出 CPLEX 安装包
4949

50-
- name: Install Nix 2.31.1
51-
run: |
52-
# 安装最新稳定版 Nix 2.31.1 (单用户模式)
53-
sh <(curl -L https://nixos.org/nix/install) --no-daemon --yes
54-
# 重新加载环境
55-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
56-
# 验证安装
57-
nix --version
50+
- name: Install Nix
51+
uses: DeterminateSystems/determinate-nix-action@v16
5852

5953
- name: Magic Nix Cache
6054
uses: DeterminateSystems/magic-nix-cache-action@main
6155

62-
- name: Configure Nix
63-
run: |
64-
# 加载 Nix 环境
65-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
66-
# 配置 Nix 以支持 Flakes
67-
mkdir -p ~/.config/nix
68-
cat > ~/.config/nix/nix.conf << EOF
69-
experimental-features = nix-command flakes
70-
allow-import-from-derivation = true
71-
EOF
72-
# 验证配置
73-
nix --version
74-
7556
- name: Clean build environment
7657
run: |
77-
# 加载 Nix 环境
78-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
79-
8058
# 清理 Nix 缓存
8159
echo "🧹 Cleaning Nix cache..."
8260
nix-collect-garbage
@@ -93,9 +71,6 @@ jobs:
9371
9472
- name: Setup Nix Flake environment
9573
run: |
96-
# 加载 Nix 环境
97-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
98-
9974
# 设置环境变量以允许非自由包(Gurobi)
10075
export NIXPKGS_ALLOW_UNFREE=1
10176
@@ -167,9 +142,6 @@ jobs:
167142

168143
- name: Load Docker image
169144
run: |
170-
# 加载 Nix 环境
171-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
172-
173145
# 设置环境变量以允许非自由包(Gurobi)
174146
export NIXPKGS_ALLOW_UNFREE=1
175147
@@ -205,9 +177,6 @@ jobs:
205177
- name: Tag Docker image
206178
if: github.event.inputs.push_images != 'false'
207179
run: |
208-
# 加载 Nix 环境
209-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
210-
211180
# 设置环境变量以允许非自由包(Gurobi)
212181
export NIXPKGS_ALLOW_UNFREE=1
213182
@@ -269,9 +238,6 @@ jobs:
269238
- name: Push Docker image
270239
if: github.event.inputs.push_images != 'false'
271240
run: |
272-
# 加载 Nix 环境
273-
. /home/runner/.nix-profile/etc/profile.d/nix.sh
274-
275241
# 设置环境变量以允许非自由包(Gurobi)
276242
export NIXPKGS_ALLOW_UNFREE=1
277243

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A secure, production-ready Docker environment for Python development with UV pac
99
- **UV Package Manager**: Fast Python package and dependency manager
1010
- **Optimization Solvers Pre-installed**:
1111
- **Gurobi**: Commercial optimization solver (12.0.3) - requires license
12+
- **COPT**: Cardinal Optimizer (8.0.2) - requires license
1213
- **CPLEX**: IBM ILOG CPLEX Optimization Studio (22.1.2) - requires license
1314
- **OR-Tools**: Google's open-source suite (GLOP, CBC, SCIP, CP-SAT) - no license required
1415
- **Non-root User**: Runs as non-privileged user for security
@@ -96,6 +97,23 @@ print(f'CPLEX Version: {cplex.__version__}')
9697
docker run --rm ghcr.io/reaslab/docker-python-runner:secure-latest /verify-cplex.sh
9798
```
9899

100+
### With COPT Optimization
101+
102+
```bash
103+
# Run COPT optimization code
104+
# COPT is pre-installed at /opt/copt
105+
docker run --rm -v $(pwd):/app \
106+
-e COPT_LICENSE_FILE=/app/client.ini \
107+
ghcr.io/reaslab/docker-python-runner:secure-latest python -c "
108+
import coptpy
109+
print(f'COPT Version: {coptpy.Envr().getVersion()}')
110+
# ... your optimization code ...
111+
"
112+
113+
# Verify COPT installation
114+
docker run --rm ghcr.io/reaslab/docker-python-runner:secure-latest /verify-copt.sh
115+
```
116+
99117
### With OR-Tools Optimization
100118

101119
```bash
@@ -146,6 +164,7 @@ docker run --rm ghcr.io/reaslab/docker-python-runner:secure-latest /verify-ortoo
146164
- **Visualization**: seaborn
147165
- **Optimization**:
148166
- **gurobipy** (Gurobi 12.0.3) - Pre-installed, requires license
167+
- **coptpy** (COPT 8.0.2) - Pre-installed, requires license
149168
- **cplex** (IBM CPLEX 22.1.2) - Pre-installed, requires license
150169
- **ortools** (Google OR-Tools) - Pre-installed, no license required ✨
151170
- **Build Tools**: cython
@@ -242,6 +261,9 @@ docker run --rm -v /path/to/gurobi.lic:/app/gurobi.lic:ro ghcr.io/reaslab/docker
242261
# Test CPLEX (verify installation)
243262
docker run --rm ghcr.io/reaslab/docker-python-runner:secure-latest /verify-cplex.sh
244263

264+
# Test COPT (verify installation)
265+
docker run --rm ghcr.io/reaslab/docker-python-runner:secure-latest /verify-copt.sh
266+
245267
# Test OR-Tools (pre-installed, verify)
246268
docker run --rm ghcr.io/reaslab/docker-python-runner:secure-latest /verify-ortools.sh
247269

build.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,9 @@ echo "📋 Secure Image Configuration:"
148148
echo " - Python Version: 3.12 (restricted environment)"
149149
echo " - Security: Dangerous modules blocked (os, subprocess, sys, etc.)"
150150
echo " - Resource Limits: 1GB memory, CPU share limits"
151-
echo " - Safe Packages: pip, setuptools, wheel, cython, numpy, scipy, pandas, matplotlib, scikit-learn, seaborn, gurobipy, cplex, ortools, pulp"
151+
echo " - Safe Packages: pip, setuptools, wheel, cython, numpy, scipy, pandas, matplotlib, scikit-learn, seaborn, gurobipy, coptpy, cplex, ortools, pulp"
152152
echo " - Gurobi: 12.0.3 (via nixpkgs)"
153+
echo " - COPT: 8.0.2 (via uv pip)"
153154
echo " - CPLEX: 22.1.2 (from installer)"
154155
echo " - Container: Read-only root filesystem, non-root user (1000:1000)"
155156
echo " - Network: Restricted (disabled by default)"

docker.nix

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,83 @@ let
294294
'';
295295
};
296296

297+
# COPT (Cardinal Optimizer) 完整安装
298+
# 包含求解器二进制文件、共享库和 Python 接口
299+
coptVersion = "8.0.2";
300+
copt = pkgs.stdenv.mkDerivation {
301+
name = "copt-${coptVersion}";
302+
src = pkgs.fetchurl {
303+
url = "https://pub.shanshu.ai/download/copt/${coptVersion}/linux64/CardinalOptimizer-${coptVersion}-lnx64.tar.gz";
304+
sha256 = "1cns2z8cic4rvisxy5bmf60241a6c7a1g1mpxvb13dzwdn94r65v";
305+
};
306+
307+
nativeBuildInputs = [ pkgs.patchelf pkgs.unzip ];
308+
buildInputs = [ pkgs.glibc pkgs.zlib pkgs.stdenv.cc.cc.lib pkgs.libffi ];
309+
310+
installPhase = ''
311+
mkdir -p $out/opt/copt
312+
tar -xzf $src -C $out/opt/copt --strip-components=1
313+
314+
# 移除不需要的例子和文档以减小镜像体积
315+
rm -rf $out/opt/copt/examples $out/opt/copt/docs
316+
317+
# 确保权限正确
318+
find $out/opt/copt -type d -exec chmod 755 {} +
319+
find $out/opt/copt/bin -type f -exec chmod 755 {} +
320+
find $out/opt/copt/lib -type f -exec chmod 644 {} +
321+
'';
322+
323+
postFixup = ''
324+
echo "Patching COPT binaries and libs for Nix..."
325+
RPATH="${pkgs.glibc}/lib:${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.libffi}/lib"
326+
INTERP="${pkgs.glibc}/lib/ld-linux-x86-64.so.2"
327+
328+
# Patch 所有的二进制执行文件
329+
find $out/opt/copt/bin -type f | while read -r f; do
330+
if patchelf --print-interpreter "$f" >/dev/null 2>&1; then
331+
patchelf --set-interpreter "$INTERP" "$f" || true
332+
patchelf --set-rpath "$RPATH" "$f" || true
333+
fi
334+
done
335+
336+
# Patch 所有的共享库
337+
find $out/opt/copt/lib -name "*.so*" -type f | while read -r lib; do
338+
patchelf --set-rpath "$RPATH" "$lib" || true
339+
done
340+
'';
341+
};
342+
343+
# COPT Python 接口(优先使用 uv pip 安装以获得最佳兼容性)
344+
coptPythonPackages = pkgs.runCommand "copt-python-packages" {
345+
nativeBuildInputs = [ pythonWithPackages pkgs.uv pkgs.cacert ];
346+
__impureHostDeps = [ "/etc/resolv.conf" "/etc/hosts" ];
347+
} ''
348+
mkdir -p $out/site-packages
349+
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
350+
export HOME="$TMPDIR/home"
351+
mkdir -p "$HOME"
352+
export UV_CACHE_DIR="$TMPDIR/.uv_cache"
353+
mkdir -p "$UV_CACHE_DIR"
354+
export UV_PYTHON_PREFERENCE="system"
355+
export UV_PYTHON="${pythonWithPackages}/bin/python3.12"
356+
357+
echo "Installing coptpy via uv pip..."
358+
# 尝试从 PyPI 安装,如果失败则从解压后的 copt 目录提取
359+
if ${pkgs.uv}/bin/uv pip install --python "$UV_PYTHON" --target $out/site-packages coptpy==${coptVersion} 2>&1; then
360+
echo "✅ coptpy installed from PyPI"
361+
else
362+
echo "⚠️ PyPI install failed, extracting from copt package..."
363+
# 查找解压后的 copt 目录中的 python 接口
364+
CP_DIR=$(find ${copt}/opt/copt -name "coptpy" -type d | head -n 1)
365+
if [ -n "$CP_DIR" ]; then
366+
cp -r "$CP_DIR" $out/site-packages/
367+
else
368+
echo "❌ Error: Could not find coptpy in package"
369+
exit 1
370+
fi
371+
fi
372+
'';
373+
297374
# Extract CPLEX Python API
298375
# First try to find it in the CPLEX installation, otherwise install via uv pip
299376
cplexPythonPackages = pkgs.runCommand "cplex-python-packages" {
@@ -449,7 +526,7 @@ let
449526
450527
# Set restricted environment
451528
# Use /.local instead of /tmp/.local because /tmp is mounted with noexec flag
452-
export PYTHONPATH="/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
529+
export PYTHONPATH="/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/opt/copt/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
453530
export PYTHONUNBUFFERED=1
454531
export PYTHONDONTWRITEBYTECODE=1
455532
@@ -460,8 +537,8 @@ let
460537
chmod 755 /.local/lib/python3.12/site-packages
461538
462539
# Restrict system tool access - ensure util-linux tools are accessible
463-
# Also include CPLEX paths so docplex can find solvers
464-
export PATH="${pkgs.coreutils}/bin:${pkgs.util-linux}/bin:/usr/local/bin:/usr/bin:/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:/opt/ibm/ILOG/CPLEX_Studio221/cpoptimizer/bin/x86-64_linux"
540+
# Also include CPLEX and COPT paths so docplex/coptpy can find solvers
541+
export PATH="${pkgs.coreutils}/bin:${pkgs.util-linux}/bin:/usr/local/bin:/usr/bin:/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:/opt/ibm/ILOG/CPLEX_Studio221/cpoptimizer/bin/x86-64_linux:/opt/copt/bin"
465542
466543
# Restrict environment variables
467544
unset HOME
@@ -619,7 +696,7 @@ finally:
619696
620697
# Set restricted environment
621698
# Use /.local instead of /tmp/.local because /tmp is mounted with noexec flag
622-
export PYTHONPATH="/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
699+
export PYTHONPATH="/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/opt/copt/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
623700
export PYTHONUNBUFFERED=1
624701
export PYTHONDONTWRITEBYTECODE=1
625702
# Some packages (sdists) require a valid HOME during build (e.g. setuptools expanduser()).
@@ -681,6 +758,7 @@ finally:
681758
secureUvScript
682759
pkgs.gurobi
683760
cplex
761+
copt
684762
pkgs.glibc
685763
pkgs.zlib
686764
pkgs.ncurses
@@ -727,6 +805,11 @@ finally:
727805
# Copy CPLEX Python API to /opt/cplex
728806
cp -r ${cplexPythonPackages}/site-packages/* $out/opt/cplex/lib/python3.12/site-packages/
729807
808+
# Create copt directory for Python API
809+
mkdir -p $out/opt/copt/lib/python3.12/site-packages
810+
# Copy COPT Python API from uv installation
811+
cp -r ${coptPythonPackages}/site-packages/* $out/opt/copt/lib/python3.12/site-packages/
812+
730813
# Create uv configuration
731814
cat > $out/etc/uv/uv.toml << 'EOFUV'
732815
# Global uv configuration
@@ -880,6 +963,51 @@ EOFPYTHON
880963
EOFSCRIPT
881964
chmod +x $out/verify-cplex.sh
882965
966+
# Create COPT verification script
967+
cat > $out/verify-copt.sh << 'EOFSCRIPT'
968+
#!/bin/bash
969+
# Verify COPT is pre-installed and working
970+
echo "Verifying COPT installation..."
971+
echo ""
972+
973+
# Set COPT environment
974+
export COPT_HOME=/opt/copt
975+
export LD_LIBRARY_PATH=$COPT_HOME/lib:$LD_LIBRARY_PATH
976+
977+
python << 'EOFPYTHON'
978+
import sys
979+
import os
980+
981+
try:
982+
import coptpy
983+
print("✓ COPT Python API is pre-installed!")
984+
print(" COPT version:", coptpy.Envr().getVersion())
985+
986+
# Try to create a small model to verify binaries
987+
env = coptpy.Envr()
988+
model = env.createModel("verify")
989+
print("✓ COPT Binary libraries are accessible!")
990+
991+
print("")
992+
print("COPT is ready to use! 🚀")
993+
994+
except ImportError as e:
995+
print("✗ COPT Python API not found:", str(e))
996+
print(" PYTHONPATH:", os.environ.get('PYTHONPATH'))
997+
sys.exit(1)
998+
except Exception as e:
999+
print("✗ Error verifying COPT:", str(e))
1000+
# It might fail due to no license, but the import should work
1001+
if "license" in str(e).lower() or "113" in str(e):
1002+
print("✓ COPT Python API and binaries are loaded (License required for model creation)")
1003+
else:
1004+
import traceback
1005+
traceback.print_exc()
1006+
sys.exit(1)
1007+
EOFPYTHON
1008+
EOFSCRIPT
1009+
chmod +x $out/verify-copt.sh
1010+
8831011
# Note: python3 binary is provided by pythonWithPackages in runtimeEnv
8841012
# No need to create a symlink here to avoid collision
8851013
@@ -959,7 +1087,7 @@ in
9591087
config = {
9601088
WorkingDir = "/tmp";
9611089
Env = [
962-
"PYTHONPATH=/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
1090+
"PYTHONPATH=/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/opt/copt/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
9631091
"PYTHONUNBUFFERED=1"
9641092
"PYTHONDONTWRITEBYTECODE=1"
9651093
# Force uv to use system Python
@@ -969,14 +1097,16 @@ in
9691097
"UV_CACHE_DIR=/tmp/.uv_cache"
9701098
"UV_PYTHON=/bin/python"
9711099
# Set PATH - put runtimeEnv/bin first to ensure our python symlink is found
972-
"PATH=/bin:${securePythonScript}/bin:${runtimeEnv}/bin:${systemPython}/bin:${pkgs.coreutils}/bin:${pkgs.util-linux}/bin:/usr/local/bin:/usr/bin:/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:/opt/ibm/ILOG/CPLEX_Studio221/cpoptimizer/bin/x86-64_linux"
1100+
"PATH=/bin:${securePythonScript}/bin:${runtimeEnv}/bin:${systemPython}/bin:${pkgs.coreutils}/bin:${pkgs.util-linux}/bin:/usr/local/bin:/usr/bin:/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:/opt/ibm/ILOG/CPLEX_Studio221/cpoptimizer/bin/x86-64_linux:/opt/copt/bin"
9731101
# Set library search path
974-
"LD_LIBRARY_PATH=${runtimeEnv}/lib:${runtimeEnv}/lib64:/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:/opt/ibm/ILOG/CPLEX_Studio221/cpoptimizer/bin/x86-64_linux"
1102+
"LD_LIBRARY_PATH=${runtimeEnv}/lib:${runtimeEnv}/lib64:/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:/opt/ibm/ILOG/CPLEX_Studio221/cpoptimizer/bin/x86-64_linux:/opt/copt/lib"
9751103
# Gurobi environment variables (using gurobi package from nixpkgs)
9761104
"GUROBI_HOME=${pkgs.gurobi}"
9771105
"GRB_LICENSE_FILE=/app/gurobi.lic"
9781106
# CPLEX environment variables
9791107
"CPLEX_HOME=/opt/ibm/ILOG/CPLEX_Studio221/cplex"
1108+
# COPT environment variables
1109+
"COPT_HOME=/opt/copt"
9801110
# SSL certificate configuration for Gurobi WLS
9811111
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
9821112
"CURL_CA_BUNDLE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"

0 commit comments

Comments
 (0)