Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/lua-interpreter.yml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions sources/lua.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions tasks/lua-interpreter/1 test fixture consumed percent format.md
Original file line number Diff line number Diff line change
@@ -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.
129 changes: 129 additions & 0 deletions tasks/lua-interpreter/1.sh
Original file line number Diff line number Diff line change
@@ -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"
50 changes: 50 additions & 0 deletions tasks/lua-interpreter/README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions tasks/lua-interpreter/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec sh "$here/1.sh" "$@"
69 changes: 69 additions & 0 deletions tests/lua-interpreter.sh
Original file line number Diff line number Diff line change
@@ -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'
Loading