File size: 6,098 Bytes
b83a9c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41a10e6
 
 
b83a9c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41a10e6
b83a9c1
41a10e6
b83a9c1
 
 
 
 
 
41a10e6
 
 
 
 
 
 
 
b83a9c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env bash
# Daemonless image builder for RunPod serverless endpoints (crane-based; no
# Docker needed — runs on any RunPod pod that has the models on disk).
#
# Layer model (order matters — cheap-to-change stuff goes LAST):
#
#   plx1029/comfyui-qwen:v6                       (base: torch + ComfyUI + rgthree)
#     + custom-nodes layer + one layer PER model  -> :<endpoint>-models-<V>   (heavy, build rarely)
#       + serverless code layer + CMD mutate      -> :<endpoint>-<V>          (tiny, build often)
#
# Because the code layer sits on top, iterating on handler/workflows/params
# re-uploads only a few MB; the model layers are content-addressed and are
# never pushed twice.
#
# Usage (from the directory holding this script):
#   ./build.sh models qwen-edit-turbo v1            # build+push heavy models tag
#   ./build.sh code   qwen-edit-turbo v1 v1         # build+push final tag on models-v1
#   ./build.sh all    qwen-edit-turbo v1            # both
#
# Env: DOCKER_HUB_ACCESS_TOKEN (required), DOCKER_HUB_USER (default plx1029),
#      BASE_IMAGE (default plx1029/comfyui-qwen:v6),
#      DEST_REPO (default docker.io/<user>/comfyui-serverless),
#      COMFY_WORKSPACE (default /workspace).

set -euo pipefail

CMD_MODE="${1:?models|code|all}"; ENDPOINT="${2:?endpoint name}"; VER="${3:?version}"
MODELS_VER="${4:-$VER}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WS="${COMFY_WORKSPACE:-/workspace}"
USER="${DOCKER_HUB_USER:-plx1029}"
BASE="${BASE_IMAGE:-plx1029/comfyui-qwen:v6}"
REPO="${DEST_REPO:-docker.io/${USER}/comfyui-serverless}"
EP_DIR="$SCRIPT_DIR/endpoints/$ENDPOINT"
BUILD_DIR="${BUILD_DIR:-/root/build}"
THREADS=$(( $(nproc) > 64 ? 64 : $(nproc) ))

[ -f "$EP_DIR/endpoint.json" ] || { echo "no endpoint.json in $EP_DIR"; exit 1; }
command -v crane >/dev/null || { echo "crane not installed"; exit 1; }
command -v pigz  >/dev/null || { echo "pigz not installed";  exit 1; }

echo "$DOCKER_HUB_ACCESS_TOKEN" | crane auth login index.docker.io -u "$USER" --password-stdin

mkdir -p "$BUILD_DIR"
# .git excluded: images are immutable, git history is dead weight (RES4LYF's
# alone is 260 MB) and Manager-updates don't apply to serverless workers
TAR_OPTS=(--owner=0 --group=0 --exclude='__pycache__' --exclude='*.pyc' --exclude='.git')

layer_from_paths() {  # $1 = output tgz, rest = paths relative to /
    local out="$1"; shift
    [ -s "$out" ] && { echo "  (cached) $out"; return; }
    tar "${TAR_OPTS[@]}" -C / -cf - "$@" | pigz -p "$THREADS" -1 > "$out.tmp"
    mv "$out.tmp" "$out"
    echo "  $(du -h "$out" | cut -f1) $out"
}

