252 lines
10 KiB
Text
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"
|
|
}
|