website-htmx-rustelo-code/lian-build/ctx-test.nu
2026-07-10 03:44:13 +01:00

252 lines
10 KiB
Text

#!/usr/bin/env nu
# Local test helper for lian-build/Dockerfile.
# Assembles the build context (rsync project + rustelo + stratumiops),
# then runs docker build --target <stage> 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<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 <name> 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"
}