From 231301df904632a0ca95dd1620e85da67951db34 Mon Sep 17 00:00:00 2001 From: i Date: Fri, 2 Oct 2026 21:16:38 -0400 Subject: [PATCH 1/2] Add pinned Lua interpreter task --- .github/workflows/lua-interpreter.yml | 35 +++++++ sources/lua.md | 16 ++++ tasks/lua-interpreter/1.sh | 129 ++++++++++++++++++++++++++ tasks/lua-interpreter/README.md | 50 ++++++++++ tasks/lua-interpreter/install.sh | 4 + tests/lua-interpreter.sh | 69 ++++++++++++++ 6 files changed, 303 insertions(+) create mode 100644 .github/workflows/lua-interpreter.yml create mode 100644 sources/lua.md create mode 100755 tasks/lua-interpreter/1.sh create mode 100644 tasks/lua-interpreter/README.md create mode 100755 tasks/lua-interpreter/install.sh create mode 100755 tests/lua-interpreter.sh diff --git a/.github/workflows/lua-interpreter.yml b/.github/workflows/lua-interpreter.yml new file mode 100644 index 0000000..a3eacbc --- /dev/null +++ b/.github/workflows/lua-interpreter.yml @@ -0,0 +1,35 @@ +name: Lua interpreter + +on: + pull_request: + paths: + - "tasks/lua-interpreter/**" + - "tests/lua-interpreter.sh" + - "sources/lua.md" + - ".github/workflows/lua-interpreter.yml" + push: + branches: + - main + paths: + - "tasks/lua-interpreter/**" + - "tests/lua-interpreter.sh" + - "sources/lua.md" + - ".github/workflows/lua-interpreter.yml" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out exact Kitchen source + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Ensure build prerequisites + run: | + sudo apt-get update + sudo apt-get install -y build-essential curl ca-certificates + + - name: Build and test pinned Lua interpreter + run: sh tests/lua-interpreter.sh diff --git a/sources/lua.md b/sources/lua.md new file mode 100644 index 0000000..428ada8 --- /dev/null +++ b/sources/lua.md @@ -0,0 +1,16 @@ +# Lua source provenance + +Checked: 2026-10-02. + +Official source: https://www.lua.org/download.html + +Pinned Kitchen release: + +- version: Lua 5.5.1; +- archive: `https://www.lua.org/ftp/lua-5.5.1.tar.gz`; +- published SHA-256: + `1c4b4068d67061f2a2231ad2b5422e77acea1487ea9890f6320af614f4373dce`. + +Lua.org identifies 5.5.1 as the current Lua 5.5 release. Kitchen pins the exact +release and digest so a future upstream release does not silently change the +interpreter used by a prepared command. diff --git a/tasks/lua-interpreter/1.sh b/tasks/lua-interpreter/1.sh new file mode 100755 index 0000000..52b53bd --- /dev/null +++ b/tasks/lua-interpreter/1.sh @@ -0,0 +1,129 @@ +#!/bin/sh +set -eu + +fail() { + printf '%s\n' "$*" >&2 + exit 1 +} + +[ "$#" -eq 1 ] || fail 'usage: 1.sh ABSOLUTE_PREFIX' +prefix=$1 +case $prefix in + /*) ;; + *) fail 'ABSOLUTE_PREFIX must be an absolute path' ;; +esac + +version=5.5.1 +archive=lua-$version.tar.gz +url=https://www.lua.org/ftp/$archive +expected_sha256=1c4b4068d67061f2a2231ad2b5422e77acea1487ea9890f6320af614f4373dce + +for required in make cc tar cp chmod mv mkdir ln readlink rm awk basename; do + command -v "$required" >/dev/null 2>&1 || fail "$required is required" +done + +hash_file() { + file=$1 + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v sha256 >/dev/null 2>&1; then + sha256 -q "$file" + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + else + fail 'sha256sum, sha256, or shasum is required' + fi +} + +bin=$prefix/bin +lua_alias=$bin/lua +luac_alias=$bin/luac +lua_target=lua-$version +luac_target=luac-$version + +check_alias() { + alias_path=$1 + expected_target=$2 + if [ -L "$alias_path" ]; then + current_target=$(readlink "$alias_path") + [ "$current_target" = "$expected_target" ] || + fail "refusing to replace existing symlink: $alias_path -> $current_target" + elif [ -e "$alias_path" ]; then + fail "refusing to replace existing path: $alias_path" + fi +} + +check_alias "$lua_alias" "$lua_target" +check_alias "$luac_alias" "$luac_target" + +lua_versioned=$bin/$lua_target +luac_versioned=$bin/$luac_target + +if [ -e "$lua_versioned" ] || [ -L "$lua_versioned" ] || [ -e "$luac_versioned" ] || [ -L "$luac_versioned" ]; then + [ -f "$lua_versioned" ] && [ -x "$lua_versioned" ] || + fail "partial or invalid existing Lua install: $lua_versioned" + [ -f "$luac_versioned" ] && [ -x "$luac_versioned" ] || + fail "partial or invalid existing Lua install: $luac_versioned" + case $("$lua_versioned" -v 2>&1) in + 'Lua 5.5.1'*) ;; + *) fail "existing $lua_versioned is not Lua 5.5.1" ;; + esac + case $("$luac_versioned" -v 2>&1) in + 'Lua 5.5.1'*) ;; + *) fail "existing $luac_versioned is not Lua 5.5.1" ;; + esac + [ -L "$lua_alias" ] || ln -s "$lua_target" "$lua_alias" + [ -L "$luac_alias" ] || ln -s "$luac_target" "$luac_alias" + "$lua_alias" -e 'assert(_VERSION == "Lua 5.5")' + printf '%s\n' "$lua_alias" + exit 0 +fi + +work=${TMPDIR:-/tmp}/kitchen-lua-$version-$$ +trap 'rm -rf "$work"' EXIT HUP INT TERM +mkdir -p "$work" + +if [ -n "${KITCHEN_LUA_ARCHIVE:-}" ]; then + [ -f "$KITCHEN_LUA_ARCHIVE" ] || fail "archive does not exist: $KITCHEN_LUA_ARCHIVE" + source_archive=$KITCHEN_LUA_ARCHIVE +else + command -v curl >/dev/null 2>&1 || fail 'curl is required when KITCHEN_LUA_ARCHIVE is not set' + source_archive=$work/$archive + curl --fail --location --proto '=https' --tlsv1.2 --output "$source_archive" "$url" +fi + +actual_sha256=$(hash_file "$source_archive") +[ "$actual_sha256" = "$expected_sha256" ] || + fail "SHA-256 mismatch for $source_archive: expected $expected_sha256, got $actual_sha256" + +tar -xzf "$source_archive" -C "$work" +source_dir=$work/lua-$version +[ -d "$source_dir/src" ] || fail "archive did not contain expected directory: lua-$version/src" + +make -C "$source_dir" CC=cc all + +built_lua=$source_dir/src/lua +built_luac=$source_dir/src/luac +[ -x "$built_lua" ] || fail 'build did not produce an executable lua interpreter' +[ -x "$built_luac" ] || fail 'build did not produce an executable luac compiler' +"$built_lua" -e 'assert(_VERSION == "Lua 5.5")' + +mkdir -p "$bin" + +install_one() { + source_path=$1 + destination=$2 + temporary=$bin/.install-$(basename "$destination")-$$ + cp "$source_path" "$temporary" + chmod 755 "$temporary" + mv "$temporary" "$destination" +} + +install_one "$built_lua" "$lua_versioned" +install_one "$built_luac" "$luac_versioned" + +[ -L "$lua_alias" ] || ln -s "$lua_target" "$lua_alias" +[ -L "$luac_alias" ] || ln -s "$luac_target" "$luac_alias" + +"$lua_alias" -e 'assert(_VERSION == "Lua 5.5")' +printf '%s\n' "$lua_alias" diff --git a/tasks/lua-interpreter/README.md b/tasks/lua-interpreter/README.md new file mode 100644 index 0000000..aa7f57c --- /dev/null +++ b/tasks/lua-interpreter/README.md @@ -0,0 +1,50 @@ +# Lua interpreter + +Target context: a Unix-like host or GitHub runner with an ISO C compiler, `make`, +`tar`, HTTPS `curl`, and a SHA-256 utility. Installation is into an explicit +prefix; the task does not use root or a system package manager. + +This task gives Kitchen a reproducible Lua interpreter without treating whatever +`lua` happens to be on a host as good enough. The source release and digest are +pinned, the archive is verified before extraction, and the interpreter is +smoke-tested before installation. + +## Requirements + +The installer must: + +- build the official Lua 5.5.1 source release; +- verify the official SHA-256 digest before extracting or compiling it; +- require an absolute installation prefix rather than guessing a global path; +- install versioned executables as `PREFIX/bin/lua-5.5.1` and + `PREFIX/bin/luac-5.5.1`; +- provide `PREFIX/bin/lua` and `PREFIX/bin/luac` symlinks when those names are + free or already point at this exact Kitchen-managed version; +- refuse to replace an unrelated file or symlink at either unversioned name; +- avoid depending on the caller's current directory; +- verify the built interpreter by executing Lua code before and after install; +- leave no build tree behind after success or failure. + +The current source provenance is recorded in `sources/lua.md`. + +## Stable entry point + +Choose the prefix from established machine state, then run: + +```sh +sh /path/to/kitchen/tasks/lua-interpreter/install.sh /absolute/prefix +``` + +For the recorded MIRO A1 or SDF convention, `$HOME/opt` is the natural prefix +only after that target context has been confirmed, yielding `$HOME/opt/bin/lua`. +The installer itself deliberately does not assume that path. + +For an already-downloaded archive, set `KITCHEN_LUA_ARCHIVE` to its absolute or +relative path. The same pinned digest check still applies: + +```sh +KITCHEN_LUA_ARCHIVE=/path/to/lua-5.5.1.tar.gz \ + sh /path/to/kitchen/tasks/lua-interpreter/install.sh /absolute/prefix +``` + +Run `sh tests/lua-interpreter.sh` for the host smoke and refusal cases. diff --git a/tasks/lua-interpreter/install.sh b/tasks/lua-interpreter/install.sh new file mode 100755 index 0000000..132e00b --- /dev/null +++ b/tasks/lua-interpreter/install.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +exec sh "$here/1.sh" "$@" diff --git a/tests/lua-interpreter.sh b/tests/lua-interpreter.sh new file mode 100755 index 0000000..6df1338 --- /dev/null +++ b/tests/lua-interpreter.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +installer=$root/tasks/lua-interpreter/install.sh + +tmp=${TMPDIR:-/tmp}/kitchen-lua-test-$$ +trap 'rm -rf "$tmp"' EXIT HUP INT TERM +mkdir -p "$tmp" + +prefix=$tmp/prefix +sh "$installer" "$prefix" >/dev/null + +[ -x "$prefix/bin/lua-5.5.1" ] || { printf 'missing versioned lua\n' >&2; exit 1; } +[ -x "$prefix/bin/luac-5.5.1" ] || { printf 'missing versioned luac\n' >&2; exit 1; } +[ -L "$prefix/bin/lua" ] || { printf 'missing lua symlink\n' >&2; exit 1; } +[ -L "$prefix/bin/luac" ] || { printf 'missing luac symlink\n' >&2; exit 1; } +[ "$(readlink "$prefix/bin/lua")" = lua-5.5.1 ] || { printf 'wrong lua symlink target\n' >&2; exit 1; } +[ "$(readlink "$prefix/bin/luac")" = luac-5.5.1 ] || { printf 'wrong luac symlink target\n' >&2; exit 1; } + +answer=$("$prefix/bin/lua" -e 'io.write(6 * 7)') +[ "$answer" = 42 ] || { printf 'lua evaluation failed: %s\n' "$answer" >&2; exit 1; } +case $("$prefix/bin/lua" -v 2>&1) in + 'Lua 5.5.1'*) ;; + *) printf 'unexpected Lua version\n' >&2; exit 1 ;; +esac +printf 'PASS real Lua evaluation\n' + +sh "$installer" "$prefix" >/dev/null +[ "$("$prefix/bin/lua" -e 'io.write(21 * 2)')" = 42 ] || { + printf 'rerun damaged interpreter\n' >&2 + exit 1 +} +printf 'PASS idempotent reinstall\n' + +protected=$tmp/protected +mkdir -p "$protected/bin" +printf '#!/bin/sh\nprintf "%s\\n" sentinel\n' > "$protected/bin/lua" +chmod 755 "$protected/bin/lua" +set +e +sh "$installer" "$protected" >"$tmp/protected.out" 2>&1 +status=$? +set -e +[ "$status" -ne 0 ] || { printf 'expected existing lua refusal\n' >&2; exit 1; } +[ "$("$protected/bin/lua")" = sentinel ] || { printf 'existing lua was changed\n' >&2; exit 1; } +[ ! -e "$protected/bin/lua-5.5.1" ] || { printf 'build/install started before alias refusal\n' >&2; exit 1; } +printf 'PASS existing lua refusal\n' + +printf 'not the Lua archive\n' > "$tmp/corrupt.tar.gz" +bad=$tmp/bad-prefix +set +e +KITCHEN_LUA_ARCHIVE=$tmp/corrupt.tar.gz sh "$installer" "$bad" >"$tmp/corrupt.out" 2>&1 +status=$? +set -e +[ "$status" -ne 0 ] || { printf 'expected corrupt archive refusal\n' >&2; exit 1; } +[ ! -e "$bad/bin/lua-5.5.1" ] || { printf 'corrupt archive installed lua\n' >&2; exit 1; } +grep -F 'SHA-256 mismatch' "$tmp/corrupt.out" >/dev/null || { + printf 'corrupt archive did not fail at digest check\n' >&2 + cat "$tmp/corrupt.out" >&2 + exit 1 +} +printf 'PASS corrupt archive refusal\n' + +set +e +sh "$installer" relative/prefix >"$tmp/relative.out" 2>&1 +status=$? +set -e +[ "$status" -ne 0 ] || { printf 'expected relative prefix refusal\n' >&2; exit 1; } +printf 'PASS relative prefix refusal\n' From a00217bf71a39301956ffbc01b792526296bed44 Mon Sep 17 00:00:00 2001 From: i Date: Fri, 2 Oct 2026 21:18:49 -0400 Subject: [PATCH 2/2] Fix Lua sentinel fixture --- .../1 test fixture consumed percent format.md | 19 +++++++++++++++++++ tests/lua-interpreter.sh | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tasks/lua-interpreter/1 test fixture consumed percent format.md diff --git a/tasks/lua-interpreter/1 test fixture consumed percent format.md b/tasks/lua-interpreter/1 test fixture consumed percent format.md new file mode 100644 index 0000000..57b8bde --- /dev/null +++ b/tasks/lua-interpreter/1 test fixture consumed percent format.md @@ -0,0 +1,19 @@ +# 1 — test fixture consumed the percent format + +Observed: 2026-10-02 on the GitHub `ubuntu-latest` runner for Kitchen PR #21. + +Candidate `1.sh` successfully downloaded the pinned Lua 5.5.1 archive, verified +it, built Lua, installed it, evaluated `6 * 7`, and passed the idempotent rerun. +The workflow then failed in the test intended to prove that an unrelated existing +`PREFIX/bin/lua` was preserved. + +The candidate had already refused the conflicting path as required. The failure +was in the fixture itself: the outer shell `printf` used to write the sentinel +script contained `%s` in its own format string. That outer `printf` consumed the +specifier before the sentinel script was written, leaving a script that printed +only a newline. The preservation assertion therefore blamed the installer for a +change it had not made. + +The regression fixture now writes `%%s` in the outer format so the generated +script contains the intended `%s`. Keep this case because it demonstrates that a +fixture can create a false failure while the mutation under test is correct. diff --git a/tests/lua-interpreter.sh b/tests/lua-interpreter.sh index 6df1338..f735896 100755 --- a/tests/lua-interpreter.sh +++ b/tests/lua-interpreter.sh @@ -35,7 +35,7 @@ printf 'PASS idempotent reinstall\n' protected=$tmp/protected mkdir -p "$protected/bin" -printf '#!/bin/sh\nprintf "%s\\n" sentinel\n' > "$protected/bin/lua" +printf '#!/bin/sh\nprintf "%%s\\n" sentinel\n' > "$protected/bin/lua" chmod 755 "$protected/bin/lua" set +e sh "$installer" "$protected" >"$tmp/protected.out" 2>&1