284 lines
10 KiB
Makefile
284 lines
10 KiB
Makefile
# Justfile for cc-libs
|
|
# Run `just ci` for full local verification.
|
|
|
|
# List available recipes.
|
|
default:
|
|
@just --list
|
|
|
|
# Install local development tooling.
|
|
install: install-git-hooks check-install generate-env
|
|
|
|
# Remove generated local environment files.
|
|
clean:
|
|
rm -f .env
|
|
|
|
# Recreate local generated files and development tooling.
|
|
reinstall: clean install
|
|
|
|
# Install Git hooks for this repository.
|
|
install-git-hooks:
|
|
@mkdir -p .git/hooks
|
|
@printf '%s\n' '#!/bin/sh' '' 'just check test' > .git/hooks/pre-commit
|
|
@chmod +x .git/hooks/pre-commit
|
|
@printf '%s\n' 'Installed .git/hooks/pre-commit'
|
|
@printf '%s\n' '#!/bin/sh' '' 'just ci' > .git/hooks/pre-push
|
|
@chmod +x .git/hooks/pre-push
|
|
@printf '%s\n' 'Installed .git/hooks/pre-push'
|
|
|
|
# Verify the CraftOS-PC harness is installed and recent enough.
|
|
check-craftos:
|
|
@command -v craftos >/dev/null 2>&1 || { \
|
|
printf '%s\n' 'craftos not found on $PATH. See docs/install-craftos-pc.md.' >&2; \
|
|
exit 1; \
|
|
}
|
|
@version="$(craftos --version)"; \
|
|
number="${version##* v}"; \
|
|
case "$number" in \
|
|
*.*.*) \
|
|
;; \
|
|
*) \
|
|
printf '%s\n' "$version"; \
|
|
printf '%s\n' 'Could not parse CraftOS-PC version. See docs/install-craftos-pc.md.' >&2; \
|
|
exit 1; \
|
|
;; \
|
|
esac; \
|
|
major="${number%%.*}"; \
|
|
rest="${number#*.}"; \
|
|
minor="${rest%%.*}"; \
|
|
patch="${rest#*.}"; \
|
|
patch="${patch%%[^0-9]*}"; \
|
|
printf '%s\n' "$version"; \
|
|
case "$major.$minor.$patch" in \
|
|
*[!0-9.]*|.*|*..*|*.) \
|
|
printf '%s\n' 'Could not parse CraftOS-PC version. See docs/install-craftos-pc.md.' >&2; \
|
|
exit 1; \
|
|
;; \
|
|
esac; \
|
|
if ! { [ "${major:-0}" -gt 2 ] || \
|
|
{ [ "${major:-0}" -eq 2 ] && [ "${minor:-0}" -gt 8 ]; } || \
|
|
{ [ "${major:-0}" -eq 2 ] && [ "${minor:-0}" -eq 8 ] && [ "${patch:-0}" -ge 3 ]; }; }; then \
|
|
printf '%s\n' 'CraftOS-PC v2.8.3 or newer is required. See docs/install-craftos-pc.md.' >&2; \
|
|
exit 1; \
|
|
fi
|
|
|
|
# Verify jq is installed.
|
|
check-jq:
|
|
@command -v jq >/dev/null 2>&1 || { \
|
|
printf '%s\n' 'jq not found on $PATH. See DEVELOPMENT.md.' >&2; \
|
|
exit 1; \
|
|
}
|
|
|
|
# Verify luacheck is installed.
|
|
check-luacheck:
|
|
@command -v luacheck >/dev/null 2>&1 || { \
|
|
printf '%s\n' 'luacheck not found on $PATH. See DEVELOPMENT.md.' >&2; \
|
|
exit 1; \
|
|
}
|
|
|
|
# Verify openssl is installed.
|
|
check-openssl:
|
|
@command -v openssl >/dev/null 2>&1 || { \
|
|
printf '%s\n' 'openssl not found on $PATH. See DEVELOPMENT.md.' >&2; \
|
|
exit 1; \
|
|
}
|
|
|
|
# Verify tools needed for local installation and CraftOS-PC launch recipes.
|
|
check-install: check-craftos check-jq check-luacheck check-openssl
|
|
|
|
# Generate local secrets on first install.
|
|
generate-env:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [ -f .env ]; then
|
|
exit 0
|
|
fi
|
|
password="$(openssl rand -hex 32)"
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
case "$line" in
|
|
TRAP_CCLIBS_OPENCODE_PASSWORD=*)
|
|
printf '%s\n' "TRAP_CCLIBS_OPENCODE_PASSWORD=$password"
|
|
;;
|
|
*)
|
|
printf '%s\n' "$line"
|
|
;;
|
|
esac
|
|
done < .env.test > .env
|
|
printf '%s\n' 'Generated .env'
|
|
|
|
# Pass args through to `craftos`, for example:
|
|
# just craftos --headless --exec 'print("__TRAPOS_TEST_OK__"); os.shutdown()'
|
|
# Launch CraftOS-PC with repo-local data and read-only repo mounts.
|
|
[positional-arguments]
|
|
craftos *args: check-install
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
repo='{{justfile_directory()}}'
|
|
argv=(--directory "$repo/.craftos")
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
argv+=(--rom /Applications/CraftOS-PC.app/Contents/Resources)
|
|
fi
|
|
argv+=(--mount-ro "/trapos=$repo")
|
|
for dir in apis programs servers startup tests; do
|
|
if [ -d "$repo/$dir" ]; then
|
|
argv+=(--mount-ro "/$dir=$repo/$dir")
|
|
fi
|
|
done
|
|
exec craftos "${argv[@]}" "$@"
|
|
|
|
# Human-only interactive REPL. LLM agents must not execute this command.
|
|
repl:
|
|
@just craftos --cli
|
|
|
|
# Local CI entry point used by Git hooks. Pass args through to `test`.
|
|
ci *args: check-craftos check
|
|
@just test {{args}}
|
|
@just test-timeout
|
|
|
|
# Run CraftOS-PC headless integration tests. Pass `--pretty` for grouped output.
|
|
[positional-arguments]
|
|
test *args:
|
|
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
if [ -f .env.test ]; then set -a; . ./.env.test; set +a; fi
|
|
|
|
pretty=0
|
|
has_output=0
|
|
timeout_seconds="${TRAP_CCLIBS_TEST_TIMEOUT_SECONDS:-3}"
|
|
case "$timeout_seconds" in ''|*[!0-9]*) printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be a positive integer' >&2; exit 1 ;; esac
|
|
if [ "$timeout_seconds" -lt 1 ]; then printf '%s\n' 'TRAP_CCLIBS_TEST_TIMEOUT_SECONDS must be >= 1' >&2; exit 1; fi
|
|
|
|
runtest_args=("$@")
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--pretty|--verbose|-v) pretty=1 ;;
|
|
--output) has_output=1 ;;
|
|
esac
|
|
done
|
|
if [ "$pretty" -eq 1 ] && [ "$has_output" -eq 0 ]; then
|
|
runtest_args+=(--output /trapos-test-output)
|
|
fi
|
|
runtest_args+=(--shutdown)
|
|
|
|
lua_quote() {
|
|
local value="$1"
|
|
value="${value//\\/\\\\}"
|
|
value="${value//\'/\\\'}"
|
|
printf "'%s'" "$value"
|
|
}
|
|
|
|
exec_code="shell.run('/programs/runtest.lua'"
|
|
for arg in "${runtest_args[@]}"; do
|
|
exec_code+=", $(lua_quote "$arg")"
|
|
done
|
|
exec_code+=")"
|
|
|
|
rom_arg=()
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
rom_arg=(--rom /Applications/CraftOS-PC.app/Contents/Resources)
|
|
fi
|
|
repo='{{justfile_directory()}}'
|
|
mount_arg=(--mount-ro "/trapos=$repo" --mount-ro "/apis=$repo/apis" --mount-ro "/programs=$repo/programs" --mount-ro "/startup=$repo/startup" --mount-ro "/tests=$repo/tests")
|
|
tmp="$(mktemp)"
|
|
data_dir="$(mktemp -d)"
|
|
output_path="$data_dir/computer/0/trapos-test-output"
|
|
|
|
craftos --directory "$data_dir" --headless "${rom_arg[@]}" "${mount_arg[@]}" --exec "$exec_code" >"$tmp" 2>&1 &
|
|
pid="$!"
|
|
( sleep "$timeout_seconds"; kill -TERM "$pid" >/dev/null 2>&1 ) &
|
|
watchdog="$!"
|
|
wait "$pid" >/dev/null 2>&1
|
|
status="$?"
|
|
kill "$watchdog" >/dev/null 2>&1 || true
|
|
wait "$watchdog" >/dev/null 2>&1 || true
|
|
if grep -q __TRAPOS_TEST_OK__ "$tmp"; then
|
|
if [ "$pretty" -eq 1 ] && [ -f "$output_path" ]; then cat "$output_path"; fi
|
|
rm -f "$tmp"
|
|
rm -rf "$data_dir"
|
|
else
|
|
red=$(printf '\033[31m'); reset=$(printf '\033[0m')
|
|
if [ "$status" -eq 143 ]; then
|
|
printf '%s\n' "${red}FAIL${reset} CraftOS integration tests timed out after ${timeout_seconds}s" >&2
|
|
else
|
|
printf '%s\n' "${red}FAIL${reset} CraftOS integration tests did not print __TRAPOS_TEST_OK__" >&2
|
|
fi
|
|
if [ -f "$output_path" ]; then cat "$output_path" >&2; fi
|
|
cat "$tmp" >&2
|
|
rm -f "$tmp"
|
|
rm -rf "$data_dir"
|
|
exit 1
|
|
fi
|
|
printf '%s\n' 'OK: CraftOS integration tests passed'
|
|
|
|
# Harness self-test: run a tests/harness fixture and assert which timeout layer
|
|
# caught it. `expect` is "lua" (libtest cancels the case) or "shell" (the shell
|
|
# watchdog kills the whole process). Not part of `ci`/`test`: these exercise the
|
|
# failure paths on purpose.
|
|
_timeout-fixture script shell_timeout extra_flag expect: check-install
|
|
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
repo='{{justfile_directory()}}'
|
|
if [ -f "$repo/.env.test" ]; then set -a; . "$repo/.env.test"; set +a; fi
|
|
rom_arg=""
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
rom_arg="--rom /Applications/CraftOS-PC.app/Contents/Resources"
|
|
fi
|
|
mount_arg="--mount-ro /apis=$repo/apis --mount-ro /programs=$repo/programs --mount-ro /tests=$repo/tests"
|
|
tmp="$(mktemp)"
|
|
data_dir="$(mktemp -d)"
|
|
output_path="$data_dir/computer/0/trapos-test-output"
|
|
exec_code="shell.run('/programs/runtest.lua', '{{script}}', '--verbose', '--output', '/trapos-test-output', {{extra_flag}} '--shutdown')"
|
|
shell_timeout="{{shell_timeout}}"
|
|
craftos --directory "$data_dir" --headless $rom_arg $mount_arg --exec "$exec_code" >"$tmp" 2>&1 &
|
|
pid="$!"
|
|
( sleep "$shell_timeout"; kill -TERM "$pid" >/dev/null 2>&1 ) &
|
|
watchdog="$!"
|
|
wait "$pid" >/dev/null 2>&1
|
|
status="$?"
|
|
kill "$watchdog" >/dev/null 2>&1 || true
|
|
wait "$watchdog" >/dev/null 2>&1 || true
|
|
combined="$(cat "$tmp"; [ -f "$output_path" ] && cat "$output_path")"
|
|
red=$(printf '\033[31m'); green=$(printf '\033[32m'); reset=$(printf '\033[0m')
|
|
rc=0
|
|
case "{{expect}}" in
|
|
lua)
|
|
if printf '%s\n' "$combined" | grep -q 'libtest timeout' && [ "$status" -ne 143 ]; then
|
|
printf '%s\n' "${green}OK${reset} libtest cancelled the case (shell watchdog not needed)"; \
|
|
else
|
|
printf '%s\n' "${red}FAIL${reset} expected a libtest timeout before the shell watchdog (status=$status)" >&2
|
|
rc=1
|
|
fi
|
|
;;
|
|
shell)
|
|
if [ "$status" -eq 143 ]; then
|
|
printf '%s\n' "${green}OK${reset} shell watchdog killed the run after ${shell_timeout}s (status 143; libtest timeout bypassed)"; \
|
|
else
|
|
printf '%s\n' "${red}FAIL${reset} expected the shell watchdog to kill the run (status=$status)" >&2
|
|
rc=1
|
|
fi
|
|
;;
|
|
*)
|
|
printf '%s\n' "${red}FAIL${reset} unknown expectation '{{expect}}'" >&2
|
|
rc=1
|
|
;;
|
|
esac
|
|
if [ "$rc" -ne 0 ]; then printf '%s\n' "$combined" >&2; fi
|
|
rm -f "$tmp"
|
|
rm -rf "$data_dir"
|
|
exit "$rc"
|
|
|
|
# Prove the libtest (Lua) timeout layer: libtest cancels the slow case quickly,
|
|
# before the shell watchdog backstop can fire.
|
|
test-timeout-lua: (_timeout-fixture "/tests/harness/slow-case.lua" "${TRAP_CCLIBS_TEST_TIMEOUT_WATCHDOG_SECONDS:-1}" "'--timeout', '0'," "lua")
|
|
|
|
# Prove the shell watchdog backstop: the slow case runs with the libtest timeout
|
|
# bypassed (--no-timeout), so the shell watchdog kills the whole process.
|
|
test-timeout-shell: (_timeout-fixture "/tests/harness/slow-case.lua" "${TRAP_CCLIBS_TEST_TIMEOUT_WATCHDOG_SECONDS:-1}" "'--no-timeout'," "shell")
|
|
|
|
# Fast regression guard for both timeout layers. Wired into `ci`.
|
|
test-timeout: test-timeout-lua test-timeout-shell
|
|
|
|
# Lint all Lua source with luacheck.
|
|
check: check-luacheck
|
|
luacheck --quiet .
|