Skip to content

Commit 10f14fb

Browse files
committed
fix: optimize build process
- Optimize build process with Nix binary caching and parallel builds - Add network optimization settings for faster package downloads - Improve build error handling and progress indicators
1 parent 9bc592e commit 10f14fb

File tree

2 files changed

+97
-31
lines changed

2 files changed

+97
-31
lines changed

build.sh

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,25 @@ docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | grep "docker-python-
4343
done
4444

4545
echo "Building with Nix dockerTools..."
46-
# Configure Nix to support Flakes, consistent with workflow
46+
# Configure Nix to support Flakes with optimizations
4747
mkdir -p ~/.config/nix
4848
cat > ~/.config/nix/nix.conf << EOF
4949
experimental-features = nix-command flakes
5050
allow-import-from-derivation = true
51+
# Build optimizations
52+
max-jobs = auto
53+
cores = 0
54+
keep-outputs = true
55+
keep-derivations = true
56+
# Binary cache (speeds up subsequent builds)
57+
substituters = https://cache.nixos.org https://nix-community.cachix.org
58+
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
59+
build-use-substitutes = true
60+
auto-optimise-store = true
61+
# Network optimization
62+
stalled-download-timeout = 300
63+
connect-timeout = 60
64+
download-attempts = 5
5165
EOF
5266

5367
# Set environment variable to allow unfree packages (Gurobi)
@@ -60,15 +74,33 @@ echo "Setting Docker image timestamp to: $CURRENT_TIMESTAMP"
6074
# Use environment variable to pass timestamp to Nix
6175
export DOCKER_IMAGE_TIMESTAMP="$CURRENT_TIMESTAMP"
6276

63-
# Use nix build command, consistent with workflow
64-
nix build .#docker-image --option sandbox false --impure
77+
# Use nix build command with optimizations
78+
echo "⏱️ Building Docker image (this will be fast on subsequent builds)..."
79+
nix build .#docker-image \
80+
--option sandbox false \
81+
--impure \
82+
--keep-going \
83+
--cores 0 \
84+
--max-jobs auto \
85+
--show-trace
6586

6687
echo "Loading Nix image into Docker..."
88+
# Check if result exists and is valid
89+
if [ ! -L result ] || [ ! -e result ]; then
90+
echo "❌ Error: Nix build result not found or invalid"
91+
exit 1
92+
fi
93+
6794
# Record image IDs and tags before loading
6895
BEFORE_IMAGES=$(docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | sort)
6996

70-
# Load Nix-built image
71-
docker load < result
97+
# Load Nix-built image (optimized: using pv for progress if available)
98+
if command -v pv &> /dev/null; then
99+
echo "📦 Loading image with progress indicator..."
100+
pv result | docker load
101+
else
102+
docker load < result
103+
fi
72104

73105
# Record image IDs and tags after loading
74106
AFTER_IMAGES=$(docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | sort)

