#!/usr/bin/env nu # Local test helper for lian-build/Dockerfile. # Assembles the build context (rsync project + rustelo + stratumiops), # then runs docker build --target for incremental verification. # # Build ARGs are read from build_directives.ncl (via nickel export) and passed # to docker build. Individual flags override NCL values when explicitly provided. # # Usage: # nu lian-build/ctx-test.nu # prepare ctx only # nu lian-build/ctx-test.nu --stage planner # verify crates/ layout # nu lian-build/ctx-test.nu --stage css # verify unocss pipeline # nu lian-build/ctx-test.nu --stage builder-deps # verify dep cook (slow) # nu lian-build/ctx-test.nu --stage builder # full Rust build (very slow) # nu lian-build/ctx-test.nu --run # build final image + smoke test # # ARG overrides (bypass NCL, force a specific value): # nu lian-build/ctx-test.nu --stage builder --bin-features "content-static,auth" # nu lian-build/ctx-test.nu --run --lamina-registry myregistry.ontoref.dev/lamina # Read artifacts[0].build_args from build_directives.ncl. # Returns a record of { ARG_NAME: string } or {} on any failure. build_directives # now exports one directive per render profile, so the profile field is selected # before reading artifacts[0].build_args. def read_build_args [script_dir: string, profile: string] { let directives = ($script_dir | path join "build_directives.ncl") if not ($directives | path exists) { return {} } if (which nickel | is-empty) { return {} } let lian_root = if "LIAN_BUILD_ROOT" in $env { $env.LIAN_BUILD_ROOT } else { $script_dir | path join ".." ".." "lian-build" | path expand } if not ($lian_root | path exists) { return {} } let res = do { ^nickel export --import-path $lian_root $directives } | complete if $res.exit_code != 0 { print $"warning: nickel eval failed — ($res.stderr | str trim)" return {} } $res.stdout | from json | get -i $profile | default {} | get -i artifacts | default [] | first | default {} | get -i build_args | default {} } # Render profile, auto-detected from rendering.ncl (same source as build-auto). def detect_profile [project_root: string]: nothing -> string { let dev_root = ($env.DEV_ROOT? | default "/Users/Akasha/Development") let detector = $"($dev_root)/rustelo/scripts/check-wasm-needed.nu" if ($detector | path exists) { let res = (do { cd $project_root ^nu $detector --routes-dir site/config --workspace-config site/config/rendering.ncl } | complete) match $res.exit_code { 0 => "leptos-hydration", 1 => "htmx-ssr", _ => "leptos-hydration" } } else { let ncl = $"($project_root)/site/config/rendering.ncl" if ($ncl | path exists) { open $ncl | parse --regex 'default_profile = "(?P

