-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker.nix
More file actions
1425 lines (1239 loc) · 56.3 KB
/
docker.nix
File metadata and controls
1425 lines (1239 loc) · 56.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{ pkgs ? import <nixpkgs> {
config.allowUnfree = true;
config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "gurobi" ];
} }:
let
# Create restricted Python environment, remove dangerous modules
restrictedPython = pkgs.python312;
# Final solution for protobuf conflicts:
# Install OR-Tools to /.local (separate from Nix store packages)
# This completely avoids buildEnv conflicts
# Main Python environment with all core packages
pythonWithPackages = restrictedPython.withPackages (ps: with ps; [
pip
setuptools
wheel
# Scientific computing packages
cython
numpy
scipy
pandas
matplotlib
scikit-learn
seaborn
# Optimization solvers
gurobipy
pulp # PuLP - Python Linear Programming (no protobuf dependency)
# Exclude: yfinance (protobuf 6.x conflicts), ortools (installed separately)
# Data processing packages
openpyxl # Excel file support for pandas
xlrd # Excel file support for pandas (legacy .xls format)
]);
# Separate OR-Tools environment (will be copied to /.local)
pythonWithOrtools = restrictedPython.withPackages (ps: with ps; [
ortools
# Automatically includes: protobuf 5.x, absl-py, immutabledict, etc.
]);
# Extract OR-Tools packages to a derivation
ortoolsPackages = pkgs.runCommand "ortools-packages" {} ''
mkdir -p $out
# Copy only the site-packages content (not bin/python to avoid conflicts)
cp -r ${pythonWithOrtools}/lib/python3.12/site-packages $out/
'';
# CPLEX Installation
# Use the installer saved in the repository folder
#
# UPGRADE INSTRUCTIONS (when updating CPLEX installer):
# 1. Download new CPLEX installer and place it in pkgs/cplex/
# 2. Update the filename below to match the new installer
# 3. The extraction logic should automatically adapt to new file sizes
# 4. If extraction fails, check if InstallAnywhere variable names changed in the script header
# 5. Test the build: nix-build docker.nix -A docker-image
# Expected changes: Usually only the filename needs updating, extraction logic remains the same
cplexInstallerPath = ./pkgs/cplex/cos_installer_preview-22.1.2.R4-M0N96ML-linux-x86-64.bin;
cplex = pkgs.stdenv.mkDerivation {
name = "cplex-22.1.2";
# Use builtins.path to import the installer into the Nix store
src = if builtins.pathExists cplexInstallerPath
then builtins.path { path = cplexInstallerPath; name = "cplex-installer.bin"; }
else pkgs.emptyDirectory; # Fallback if not found
unpackPhase = "true"; # We'll do extraction in installPhase to keep files in scope
# Dependencies for extraction and cleanup
nativeBuildInputs = [
pkgs.coreutils
pkgs.findutils
pkgs.unzip
pkgs.patchelf
];
# We will not use autoPatchelf on the installer
dontAutoPatchelf = true;
# Enable parallel extraction where possible
enableParallelBuilding = true;
buildInputs = [
pkgs.glibc
pkgs.zlib
pkgs.stdenv.cc.cc.lib
pkgs.libffi
pkgs.libxcrypt-legacy
pkgs.fontconfig
pkgs.freetype
pkgs.curl
pkgs.unixODBC
pkgs.sqlite
pkgs.xorg.libX11
pkgs.xorg.libXext
pkgs.xorg.libXrender
pkgs.xorg.libXtst
pkgs.xorg.libXi
pkgs.xorg.libXmu
pkgs.xorg.libSM
pkgs.xorg.libICE
pkgs.alsa-lib
pkgs.mariadb-connector-c
pkgs.libnsl
];
# CPLEX installer and JRE bundle often contain broken symlinks
# and other issues that Nix checks for by default.
dontCheckForBrokenSymlinks = true;
# Prevent audit failures for temp directories in the installer
noAuditTmpdir = true;
installPhase = ''
# Extract the embedded archive from the InstallAnywhere self-extractor
# This method bypasses the problematic InstallAnywhere installer execution
# and directly extracts the CPLEX files from the embedded archives
#
# UPGRADE NOTE: When updating to a new CPLEX installer version:
# 1. Replace the installer file in pkgs/cplex/
# 2. The variable names (BLOCKSIZE, JRESTART, etc.) should remain the same
# 3. Only the values may change - the parsing logic should work automatically
#
echo "Extracting embedded archive from CPLEX installer..."
# Parse InstallAnywhere variables from the script header
BLOCKSIZE=$(grep -m1 "^BLOCKSIZE=" "$src" 2>/dev/null | cut -d= -f2 || echo "32768")
JRESTART=$(grep -m1 "^JRESTART=" "$src" 2>/dev/null | cut -d= -f2 || echo "5")
JREREALSIZE=$(grep -m1 "^JREREALSIZE=" "$src" 2>/dev/null | cut -d= -f2 || echo "48207661")
ARCHREALSIZE=$(grep -m1 "^ARCHREALSIZE=" "$src" 2>/dev/null | cut -d= -f2 || echo "6804302")
RESREALSIZE=$(grep -m1 "^RESREALSIZE=" "$src" 2>/dev/null | cut -d= -f2 || echo "409293865")
echo "Parsed InstallAnywhere variables:"
echo " BLOCKSIZE: $BLOCKSIZE"
echo " JRESTART: $JRESTART"
echo " RESREALSIZE: $RESREALSIZE"
# Calculate resource archive start position
JRE_BLOCKS=$(( (JREREALSIZE + BLOCKSIZE - 1) / BLOCKSIZE ))
ARCHSTART_BLOCKS=$(( JRESTART + JRE_BLOCKS ))
ARCH_BLOCKS=$(( (ARCHREALSIZE + BLOCKSIZE - 1) / BLOCKSIZE ))
RESSTART_BLOCKS=$(( ARCHSTART_BLOCKS + ARCH_BLOCKS ))
RESOURCE_START_BYTES=$(( RESSTART_BLOCKS * BLOCKSIZE ))
echo "Resource archive starts at byte: $RESOURCE_START_BYTES"
echo "Extracting resource archive (~$((RESREALSIZE / 1048576))MB)..."
# Extract resource archive (contains actual CPLEX files)
mkdir -p archive_extract
cd archive_extract
# Extract using dd - avoid pipes to prevent SIGPIPE errors
# Use bs=1 for exact byte positioning, even if slower
echo "Extracting $RESREALSIZE bytes starting at offset $RESOURCE_START_BYTES..."
dd if="$src" bs=1 skip=$RESOURCE_START_BYTES count=$RESREALSIZE of=resources.zip 2>/dev/null
if [ -f resources.zip ] && [ -s resources.zip ]; then
ACTUAL_SIZE=$(stat -c%s resources.zip 2>/dev/null || stat -f%z resources.zip 2>/dev/null)
echo "✅ Extracted resource archive ($ACTUAL_SIZE bytes)"
if [ "$ACTUAL_SIZE" -ne "$RESREALSIZE" ]; then
echo "⚠️ Warning: Size mismatch (expected $RESREALSIZE, got $ACTUAL_SIZE)"
# Try to fix by truncating or padding
if [ "$ACTUAL_SIZE" -gt "$RESREALSIZE" ]; then
truncate -s $RESREALSIZE resources.zip 2>/dev/null || true
fi
fi
echo "Extracting resource ZIP contents..."
# Use parallel unzip if available, otherwise fall back to regular unzip
if command -v unzip >/dev/null 2>&1; then
unzip -q resources.zip || {
echo "❌ Error: Failed to extract resource ZIP archive"
echo "Checking ZIP file integrity..."
file resources.zip || true
exit 1
}
fi
echo "✅ Successfully extracted resource archive"
else
echo "❌ Error: Failed to extract resource archive file"
exit 1
fi
echo "Processing extracted CPLEX archive..."
# The CPLEX files are in a nested JAR file inside the resource ZIP
CPLEX_JAR=$(find . -name "*CPLEXOptimizationStudio*.jar" -type f | head -n 1)
if [ -z "$CPLEX_JAR" ] || [ ! -f "$CPLEX_JAR" ]; then
echo "❌ Error: CPLEX JAR file not found"
echo "Searching for JAR files..."
find . -name "*.jar" -type f
exit 1
fi
echo "Found CPLEX JAR: $CPLEX_JAR"
JAR_SIZE=$(stat -c%s "$CPLEX_JAR" 2>/dev/null || stat -f%z "$CPLEX_JAR" 2>/dev/null)
echo "Extracting CPLEX JAR (~$((JAR_SIZE / 1048576))MB, using optimized extraction)..."
# Extract the JAR file (JAR files are ZIP archives)
# Use -n to skip existing files if re-extracting (speeds up retries)
mkdir -p cplex_extract
cd cplex_extract
unzip -q -n "../$CPLEX_JAR" 2>/dev/null || unzip -q "../$CPLEX_JAR" || {
echo "❌ Error: Failed to extract CPLEX JAR"
exit 1
}
echo "✅ Successfully extracted CPLEX JAR"
# Find the CPLEX installation directory
CPLEX_DIR=""
if CPLEX_DIR=$(find . -type d -name "cplex" -exec test -d {}/bin/x86-64_linux \; -print | head -n 1); then
echo "✅ Found CPLEX directory: $CPLEX_DIR"
elif CPLEX_DIR=$(find . -type d -path "*/CPLEX_Studio221/cplex" | head -n 1); then
echo "✅ Found CPLEX directory: $CPLEX_DIR"
elif CPLEX_DIR=$(find . -type d -path "*/cplex/bin/x86-64_linux" | xargs dirname | xargs dirname | head -n 1); then
echo "✅ Found CPLEX directory: $CPLEX_DIR"
else
echo "❌ Error: Could not locate CPLEX directory in extracted JAR"
echo "Directory structure:"
find . -type d | head -30
exit 1
fi
# Copy CPLEX and CP Optimizer to output directory
echo "Copying solvers to output directory..."
mkdir -p $out/opt/ibm/ILOG/CPLEX_Studio221
# Use a robust way to find directories
SOLVER_BASE=$(find . -type d -path "*/opt/ibm/ILOG/CPLEX_Studio221" -print -quit || true)
if [ -n "$SOLVER_BASE" ]; then
cp -r "$SOLVER_BASE"/* $out/opt/ibm/ILOG/CPLEX_Studio221/
else
for s in cplex cpoptimizer concert opl docplex; do
S_PATH=$(find . -type d -name "$s" ! -path "*/examples/*" -print -quit || true)
[ -n "$S_PATH" ] && cp -r "$S_PATH" $out/opt/ibm/ILOG/CPLEX_Studio221/
done
fi
# Cleanup large irrelevant files to keep image size down
find $out/opt/ibm/ILOG -type d -name "examples" -exec rm -rf {} + || true
find $out/opt/ibm/ILOG -type f -name "*.pdf" -delete || true
# Fix permissions: some extracted payloads preserve read-only modes (e.g. 444),
# which makes CPLEX binaries non-executable inside the final image.
echo "Fixing CPLEX file permissions..."
if [ -d "$out/opt/ibm/ILOG/CPLEX_Studio221" ]; then
# Ensure directories are searchable
find "$out/opt/ibm/ILOG/CPLEX_Studio221" -type d -exec chmod 755 {} + || true
# Make binaries executable in any */bin/x86-64_linux directory (exclude shared libs)
find "$out/opt/ibm/ILOG/CPLEX_Studio221" -type d -path "*/bin/x86-64_linux" | while read -r d; do
find "$d" -maxdepth 1 -type f ! -name "*.so*" -exec chmod 755 {} + || true
find "$d" -maxdepth 1 -type f -name "*.so*" -exec chmod 644 {} + || true
done
fi
# Verify the core directory exists
if [ -d "$out/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux" ]; then
echo "✅ CPLEX core binaries found in output directory"
echo "CPLEX installation structure:"
find $out/opt/ibm/ILOG/CPLEX_Studio221 -maxdepth 3 -type d | head -20 || true
# Also look for Python API in the extracted structure before copying
echo "Searching for Python API in extracted JAR..."
PYTHON_API_DIR=$(find . -type d -name "python" ! -path "*/examples/*" -print -quit || true)
if [ -n "$PYTHON_API_DIR" ]; then
echo "Found Python API directory: $PYTHON_API_DIR"
# Look for cplex Python package within
CPLEX_PYTHON_PKG=$(find "$PYTHON_API_DIR" -name "__init__.py" -path "*/cplex/__init__.py" -print -quit | xargs dirname || true)
if [ -n "$CPLEX_PYTHON_PKG" ] && [ "$CPLEX_PYTHON_PKG" != "." ]; then
echo "Found CPLEX Python package: $CPLEX_PYTHON_PKG"
# Copy Python API to a known location
mkdir -p $out/opt/ibm/ILOG/CPLEX_Studio221/cplex/python
cp -r "$CPLEX_PYTHON_PKG"/* $out/opt/ibm/ILOG/CPLEX_Studio221/cplex/python/ 2>/dev/null || true
fi
fi
else
echo "❌ Error: CPLEX core binaries not found in expected location"
echo "Output directory structure:"
find $out -maxdepth 5 -type d | head -30 || true
exit 1
fi
'';
# Post-install: Patch installed libraries to work with Nix's glibc
postFixup = ''
echo "Patching CPLEX ELF files for Nix compatibility..."
RPATH="${pkgs.glibc}/lib:${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.libffi}/lib"
INTERP="${pkgs.glibc}/lib/ld-linux-x86-64.so.2"
# 1) Patch ELF executables (and any ELF that has an interpreter) so they can run in the image.
# Missing/incorrect interpreter path is what causes: "cannot execute: required file not found".
find "$out/opt/ibm/ILOG/CPLEX_Studio221" -type f | while read -r f; do
if patchelf --print-interpreter "$f" >/dev/null 2>&1; then
patchelf --set-interpreter "$INTERP" "$f" || true
patchelf --set-rpath "$RPATH" "$f" || true
fi
done
# 2) Patch shared libraries rpath (they won't have an interpreter).
find "$out/opt/ibm/ILOG/CPLEX_Studio221" -type f \( -name "*.so" -o -name "*.so.*" \) | while read -r lib; do
patchelf --set-rpath "$RPATH" "$lib" || true
done
'';
};
# COPT (Cardinal Optimizer) 完整安装
# 包含求解器二进制文件、共享库和 Python 接口
coptVersion = "8.0.2";
# MOSEK Python API (PyPI package)
# Latest stable: 11.0.30 (Released: Nov. 18, 2025)
mosekVersion = "11.0.30";
copt = pkgs.stdenv.mkDerivation {
name = "copt-${coptVersion}";
src = pkgs.fetchurl {
url = "https://pub.shanshu.ai/download/copt/${coptVersion}/linux64/CardinalOptimizer-${coptVersion}-lnx64.tar.gz";
sha256 = "1cns2z8cic4rvisxy5bmf60241a6c7a1g1mpxvb13dzwdn94r65v";
# Network optimization
curlOptsList = [ "--retry" "5" "--retry-delay" "10" "--connect-timeout" "60" ];
};
nativeBuildInputs = [ pkgs.patchelf pkgs.unzip ];
buildInputs = [ pkgs.glibc pkgs.zlib pkgs.stdenv.cc.cc.lib pkgs.libffi ];
# Enable parallel operations
enableParallelBuilding = true;
installPhase = ''
mkdir -p $out/opt/copt
tar -xzf $src -C $out/opt/copt --strip-components=1
# 移除不需要的例子和文档以减小镜像体积
rm -rf $out/opt/copt/examples $out/opt/copt/docs
# 确保权限正确
find $out/opt/copt -type d -exec chmod 755 {} +
find $out/opt/copt/bin -type f -exec chmod 755 {} +
find $out/opt/copt/lib -type f -exec chmod 644 {} +
'';
postFixup = ''
echo "Patching COPT binaries and libs for Nix..."
RPATH="${pkgs.glibc}/lib:${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:${pkgs.libffi}/lib"
INTERP="${pkgs.glibc}/lib/ld-linux-x86-64.so.2"
# Patch 所有的二进制执行文件
find $out/opt/copt/bin -type f | while read -r f; do
if patchelf --print-interpreter "$f" >/dev/null 2>&1; then
patchelf --set-interpreter "$INTERP" "$f" || true
patchelf --set-rpath "$RPATH" "$f" || true
fi
done
# Patch 所有的共享库
find $out/opt/copt/lib -name "*.so*" -type f | while read -r lib; do
patchelf --set-rpath "$RPATH" "$lib" || true
done
'';
};
# MOSEK Python 接口(直接通过 uv pip 从 PyPI 安装)
# MOSEK 提供免费的学术许可证和30天试用许可证
# PyPI: https://pypi.org/project/Mosek/
# 学术许可证: https://www.mosek.com/products/academic-licenses/
# 固定版本以保证镜像可复现
mosekPythonPackages = pkgs.runCommand "mosek-python-packages" {
nativeBuildInputs = [ pythonWithPackages pkgs.uv pkgs.cacert ];
__impureHostDeps = [ "/etc/resolv.conf" "/etc/hosts" ];
# Enable better caching by declaring output hash
preferLocalBuild = false;
allowSubstitutes = false;
} ''
mkdir -p $out/site-packages
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
export UV_CACHE_DIR="$TMPDIR/.uv_cache"
mkdir -p "$UV_CACHE_DIR"
export UV_PYTHON_PREFERENCE="system"
export UV_PYTHON="${pythonWithPackages}/bin/python3.12"
# Network optimization: increase timeouts and retries
export UV_HTTP_TIMEOUT="300"
export UV_NO_PROGRESS="1"
export UV_CONCURRENT_DOWNLOADS="5"
echo "Installing MOSEK Python API (Mosek==${mosekVersion}) via uv pip..."
if ${pkgs.uv}/bin/uv pip install --python "$UV_PYTHON" --target $out/site-packages "Mosek==${mosekVersion}" 2>&1; then
echo "✅ MOSEK Python package installed from PyPI"
# 显示安装的版本
${pkgs.uv}/bin/uv pip list --python "$UV_PYTHON" | grep -i mosek || true
else
echo "❌ Error: Failed to install MOSEK Python package from PyPI"
exit 1
fi
'';
# COPT Python 接口(优先使用 uv pip 安装以获得最佳兼容性)
coptPythonPackages = pkgs.runCommand "copt-python-packages" {
nativeBuildInputs = [ pythonWithPackages pkgs.uv pkgs.cacert ];
__impureHostDeps = [ "/etc/resolv.conf" "/etc/hosts" ];
preferLocalBuild = false;
allowSubstitutes = false;
} ''
mkdir -p $out/site-packages
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
export UV_CACHE_DIR="$TMPDIR/.uv_cache"
mkdir -p "$UV_CACHE_DIR"
export UV_PYTHON_PREFERENCE="system"
export UV_PYTHON="${pythonWithPackages}/bin/python3.12"
# Network optimization
export UV_HTTP_TIMEOUT="300"
export UV_NO_PROGRESS="1"
export UV_CONCURRENT_DOWNLOADS="5"
echo "Installing coptpy via uv pip..."
# 尝试从 PyPI 安装,如果失败则从解压后的 copt 目录提取
if ${pkgs.uv}/bin/uv pip install --python "$UV_PYTHON" --target $out/site-packages coptpy==${coptVersion} 2>&1; then
echo "✅ coptpy installed from PyPI"
else
echo "⚠️ PyPI install failed, extracting from copt package..."
# 查找解压后的 copt 目录中的 python 接口
CP_DIR=$(find ${copt}/opt/copt -name "coptpy" -type d | head -n 1)
if [ -n "$CP_DIR" ]; then
cp -r "$CP_DIR" $out/site-packages/
else
echo "❌ Error: Could not find coptpy in package"
exit 1
fi
fi
'';
# Extract CPLEX Python API
# First try to find it in the CPLEX installation, otherwise install via uv pip
cplexPythonPackages = pkgs.runCommand "cplex-python-packages" {
nativeBuildInputs = [ pythonWithPackages pkgs.uv pkgs.cacert pkgs.curl ];
# Allow network access to download pip packages
# Note: This requires --option sandbox false or network access enabled
__impureHostDeps = [ "/etc/resolv.conf" "/etc/hosts" ];
# Mark as impure to allow network access
preferLocalBuild = false;
allowSubstitutes = false;
} ''
mkdir -p $out/site-packages
echo "Searching for CPLEX Python API in ${cplex}..."
# Method 1: Look for python/<version>/<arch>/cplex structure
# CPLEX provides Python API in version-specific directories like:
# cplex/python/3.10/x86-64_linux/cplex/
# cplex/python/3.11/x86-64_linux/cplex/
# cplex/python/3.12/x86-64_linux/cplex/
CPLEX_API_DIR=""
PYTHON_BASE="${cplex}/opt/ibm/ILOG/CPLEX_Studio221/cplex/python"
if [ -d "$PYTHON_BASE" ]; then
echo "Found Python base directory: $PYTHON_BASE"
# Prefer Python 3.12, then 3.11, then 3.10, then any version
PYTHON_VERSIONS="3.12 3.11 3.10"
FOUND=0
for PREFERRED_VERSION in $PYTHON_VERSIONS; do
VERSION_DIR="$PYTHON_BASE/$PREFERRED_VERSION"
if [ -d "$VERSION_DIR" ]; then
echo "Checking preferred Python version: $PREFERRED_VERSION"
# Look for architecture-specific directory (x86-64_linux, etc.)
for ARCH_DIR in "$VERSION_DIR"/*; do
if [ -d "$ARCH_DIR" ]; then
ARCH=$(basename "$ARCH_DIR")
echo "Checking architecture: $ARCH"
# Look for cplex package directory
CPLEX_PKG_DIR="$ARCH_DIR/cplex"
if [ -d "$CPLEX_PKG_DIR" ] && [ -f "$CPLEX_PKG_DIR/__init__.py" ]; then
CPLEX_API_DIR="$CPLEX_PKG_DIR"
echo "✅ Found CPLEX Python API for Python $PREFERRED_VERSION: $CPLEX_API_DIR"
FOUND=1
break 2
fi
fi
done
fi
done
# If preferred versions not found, try any version
if [ $FOUND -eq 0 ]; then
echo "Preferred Python versions not found, trying any available version..."
for VERSION_DIR in "$PYTHON_BASE"/*; do
if [ -d "$VERSION_DIR" ]; then
VERSION=$(basename "$VERSION_DIR")
echo "Checking Python version: $VERSION"
for ARCH_DIR in "$VERSION_DIR"/*; do
if [ -d "$ARCH_DIR" ]; then
ARCH=$(basename "$ARCH_DIR")
CPLEX_PKG_DIR="$ARCH_DIR/cplex"
if [ -d "$CPLEX_PKG_DIR" ] && [ -f "$CPLEX_PKG_DIR/__init__.py" ]; then
CPLEX_API_DIR="$CPLEX_PKG_DIR"
echo "✅ Found CPLEX Python API for Python $VERSION: $CPLEX_API_DIR"
FOUND=1
break 2
fi
fi
done
fi
done
fi
fi
# Method 2: Direct search for cplex/__init__.py in python directories
if [ -z "$CPLEX_API_DIR" ]; then
echo "Trying alternative search method..."
CPLEX_API_DIR=$(find ${cplex} -name "__init__.py" -path "*/python/*/cplex/__init__.py" ! -path "*/examples/*" -print | xargs -I {} dirname {} | head -n 1)
fi
# Method 3: Search for any cplex Python package directory
if [ -z "$CPLEX_API_DIR" ]; then
CPLEX_API_DIR=$(find ${cplex} -name "__init__.py" -path "*/cplex/__init__.py" ! -path "*/examples/*" -print | xargs -I {} dirname {} | head -n 1)
fi
if [ -n "$CPLEX_API_DIR" ] && [ -d "$CPLEX_API_DIR" ]; then
echo "✅ Found CPLEX Python API in: $CPLEX_API_DIR"
cp -r "$CPLEX_API_DIR" $out/site-packages/
echo "✅ CPLEX Python API copied successfully"
echo "Contents:"
ls -la $out/site-packages/cplex/ | head -10
else
echo "⚠️ CPLEX Python API directory not found in installation package"
echo "Installing CPLEX Python API via uv pip..."
echo ""
# Set up environment for uv pip installation
export CPLEX_HOME=${cplex}/opt/ibm/ILOG/CPLEX_Studio221/cplex
export LD_LIBRARY_PATH=${cplex}/opt/ibm/ILOG/CPLEX_Studio221/cplex/bin/x86-64_linux:$LD_LIBRARY_PATH
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export PYTHON=${pythonWithPackages}/bin/python3.12
# Ensure uv has writable cache + HOME during build
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
export UV_CACHE_DIR="$TMPDIR/.uv_cache"
mkdir -p "$UV_CACHE_DIR"
# Avoid flaky downloads: increase timeouts and disable progress UI
export UV_HTTP_TIMEOUT="300"
export UV_NO_PROGRESS="1"
# Configure uv to use system Python
export UV_PYTHON_PREFERENCE="system"
export UV_PYTHON="$PYTHON"
# Install cplex and docplex package using uv pip
# Use --target to install to our output directory
# Use --python to specify Python interpreter explicitly
echo "Attempting to install cplex and docplex via uv pip from PyPI..."
echo "Python interpreter: $PYTHON"
echo "Target directory: $out/site-packages"
if ${pkgs.uv}/bin/uv pip install --python "$PYTHON" --target $out/site-packages cplex docplex 2>&1; then
echo "✅ Successfully installed CPLEX and docplex Python API via uv pip"
echo "Installed packages:"
ls -la $out/site-packages/ | head -10
# Verify installation
if [ -f "$out/site-packages/cplex/__init__.py" ] && [ -f "$out/site-packages/docplex/__init__.py" ]; then
echo "✅ Verified: CPLEX and docplex Python packages are correctly installed"
else
echo "⚠️ Warning: Packages installed but some __init__.py not found"
find $out/site-packages -maxdepth 2 -type d
fi
else
echo "❌ Error: Failed to install packages via uv pip"
echo "This might be due to:"
echo " 1. Network restrictions (check if build.sh uses --option sandbox false)"
echo " 2. PyPI unavailability"
echo " 3. CPLEX package not available on PyPI"
echo ""
echo "Build is configured to PREINSTALL CPLEX Python API; failing hard."
exit 1
fi
fi
'';
# Use main Python environment (only reference it once)
systemPython = pythonWithPackages;
# Create restricted Python interpreter startup script
securePythonScript = pkgs.writeScriptBin "python" ''
#!${pkgs.bash}/bin/bash
# Set restricted environment
# Use /.local instead of /tmp/.local because /tmp is mounted with noexec flag
export PYTHONPATH="/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/opt/copt/lib/python3.12/site-packages:/opt/mosek/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
export PYTHONUNBUFFERED=1
export PYTHONDONTWRITEBYTECODE=1
# Create writable site-packages directory if it doesn't exist
mkdir -p /.local/lib/python3.12/site-packages
# Ensure the directory is writable and has correct permissions
chmod 755 /.local/lib/python3.12/site-packages
# Restrict system tool access - ensure util-linux tools are accessible
# Also include CPLEX and COPT paths so docplex/coptpy can find solvers
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"
# Restrict environment variables
# NOTE: do not unset HOME, it breaks some packages (setuptools, etc.) and causes 'HOME:-' directory creation
export HOME=/home/python-user
unset USER
unset LOGNAME
unset MAIL
# Start restricted Python interpreter using system Python
exec ${systemPython}/bin/python3.12 -S -c "
import sys
import builtins
# Dangerous modules list - excluding scientific computing modules
# Note: sys module is safe and needed for package path management
# os module is partially restricted - only dangerous functions are blocked
DANGEROUS_MODULES = {
'subprocess', 'importlib', 'exec', 'eval', 'compile',
'__import__', 'open', 'file', 'input', 'raw_input', 'urllib', 'requests',
'socket', 'ftplib', 'smtplib', 'poplib', 'imaplib', 'nntplib', 'telnetlib'
}
# Override __import__ function to block dangerous modules
original_import = builtins.__import__
def safe_import(name, *args, **kwargs):
if name in DANGEROUS_MODULES:
raise ImportError(f\"Module '{name}' is not allowed in secure environment\")
return original_import(name, *args, **kwargs)
builtins.__import__ = safe_import
# Override exec and eval functions
def safe_exec(*args, **kwargs):
raise RuntimeError('exec() is not allowed in secure environment')
def safe_eval(*args, **kwargs):
raise RuntimeError('eval() is not allowed in secure environment')
builtins.exec = safe_exec
builtins.eval = safe_eval
# Override compile function
def safe_compile(*args, **kwargs):
raise RuntimeError('compile() is not allowed in secure environment')
builtins.compile = safe_compile
# Override open function
def safe_open(*args, **kwargs):
raise RuntimeError('open() is not allowed in secure environment')
builtins.open = safe_open
# Override input function
def safe_input(*args, **kwargs):
raise RuntimeError('input() is not allowed in secure environment')
builtins.input = safe_input
# Configure sys.path to include uv-installed packages
import sys
import os
# Restrict dangerous os functions while allowing safe ones
original_os_module = os
dangerous_os_functions = {
'system', 'popen', 'execv', 'execve', 'execvp', 'execvpe',
'spawnv', 'spawnve', 'spawnvp', 'spawnvpe', 'fork', 'kill',
'killpg', 'wait', 'waitpid', 'wait3', 'wait4'
}
def safe_os_function(name, *args, **kwargs):
if name in dangerous_os_functions:
raise RuntimeError(f'os.{name}() is not allowed in secure environment')
return getattr(original_os_module, name)(*args, **kwargs)
# Override dangerous os functions
for func_name in dangerous_os_functions:
if hasattr(os, func_name):
setattr(os, func_name, lambda *args, **kwargs: safe_os_function(func_name, *args, **kwargs))
# Add uv-installed packages directory to sys.path
# Use /.local instead of /tmp/.local because /tmp is mounted with noexec flag
uv_packages_path = "/.local/lib/python3.12/site-packages"
if os.path.exists(uv_packages_path) and uv_packages_path not in sys.path:
sys.path.insert(0, uv_packages_path)
# Set resource limits
import resource
import signal
# Set memory limit (2GB)
resource.setrlimit(resource.RLIMIT_AS, (2 * 1024 * 1024 * 1024, 2 * 1024 * 1024 * 1024))
# Set CPU time limit (200 seconds)
resource.setrlimit(resource.RLIMIT_CPU, (200, 200))
# Set recursion depth limit
sys.setrecursionlimit(1000)
# Set timeout handling
def timeout_handler(signum, frame):
raise TimeoutError('Code execution timeout')
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(200) # 200 second timeout
# Handle different argument patterns
if len(sys.argv) == 1:
# No arguments, read from stdin
try:
exec(sys.stdin.read())
except EOFError:
# No input, start interactive mode
import code
code.interact()
elif len(sys.argv) == 2 and sys.argv[1] == '--version':
# Handle --version flag
print(f'Python {sys.version.split()[0]}')
elif len(sys.argv) == 2 and sys.argv[1].startswith('-'):
# Handle other flags like -c, -m, etc.
if sys.argv[1] == '-c' and len(sys.argv) == 3:
# Execute code with security restrictions
exec(sys.argv[2])
elif sys.argv[1] == '-m' and len(sys.argv) == 3:
# Execute module with security restrictions
import runpy
runpy.run_module(sys.argv[2], run_name='__main__')
else:
# Other flags - execute with security restrictions
original_os_module.execv('${systemPython}/bin/python3.12', ['python3.12'] + sys.argv[1:])
else:
# Execute file or code
try:
if len(sys.argv) > 1 and not sys.argv[1].startswith('-'):
# Execute file with security restrictions
with open(sys.argv[1], 'r') as f:
code = f.read()
exec(code)
else:
# Execute with security restrictions
original_os_module.execv('${systemPython}/bin/python3.12', ['python3.12'] + sys.argv[1:])
except Exception as e:
print(f'Error: {e}', file=sys.stderr)
sys.exit(1)
finally:
signal.alarm(0) # Cancel timeout
" "$@"
'';
# Create secure uv startup script
secureUvScript = pkgs.writeScriptBin "uv" ''
#!${pkgs.bash}/bin/bash
# Set restricted environment
# Use /.local instead of /tmp/.local because /tmp is mounted with noexec flag
export PYTHONPATH="/app:/opt/ortools/lib/python3.12/site-packages:/opt/cplex/lib/python3.12/site-packages:/opt/copt/lib/python3.12/site-packages:/opt/mosek/lib/python3.12/site-packages:/.local/lib/python3.12/site-packages"
export PYTHONUNBUFFERED=1
export PYTHONDONTWRITEBYTECODE=1
# Some packages (sdists) require a valid HOME during build (e.g. setuptools expanduser()).
# Keep HOME set to a writable directory.
export HOME=/home/python-user
# Force uv to use system Python
export UV_PYTHON_PREFERENCE="system"
export UV_LINK_MODE="copy"
# Configure uv to install packages to writable directory
export UV_PYTHON_SITE_PACKAGES="/.local/lib/python3.12/site-packages"
# Set UV cache directory to writable location
export UV_CACHE_DIR="/tmp/.uv_cache"
# Set Python interpreter path explicitly
export UV_PYTHON="${systemPython}/bin/python3.12"
# Restrict network access - only allow HTTPS
export HTTP_PROXY=""
export HTTPS_PROXY=""
export http_proxy=""
export https_proxy=""
# Restrict system tool access - ensure util-linux tools are accessible
export PATH="${pkgs.coreutils}/bin:${pkgs.util-linux}/bin:/usr/local/bin:/usr/bin"
# Restrict environment variables
# NOTE: do not unset HOME, it breaks building some packages.
unset MAIL
# Create necessary directories
mkdir -p /tmp/.uv_cache
mkdir -p /.local/lib/python3.12/site-packages
mkdir -p "$HOME" || true
# Start uv
exec ${pkgs.uv}/bin/uv "$@"
'';
# Python environment with gurobipy pre-installed
pythonWithGurobipy = pythonWithPackages;
# Create a small derivation to provide the 'python' symlink
pythonSymlink = pkgs.runCommand "python-symlink" {} ''
mkdir -p $out/bin
ln -s ${systemPython}/bin/python3.12 $out/bin/python
ln -s ${systemPython}/bin/python3.12 $out/bin/python3
'';
# Create secure environment with all necessary dependencies
runtimeEnv = pkgs.buildEnv {
name = "python-runtime-secure";
ignoreCollisions = true;
paths = [
systemPython
pythonSymlink
secureUvScript
pkgs.gurobi
cplex
copt
pkgs.glibc
pkgs.zlib
pkgs.ncurses
pkgs.openssl
pkgs.libffi
pkgs.bash
pkgs.coreutils
pkgs.util-linux
pkgs.curl
pkgs.gnutar
pkgs.gzip
pkgs.lapack
pkgs.blas
pkgs.gcc.cc.lib
pkgs.stdenv.cc.cc.lib
# Chinese font support for matplotlib
# Note: noto-fonts-cjk has been renamed to noto-fonts-cjk-sans
pkgs.noto-fonts-cjk-sans
pkgs.noto-fonts-cjk-serif
pkgs.fontconfig
];
};
# Create a package with necessary directories and files for non-root user
dockerSetup = pkgs.stdenv.mkDerivation {
name = "docker-setup";
buildCommand = ''
# Create directories with proper ownership
mkdir -p $out/app $out/bin $out/tmp $out/etc/uv $out/home/python-user
mkdir -p $out/etc/passwd.d $out/etc/group.d $out/etc/shadow.d
mkdir -p $out/.local/lib/python3.12/site-packages
mkdir -p $out/usr/share/fonts/truetype/noto-cjk
mkdir -p $out/etc/fonts/conf.d
# Create fontconfig main configuration file
# Fontconfig requires fonts.conf to exist, even if minimal
cat > $out/etc/fonts/fonts.conf << 'FONTSCONF'
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- Font directory list -->
<dir>/usr/share/fonts</dir>
<dir>/usr/share/fonts/truetype</dir>
<dir>/usr/share/fonts/truetype/noto-cjk</dir>
<dir>/usr/share/fonts/opentype</dir>
<dir>/usr/share/fonts/opentype/noto</dir>
<!-- Include conf.d directory for additional configurations -->
<include ignore_missing="yes">conf.d</include>
<!-- Rescan interval (in seconds) -->
<rescan>
<int>30</int>
</rescan>
</fontconfig>
FONTSCONF
# Create uv cache dir
mkdir -p $out/tmp/.uv_cache
# Create symbolic link for python to satisfy uv and other tools
# Note: We do this in dockerSetup now to avoid duplication
# ln -sf ${systemPython}/bin/python3.12 $out/bin/python
# ln -sf ${systemPython}/bin/python3.12 $out/bin/python3
# Create ortools directory in a separate location (not /.local since it's tmpfs in containers)
mkdir -p $out/opt/ortools/lib/python3.12/site-packages
# Copy OR-Tools to /opt/ortools to avoid protobuf conflicts and tmpfs覆盖
# This allows OR-Tools to use its own protobuf version (5.x)
cp -r ${ortoolsPackages}/site-packages/* $out/opt/ortools/lib/python3.12/site-packages/
# Create cplex directory for Python API
mkdir -p $out/opt/cplex/lib/python3.12/site-packages
# Copy CPLEX Python API to /opt/cplex
cp -r ${cplexPythonPackages}/site-packages/* $out/opt/cplex/lib/python3.12/site-packages/
# Create copt directory for Python API
mkdir -p $out/opt/copt/lib/python3.12/site-packages
# Copy COPT Python API from uv installation (PyPI version handles library loading correctly)
cp -r ${coptPythonPackages}/site-packages/* $out/opt/copt/lib/python3.12/site-packages/
# Create mosek directory for Python API
mkdir -p $out/opt/mosek/lib/python3.12/site-packages
# Copy MOSEK Python API from uv installation
cp -r ${mosekPythonPackages}/site-packages/* $out/opt/mosek/lib/python3.12/site-packages/
# Copy Noto CJK fonts to system font directory at build time
# Note: noto-fonts-cjk has been split into noto-fonts-cjk-sans and noto-fonts-cjk-serif
# Font files are typically in share/fonts/opentype/noto-cjk/ or share/fonts/truetype/noto-cjk/
echo "Copying Noto CJK fonts..."
# Copy all font files from both packages (sans and serif)
for font_pkg in "${pkgs.noto-fonts-cjk-sans}" "${pkgs.noto-fonts-cjk-serif}"; do
# Check opentype/noto-cjk directory (most common location)
if [ -d "$font_pkg/share/fonts/opentype/noto-cjk" ]; then
echo " Copying from $font_pkg/share/fonts/opentype/noto-cjk"
find "$font_pkg/share/fonts/opentype/noto-cjk" -type f \( -name "*.ttf" -o -name "*.otf" -o -name "*.ttc" -o -name "*.otf.ttc" \) -exec cp {} $out/usr/share/fonts/truetype/noto-cjk/ \; 2>/dev/null || true
fi
# Check truetype/noto-cjk directory
if [ -d "$font_pkg/share/fonts/truetype/noto-cjk" ]; then
echo " Copying from $font_pkg/share/fonts/truetype/noto-cjk"
find "$font_pkg/share/fonts/truetype/noto-cjk" -type f \( -name "*.ttf" -o -name "*.otf" -o -name "*.ttc" -o -name "*.otf.ttc" \) -exec cp {} $out/usr/share/fonts/truetype/noto-cjk/ \; 2>/dev/null || true
fi
# Also check top-level fonts directory (fallback)
if [ -d "$font_pkg/share/fonts" ]; then
echo " Copying from $font_pkg/share/fonts (recursive)"
find "$font_pkg/share/fonts" -type f \( -name "*.ttf" -o -name "*.otf" -o -name "*.ttc" -o -name "*.otf.ttc" \) -exec cp {} $out/usr/share/fonts/truetype/noto-cjk/ \; 2>/dev/null || true
fi
done
# Verify fonts were copied
FONT_COUNT=$(find $out/usr/share/fonts/truetype/noto-cjk -type f 2>/dev/null | wc -l)
if [ "$FONT_COUNT" -eq 0 ]; then
echo " WARNING: No font files were copied! Check font package paths."
else
echo " Successfully copied $FONT_COUNT font files to $out/usr/share/fonts/truetype/noto-cjk"
fi
# Create symbolic links in alternative paths for compatibility with user scripts
# This allows scripts that check specific paths (like /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc) to find the fonts
mkdir -p $out/usr/share/fonts/opentype/noto
if [ -f $out/usr/share/fonts/truetype/noto-cjk/NotoSansCJK-VF.otf.ttc ]; then
ln -sf ../../truetype/noto-cjk/NotoSansCJK-VF.otf.ttc $out/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc 2>/dev/null || true
echo " Created symlink: /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
fi
# Create fontconfig configuration
# IMPORTANT: No leading spaces before XML declaration
cat > $out/etc/fonts/conf.d/99-noto-cjk.conf << 'FONTCONF'
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/usr/share/fonts/truetype/noto-cjk</dir>
<alias>
<family>sans-serif</family>
<prefer>
<family>Noto Sans CJK SC</family>
<family>Noto Sans CJK TC</family>
<family>Noto Sans CJK JP</family>
<family>Noto Sans CJK KR</family>
</prefer>
</alias>
<alias>
<family>serif</family>
<prefer>
<family>Noto Serif CJK SC</family>
<family>Noto Serif CJK TC</family>
<family>Noto Serif CJK JP</family>
<family>Noto Serif CJK KR</family>
</prefer>
</alias>
</fontconfig>
FONTCONF
# Create matplotlib configuration directory and file at build time
mkdir -p $out/home/python-user/.matplotlib
cat > $out/home/python-user/.matplotlib/matplotlibrc << 'MATPLOTLIBRC'
font.family: sans-serif
font.sans-serif: Noto Sans CJK JP, Noto Sans CJK TC, Noto Sans CJK SC, Noto Sans CJK KR, DejaVu Sans
axes.unicode_minus: False
MATPLOTLIBRC
# Create font setup script for runtime
cat > $out/setup-fonts.sh << 'EOF'
#!/bin/bash
# Setup Chinese fonts for matplotlib
echo "Setting up Chinese font support..."
# Ensure user cache directories exist (these can be created in tmpfs)
mkdir -p /home/python-user/.cache/fontconfig 2>/dev/null || true
mkdir -p /home/python-user/.matplotlib 2>/dev/null || true
# Copy matplotlibrc from Nix store to runtime directory (if it exists)
# The runtime directory is tmpfs, so we need to copy the config file
if [ -f /nix/store/*-docker-setup/home/python-user/.matplotlib/matplotlibrc ]; then
MATPLOTLIBRC_SOURCE=$(find /nix/store -path "*/docker-setup/home/python-user/.matplotlib/matplotlibrc" 2>/dev/null | head -1)
if [ -n "$MATPLOTLIBRC_SOURCE" ]; then
cp "$MATPLOTLIBRC_SOURCE" /home/python-user/.matplotlib/matplotlibrc 2>/dev/null || true