Files
PlantAI-rag-new/tooling/legacy_docker_packager/build_package.sh
T
jerryandClaude Fable 5 27946edc2b P0/M1: 收编 encryption-test 工具链入 tooling/
- legacy_plantai_server_package/: phase_a–f + compile_* + bin/plantai_admin.py
  + tools/(audit/elf_visibility/smoke/build_delivery_manifest) 完整基线
- legacy_docker_packager/: protected 形态参考
- crypto/client/: plantai_core.py(AES-GCM 原语) + install_plantai.sh 参考
- secrets/provision 严格排除(gitignore),入 git 前已脱敏清理
- LEGACY_MAPPING.md 记录源与改造方向

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:59:29 +08:00

549 lines
20 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build three independent runtime images from clean official Python bases.
set -euo pipefail
umask 027
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${PACK_ENV_FILE:-$SCRIPT_DIR/pack.env}"
if [[ ! -f "$ENV_FILE" ]]; then
ENV_FILE="$SCRIPT_DIR/pack.env.example"
echo "[config] pack.env not found; using pack.env.example"
fi
# shellcheck source=/dev/null
. "$ENV_FILE"
: "${ARTIFACT_ROOT:?missing ARTIFACT_ROOT}"
: "${PLANTAI_ARTIFACT:?missing PLANTAI_ARTIFACT}"
: "${RAG_ARTIFACT:?missing RAG_ARTIFACT}"
: "${QA_ARTIFACT:?missing QA_ARTIFACT}"
: "${PLANTAI_BASE_IMAGE:?missing PLANTAI_BASE_IMAGE}"
: "${RAG_BASE_IMAGE:?missing RAG_BASE_IMAGE}"
: "${QA_BASE_IMAGE:?missing QA_BASE_IMAGE}"
: "${PLANTAI_IMAGE_REPOSITORY:?missing PLANTAI_IMAGE_REPOSITORY}"
: "${RAG_IMAGE_REPOSITORY:?missing RAG_IMAGE_REPOSITORY}"
: "${QA_IMAGE_REPOSITORY:?missing QA_IMAGE_REPOSITORY}"
: "${VERSION:?missing VERSION}"
PLATFORM="${PLATFORM:-linux/amd64}"
BUILD_NETWORK="${BUILD_NETWORK:-host}"
PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.org/simple}"
TORCH_INDEX_URL="${TORCH_INDEX_URL:-https://download.pytorch.org/whl/cpu}"
TORCH_CPU_VERSION="${TORCH_CPU_VERSION:-2.13.0+cpu}"
INCLUDE_INFRA_IMAGES="${INCLUDE_INFRA_IMAGES:-1}"
PULL_INFRA_IMAGES="${PULL_INFRA_IMAGES:-1}"
INCLUDE_PLANTAI_STATIC_DATA="${INCLUDE_PLANTAI_STATIC_DATA:-1}"
DROP_COMPILED_PY_SIBLINGS="${DROP_COMPILED_PY_SIBLINGS:-1}"
CHECK_ONLY="${CHECK_ONLY:-0}"
MIN_FREE_GB="${MIN_FREE_GB:-60}"
REDIS_IMAGE="${REDIS_IMAGE:-redis:7}"
QDRANT_IMAGE="${QDRANT_IMAGE:-qdrant/qdrant:latest}"
NEO4J_IMAGE="${NEO4J_IMAGE:-neo4j:5.15.0-community}"
PLANTAI_IMAGE_REF="${PLANTAI_IMAGE_REPOSITORY}:${VERSION}"
RAG_IMAGE_REF="${RAG_IMAGE_REPOSITORY}:${VERSION}"
QA_IMAGE_REF="${QA_IMAGE_REPOSITORY}:${VERSION}"
WORK_DIR="$SCRIPT_DIR/work"
CONTEXT_DIR="$WORK_DIR/context"
PACKAGE_STAGE="$WORK_DIR/package"
PRODUCT_NAME="plantai_protected_product"
PRODUCT_DIR="$PACKAGE_STAGE/$PRODUCT_NAME"
DIST_DIR="$SCRIPT_DIR/dist"
ARCHIVE="$DIST_DIR/plantai-protected-docker-${VERSION}-linux-amd64.tar.zst"
IMAGE_ARCHIVE_NAME="plantai-protected-images-${VERSION}.tar.zst"
die() {
echo "[error] $*" >&2
exit 1
}
[[ "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] ||
die "unsafe VERSION value: $VERSION"
for value in "$PLANTAI_IMAGE_REPOSITORY" "$RAG_IMAGE_REPOSITORY" "$QA_IMAGE_REPOSITORY"; do
[[ "$value" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] ||
die "unsafe version or image repository value: $value"
done
[[ "$PLATFORM" == "linux/amd64" ]] ||
die "current hardened .so artifacts only support linux/amd64: $PLATFORM"
[[ "$BUILD_NETWORK" =~ ^[A-Za-z0-9_.:-]+$ ]] ||
die "unsafe BUILD_NETWORK value: $BUILD_NETWORK"
for flag_name in INCLUDE_INFRA_IMAGES PULL_INFRA_IMAGES INCLUDE_PLANTAI_STATIC_DATA DROP_COMPILED_PY_SIBLINGS CHECK_ONLY; do
flag_value="${!flag_name}"
[[ "$flag_value" == "0" || "$flag_value" == "1" ]] ||
die "$flag_name must be 0 or 1"
done
[[ "$MIN_FREE_GB" =~ ^[0-9]+$ && "$MIN_FREE_GB" -ge 20 ]] ||
die "MIN_FREE_GB must be an integer >= 20"
[[ "$PLANTAI_BASE_IMAGE" == python:3.11-* ]] ||
die "PLANTAI_BASE_IMAGE must be a clean official python:3.11-* image"
[[ "$RAG_BASE_IMAGE" == python:3.11-* ]] ||
die "RAG_BASE_IMAGE must be a clean official python:3.11-* image"
[[ "$QA_BASE_IMAGE" == python:3.12-* ]] ||
die "QA_BASE_IMAGE must be a clean official python:3.12-* image"
[[ "$PIP_INDEX_URL" =~ ^https:// ]] ||
die "PIP_INDEX_URL must use HTTPS"
[[ "$TORCH_INDEX_URL" =~ ^https:// ]] ||
die "TORCH_INDEX_URL must use HTTPS"
[[ "$TORCH_CPU_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\+cpu$ ]] ||
die "TORCH_CPU_VERSION must be an exact CPU wheel version such as 2.13.0+cpu"
[[ "$INCLUDE_PLANTAI_STATIC_DATA" == "1" ]] ||
die "INCLUDE_PLANTAI_STATIC_DATA must be 1: Identify requires Pfam-A and the offline family mapping"
for command_name in docker rsync python3 zstd tar sha256sum realpath find; do
command -v "$command_name" >/dev/null 2>&1 || die "missing command: $command_name"
done
docker info >/dev/null 2>&1 || die "Docker daemon is unavailable"
docker compose version >/dev/null 2>&1 || die "Docker Compose v2 is unavailable"
for dockerfile in Dockerfile.plantai Dockerfile.rag Dockerfile.qa; do
[[ -f "$SCRIPT_DIR/$dockerfile" ]] || die "missing $dockerfile"
if grep -Eq 'plantai-allenv|FROM[[:space:]]+scratch' "$SCRIPT_DIR/$dockerfile"; then
die "$dockerfile contains a forbidden allenv/snapshot build path"
fi
done
grep -Eq 'apt-get install.*hmmer.*ncbi-blast\+' "$SCRIPT_DIR/Dockerfile.plantai" ||
die "Dockerfile.plantai must install the minimum Identify native tools"
for dockerfile in Dockerfile.rag Dockerfile.qa; do
if grep -Eq 'apt-get' "$SCRIPT_DIR/$dockerfile"; then
die "$dockerfile unexpectedly installs system packages"
fi
grep -q 'TORCH_INDEX_URL' "$SCRIPT_DIR/$dockerfile" ||
die "$dockerfile must install Torch from the dedicated CPU index"
done
if grep -Eq '^torch([<>=!~]|$)' "$SCRIPT_DIR/requirements/rag.txt" \
"$SCRIPT_DIR/requirements/qa.txt"; then
die "Torch must not be resolved from the general requirements files"
fi
grep -q '^ARG BASE_IMAGE=python:3.11-' "$SCRIPT_DIR/Dockerfile.plantai" ||
die "Dockerfile.plantai does not start from Python 3.11"
grep -q '^ARG BASE_IMAGE=python:3.11-' "$SCRIPT_DIR/Dockerfile.rag" ||
die "Dockerfile.rag does not start from Python 3.11"
grep -q '^ARG BASE_IMAGE=python:3.12-' "$SCRIPT_DIR/Dockerfile.qa" ||
die "Dockerfile.qa does not start from Python 3.12"
require_free_space() {
local path="$1" label="$2" available_kb required_kb
available_kb="$(df -Pk "$path" | awk 'NR == 2 {print $4}')"
[[ "$available_kb" =~ ^[0-9]+$ ]] || die "cannot read free space: $path"
required_kb=$((MIN_FREE_GB * 1024 * 1024))
(( available_kb >= required_kb )) ||
die "$label needs at least ${MIN_FREE_GB}GB free: $path"
echo "[disk] $label free=$((available_kb / 1024 / 1024))GB required>=${MIN_FREE_GB}GB"
}
ARTIFACT_ROOT="$(realpath "$ARTIFACT_ROOT")"
for artifact in "$PLANTAI_ARTIFACT" "$RAG_ARTIFACT" "$QA_ARTIFACT"; do
[[ -d "$artifact" ]] || die "stage A artifact does not exist: $artifact"
resolved="$(realpath "$artifact")"
case "$resolved" in
"$ARTIFACT_ROOT"/*) ;;
*) die "artifact must be inside ARTIFACT_ROOT: $resolved" ;;
esac
[[ -f "$resolved/.hardened-build.json" ]] ||
die "artifact is missing .hardened-build.json: $resolved"
done
python3 - "$PLANTAI_ARTIFACT" "$RAG_ARTIFACT" "$QA_ARTIFACT" <<'PY'
import json
import pathlib
import sys
expected = [
("PlantAI", "3.11", ".cpython-311-"),
("RAG", "3.11", ".cpython-311-"),
("QA", "3.12", ".cpython-312-"),
]
for root_arg, (project, version_prefix, suffix_fragment) in zip(sys.argv[1:], expected):
root = pathlib.Path(root_arg)
marker = json.loads((root / ".hardened-build.json").read_text(encoding="utf-8"))
errors = []
if marker.get("project") != project:
errors.append(f"project={marker.get('project')!r}")
if not str(marker.get("python_version", "")).startswith(version_prefix):
errors.append(f"python_version={marker.get('python_version')!r}")
if suffix_fragment not in str(marker.get("extension_suffix", "")):
errors.append(f"extension_suffix={marker.get('extension_suffix')!r}")
if int(marker.get("extension_count", 0)) <= 0:
errors.append("extension_count<=0")
if errors:
raise SystemExit(f"{root}: invalid build marker: {', '.join(errors)}")
print(
f"[marker] {project}: python={marker['python_version']} "
f"extensions={marker['extension_count']} tree={marker.get('tree_sha256', '')}"
)
PY
safe_reset_dir() {
local target="$1" resolved_parent
resolved_parent="$(realpath -m "$(dirname "$target")")"
[[ "$resolved_parent" == "$WORK_DIR" || "$resolved_parent" == "$PACKAGE_STAGE" ]] ||
die "refusing to clean outside the tool work directory: $target"
rm -rf -- "$target"
mkdir -p "$target"
}
mkdir -p "$WORK_DIR" "$DIST_DIR"
DOCKER_ROOT="$(docker info --format '{{.DockerRootDir}}')"
require_free_space "$DOCKER_ROOT" "Docker data directory"
require_free_space "$DIST_DIR" "release directory"
safe_reset_dir "$CONTEXT_DIR"
safe_reset_dir "$PACKAGE_STAGE"
RSYNC_EXCLUDES=(
--exclude '/input/'
--exclude '/workspace/'
--exclude '/logs/'
--exclude '/cost_logs/'
--exclude '/memory_storage/'
--exclude '/service_data/'
--exclude '/outputs/'
--exclude '/output/'
--exclude '/final/'
--exclude '/backup/'
--exclude '/backups/'
--exclude '/tmp/'
--exclude '/temp/'
--exclude '/tests/'
--exclude '/test_integration/'
--exclude '/benchmarks/'
--exclude '/.git/'
--exclude '/.agents/'
--exclude '/.codex/'
--exclude '/.cursor/'
--exclude '/.idea/'
--exclude '/.pytest_cache/'
--exclude '/__pycache__/'
--exclude '**/__pycache__/'
--exclude '*.pyc'
--exclude '*.pyo'
--exclude '*.c'
--exclude '*.cpp'
--exclude '*.log'
--exclude '*.bak'
--exclude '*.backup'
--exclude '*.backup.*'
--exclude '.env'
--exclude '*.key'
--exclude '*.pem'
--exclude 'license.json'
--exclude 'asset.key.enc'
)
sync_artifact() {
local source="$1" destination="$2" project="$3"
mkdir -p "$destination"
echo "[stage] $project: $source -> $destination"
rsync -a --delete --safe-links "${RSYNC_EXCLUDES[@]}" "$source/" "$destination/"
}
sync_artifact "$PLANTAI_ARTIFACT" "$CONTEXT_DIR/PlantAI" PlantAI
sync_artifact "$RAG_ARTIFACT" "$CONTEXT_DIR/RAG" RAG
sync_artifact "$QA_ARTIFACT" "$CONTEXT_DIR/QA" QA
PFAM_MAPPING_SOURCE="$SCRIPT_DIR/runtime-assets/pfam_mapping.json"
[[ -s "$PFAM_MAPPING_SOURCE" ]] ||
die "missing offline Pfam family mapping: $PFAM_MAPPING_SOURCE"
mkdir -p "$CONTEXT_DIR/PlantAI/data/rag_db"
cp "$PFAM_MAPPING_SOURCE" "$CONTEXT_DIR/PlantAI/data/rag_db/pfam_mapping.json"
python3 - "$CONTEXT_DIR/PlantAI/data/rag_db/pfam_mapping.json" <<'PY'
import json
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
mapping = json.loads(path.read_text(encoding="utf-8"))
expected = {
"SPL": "PF03110",
"SBP": "PF03110",
"WRKY": "PF03106",
"NAC": "PF02365",
}
if not isinstance(mapping, dict):
raise SystemExit(f"{path}: expected a JSON object")
for family, accession in expected.items():
if mapping.get(family) != accession:
raise SystemExit(
f"{path}: expected {family}={accession}, got {mapping.get(family)!r}"
)
print(f"[stage] offline Pfam family mapping entries={len(mapping)}")
PY
if [[ "$DROP_COMPILED_PY_SIBLINGS" == "1" ]]; then
python3 - "$CONTEXT_DIR" <<'PY'
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
for project in ("PlantAI", "RAG", "QA"):
removed = 0
for py_path in (root / project).rglob("*.py"):
stem = py_path.name[:-3]
compiled = list(py_path.parent.glob(stem + ".cpython-*.so"))
compiled += list(py_path.parent.glob(stem + ".abi3.so"))
if compiled:
py_path.unlink()
removed += 1
print(f"[stage] {project}: removed compiled .py siblings={removed}")
PY
fi
if [[ "$INCLUDE_PLANTAI_STATIC_DATA" != "1" ]]; then
rm -rf -- "$CONTEXT_DIR/PlantAI/data"
echo "[stage] excluded PlantAI/data"
fi
if [[ "$INCLUDE_PLANTAI_STATIC_DATA" == "1" ]]; then
for pfam_file in \
Pfam-A.hmm \
Pfam-A.hmm.h3f \
Pfam-A.hmm.h3i \
Pfam-A.hmm.h3m \
Pfam-A.hmm.h3p; do
[[ -s "$CONTEXT_DIR/PlantAI/data/hmm/$pfam_file" ]] ||
die "missing required Identify Pfam asset: PlantAI/data/hmm/$pfam_file"
done
[[ -s "$CONTEXT_DIR/PlantAI/data/rag_db/pfam_mapping.json" ]] ||
die "missing required Identify offline mapping: PlantAI/data/rag_db/pfam_mapping.json"
fi
mapfile -t staged_links < <(find "$CONTEXT_DIR" -type l -print)
if (( ${#staged_links[@]} > 0 )); then
printf '%s\n' "${staged_links[@]}" >&2
die "isolated build context still contains symbolic links"
fi
mapfile -t forbidden_files < <(
find "$CONTEXT_DIR" -type f \
\( -name '.env' -o -name '*.key' -o -name '*.pem' \
-o -name 'license.json' -o -name 'asset.key.enc' \) -print
)
if (( ${#forbidden_files[@]} > 0 )); then
printf '%s\n' "${forbidden_files[@]}" >&2
die "isolated build context contains forbidden files"
fi
python3 - "$CONTEXT_DIR" "$PLANTAI_BASE_IMAGE" "$RAG_BASE_IMAGE" "$QA_BASE_IMAGE" \
"$PLANTAI_IMAGE_REF" "$RAG_IMAGE_REF" "$QA_IMAGE_REF" "$PLATFORM" \
"$DROP_COMPILED_PY_SIBLINGS" <<'PY'
import datetime
import json
import pathlib
import sys
context = pathlib.Path(sys.argv[1])
manifest = {
"package_format": "plantai-protected-docker-v2",
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"build_policy": {
"full_environment_in_image": False,
"reuses_existing_application_image": False,
"no_cache": True,
"pull_clean_base_images": True,
},
"base_images": {
"PlantAI": sys.argv[2],
"RAG": sys.argv[3],
"QA": sys.argv[4],
},
"application_images": {
"PlantAI": sys.argv[5],
"RAG": sys.argv[6],
"QA": sys.argv[7],
},
"platform": sys.argv[8],
"projects": {
name: json.loads((context / name / ".hardened-build.json").read_text(encoding="utf-8"))
for name in ("PlantAI", "RAG", "QA")
},
"excluded_runtime_directories": [
"input", "workspace", "logs", "cost_logs", "memory_storage",
"service_data", "outputs", "backup", "tmp", "tests"
],
"compiled_python_siblings_removed": sys.argv[9] == "1",
"identify_offline_pfam_mapping": "PlantAI/data/rag_db/pfam_mapping.json",
"hardware_binding": False,
}
(context / "PACKAGE-MANIFEST.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
PY
du -sh "$CONTEXT_DIR/PlantAI" "$CONTEXT_DIR/RAG" "$CONTEXT_DIR/QA"
render_compose_env() {
local destination="$1"
sed \
-e "s|@@PLANTAI_IMAGE_REF@@|$PLANTAI_IMAGE_REF|g" \
-e "s|@@RAG_IMAGE_REF@@|$RAG_IMAGE_REF|g" \
-e "s|@@QA_IMAGE_REF@@|$QA_IMAGE_REF|g" \
"$SCRIPT_DIR/bbav5.env.example" >"$destination"
}
if [[ "$CHECK_ONLY" == "1" ]]; then
CHECK_ENV="$WORK_DIR/check-bbav5.env"
SERVICE_CHECK_ENV="$SCRIPT_DIR/bbav5.env"
[[ ! -e "$SERVICE_CHECK_ENV" ]] ||
die "check-only mode refuses to overwrite existing $SERVICE_CHECK_ENV"
render_compose_env "$CHECK_ENV"
cp "$CHECK_ENV" "$SERVICE_CHECK_ENV"
cleanup_check_env() {
rm -f -- "$SERVICE_CHECK_ENV" "$CHECK_ENV"
}
trap cleanup_check_env EXIT
docker compose --env-file "$CHECK_ENV" -f "$SCRIPT_DIR/docker-compose.yml" config >/dev/null
cleanup_check_env
trap - EXIT
echo "[check-only] paths, ABI, exclusions, staging, Docker policy and Compose passed"
echo "[check-only] no image pull, image build, image save or release compression was run"
exit 0
fi
echo "[pull] clean official Python base images"
docker pull --platform "$PLATFORM" "$PLANTAI_BASE_IMAGE"
if [[ "$RAG_BASE_IMAGE" != "$PLANTAI_BASE_IMAGE" ]]; then
docker pull --platform "$PLATFORM" "$RAG_BASE_IMAGE"
fi
docker pull --platform "$PLATFORM" "$QA_BASE_IMAGE"
export DOCKER_BUILDKIT=1
build_application_image() {
local project="$1" dockerfile="$2" base_image="$3" image_ref="$4"
echo "[build] $project -> $image_ref from $base_image"
docker build \
--pull \
--no-cache \
--network "$BUILD_NETWORK" \
--platform "$PLATFORM" \
--build-arg "BASE_IMAGE=$base_image" \
--build-arg "PIP_INDEX_URL=$PIP_INDEX_URL" \
--build-arg "TORCH_INDEX_URL=$TORCH_INDEX_URL" \
--build-arg "TORCH_CPU_VERSION=$TORCH_CPU_VERSION" \
-f "$SCRIPT_DIR/$dockerfile" \
-t "$image_ref" \
"$SCRIPT_DIR"
}
build_application_image PlantAI Dockerfile.plantai "$PLANTAI_BASE_IMAGE" "$PLANTAI_IMAGE_REF"
build_application_image RAG Dockerfile.rag "$RAG_BASE_IMAGE" "$RAG_IMAGE_REF"
build_application_image QA Dockerfile.qa "$QA_BASE_IMAGE" "$QA_IMAGE_REF"
verify_image() {
local project="$1" image_ref="$2" root="$3" expected_minor="$4"
echo "[verify] $project image structure and Python $expected_minor"
docker run --rm --entrypoint sh "$image_ref" -c "
set -eu
test -f '$root/.hardened-build.json'
test \"\$(find '$root' -type f -name '*.so' | wc -l)\" -gt 0
python -c \"import sys; assert '.'.join(map(str, sys.version_info[:2])) == '$expected_minor'\"
if find '$root' -type f \\
\\( -name '.env' -o -name '*.key' -o -name '*.pem' \\
-o -name 'license.json' -o -name 'asset.key.enc' \\) \\
-print -quit | grep -q .; then
echo 'forbidden file found in image' >&2
exit 1
fi
"
}
verify_image PlantAI "$PLANTAI_IMAGE_REF" /opt/plantai 3.11
verify_image RAG "$RAG_IMAGE_REF" /opt/rag 3.11
verify_image QA "$QA_IMAGE_REF" /opt/qa 3.12
verify_cpu_torch() {
local project="$1" image_ref="$2"
echo "[verify] $project uses pinned CPU-only Torch $TORCH_CPU_VERSION"
docker run --rm --entrypoint python "$image_ref" -c "
import torch
assert torch.__version__ == '$TORCH_CPU_VERSION', torch.__version__
assert torch.version.cuda is None, torch.version.cuda
print(torch.__version__)
"
}
verify_cpu_torch RAG "$RAG_IMAGE_REF"
verify_cpu_torch QA "$QA_IMAGE_REF"
echo "[verify] PlantAI Identify native tools and Pfam database"
docker run --rm --entrypoint sh "$PLANTAI_IMAGE_REF" -c '
set -eu
command -v hmmscan
command -v hmmsearch
command -v blastp
command -v makeblastdb
test -s /opt/plantai/data/hmm/Pfam-A.hmm
test -s /opt/plantai/data/hmm/Pfam-A.hmm.h3f
test -s /opt/plantai/data/hmm/Pfam-A.hmm.h3i
test -s /opt/plantai/data/hmm/Pfam-A.hmm.h3m
test -s /opt/plantai/data/hmm/Pfam-A.hmm.h3p
test -s /opt/plantai/data/rag_db/pfam_mapping.json
python -c "import json; p=json.load(open(\"/opt/plantai/data/rag_db/pfam_mapping.json\")); assert p[\"SPL\"]==\"PF03110\""
'
images=("$PLANTAI_IMAGE_REF" "$RAG_IMAGE_REF" "$QA_IMAGE_REF")
if [[ "$INCLUDE_INFRA_IMAGES" == "1" ]]; then
for image in "$REDIS_IMAGE" "$QDRANT_IMAGE" "$NEO4J_IMAGE"; do
if [[ "$PULL_INFRA_IMAGES" == "1" ]]; then
docker pull --platform "$PLATFORM" "$image"
else
docker image inspect "$image" >/dev/null 2>&1 ||
die "missing infrastructure image and PULL_INFRA_IMAGES=0: $image"
fi
images+=("$image")
done
fi
mkdir -p \
"$PRODUCT_DIR/images" \
"$PRODUCT_DIR/input" \
"$PRODUCT_DIR/workspace" \
"$PRODUCT_DIR/data" \
"$PRODUCT_DIR/logs" \
"$PRODUCT_DIR/models" \
"$PRODUCT_DIR/service_data/memory_storage" \
"$PRODUCT_DIR/service_data/cost_logs" \
"$PRODUCT_DIR/service_data/reproducibility" \
"$PRODUCT_DIR/service_data/redis" \
"$PRODUCT_DIR/service_data/qdrant" \
"$PRODUCT_DIR/service_data/neo4j"
echo "[export] ${images[*]}"
: >"$PRODUCT_DIR/IMAGE-IDS.txt"
for image in "${images[@]}"; do
printf '%s %s\n' \
"$image" \
"$(docker image inspect "$image" --format '{{.Id}}')" \
>>"$PRODUCT_DIR/IMAGE-IDS.txt"
done
docker save "${images[@]}" | zstd -T0 -10 -o "$PRODUCT_DIR/images/$IMAGE_ARCHIVE_NAME"
cp "$SCRIPT_DIR/docker-compose.yml" "$PRODUCT_DIR/"
render_compose_env "$PRODUCT_DIR/bbav5.env.example"
for runtime_script in install start stop status logs health-check; do
cp "$SCRIPT_DIR/runtime/$runtime_script.sh" "$PRODUCT_DIR/$runtime_script.sh"
done
cp "$SCRIPT_DIR/README.md" "$PRODUCT_DIR/README.md"
cp "$CONTEXT_DIR/PACKAGE-MANIFEST.json" "$PRODUCT_DIR/"
chmod +x "$PRODUCT_DIR"/*.sh
(
cd "$PRODUCT_DIR"
sha256sum \
"images/$IMAGE_ARCHIVE_NAME" \
docker-compose.yml \
bbav5.env.example \
IMAGE-IDS.txt \
PACKAGE-MANIFEST.json >SHA256SUMS
)
if [[ -e "$ARCHIVE" && "${OVERWRITE:-0}" != "1" ]]; then
die "release archive exists; set OVERWRITE=1 only after confirming replacement: $ARCHIVE"
fi
rm -f -- "$ARCHIVE" "$ARCHIVE.sha256"
echo "[package] $ARCHIVE"
tar -C "$PACKAGE_STAGE" --zstd -cf "$ARCHIVE" "$PRODUCT_NAME"
(
cd "$DIST_DIR"
sha256sum "$(basename "$ARCHIVE")" >"$(basename "$ARCHIVE").sha256"
)
echo "[done] Docker package complete"
ls -lh "$ARCHIVE" "$ARCHIVE.sha256"
printf '[image] %s\n' "$PLANTAI_IMAGE_REF" "$RAG_IMAGE_REF" "$QA_IMAGE_REF"
echo "[customer] tar --zstd -xf $(basename "$ARCHIVE")"