#!/bin/sh # ============================================================================= # ZDDC shared build helpers — sourced by each tool's build.sh # # Usage in a tool build.sh: # root_dir=$(cd "$(dirname "$0")" && pwd) # . "$root_dir/../shared/build-lib.sh" # # Provides: # ensure_exists — abort with error if file missing # concat_files — cat each relative path under $root_dir # build_timestamp — ISO UTC timestamp string, set at source time; # used as build_label for dev builds # escape_js_close_tags # — copy to with all '. # The JS engine treats \/ as a regular slash, # so runtime behaviour is unchanged. # compute_build_label [--release []] # — sets globals: build_label, build_version, # is_release, is_red, channel. # See "Channels and release args" below. # promote_release — for stable / alpha / beta, copy the dist # HTML into website/releases/. Stable cuts # write the immutable per-version file + # refresh five symlinks (_v, _v, # _stable, _beta, _alpha) and tag # -v. Alpha/beta cuts # overwrite the channel mirror in place # and cascade alpha → beta. No git tags # for alpha/beta cuts; no Codeberg upload # for HTML tools. See ARCHITECTURE.md # "Channels" for the full table. # # Channels and release args: # dev build, dist/ only, label # "v-alpha · · [-dirty]" (red). # No website/releases/ side-effect. To publish, re-run # with `--release alpha`. # --release stable, auto-bump patch from latest tag (or 0.0.1). # Writes per-version file + symlinks; tags vX.Y.Z. # --release X.Y.Z stable, explicit version. # --release alpha alpha channel cut at HEAD; # label "v-alpha · · " (red). # Overwrites _alpha.html. No tag. # --release beta beta channel; label "v-beta · · ". # Overwrites _beta.html. Cascades _alpha.html # → _beta.html (symlink). No tag. # --release error. # ============================================================================= # Abort if root_dir is not set by the caller if [ -z "${root_dir:-}" ]; then echo "build-lib.sh: root_dir must be set before sourcing this file" >&2 exit 1 fi # NOTE: shared/publish-codeberg-release.sh is no longer sourced here. # HTML tools publish to website/releases/ as committed static files; only # zddc-server/release.sh uploads to Codeberg (it sources the helper directly). # Fail hard on any missing source file ensure_exists() { _path="$1" if [ ! -f "$_path" ]; then echo "error: missing file: $_path" >&2 exit 1 fi } # Concatenate files listed as positional args, each relative to root_dir concat_files() { for _rel do ensure_exists "$root_dir/$_rel" cat "$root_dir/$_rel" printf '\n' done } # ISO UTC build timestamp — set once when this file is sourced build_timestamp=$(date -u +"%Y-%m-%d %H:%M:%S") # Read shared/favicon.svg, base64-encode it, and assemble a data: URI suitable # for inlining into a tag. # Set once at source time so every tool's build.sh can pick it up via the # $favicon_data_uri global. Editing shared/favicon.svg auto-propagates to all # tools on the next build. _favicon_path="$root_dir/../shared/favicon.svg" if [ -f "$_favicon_path" ]; then favicon_data_uri="data:image/svg+xml;base64,$(base64 -w 0 "$_favicon_path")" else favicon_data_uri="" fi # Rewrite ' tag. Per the HTML5 spec # only block — other tags like are safe # inside a script's text content. Narrowly targeting "$2" } # Validate that $1 is a strict X.Y.Z numeric version, where each component # is a non-empty numeric string. Exits with an error if not. _validate_semver() { _v="$1" _bad() { echo "error: invalid release argument: '$_v' (expected: alpha, beta, or X.Y.Z stable version)" >&2 exit 1 } _v1="${_v%%.*}" _rest="${_v#*.}" [ "$_rest" = "$_v" ] && _bad _v2="${_rest%%.*}" _v3="${_rest#*.}" { [ "$_v3" = "$_rest" ] || [ "$_v3" != "${_v3%.*}" ]; } && _bad case "$_v1" in '' | *[!0-9]*) _bad ;; esac case "$_v2" in '' | *[!0-9]*) _bad ;; esac case "$_v3" in '' | *[!0-9]*) _bad ;; esac } # Compute build label and channel. Reads positional args: # compute_build_label [--release []] # Sets global variables: # build_label — text rendered into the page's {{BUILD_LABEL}} slot # build_version — bare semver string (stable releases only) # is_release — "1" for any --release invocation, else "0" # is_red — "1" if the label should render red+bold (dev/alpha/beta), else "0" # channel — "stable" / "alpha" / "beta" / "" (dev) # # Versioning: pre-release semver. The next-stable target is computed from # the latest clean tool-vX.Y.Z tag (patch-bump). Plain builds and # `--release alpha`/`--release beta` carry the next-stable target as a # pre-release suffix in the on-page label so users can see which stable # the alpha/beta is working toward. Stable releases write a clean # vX.Y.Z label and tag. # # HTML tools do NOT tag alpha/beta cuts (consistent with current # behavior — alpha and beta artifacts are mutable files, not immutable # per-build snapshots). The label distinguishes plain dev builds from # explicit channel cuts via the timestamp granularity (full ts + dirty # marker for plain builds vs. date-only for `--release alpha|beta`). compute_build_label() { _tool="$1" _flag="${2:-}" _arg="${3:-}" is_release=0 is_red=1 channel="" build_version="" # Compute the next-stable target once for label inclusion. _next_stable=$(_next_stable_for_tool "$_tool") if [ "$_flag" != "--release" ]; then # Plain builds are dev builds — labeled as the alpha channel because # that's what the next formal cut would produce, but no Codeberg upload # happens until `--release alpha` is invoked. Full timestamp (granular # than date) and -dirty marker distinguish iterative dev builds from # formal `--release alpha` cuts (which stamp date-only). _sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown") if ! git -C "$root_dir" diff --quiet HEAD 2>/dev/null; then _sha="${_sha}-dirty" fi channel="alpha" build_label="v${_next_stable}-alpha · ${build_timestamp} · ${_sha}" return 0 fi is_release=1 case "$_arg" in alpha | beta) channel="$_arg" _date=$(date -u +"%Y-%m-%d") _sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown") build_label="v${_next_stable}-${channel} · ${_date} · ${_sha}" return 0 ;; '') # Stable cut, auto-bump patch. build_version="$_next_stable" ;; *) _validate_semver "$_arg" build_version="$_arg" ;; esac channel="stable" is_red=0 build_label="v${build_version}" } # Compute the next-stable target version for a tool — i.e., the patch-bump # of the latest clean -vX.Y.Z tag. Used by compute_build_label to # embed the target version in alpha/beta labels. _next_stable_for_tool() { _t="$1" _latest=$(git -C "$root_dir" tag --list "${_t}-v*" 2>/dev/null \ | grep -E "^${_t}-v[0-9]+\.[0-9]+\.[0-9]+\$" \ | sed "s|^${_t}-v||" \ | sort -V \ | tail -1) [ -n "$_latest" ] || _latest="0.0.0" _major="${_latest%%.*}" _rest="${_latest#*.}" _minor="${_rest%%.*}" _patch="${_rest#*.}" echo "${_major}.${_minor}.$((_patch + 1))" } # Promote a built dist file to website/releases/. Reads from caller scope: # $channel ("stable" / "alpha" / "beta"), $build_version (stable only), # $output_html, $root_dir. # # Stable cuts: # 1. Skip if source unchanged since latest stable tag. # 2. Copy dist HTML → website/releases/_v.html (immutable). # 3. Refresh symlinks: _v, _v, _stable, _beta, _alpha all → the # new versioned file. Cascade rule: stable cut means beta and alpha # reset to stable (no active dev on either downstream channel). # 4. Tag the commit -v. # # Alpha/beta cuts: # 1. Overwrite website/releases/_.html with dist HTML # (replaces a symlink with real bytes if one was there). # 2. For beta: cascade _alpha.html → _beta.html (symlink), # since alpha defaults to beta when no active alpha. # 3. No tag — channel URLs are stable URLs by design; counters defeat # that. The on-page label encodes · for traceability. # # Plain dev builds (no --release): never call promote_release. # # No Codeberg upload — HTML tools live in git. zddc-server's release.sh # handles binary uploads to Codeberg directly (different distribution model). promote_release() { _tool="$1" _releases_dir="$root_dir/../website/releases" if [ ! -d "$_releases_dir" ]; then echo "promote_release: $_releases_dir not found" >&2 return 1 fi case "$channel" in stable) if [ -z "$build_version" ]; then echo "promote_release: stable channel but no build_version" >&2 return 1 fi _latest=$(git -C "$root_dir" tag --list "${_tool}-v*" 2>/dev/null \ | grep -E "^${_tool}-v[0-9]+\.[0-9]+\.[0-9]+\$" \ | sort -V | tail -1) if [ -n "$_latest" ] && git -C "$root_dir" diff --quiet "$_latest" HEAD -- . ../shared 2>/dev/null; then echo "${_tool}: no source changes since $_latest — skipping" return 0 fi _promote_stable "$_tool" "$build_version" "$_releases_dir" ;; alpha | beta) _promote_channel "$_tool" "$channel" "$_releases_dir" ;; *) echo "promote_release: unknown channel '$channel'" >&2 return 1 ;; esac } # Stable cut: per-version file + 5 symlinks + git tag. _promote_stable() { _t="$1" _ver="$2" _rdir="$3" _major="${_ver%%.*}" _rest="${_ver#*.}" _minor="${_rest%%.*}" _versioned="${_t}_v${_ver}.html" cp "$output_html" "$_rdir/$_versioned" echo "Wrote $_rdir/$_versioned" # Refresh the 5 symlinks. Cascade: stable cut → beta + alpha both # reset to stable (no active dev on either downstream channel). for _sym in "${_t}_v${_major}.${_minor}.html" \ "${_t}_v${_major}.html" \ "${_t}_stable.html" \ "${_t}_beta.html" \ "${_t}_alpha.html"; do ln -sfn "$_versioned" "$_rdir/$_sym" echo " $_sym → $_versioned" done _tag="${_t}-v${_ver}" if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then _existing=$(git -C "$root_dir" rev-list -n 1 "$_tag") _head=$(git -C "$root_dir" rev-parse HEAD) if [ "$_existing" != "$_head" ]; then echo "promote_release: tag $_tag exists at $_existing but HEAD is $_head" >&2 return 1 fi echo "(tag $_tag already at HEAD)" else git -C "$root_dir" tag "$_tag" echo "tagged $_tag" fi echo "Released ${_t} v${_ver} (stable)" echo " publish git tag with: git push origin $_tag" } # Alpha/beta cut: overwrite mutable channel mirror; cascade alpha → beta # on a beta cut (alpha defaults to beta when no active alpha). _promote_channel() { _t="$1" _ch="$2" _rdir="$3" _file="${_t}_${_ch}.html" # Replace symlink (if present) with real bytes by removing first; # cp -f follows symlinks and would overwrite the symlink target. rm -f "$_rdir/$_file" cp "$output_html" "$_rdir/$_file" echo "Wrote $_rdir/$_file" if [ "$_ch" = "beta" ]; then ln -sfn "$_file" "$_rdir/${_t}_alpha.html" echo " ${_t}_alpha.html → $_file" fi echo "Released ${_t} ${_ch}" }