#!/usr/bin/env nu # Docker Build Validation Script # Validates cargo-chef optimization by measuring build times and image sizes # # Usage: # docker-validate-builds.nu extension-registry # docker-validate-builds.nu orchestrator --iterations 3 # # Environment: # PROVISIONING_ROOT - Optional: override project root detection # # Metrics: # - Cold build (no cache) # - Warm build (dependency cache) # - Incremental build (source change only) # - Image size comparison # Search up directory tree for provisioning root (helper) def search-up-for-provisioning [dir: string]: nothing -> string { if ($"($dir)/provisioning/schemas/platform" | path exists) { $"($dir)/provisioning" } else { let parent = ($dir | path dirname) if $parent == $dir { "" } else { search-up-for-provisioning $parent } } } # Detect provisioning project root def get-provisioning-root []: nothing -> string { # 1. Check environment variable if ($env.PROVISIONING_ROOT? != null) { return $env.PROVISIONING_ROOT } # 2. Check if we're in provisioning/ directory let cwd = (pwd) if ($cwd | path basename) == "provisioning" { # We're inside provisioning/, use current dir if ("schemas/platform" | path exists) { return "." } } # 3. Check if provisioning/ exists as subdirectory if ("provisioning/schemas/platform" | path exists) { return "provisioning" } # 4. Search up the directory tree let found = (search-up-for-provisioning $cwd) if $found != "" { return $found } # 5. Fallback to "provisioning" (will fail with clear error) "provisioning" } # Run build benchmark for a service def main [ service: string, # Service to benchmark --iterations: int = 1, # Number of iterations per test --skip-cold, # Skip cold build (fastest) --registry: string = "localhost:5000" # Registry for cache ]: nothing -> record { print $"Docker Build Validation: ($service)" print "========================================" print "" # Step 1: Generate Dockerfile print "→ Generating Dockerfile..." let prov_root = (get-provisioning-root) let gen_script = $"($prov_root)/scripts/docker-generate-builds.nu" let gen_result = (nu $gen_script $service --mode solo) if not $gen_result.ok { error make { msg: $"Failed to generate Dockerfile: ($gen_result.err)" } } print $" ✓ Generated: ($gen_result.path)" print "" let prov_root_build = (get-provisioning-root) let build_context = $"($prov_root_build)/platform" let dockerfile_path = $gen_result.path let image_tag = $"provisioning-($service):test" # Step 2: Cold build (no cache) let cold_results = if not $skip_cold { print "→ Running COLD build (no cache)..." print " This measures full build time including dependencies" let cold_times = (1..$iterations | each {|i| print $" Iteration ($i)/($iterations)..." # Clear Docker build cache docker builder prune --all --force | complete | ignore let start = (date now) let result = ( docker buildx build --file $dockerfile_path --tag $"($image_tag)-cold" --no-cache --progress plain $build_context | complete ) let end = (date now) let duration = (($end - $start) | into int) / 1_000_000_000 if $result.exit_code != 0 { error make { msg: $"Cold build failed: ($result.stderr)" } } print $" ✓ Completed in ($duration)s" $duration }) let avg = ($cold_times | math avg) let min = ($cold_times | math min) let max = ($cold_times | math max) print "" print $" Average: ($avg)s, Min: ($min)s, Max: ($max)s" print "" { avg: $avg, min: $min, max: $max, times: $cold_times } } else { print "→ Skipping COLD build" print "" null } # Step 3: Warm build (with dependency cache) print "→ Running WARM build (with cargo-chef cache)..." print " This measures build time with cached dependencies" let warm_times = (1..$iterations | each {|i| print $" Iteration ($i)/($iterations)..." let start = (date now) let result = ( docker buildx build --file $dockerfile_path --tag $"($image_tag)-warm" --cache-from $"type=registry,ref=($registry)/cache:($service)" --cache-to $"type=registry,ref=($registry)/cache:($service),mode=max" --progress plain $build_context | complete ) let end = (date now) let duration = (($end - $start) | into int) / 1_000_000_000 if $result.exit_code != 0 { error make { msg: $"Warm build failed: ($result.stderr)" } } print $" ✓ Completed in ($duration)s" $duration }) let warm_avg = ($warm_times | math avg) let warm_min = ($warm_times | math min) let warm_max = ($warm_times | math max) print "" print $" Average: ($warm_avg)s, Min: ($warm_min)s, Max: ($warm_max)s" print "" # Step 4: Get image sizes print "→ Measuring image sizes..." let size_warm = ( docker images $"($image_tag)-warm" --format "{{.Size}}" | str trim ) print $" Warm build image: ($size_warm)" print "" # Step 5: Calculate savings let results = if not $skip_cold { let cache_savings = (($cold_results.avg - $warm_avg) / $cold_results.avg * 100) print "Summary:" print "========" print $" Cold build (no cache): ($cold_results.avg)s" print $" Warm build (dep cache): ($warm_avg)s" print $" Cache savings: ($cache_savings | into int)%" print $" Image size: ($size_warm)" print "" { service: $service, cold: $cold_results, warm: { avg: $warm_avg, min: $warm_min, max: $warm_max, times: $warm_times }, cache_savings_percent: $cache_savings, image_size: $size_warm } } else { print "Summary:" print "========" print $" Warm build (dep cache): ($warm_avg)s" print $" Image size: ($size_warm)" print "" { service: $service, cold: null, warm: { avg: $warm_avg, min: $warm_min, max: $warm_max, times: $warm_times }, cache_savings_percent: null, image_size: $size_warm } } $results } # Quick validation (warm build only, 1 iteration) export def "main quick" [ service: string ]: nothing -> record { main $service --iterations 1 --skip-cold } # Full benchmark (cold + warm, 3 iterations) export def "main full" [ service: string ]: nothing -> record { main $service --iterations 3 }