#!/usr/bin/env bash # scripts/build_frontends.sh — compile the ten grammar frontends on # b3u.dev and install them where the pipeline expects. # # The decomposer stage reads the Python source through one compiled # frontend per grammar family. Those are built artifacts, so a fresh # checkout has none — and building them locally needs a compiler # toolchain the service exists to spare you. # # This uploads each `.sgr`, compiles it on the service, downloads the # frontend, and installs the executable as sgdl//bin/ccsf. # Ten upload-compile-download cycles, one command. # # scripts/build_frontends.sh [family ...] default: all ten # scripts/build_frontends.sh --local [family ...] build with a local # cppcc instead of the # service ($CPPCCHOME) # # ⚠ --local exists because the service can be unreachable for reasons no # password fixes -- a private host in front of it, an outage, no network, # or simply no subscription. Everything --local needs already shipped in # this archive -- each family carries its own build/makeall -- so if you # have a licensed cppcc, you are not blocked on the service. # # Environment: # B3U_BASE, B3U_EMAIL, B3U_PASSWORD required # B3U_DOWNLOADS default ~/Downloads # CF_ACCESS_CLIENT_ID/-_SECRET optional gateway headers # # Exit codes: # 0 every requested family installed # 1 one or more failed # 2 setup missing set -u HERE="$(cd "$(dirname "$0")/.." && pwd)" cd "$HERE" LOCAL=0 if [ "${1:-}" = "--local" ]; then LOCAL=1; shift; fi fails=0 pass() { echo " PASS $1"; } fail() { echo " FAIL $1"; fails=$((fails + 1)); } families=("$@") if [ "${#families[@]}" -eq 0 ]; then families=($(ls -d sgdl/F*/ 2>/dev/null | xargs -n1 basename)) fi # ---- local path: compile with cppcc, no service, no credentials ------- if [ "$LOCAL" = 1 ]; then : "${CPPCCHOME:?--local needs CPPCCHOME pointing at a cppcc installation}" [ -x "$CPPCCHOME/bin/cppcc" ] \ || { echo "no cppcc binary at $CPPCCHOME/bin/cppcc" >&2; exit 2; } echo "== local build via $CPPCCHOME/bin/cppcc ==" for fam in "${families[@]}"; do n="${fam%%_*}"; n="${n#F}"; want="sgdl/$fam/bin/ccsf$n" [ -f "sgdl/$fam/build/makeall" ] \ || { fail "$fam -- no build/makeall in this archive"; continue; } if out=$( cd "sgdl/$fam/build" && timeout 900 bash makeall 2>&1 ); then [ -x "$want" ] && pass "$fam -> $want" \ || { fail "$fam -- makeall succeeded but $want is missing" echo "$out" | tail -6 | sed 's/^/ /'; } else fail "$fam -- local build failed" echo "$out" | tail -8 | sed 's/^/ /' fi done echo if [ "$fails" -eq 0 ]; then echo "BUILD FRONTENDS (local): ALL PASS" echo " now run: scripts/convert_and_test.sh" exit 0 fi echo "BUILD FRONTENDS (local): $fails FAILURE(S)"; exit 1 fi # ---- service path ----------------------------------------------------- : "${B3U_BASE:?set B3U_BASE (e.g. https://b3u.dev)}" : "${B3U_EMAIL:?set B3U_EMAIL}" : "${B3U_PASSWORD:?set B3U_PASSWORD}" DOWNLOADS="${B3U_DOWNLOADS:-$HOME/Downloads}" mkdir -p "$DOWNLOADS" CURL=(curl -s) if [ -n "${CF_ACCESS_CLIENT_ID:-}" ]; then CURL+=(-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" -H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET:?}") fi command -v unzip >/dev/null || { echo "unzip required" >&2; exit 2; } echo "== login ==" WALL_ALTERNATIVES=" scripts/convert_and_test.sh --quick needs no frontends and still runs the full 293-assertion suite. scripts/build_frontends.sh --local builds the ten frontends with a local cppcc, if you have one licensed. Set CPPCCHOME; everything else already shipped in this archive." # ⚠ THE LOGIN MUST DIAGNOSE, NOT TRACEBACK. This piped curl straight # into json.load, so ANY non-JSON reply -- a Cloudflare Access redirect, # a 502, a captive portal -- surfaced as a raw Python JSONDecodeError # followed by "check B3U_EMAIL/B3U_PASSWORD", which was both wrong and # unactionable. Found by a user whose credentials were perfectly fine # and who was told to check them anyway. _lb=$(mktemp); _lh=$(mktemp) code=$("${CURL[@]}" -o "$_lb" -D "$_lh" -w '%{http_code}' \ -X POST "$B3U_BASE/login" -H 'Content-Type: application/json' \ -d "{\"email\":\"$B3U_EMAIL\",\"password\":\"$B3U_PASSWORD\"}") token=$(python3 -c 'import json,sys try: print(json.load(open(sys.argv[1])).get("session_token","")) except Exception: print("")' "$_lb") if [ -n "$token" ]; then pass "authenticated" else if grep -qi 'cloudflareaccess\.com' "$_lh" "$_lb" 2>/dev/null; then fail "login -- $B3U_BASE is behind a Cloudflare Access wall (HTTP $code)" echo " The wall answers BEFORE the application does, so no" echo " email or password can reach it. Your credentials are" echo " not the problem." echo echo " b3u.dev itself is open -- so a wall in front of it means" echo " B3U_BASE is pointed at a PRIVATE host (a staging or" echo " preview hostname), not at the public service. Check" echo " B3U_BASE; it should be https://b3u.dev. What works" echo " without the service at all is below." if [ -n "${WALL_ALTERNATIVES:-}" ]; then echo "$WALL_ALTERNATIVES" fi # (CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET are honoured above # if you hold a service token for a private host. Reaching the # public service needs no token.) elif [ "$code" = 401 ] || [ "$code" = 403 ]; then fail "login -- rejected (HTTP $code). Check B3U_EMAIL / B3U_PASSWORD." elif [ "$code" = 000 ]; then fail "login -- could not reach $B3U_BASE at all (DNS, TLS or network)." else fail "login -- no session_token in the reply (HTTP $code)" echo " first bytes: $(head -c 160 "$_lb" | tr -d '\r\n')" fi rm -f "$_lb" "$_lh"; exit 1 fi rm -f "$_lb" "$_lh" AUTH=(-H "Authorization: Bearer $token") for fam in "${families[@]}"; do sgr="sgdl/$fam/build/$fam.sgr" [ -f "$sgr" ] || { fail "$fam — no grammar at $sgr"; continue; } # The frontend's name comes from the GRAMMAR's first identifier # ("F7"), not from the unit; the pipeline expects it lowercased # ("ccsf7"), which is the local build convention. Rename on install # rather than renaming the grammar — the grammar is the artifact # readers compile, and it should stay as published. gid=$(grep -m1 -oE '^\(\w+' "$sgr" | tr -d '(') unit=$(echo "$gid" | tr 'A-Z' 'a-z') want="sgdl/$fam/bin/ccs$unit" echo "== $fam ($gid -> $want) ==" code=$("${CURL[@]}" "${AUTH[@]}" -o "$DOWNLOADS/$unit-bundles.zip" \ -w '%{http_code}' -F "file=@$sgr" "$B3U_BASE/units/$unit/generate") [ "$code" = 200 ] || { fail "$fam — compile returned $code"; continue; } code=$("${CURL[@]}" "${AUTH[@]}" -o "$DOWNLOADS/$unit-frontend.zip" \ -w '%{http_code}' "$B3U_BASE/units/$unit/frontend") [ "$code" = 200 ] || { fail "$fam — frontend download returned $code"; continue; } work=$(mktemp -d) ( cd "$work" && unzip -oq "$DOWNLOADS/$unit-frontend.zip" ) # the executable is the one non-text member; find it by mode exe=$(find "$work" -maxdepth 1 -type f -perm -u+x | head -1) if [ -z "$exe" ]; then exe=$(find "$work" -maxdepth 1 -type f -name "ccs*" | head -1) fi if [ -z "$exe" ]; then fail "$fam — no executable in the frontend zip"; rm -rf "$work"; continue fi mkdir -p "sgdl/$fam/bin" install -m 755 "$exe" "$want" rm -rf "$work" [ -x "$want" ] && pass "$fam installed" || fail "$fam — install failed" done echo if [ "$fails" -eq 0 ]; then echo "BUILD FRONTENDS: ALL PASS" echo " now run: scripts/convert_and_test.sh" exit 0 else echo "BUILD FRONTENDS: $fails FAILURE(S)" exit 1 fi