[^"]+)"' | get p?.0? | default "leptos-hydration" } else { "leptos-hydration" } } } # Resolve a single ARG value: CLI flag wins if non-empty, else NCL, else fallback. def resolve [flag: string, ncl: record, key: string, fallback: string] { if not ($flag | is-empty) { $flag } else { $ncl | get -i $key | default $fallback } } # Verify required lamina registry images exist before starting the build. # Uses `docker manifest inspect` (metadata only, no layer download). # Exits with a clear error listing the lamina commands needed to fix missing layers. def preflight_lamina [registry: string, profile: string, rust_version: string, node_version: string] { # Disjoint layer requirements per profile: leptos-hydration needs the wasm # toolchain (lamina/leptos) + node (css stage); htmx-ssr builds on the thin # lamina/rust base with no wasm and no node/css stage. let required = if $profile == "htmx-ssr" { [ { image: $"($registry)/nickel:latest", fix: "just build nickel" }, { image: $"($registry)/rust:($rust_version)", fix: "just build rust" }, ] } else { [ { image: $"($registry)/nickel:latest", fix: "just build nickel" }, { image: $"($registry)/leptos:($rust_version)", fix: "just build wasm && just build leptos" }, { image: $"($registry)/node:($node_version)", fix: "just build node" }, ] } let missing = $required | filter { |r| let res = do { ^docker manifest inspect $r.image } | complete $res.exit_code != 0 } if ($missing | is-empty) { return } print "\nerror: required lamina layers not available in registry:" for m in $missing { print $" ($m.image)" print $" → run in lamina/: ($m.fix)" } error make { msg: "build blocked: lamina layers missing — see above" } } def main [ --stage: string = "" --run --keep --profile: string = "" # leptos-hydration | htmx-ssr (default: auto-detect) --platform: string = "linux/arm64" --rust-version: string = "" --node-version: string = "" --lamina-registry: string = "" --bin-name: string = "" --leptos-output-name: string = "" --bin-features: string = "" --server-port: string = "" ] { let script_dir = $env.FILE_PWD let project_root = ($script_dir | path join "..") | path expand let framework_root = ($project_root | path join ".." "rustelo") | path expand let stratumiops_root = ($project_root | path join ".." "stratumiops") | path expand let profile = if ($profile | is-not-empty) { $profile } else { detect_profile $project_root } if $profile not-in ["leptos-hydration" "htmx-ssr"] { error make { msg: $"invalid --profile '($profile)' (leptos-hydration | htmx-ssr)" } } let is_htmx = ($profile == "htmx-ssr") let ncl = read_build_args $script_dir $profile let default_features = if $is_htmx { "htmx-ssr" } else { "content-static" } let rust_version = resolve $rust_version $ncl "RUST_VERSION" "1.89" let nickel_version = resolve "" $ncl "NICKEL_VERSION" "1.16.0" let node_version = resolve $node_version $ncl "NODE_VERSION" "22" let lamina_registry = resolve $lamina_registry $ncl "LAMINA_REGISTRY" "registry.ontoref.dev/lamina" let bin_name = resolve $bin_name $ncl "BIN_NAME" "rustelo-htmx-server" let leptos_output_name = resolve $leptos_output_name $ncl "LEPTOS_OUTPUT_NAME" "website" let bin_features = resolve $bin_features $ncl "BIN_FEATURES" $default_features let server_port = resolve $server_port $ncl "SERVER_PORT" "3000" if not ($framework_root | path exists) { error make { msg: $"rustelo not found at ($framework_root)" } } # Pre-flight: fail fast with actionable message before any rsync or build. preflight_lamina $lamina_registry $profile $rust_version $node_version let ctx = (^mktemp -d | str trim) print $"profile: ($profile)" print $"ctx: ($ctx)" print $"project: ($project_root)" print $"framework: ($framework_root)" print $"lamina: ($lamina_registry)" print $"build_args: RUST_VERSION=($rust_version) BIN_FEATURES=($bin_features)" # Sync project into context root. print "syncing project..." ^rsync -a --delete ...[ "--exclude=.git/" "--exclude=target/" "--exclude=node_modules/" "--exclude=.coder/" "--exclude=works-pv/" "--exclude=lian-build/ctx-*" $"($project_root)/" $"($ctx)/" ] # Sync rustelo as ctx/rustelo/ — Dockerfile uses COPY rustelo/ (primary context). print "syncing rustelo..." ^rsync -a --delete ...[ "--exclude=.git/" "--exclude=target/" $"($framework_root)/" $"($ctx)/rustelo/" ] # Sync stratumiops if present — may be a transitive path dep of rustelo. if ($stratumiops_root | path exists) { print "syncing stratumiops..." ^rsync -a --delete ...[ "--exclude=.git/" "--exclude=target/" $"($stratumiops_root)/" $"($ctx)/stratumiops/" ] } print $"context ready: ($ctx)" # Dockerfile suffix === profile name (no short forms). let dockerfile = ($project_root | path join "lian-build" $"Dockerfile.($profile)") # Common ARGs both Dockerfiles declare. NODE_VERSION and LEPTOS_OUTPUT_NAME # only exist in Dockerfile.leptos (no node/css stage or WASM output in htmx). let common_args = [ "--build-arg" $"RUST_VERSION=($rust_version)" "--build-arg" $"NICKEL_VERSION=($nickel_version)" "--build-arg" $"LAMINA_REGISTRY=($lamina_registry)" "--build-arg" $"BIN_NAME=($bin_name)" "--build-arg" $"BIN_FEATURES=($bin_features)" "--build-arg" $"SERVER_PORT=($server_port)" ] let build_args = if $is_htmx { $common_args } else { $common_args ++ [ "--build-arg" $"NODE_VERSION=($node_version)" "--build-arg" $"LEPTOS_OUTPUT_NAME=($leptos_output_name)" ] } if $run { let tag = "website:local-test" print $"\nbuilding final image: ($tag)" ^docker build ...([ "--platform" $platform "-t" $tag "-f" $dockerfile ] ++ $build_args ++ [$ctx]) if not $keep { ^rm -rf $ctx } let content_vol = $"($project_root)/site/content:/var/www/site/content:ro" let i18n_vol = $"($project_root)/site/i18n:/var/www/site/i18n:ro" print "\nsmoke test — ctrl-c to stop" print $"health: http://localhost:($server_port)/health" ^docker run --rm -p $"($server_port):($server_port)" -v $content_vol -v $i18n_vol $tag return } if ($stage | is-empty) { print $"\nctx ready at: ($ctx)" print "pass --stage to build a stage, or --run for the full image" if not $keep { ^rm -rf $ctx } return } print $"\nbuilding stage: ($stage)" ^docker build ...([ "--platform" $platform "--target" $stage "-t" $"jpl-($stage):test" "-f" $dockerfile ] ++ $build_args ++ [$ctx]) if $keep { print $"ctx preserved: ($ctx)" } else { ^rm -rf $ctx } print $"\nimage: jpl-($stage):test" }