docker.nix

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ let
7373
];
7474
# We will not use autoPatchelf on the installer
7575
dontAutoPatchelf = true;
76+
77+
# Enable parallel extraction where possible
78+
enableParallelBuilding = true;
7679
buildInputs = [
7780
pkgs.glibc
7881
pkgs.zlib
@@ -158,14 +161,17 @@ let
158161
fi
159162
fi
160163
161-
echo "Extracting resource ZIP contents..."
164+
echo "Extracting resource ZIP contents..."
165+
# Use parallel unzip if available, otherwise fall back to regular unzip
166+
if command -v unzip >/dev/null 2>&1; then
162167
unzip -q resources.zip || {
163168
echo "❌ Error: Failed to extract resource ZIP archive"
164169
echo "Checking ZIP file integrity..."
165170
file resources.zip || true
166171
exit 1
167172
}
168-
echo "✅ Successfully extracted resource archive"
173+
fi
174+
echo "✅ Successfully extracted resource archive"
169175
else
170176
echo "❌ Error: Failed to extract resource archive file"
171177
exit 1
@@ -185,12 +191,13 @@ let
185191
186192
echo "Found CPLEX JAR: $CPLEX_JAR"
187193
JAR_SIZE=$(stat -c%s "$CPLEX_JAR" 2>/dev/null || stat -f%z "$CPLEX_JAR" 2>/dev/null)
188-
echo "Extracting CPLEX JAR (~$((JAR_SIZE / 1048576))MB, this may take 1-3 minutes)..."
194+
echo "Extracting CPLEX JAR (~$((JAR_SIZE / 1048576))MB, using optimized extraction)..."
189195
190196
# Extract the JAR file (JAR files are ZIP archives)
197+
# Use -n to skip existing files if re-extracting (speeds up retries)
191198
mkdir -p cplex_extract
192199
cd cplex_extract
193-
unzip -q "../$CPLEX_JAR" || {
200+
unzip -q -n "../$CPLEX_JAR" 2>/dev/null || unzip -q "../$CPLEX_JAR" || {
194201
echo "❌ Error: Failed to extract CPLEX JAR"
195202
exit 1
196203
}
@@ -305,11 +312,16 @@ let
305312
src = pkgs.fetchurl {
306313
url = "https://pub.shanshu.ai/download/copt/${coptVersion}/linux64/CardinalOptimizer-${coptVersion}-lnx64.tar.gz";
307314
sha256 = "1cns2z8cic4rvisxy5bmf60241a6c7a1g1mpxvb13dzwdn94r65v";
315+
# Network optimization
316+
curlOptsList = [ "--retry" "5" "--retry-delay" "10" "--connect-timeout" "60" ];
308317
};
309318

310319
nativeBuildInputs = [ pkgs.patchelf pkgs.unzip ];
311320
buildInputs = [ pkgs.glibc pkgs.zlib pkgs.stdenv.cc.cc.lib pkgs.libffi ];
312321

322+
# Enable parallel operations
323+
enableParallelBuilding = true;
324+
313325
installPhase = ''
314326
mkdir -p $out/opt/copt
315327
tar -xzf $src -C $out/opt/copt --strip-components=1
@@ -351,6 +363,9 @@ let
351363
mosekPythonPackages = pkgs.runCommand "mosek-python-packages" {
352364
nativeBuildInputs = [ pythonWithPackages pkgs.uv pkgs.cacert ];
353365
__impureHostDeps = [ "/etc/resolv.conf" "/etc/hosts" ];
366+
# Enable better caching by declaring output hash
367+
preferLocalBuild = false;
368+
allowSubstitutes = false;
354369
} ''
355370
mkdir -p $out/site-packages
356371
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
@@ -360,6 +375,10 @@ let
360375
mkdir -p "$UV_CACHE_DIR"
361376
export UV_PYTHON_PREFERENCE="system"
362377
export UV_PYTHON="${pythonWithPackages}/bin/python3.12"
378+
# Network optimization: increase timeouts and retries
379+
export UV_HTTP_TIMEOUT="300"
380+
export UV_NO_PROGRESS="1"
381+
export UV_CONCURRENT_DOWNLOADS="5"
363382
364383
echo "Installing MOSEK Python API (Mosek==${mosekVersion}) via uv pip..."
365384
if ${pkgs.uv}/bin/uv pip install --python "$UV_PYTHON" --target $out/site-packages "Mosek==${mosekVersion}" 2>&1; then
@@ -376,6 +395,8 @@ let
376395
coptPythonPackages = pkgs.runCommand "copt-python-packages" {
377396
nativeBuildInputs = [ pythonWithPackages pkgs.uv pkgs.cacert ];
378397
__impureHostDeps = [ "/etc/resolv.conf" "/etc/hosts" ];
398+
preferLocalBuild = false;
399+
allowSubstitutes = false;
379400
} ''
380401
mkdir -p $out/site-packages
381402
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
@@ -385,6 +406,10 @@ let
385406
mkdir -p "$UV_CACHE_DIR"
386407
export UV_PYTHON_PREFERENCE="system"
387408
export UV_PYTHON="${pythonWithPackages}/bin/python3.12"
409+
# Network optimization
410+
export UV_HTTP_TIMEOUT="300"
411+
export UV_NO_PROGRESS="1"
412+
export UV_CONCURRENT_DOWNLOADS="5"
388413
389414
echo "Installing coptpy via uv pip..."
390415
# 尝试从 PyPI 安装,如果失败则从解压后的 copt 目录提取
@@ -573,7 +598,8 @@ let
573598
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"
574599
575600
# Restrict environment variables
576-
unset HOME
601+
# NOTE: do not unset HOME, it breaks some packages (setuptools, etc.) and causes 'HOME:-' directory creation
602+
export HOME=/home/python-user
577603
unset USER
578604
unset LOGNAME
579605
unset MAIL
@@ -702,18 +728,18 @@ elif len(sys.argv) == 2 and sys.argv[1].startswith('-'):
702728
runpy.run_module(sys.argv[2], run_name='__main__')
703729
else:
704730
# Other flags - execute with security restrictions
705-
exec('${systemPython}/bin/python3.12 "$@"')
706-
else:
707-
# Execute file or code
708-
try:
709-
if len(sys.argv) > 1 and not sys.argv[1].startswith('-'):
710-
# Execute file with security restrictions
711-
with open(sys.argv[1], 'r') as f:
712-
code = f.read()
713-
exec(code)
714-
else:
715-
# Execute with security restrictions
716-
exec('${systemPython}/bin/python3.12 "$@"')
731+
original_os_module.execv('${systemPython}/bin/python3.12', ['python3.12'] + sys.argv[1:])
732+
else:
733+
# Execute file or code
734+
try:
735+
if len(sys.argv) > 1 and not sys.argv[1].startswith('-'):
736+
# Execute file with security restrictions
737+
with open(sys.argv[1], 'r') as f:
738+
code = f.read()
739+
exec(code)
740+
else:
741+
# Execute with security restrictions
742+
original_os_module.execv('${systemPython}/bin/python3.12', ['python3.12'] + sys.argv[1:])
717743
except Exception as e:
718744
print(f'Error: {e}', file=sys.stderr)
719745
sys.exit(1)
@@ -733,7 +759,7 @@ finally:
733759
export PYTHONDONTWRITEBYTECODE=1
734760
# Some packages (sdists) require a valid HOME during build (e.g. setuptools expanduser()).
735761
# Keep HOME set to a writable directory.
736-
export HOME="${HOME:-/home/python-user}"
762+
export HOME=/home/python-user
737763
738764
# Force uv to use system Python
739765
export UV_PYTHON_PREFERENCE="system"
@@ -839,7 +865,7 @@ finally:
839865
840866
# Create copt directory for Python API
841867
mkdir -p $out/opt/copt/lib/python3.12/site-packages
842-
# Copy COPT Python API from uv installation
868+
# Copy COPT Python API from uv installation (PyPI version handles library loading correctly)
843869
cp -r ${coptPythonPackages}/site-packages/* $out/opt/copt/lib/python3.12/site-packages/
844870
845871
# Create mosek directory for Python API
@@ -1079,18 +1105,26 @@ EOFPYTHON
10791105
};
10801106

10811107
in
1082-
# Use buildImage to avoid diffID conflicts
1083-
pkgs.dockerTools.buildImage {
1108+
# Use buildLayeredImage for better caching and faster rebuilds
1109+
# This creates a multi-layer image where unchanged layers can be reused
1110+
pkgs.dockerTools.buildLayeredImage {
10841111
name = "ghcr.io/reaslab/docker-python-runner";
10851112
tag = "secure-latest";
10861113
# Set proper creation timestamp from environment variable
10871114
created = if builtins.getEnv "DOCKER_IMAGE_TIMESTAMP" != "" then builtins.getEnv "DOCKER_IMAGE_TIMESTAMP" else "now";
10881115

1089-
copyToRoot = pkgs.buildEnv {
1090-
name = "image-root";
1091-
paths = [ runtimeEnv dockerSetup pkgs.cacert ];
1092-
ignoreCollisions = true; # Allow multiple packages to provide /bin/python
1093-
};
1116+
# Max layers: Docker supports up to 125 layers, we use 100 for safety
1117+
# Nix will automatically distribute contents across layers based on dependencies
1118+
maxLayers = 100;
1119+
1120+
# Contents are automatically layered by Nix based on dependency graph
1121+
# Most frequently changed items (like scripts) go in top layers
1122+
# Rarely changed items (like system libraries) go in bottom layers
1123+
contents = [
1124+
runtimeEnv # Layer group 1: System Python, libraries (rarely changes)
1125+
dockerSetup # Layer group 2: Setup scripts and configs (occasionally changes)
1126+
pkgs.cacert # Layer group 3: CA certificates (rarely changes)
1127+
];
10941128

10951129
# IMPORTANT:
10961130
# We intentionally do NOT use `runAsRoot` (it would require KVM on this host).

0 commit comments

Comments
 (0)