build_models() {
    local tag="$REPO:$ENDPOINT-models-$VER"
    echo "== building $tag on $BASE"
    local layers=()

    # custom nodes the endpoint needs that aren't already in the base image
    # (BASE_HAS_NODES lists packs baked into $BASE; rgthree ships in v6)
    local base_has=" ${BASE_HAS_NODES:-rgthree-comfy} "
    local nodes n paths=()
    nodes=$(python3 -c "import json;print(' '.join(json.load(open('$EP_DIR/endpoint.json'))['custom_nodes']))")
    for n in $nodes; do
        case "$base_has" in *" $n "*) continue ;; esac
        [ -d "$WS/ComfyUI/custom_nodes/$n" ] || { echo "missing custom node $n"; exit 1; }
        paths+=("workspace/ComfyUI/custom_nodes/$n")
    done
    if [ "${#paths[@]}" -gt 0 ]; then
        echo "-- custom nodes layer: ${paths[*]}"
        layer_from_paths "$BUILD_DIR/$ENDPOINT-nodes.tgz" "${paths[@]}"
        layers+=(-f "$BUILD_DIR/$ENDPOINT-nodes.tgz")
    fi

    # one layer per model file/dir (content-addressed => pushed exactly once, ever)
    while read -r m; do
        [ -e "$WS/ComfyUI/$m" ] || { echo "missing model $WS/ComfyUI/$m — download it first"; exit 1; }
        local tgz="$BUILD_DIR/model-$(basename "$m").tgz"
        echo "-- model layer: $m"
        layer_from_paths "$tgz" "workspace/ComfyUI/$m"
        layers+=(-f "$tgz")
    done < <(python3 -c "import json;print('\n'.join(json.load(open('$EP_DIR/endpoint.json'))['models']))")

    # extra prebuilt layers (e.g. a pip-deps delta captured on the build pod)
    while read -r x; do
        [ -z "$x" ] && continue
        [ -s "$BUILD_DIR/$x" ] || { echo "missing extra layer $BUILD_DIR/$x"; exit 1; }
        echo "-- extra layer: $x"
        layers+=(-f "$BUILD_DIR/$x")
    done < <(python3 -c "import json;print('\n'.join(json.load(open('$EP_DIR/endpoint.json')).get('extra_layers',[])))")

    crane append -b "$BASE" "${layers[@]}" -t "$tag"
    echo "== pushed $tag"
}

build_code() {
    local models_tag="$REPO:$ENDPOINT-models-$MODELS_VER"
    local tag="$REPO:$ENDPOINT-$VER"
    echo "== building $tag on $models_tag"

    local stage="$BUILD_DIR/stage-code"
    rm -rf "$stage"
    mkdir -p "$stage/opt/serverless/src" "$stage/opt/serverless/lib" \
             "$stage/workspace/ComfyUI/comfy"
    cp -r "$SCRIPT_DIR"/handler.py "$SCRIPT_DIR"/start.sh "$SCRIPT_DIR"/endpoints \
          "$SCRIPT_DIR"/tests "$stage/opt/serverless/src/"
    # only THIS endpoint's config goes into the image
    find "$stage/opt/serverless/src/endpoints" -mindepth 1 -maxdepth 1 \
         ! -name "$ENDPOINT" -exec rm -rf {} +
    # vendored python deps for the handler (runpod sdk); build them if absent
    if [ ! -d /opt/serverless/lib/runpod ]; then
        pip install -q --target /opt/serverless/lib runpod
    fi
    cp -r /opt/serverless/lib/. "$stage/opt/serverless/lib/"
    # local core patches (ComfyUI #14573 aimdo fix) ride along with the code
    cp "$WS/ComfyUI/comfy/model_management.py" "$stage/workspace/ComfyUI/comfy/"
    chmod +x "$stage/opt/serverless/src/start.sh"

    tar "${TAR_OPTS[@]}" --exclude='local-out-*' -C "$stage" -cf - . \
        | pigz -p "$THREADS" -1 > "$BUILD_DIR/code.tgz"
    crane append -b "$models_tag" -f "$BUILD_DIR/code.tgz" -t "$tag"
    crane mutate "$tag" --cmd "/opt/serverless/src/start.sh" -t "$tag"
    echo "== pushed $tag"
}

case "$CMD_MODE" in
    models) build_models ;;
    code)   build_code ;;
    all)    build_models; MODELS_VER="$VER" build_code ;;
    *) echo "unknown mode $CMD_MODE"; exit 1 ;;
esac