|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +# Batch runner for TopoGen: run generate+build for each config in a folder. |
| 4 | +# |
| 5 | +# Usage: |
| 6 | +# ./build.sh [--include PATTERN ...] [--exclude PATTERN ...] [--force] [--build-only] <configs_dir> <output_dir> |
| 7 | +# |
| 8 | +# Example: |
| 9 | +# ./build.sh examples scenarios |
| 10 | +# ./build.sh --include "small_*" --exclude "*clos*" examples scenarios |
| 11 | +# ./build.sh --force examples scenarios |
| 12 | +# ./build.sh --build-only examples scenarios |
| 13 | +# |
| 14 | +# Behavior: |
| 15 | +# - Finds .yml/.yaml files directly under <configs_dir> (no recursion). |
| 16 | +# - For each config, creates <output_dir>/<config_stem>/ and runs both stages |
| 17 | +# in that directory so artefacts are kept together: |
| 18 | +# <config_stem>_integrated_graph.json |
| 19 | +# <config_stem>_scenario.yml |
| 20 | +# generate.log, build.log |
| 21 | +# - Prints a concise emoji summary at the end. |
| 22 | + |
| 23 | +set -u -o pipefail |
| 24 | + |
| 25 | +die() { |
| 26 | + echo "❌ $*" >&2 |
| 27 | + exit 1 |
| 28 | +} |
| 29 | + |
| 30 | +# Resolve an absolute path without relying on realpath. |
| 31 | +abs_path() { |
| 32 | + local p="$1" |
| 33 | + local dir base |
| 34 | + dir=$(cd "$(dirname -- "$p")" && pwd) || return 1 |
| 35 | + base=$(basename -- "$p") |
| 36 | + printf '%s/%s' "$dir" "$base" |
| 37 | +} |
| 38 | + |
| 39 | +# Parse options: --include/--exclude support multiple occurrences |
| 40 | +INCLUDE_PATTERNS=() |
| 41 | +EXCLUDE_PATTERNS=() |
| 42 | +FORCE=false |
| 43 | +BUILD_ONLY=false |
| 44 | + |
| 45 | +print_usage() { |
| 46 | + cat >&2 <<EOF |
| 47 | +Usage: $0 [--include PATTERN ...] [--exclude PATTERN ...] [--force] [--build-only] <configs_dir> <output_dir> |
| 48 | +
|
| 49 | +Examples: |
| 50 | + $0 examples scenarios |
| 51 | + $0 --include "small_*" examples scenarios |
| 52 | + $0 --exclude "*clos*" examples scenarios |
| 53 | + $0 --include "small_*" --exclude "*clos*" examples scenarios |
| 54 | + $0 --force examples scenarios |
| 55 | + $0 --build-only examples scenarios |
| 56 | +
|
| 57 | +Notes: |
| 58 | + - PATTERNs are shell globs matched against the config file basename (e.g., small_test.yml). |
| 59 | + - Multiple --include patterns act as OR; --exclude patterns remove matches. |
| 60 | + - --force ignores cached integrated graphs and runs generation unconditionally. |
| 61 | + - --build-only skips the generate stage and runs only the build stage. |
| 62 | +EOF |
| 63 | +} |
| 64 | + |
| 65 | +ARGS=() |
| 66 | +while [[ $# -gt 0 ]]; do |
| 67 | + case "$1" in |
| 68 | + --include) |
| 69 | + shift || true |
| 70 | + [[ ${1-} ]] || die "Missing PATTERN after --include" |
| 71 | + INCLUDE_PATTERNS+=("$1"); shift || true ;; |
| 72 | + --exclude) |
| 73 | + shift || true |
| 74 | + [[ ${1-} ]] || die "Missing PATTERN after --exclude" |
| 75 | + EXCLUDE_PATTERNS+=("$1"); shift || true ;; |
| 76 | + --force) |
| 77 | + FORCE=true; shift || true ;; |
| 78 | + --build-only) |
| 79 | + BUILD_ONLY=true; shift || true ;; |
| 80 | + -h|--help) |
| 81 | + print_usage; exit 0 ;; |
| 82 | + --) |
| 83 | + shift; break ;; |
| 84 | + --*) |
| 85 | + die "Unknown option: $1" ;; |
| 86 | + *) |
| 87 | + ARGS+=("$1"); shift || true ;; |
| 88 | + esac |
| 89 | +done |
| 90 | + |
| 91 | +# Append any remaining args |
| 92 | +if [[ $# -gt 0 ]]; then |
| 93 | + ARGS+=("$@") |
| 94 | +fi |
| 95 | + |
| 96 | +if [[ ${#ARGS[@]} -ne 2 ]]; then |
| 97 | + print_usage |
| 98 | + exit 2 |
| 99 | +fi |
| 100 | + |
| 101 | +CONFIGS_DIR_RAW="${ARGS[0]}" |
| 102 | +OUTPUT_DIR_RAW="${ARGS[1]}" |
| 103 | + |
| 104 | +[[ -d "$CONFIGS_DIR_RAW" ]] || die "Configs directory not found: $CONFIGS_DIR_RAW" |
| 105 | +mkdir -p "$OUTPUT_DIR_RAW" || die "Cannot create output directory: $OUTPUT_DIR_RAW" |
| 106 | + |
| 107 | +# Canonical absolute paths |
| 108 | +CONFIGS_DIR=$(abs_path "$CONFIGS_DIR_RAW") |
| 109 | +OUTPUT_DIR=$(abs_path "$OUTPUT_DIR_RAW") |
| 110 | + |
| 111 | +# Detect TopoGen invoker once: prefer installed CLI; otherwise python -m. |
| 112 | +TOPGEN_INVOKE=(topogen) |
| 113 | +if ! command -v "${TOPGEN_INVOKE[0]}" >/dev/null 2>&1; then |
| 114 | + if command -v python3 >/dev/null 2>&1; then |
| 115 | + TOPGEN_INVOKE=(python3 -m topogen) |
| 116 | + elif command -v python >/dev/null 2>&1; then |
| 117 | + TOPGEN_INVOKE=(python -m topogen) |
| 118 | + else |
| 119 | + die "Neither 'topogen' nor Python found on PATH. Activate your venv or install TopoGen." |
| 120 | + fi |
| 121 | +fi |
| 122 | + |
| 123 | +echo "🚀 TopoGen batch run" |
| 124 | +OUTPUT_DIR_PRINT=${OUTPUT_DIR%/.} |
| 125 | +CONFIGS_DIR_PRINT=${CONFIGS_DIR%/.} |
| 126 | +echo "📁 Configs: $CONFIGS_DIR_PRINT" |
| 127 | +echo "📂 Output: $OUTPUT_DIR_PRINT" |
| 128 | +if [[ ${#INCLUDE_PATTERNS[@]} -gt 0 ]]; then |
| 129 | + echo "🔎 Include: ${INCLUDE_PATTERNS[*]}" |
| 130 | +else |
| 131 | + echo "🔎 Include: (none)" |
| 132 | +fi |
| 133 | +if [[ ${#EXCLUDE_PATTERNS[@]} -gt 0 ]]; then |
| 134 | + echo "🔎 Exclude: ${EXCLUDE_PATTERNS[*]}" |
| 135 | +else |
| 136 | + echo "🔎 Exclude: (none)" |
| 137 | +fi |
| 138 | +echo "⚙️ Force: $FORCE" |
| 139 | +echo "🏗️ Build-only: $BUILD_ONLY" |
| 140 | +echo |
| 141 | + |
| 142 | +# Collect summary statistics |
| 143 | +total=0 |
| 144 | +gen_ok=0 |
| 145 | +gen_fail=0 |
| 146 | +gen_config=0 |
| 147 | +gen_cached=0 |
| 148 | +build_ok=0 |
| 149 | +build_validation=0 |
| 150 | +build_runtime=0 |
| 151 | +build_config=0 |
| 152 | +build_skipped=0 |
| 153 | +build_other=0 |
| 154 | + |
| 155 | +row_names=() |
| 156 | +row_gen=() |
| 157 | +row_build=() |
| 158 | +w_name=6 # min width for header 'Config' |
| 159 | +w_gen=8 # min width for header 'Generate' |
| 160 | +w_build=5 # min width for header 'Build' |
| 161 | + |
| 162 | +passes_filters() { |
| 163 | + # return 0 if basename passes include/exclude filters, else 1 |
| 164 | + local name="$1" |
| 165 | + # Includes: if provided, require any to match |
| 166 | + if [[ ${#INCLUDE_PATTERNS[@]} -gt 0 ]]; then |
| 167 | + local any=1 |
| 168 | + for pat in "${INCLUDE_PATTERNS[@]}"; do |
| 169 | + if [[ $name == $pat ]]; then any=0; break; fi |
| 170 | + done |
| 171 | + if (( any != 0 )); then return 1; fi |
| 172 | + fi |
| 173 | + # Excludes: drop if any matches |
| 174 | + if [[ ${#EXCLUDE_PATTERNS[@]} -gt 0 ]]; then |
| 175 | + for pat in "${EXCLUDE_PATTERNS[@]}"; do |
| 176 | + if [[ $name == $pat ]]; then return 1; fi |
| 177 | + done |
| 178 | + fi |
| 179 | + return 0 |
| 180 | +} |
| 181 | + |
| 182 | +# Enumerate configs (non-recursive) |
| 183 | +while IFS= read -r -d '' cfg; do |
| 184 | + cfg_abs=$(abs_path "$cfg") |
| 185 | + cfg_name=$(basename -- "$cfg") |
| 186 | + # Apply include/exclude filters on basename |
| 187 | + if ! passes_filters "$cfg_name"; then |
| 188 | + continue |
| 189 | + fi |
| 190 | + total=$((total + 1)) |
| 191 | + stem=${cfg_name%.*} |
| 192 | + workdir="$OUTPUT_DIR/$stem" |
| 193 | + mkdir -p "$workdir" || die "Cannot create work directory: $workdir" |
| 194 | + |
| 195 | + echo "➡️ Processing $cfg_name" |
| 196 | + echo " 📦 Workdir: $(abs_path "$workdir")" |
| 197 | + |
| 198 | + graph_work="$workdir/${stem}_integrated_graph.json" |
| 199 | + |
| 200 | + gen_ec=0 |
| 201 | + if [[ "$BUILD_ONLY" == "true" ]]; then |
| 202 | + # Explicitly skip generate stage, proceed straight to build |
| 203 | + echo "⏭️ Skipping generate due to --build-only" | tee "$workdir/generate.log" >/dev/null |
| 204 | + gen_ec=100 # treat as cached/ready so build runs |
| 205 | + else |
| 206 | + if [[ "$FORCE" == "true" ]]; then |
| 207 | + run_generate=true |
| 208 | + else |
| 209 | + if [[ -f "$graph_work" ]]; then |
| 210 | + run_generate=false |
| 211 | + else |
| 212 | + run_generate=true |
| 213 | + fi |
| 214 | + fi |
| 215 | + |
| 216 | + if [[ "$run_generate" == "true" ]]; then |
| 217 | + # Run generate writing artefacts directly to workdir via -o |
| 218 | + ("${TOPGEN_INVOKE[@]}" generate "$cfg_abs" -o "$workdir") 2>&1 | tee "$workdir/generate.log" |
| 219 | + gen_ec=${PIPESTATUS[0]} |
| 220 | + else |
| 221 | + # Use cached artefacts |
| 222 | + : # artefacts already in workdir from previous run |
| 223 | + echo "⏭️ Skipping generate: found existing ${stem}_integrated_graph.json" | tee "$workdir/generate.log" >/dev/null |
| 224 | + gen_ec=100 # special code for 'cached' |
| 225 | + fi |
| 226 | + fi |
| 227 | + |
| 228 | + if [[ $gen_ec -eq 0 ]]; then |
| 229 | + gen_icon="✅" |
| 230 | + gen_ok=$((gen_ok + 1)) |
| 231 | + elif [[ $gen_ec -eq 2 ]]; then |
| 232 | + gen_icon="❌" |
| 233 | + gen_config=$((gen_config + 1)) |
| 234 | + elif [[ $gen_ec -eq 100 ]]; then |
| 235 | + gen_icon="⏭️" |
| 236 | + gen_cached=$((gen_cached + 1)) |
| 237 | + else |
| 238 | + gen_icon="❌" |
| 239 | + gen_fail=$((gen_fail + 1)) |
| 240 | + fi |
| 241 | + |
| 242 | + # Run build only if generate succeeded |
| 243 | + build_icon="⏭️" |
| 244 | + build_note="skipped" |
| 245 | + build_ec=-1 |
| 246 | + scenario_out="$workdir/${stem}_scenario.yml" |
| 247 | + if [[ $gen_ec -eq 0 || $gen_ec -eq 100 ]]; then |
| 248 | + # Run build writing scenario into workdir via -o |
| 249 | + ("${TOPGEN_INVOKE[@]}" build "$cfg_abs" -o "$scenario_out") 2>&1 | tee "$workdir/build.log" |
| 250 | + build_ec=${PIPESTATUS[0]} |
| 251 | + case "$build_ec" in |
| 252 | + 0) |
| 253 | + build_icon="✅"; build_note="ok"; build_ok=$((build_ok + 1));; |
| 254 | + 3) |
| 255 | + build_icon="⚠️"; build_note="validation failed"; build_validation=$((build_validation + 1));; |
| 256 | + 2) |
| 257 | + build_icon="❌"; build_note="config error"; build_config=$((build_config + 1));; |
| 258 | + 1) |
| 259 | + build_icon="❌"; build_note="runtime error"; build_runtime=$((build_runtime + 1));; |
| 260 | + *) |
| 261 | + build_icon="❌"; build_note="exit $build_ec"; build_other=$((build_other + 1));; |
| 262 | + esac |
| 263 | + : |
| 264 | + else |
| 265 | + build_skipped=$((build_skipped + 1)) |
| 266 | + fi |
| 267 | + |
| 268 | + # Summary line for this config |
| 269 | + gen_col="$gen_icon" |
| 270 | + if [[ $gen_ec -eq 2 ]]; then gen_col="❌ config"; fi |
| 271 | + if [[ $gen_ec -eq 100 ]]; then gen_col="⏭️ cached"; fi |
| 272 | + if [[ $gen_ec -ne 0 && $gen_ec -ne 2 && $gen_ec -ne 100 ]]; then gen_col="❌ runtime"; fi |
| 273 | + |
| 274 | + build_col="$build_icon" |
| 275 | + case "$build_ec" in |
| 276 | + 0) build_col="✅" ;; |
| 277 | + 3) build_col="⚠️ validation" ;; |
| 278 | + 2) build_col="❌ config" ;; |
| 279 | + 1) build_col="❌ runtime" ;; |
| 280 | + -1) build_col="⏭️ skipped" ;; |
| 281 | + *) build_col="❓ other" ;; |
| 282 | + esac |
| 283 | + |
| 284 | + row_names+=("$cfg_name") |
| 285 | + row_gen+=("$gen_col") |
| 286 | + row_build+=("$build_col") |
| 287 | + # Update column widths (character counts) |
| 288 | + name_len=$(printf %s "$cfg_name" | wc -m | tr -d ' ') |
| 289 | + gen_len=$(printf %s "$gen_col" | wc -m | tr -d ' ') |
| 290 | + build_len=$(printf %s "$build_col" | wc -m | tr -d ' ') |
| 291 | + (( name_len > w_name )) && w_name=$name_len |
| 292 | + (( gen_len > w_gen )) && w_gen=$gen_len |
| 293 | + (( build_len > w_build )) && w_build=$build_len |
| 294 | + |
| 295 | + echo |
| 296 | +done < <(find "$CONFIGS_DIR" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) |
| 297 | + |
| 298 | +if [[ $total -eq 0 ]]; then |
| 299 | + echo "⚠️ No YAML config files matched in $CONFIGS_DIR" >&2 |
| 300 | + echo " Include: ${INCLUDE_PATTERNS[*]:-(none)}" >&2 |
| 301 | + echo " Exclude: ${EXCLUDE_PATTERNS[*]:-(none)}" >&2 |
| 302 | + exit 2 |
| 303 | +fi |
| 304 | + |
| 305 | +echo "======================" |
| 306 | +echo "📋 Summary" |
| 307 | +printf "%-*s %-*s %-*s\n" "$w_name" "Config" "$w_gen" "Generate" "$w_build" "Build" |
| 308 | +dash_len=$((w_name + 2 + w_gen + 2 + w_build)) |
| 309 | +printf '%*s\n' "$dash_len" '' | tr ' ' '-' |
| 310 | +for i in "${!row_names[@]}"; do |
| 311 | + printf "%-*s %-*s %-*s\n" "$w_name" "${row_names[$i]}" "$w_gen" "${row_gen[$i]}" "$w_build" "${row_build[$i]}" |
| 312 | +done |
| 313 | +echo "----------------------" |
| 314 | +echo "🧮 Totals: $total configs" |
| 315 | +echo " • Generate: ✅ $gen_ok | ⏭️ $gen_cached (cached) | ❌ $gen_fail (runtime) | ❌ $gen_config (config)" |
| 316 | +echo " • Build: ✅ $build_ok | ⚠️ $build_validation (validation) | ❌ $build_runtime (runtime) | ❌ $build_config (config) | ⏭️ $build_skipped (skipped) | ❓ $build_other (other)" |
| 317 | +echo "======================" |
| 318 | + |
| 319 | +echo "Done. ✨" |
0 commit comments