|
1 | | -{ pkgs, ... }: |
| 1 | +{ |
| 2 | + pkgs, |
| 3 | + lib, |
| 4 | + ... |
| 5 | +}: |
2 | 6 | let |
3 | | - allComponents = "chat image-analysis flux-image-gen"; |
4 | 7 | imagePrefix = "ghcr.io/stackhpc/azimuth-llm"; |
5 | | - webAppsDir = "./web-apps"; |
| 8 | + webAppsDir = ./web-apps; |
| 9 | + |
| 10 | + # Discover components: subdirs of web-apps/ that contain a Dockerfile. |
| 11 | + # Keeps `build`, `scan`, and the help text in sync with the filesystem |
| 12 | + # so adding a web-app requires no edits here. |
| 13 | + componentList = |
| 14 | + let |
| 15 | + entries = builtins.readDir webAppsDir; |
| 16 | + isComponent = |
| 17 | + name: entries.${name} == "directory" && builtins.pathExists (webAppsDir + "/${name}/Dockerfile"); |
| 18 | + in |
| 19 | + lib.sort lib.lessThan (lib.filter isComponent (builtins.attrNames entries)); |
6 | 20 |
|
7 | | - # Resolves "all" or empty arg to the full list, validates otherwise. |
8 | | - resolveComponents = '' |
9 | | - ALL_COMPONENTS="${allComponents}" |
| 21 | + # Shell prelude shared by every script that operates on components. |
| 22 | + # Provides: |
| 23 | + # resolve_components <input> - echo input (validated) or all components |
| 24 | + # image_name <component> - echo the full image reference |
| 25 | + componentPrelude = '' |
| 26 | + ALL_COMPONENTS="${lib.concatStringsSep " " componentList}" |
10 | 27 |
|
11 | 28 | resolve_components() { |
12 | 29 | local input="$1" |
13 | 30 | if [ -z "$input" ] || [ "$input" = "all" ]; then |
14 | 31 | echo "$ALL_COMPONENTS" |
15 | | - else |
16 | | - for c in $input; do |
17 | | - if ! echo " $ALL_COMPONENTS " | grep -q " $c "; then |
18 | | - echo "Unknown component: $c" >&2 |
19 | | - echo "Available: $ALL_COMPONENTS" >&2 |
20 | | - return 1 |
21 | | - fi |
22 | | - done |
23 | | - echo "$input" |
| 32 | + return 0 |
24 | 33 | fi |
| 34 | + for c in $input; do |
| 35 | + if ! echo " $ALL_COMPONENTS " | grep -q " $c "; then |
| 36 | + echo "Unknown component: $c" >&2 |
| 37 | + echo "Available: $ALL_COMPONENTS" >&2 |
| 38 | + return 1 |
| 39 | + fi |
| 40 | + done |
| 41 | + echo "$input" |
25 | 42 | } |
26 | 43 |
|
27 | 44 | image_name() { |
|
44 | 61 | # CI tooling |
45 | 62 | jq |
46 | 63 | yq-go |
47 | | - # Python |
| 64 | + curl |
| 65 | + # Python toolchain. |
| 66 | + # devenv is NOT the source of truth for Python deps: the pinned |
| 67 | + # requirements.txt in each web-app is, and uv installs from it. |
| 68 | + # This guarantees the devenv .venv matches the Dockerfile-built image. |
| 69 | + # The Dockerfile must use the same Python minor version. |
48 | 70 | python311 |
| 71 | + uv |
49 | 72 | ruff |
50 | 73 | black |
51 | 74 | ]; |
52 | 75 |
|
| 76 | + # PyPI wheels (numpy, scipy, pillow, ...) are linked against system libs |
| 77 | + # that don't exist on NixOS at standard FHS paths. Expose them via |
| 78 | + # LD_LIBRARY_PATH so the wheels' C extensions can load. |
| 79 | + env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ |
| 80 | + pkgs.stdenv.cc.cc.lib # libstdc++.so.6, libgcc_s.so.1 |
| 81 | + pkgs.zlib # libz.so.1 (numpy, scipy, pillow) |
| 82 | + pkgs.libjpeg # libjpeg.so (pillow) |
| 83 | + pkgs.libpng # libpng.so (pillow) |
| 84 | + pkgs.libwebp # libwebp.so (pillow) |
| 85 | + ]; |
| 86 | + |
53 | 87 | treefmt = { |
54 | 88 | enable = true; |
55 | 89 | config.programs = { |
|
69 | 103 |
|
70 | 104 | scripts = { |
71 | 105 | build.exec = '' |
72 | | - ${resolveComponents} |
| 106 | + ${componentPrelude} |
| 107 | +
|
73 | 108 | TAG="latest" |
74 | 109 | COMPONENT="" |
75 | 110 | while [ $# -gt 0 ]; do |
|
78 | 113 | *) COMPONENT="$COMPONENT $1"; shift ;; |
79 | 114 | esac |
80 | 115 | done |
81 | | - COMPONENT="''${COMPONENT## }" |
82 | 116 |
|
83 | | - TARGETS=$(resolve_components "$COMPONENT") || exit 1 |
| 117 | + TARGETS=$(resolve_components "''${COMPONENT## }") || exit 1 |
84 | 118 | for c in $TARGETS; do |
85 | 119 | echo "==> Building $c (tag: $TAG)" |
86 | 120 | docker build \ |
87 | 121 | -t "$(image_name "$c"):$TAG" \ |
88 | | - -f ${webAppsDir}/"$c"/Dockerfile \ |
89 | | - ${webAppsDir}/ |
| 122 | + -f ./web-apps/"$c"/Dockerfile \ |
| 123 | + ./web-apps/ |
90 | 124 | done |
91 | 125 | ''; |
92 | 126 |
|
93 | 127 | scan.exec = '' |
94 | | - ${resolveComponents} |
| 128 | + ${componentPrelude} |
| 129 | +
|
95 | 130 | TAG="latest" |
96 | 131 | FAIL_ON="critical" |
97 | 132 | COMPONENT="" |
|
102 | 137 | *) COMPONENT="$COMPONENT $1"; shift ;; |
103 | 138 | esac |
104 | 139 | done |
105 | | - COMPONENT="''${COMPONENT## }" |
106 | 140 |
|
107 | | - TARGETS=$(resolve_components "$COMPONENT") || exit 1 |
| 141 | + TARGETS=$(resolve_components "''${COMPONENT## }") || exit 1 |
108 | 142 | EXIT=0 |
109 | 143 | for c in $TARGETS; do |
110 | 144 | build "$c" --tag "$TAG" |
111 | | -
|
112 | 145 | IMAGE="$(image_name "$c"):$TAG" |
113 | 146 | echo "" |
114 | 147 | echo "==> Scanning $IMAGE (fail-on: $FAIL_ON)" |
115 | | - if ! grype "$IMAGE" --fail-on "$FAIL_ON" --only-fixed; then |
116 | | - EXIT=1 |
117 | | - fi |
| 148 | + grype "$IMAGE" --fail-on "$FAIL_ON" --only-fixed || EXIT=1 |
118 | 149 | done |
119 | 150 | exit $EXIT |
120 | 151 | ''; |
| 152 | + |
| 153 | + # ---------------- omni helpers ---------------- |
| 154 | + # |
| 155 | + # omni-venv installs omni's Python deps into web-apps/omni/.venv from |
| 156 | + # requirements.txt, re-running only when requirements.txt is newer than |
| 157 | + # the install stamp. The `../utils` line in requirements.txt is resolved |
| 158 | + # by uv as a local path dep (same mechanism the Dockerfile uses, modulo |
| 159 | + # the sed rewrite). |
| 160 | + # |
| 161 | + # omni-run ensures the venv is up to date, activates it, and launches |
| 162 | + # the Gradio UI. |
| 163 | + |
| 164 | + omni-venv.exec = '' |
| 165 | + set -e |
| 166 | + cd ./web-apps/omni |
| 167 | + STAMP=.venv/.installed |
| 168 | + if [ ! -d .venv ] || [ requirements.txt -nt "$STAMP" ]; then |
| 169 | + echo "==> Creating/refreshing ./web-apps/omni/.venv (Python 3.11)" |
| 170 | + uv venv --python 3.11 .venv |
| 171 | + # shellcheck disable=SC1091 |
| 172 | + . .venv/bin/activate |
| 173 | + echo "==> Installing dependencies from requirements.txt" |
| 174 | + uv pip install -r requirements.txt |
| 175 | + touch "$STAMP" |
| 176 | + fi |
| 177 | + ''; |
| 178 | + |
| 179 | + omni-run.exec = '' |
| 180 | + set -e |
| 181 | + omni-venv |
| 182 | + cd ./web-apps/omni |
| 183 | + # shellcheck disable=SC1091 |
| 184 | + . .venv/bin/activate |
| 185 | + echo "==> Starting Omni interface on http://localhost:7860" |
| 186 | + exec python app.py "$@" |
| 187 | + ''; |
121 | 188 | }; |
122 | 189 |
|
123 | 190 | enterShell = '' |
124 | 191 | echo "$GREET" |
125 | 192 | echo "" |
126 | | - echo "Commands (component = chat | image-analysis | flux-image-gen | omit for all):" |
| 193 | + echo "Commands (component = ${lib.concatStringsSep " | " componentList} | omit for all):" |
127 | 194 | echo "" |
128 | 195 | echo " prek -a Format/lint all files" |
129 | 196 | echo " build [component] [--tag TAG] Build container image(s)" |
130 | 197 | echo " scan [component] [--tag TAG] [--fail-on SEV] Build if needed + Grype scan" |
131 | 198 | echo "" |
| 199 | + echo "Omni (multimodal UI) helpers:" |
| 200 | + echo "" |
| 201 | + echo " omni-run Start the Omni Gradio UI on :7860" |
| 202 | + echo "" |
132 | 203 | ''; |
133 | 204 | } |
0 commit comments