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
70 changes: 70 additions & 0 deletions .github/actions/mtest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: mtest
description: >
The mtest engine the conformance suite reports to: the release .github/versions.env pins (MTEST_VERSION), an
xlings package (xim:mtest) that the index verifies, else the release bundle checked against MTEST_SHA256;
MTEST names its launcher. A job that has no xlings yet gets the pinned one first (XLINGS_VERSION).
runs:
using: composite
steps:
- name: xlings, where the job has none
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
command -v xlings > /dev/null 2>&1 && exit 0
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 \
https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
- name: xlings, where the job has none (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
if (Get-Command xlings -ErrorAction SilentlyContinue) { exit 0 }
irm https://d2learn.org/xlings-install.ps1.txt | iex
"$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
. .github/versions.env # MTEST_VERSION, MTEST_SHA256: no workflow states the pin
# xlings by the configured mirror (GLOBAL), then by CN (GitCode first), each with a refreshed index
# (a cached index can predate the release, and xlings keeps it when an anonymous refresh fails); else
# the release bundle itself, checked against MTEST_SHA256: the web address, then the API's, which
# answered 200 while the web one answered 504 (GitHub's 2026-11-09 incident).
launcher=""
for mirror in GLOBAL CN; do
xlings config --mirror "$mirror" > /dev/null 2>&1 || true
xlings update > /dev/null 2>&1 || true
if xlings install "mtest@$MTEST_VERSION" -y -g; then launcher=$(command -v mtest); break; fi
done
# The package's own launcher, not the xvm shim: on Windows the shim runs the package's alias through
# cmd, which reads `&` in an argument (`--tag "on:win32-x64&release"`) as a second command.
exe=''; [ "$RUNNER_OS" != Windows ] || exe='.exe'
own="${XLINGS_HOME:-$HOME/.xlings}/data/xpkgs/xim-x-mtest/$MTEST_VERSION/bin/mtest$exe"
if [ -n "$launcher" ] && [ -f "$own" ]; then launcher="$own"; fi
xlings config --mirror GLOBAL > /dev/null 2>&1 || true
if [ -z "$launcher" ]; then
echo "::warning::xlings could not install mtest $MTEST_VERSION; taking the release itself"
archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz"
temp="$RUNNER_TEMP"; python=python3
if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi
if ! curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$temp/$archive" \
"https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive"; then
id=$(curl -fsSL --retry 3 --retry-all-errors -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/mcpp-community/mtest/releases/tags/v$MTEST_VERSION" \
| "$python" -c 'import json,sys; print(next(str(a["id"]) for a in json.load(sys.stdin)["assets"] if a["name"] == sys.argv[1]))' "$archive")
curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/octet-stream" -o "$temp/$archive" \
"https://api.github.com/repos/mcpp-community/mtest/releases/assets/$id"
fi
actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive")
[ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the workflow pins $MTEST_SHA256"; exit 1; }
mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest"
launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest"
[ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe"
fi
"$launcher" --version
"$launcher" --version | grep -qF "$MTEST_VERSION" || { echo "::error::the mtest at $launcher is not $MTEST_VERSION"; exit 1; }
echo "MTEST=$launcher" >> "$GITHUB_ENV"
47 changes: 47 additions & 0 deletions .github/actions/test-report/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: test-report
description: >
The runs `mcpp test` kept in the mtest store's form: taken into one store of
the job, written as one Markdown report of this workflow run's runs into the
job's summary, and kept as an artifact for the report over every job of the
workflow run (ci.yml `report`).
inputs:
name:
description: the artifact's name, unique in the workflow run
required: true
stores:
description: the stores `mcpp test` kept in this job, relative to the workspace, separated by spaces
required: false
default: .spec/conformance/.mtest
runs:
using: composite
steps:
- uses: ./.github/actions/mtest
- shell: bash
env:
STORES: ${{ inputs.stores }}
run: |
set -euo pipefail
# The POSIX spelling of the runner's directories for this shell; the
# artifact's path below is the runner's own.
temp="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")"
workspace="$(cygpath -u "$GITHUB_WORKSPACE" 2> /dev/null || printf '%s' "$GITHUB_WORKSPACE")"
job="$temp/mtest-job"
mkdir -p "$job"
taken=0
for store in $STORES; do
[ -f "$workspace/$store/index.ndjson" ] || continue
(cd "$job" && "$MTEST" store import --from "$workspace/$store")
taken=1
done
if [ "$taken" = 0 ]; then echo "mcpp test kept no run here"; exit 0; fi
(cd "$job" && "$MTEST" report --format md --group "$GITHUB_RUN_ID") >> "$GITHUB_STEP_SUMMARY" \
|| echo "::warning::no run of this workflow run in the store"
echo "MTEST_JOB_STORE=$RUNNER_TEMP/mtest-job/.mtest" >> "$GITHUB_ENV"
- uses: actions/upload-artifact@v4
if: env.MTEST_JOB_STORE != ''
with:
name: ${{ inputs.name }}
path: ${{ env.MTEST_JOB_STORE }}
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
5 changes: 5 additions & 0 deletions .github/versions.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The engines CI pins, read by the composite actions (.github/actions/mtest).
# mtest: an xlings package (xim:mtest) whose sha256 the index states; the release
# bundle's sha256 checks the fallback download.
MTEST_VERSION=0.5.2
MTEST_SHA256=be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

env:
MCPP_VERSION: 2026.11.9.1
XLINGS_VERSION: v2026.10.26.1
XLINGS_NON_INTERACTIVE: '1'

jobs:
# THE CONFORMANCE SUITE, ABOVE THE SYSTEM'S OWN C LIBRARY, on each system this
# implementation is examined on: Linux with glibc and macOS with libSystem,
# natively. The suite runs through the mtest engine (tools/run-conformance.sh
# with MTEST set): one case per interface, its observations within it. Its
# three-way check compares the manifest's provides-interfaces, kal_interfaces()
# and the names the artefact exports, so an interface stated and not provided,
# or provided and not stated, fails the job.
conformance:
name: conformance (${{ matrix.name }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- { name: 'linux-x86_64, gcc', os: ubuntu-24.04, toolchain: 'gcc@16.1.0' }
- { name: 'linux-x86_64, llvm', os: ubuntu-24.04, toolchain: 'llvm@22.1.8' }
- { name: 'macos-arm64, llvm', os: macos-15, toolchain: 'llvm@20.1.7' }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4

- name: The committed manifest names no local directory
run: |
set -euo pipefail
bad=$(grep -nE '^[a-z-]+ = \{[^}]*path = "(/|[A-Za-z]:)' mcpp.toml || true)
if [ -n "$bad" ]; then
echo "::error::the committed manifest names an absolute path"; printf '%s\n' "$bad"; exit 1
fi

# The specification of the same-named branch where one exists, else its
# default branch: a change spanning both is examined as written.
- name: The specification
run: |
git clone --quiet https://github.com/mcpplibs/openkal.git .spec
branch='${{ github.head_ref || github.ref_name }}'
if git -C .spec rev-parse --verify --quiet "origin/$branch" > /dev/null; then
git -C .spec checkout --quiet "origin/$branch"
echo "the specification is at $branch"
else
echo "the specification has no $branch; its default branch is used"
fi

- name: Install xlings
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \
| bash -s "$XLINGS_VERSION"
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install mcpp
run: |
xlings update
xlings install "mcpp@$MCPP_VERSION" -y -g
mcpp --version
mcpp self config --mirror GLOBAL

- name: Select the toolchain
run: |
spec='${{ matrix.toolchain }}'
mcpp toolchain install "${spec%@*}" "${spec#*@}"
mcpp toolchain default "$spec"
mcpp toolchain list

- uses: ./.github/actions/mtest

# Every interface the manifest states, and no other: the sets named are
# the conformance suite's features for exactly those interfaces.
- name: Every interface this implementation states
run: |
set -euo pipefail
bash .spec/tools/run-conformance.sh openkal-posix . \
full,exec,random,terminal,space,timeout,map,region

- name: The failing cases, explained
if: failure()
run: |
[ -n "${MTEST:-}" ] && [ -d .spec/conformance/.mtest ] || exit 0
( cd .spec/conformance && "$MTEST" explain ) || true

# The runs, reported and kept for the report job.
- uses: ./.github/actions/test-report
if: always()
with:
name: mtest-${{ github.job }}-${{ strategy.job-index }}

# EVERY JOB'S RUNS AS ONE REPORT (`mtest ci report`, the workflow run's group).
report:
name: tests (report)
needs: conformance
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/mtest
- uses: actions/download-artifact@v4
with:
pattern: mtest-*
path: ${{ runner.temp }}/stores
- name: Every job's runs, reported together
shell: bash
run: |
set -euo pipefail
found=0
for store in "$RUNNER_TEMP"/stores/*/; do
[ -f "$store/index.ndjson" ] || continue
"$MTEST" store import --from "$store"
found=1
done
if [ "$found" = 0 ]; then echo "no job kept a run" >> "$GITHUB_STEP_SUMMARY"; exit 0; fi
"$MTEST" ci report --group "$GITHUB_RUN_ID" --out "$RUNNER_TEMP/tests.md"
cat "$RUNNER_TEMP/tests.md" >> "$GITHUB_STEP_SUMMARY"
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# openkal-posix

An implementation of [openkal](https://github.com/mcpplibs/openkal) written above
a POSIX C library.

```toml
[dependencies]
openkal-posix = "0.1.0"
```

## What it is for

openkal's other implementations reach a system through its own interface: Linux's
system calls, Mach and BSD on macOS, the Win32 and object-manager interfaces on
Windows, the supervisor binary interface beneath a bare machine. Some systems
state no such interface. OpenBSD admits system calls only from its C library;
Haiku, GNU/Hurd and illumos are reached through theirs. On those systems the C
library is the stable interface, and an implementation of openkal is written
above it or not at all.

This package is that implementation. It carries a program written to openkal,
and the image runner `okrun`, to any system with a POSIX C library and no
implementation of its own. It is examined on Linux (glibc) and macOS (libSystem),
where the other implementations are the reference against which it is compared.

## Where it may be placed

```toml
provides = ["mcpp:kernel-abi=openkal"]
requires = ["mcpp:c-abi"]
```

It is written above a C library, so it is placed above one: the system's own,
in a program the system's toolchain builds, or beneath `okrun`. It is never
placed beneath a C library that is itself written above openkal, such as
openkal-musl. That C library's functions would call this implementation, and
this implementation would call them back: `kal_time_wall` calls `clock_gettime`,
which would call `kal_time_wall`. The requirement states the arrangement. An
engine that checks that the requirements of the layers form no circle (mcpp
2026.11.10.1, `layer-cycle`) refuses that graph before anything is compiled.

## What it provides

| interface | provided | how |
|---|---|---|
| `openkal.abort` | yes | `_exit`, `abort` |
| `openkal.stream` | yes | descriptors |
| `openkal.memory` | yes | `posix_memalign`; the system's pages from `sysconf` |
| `openkal.env` | yes | the arguments and environment the initialisers are given; `uname` for the system's name |
| `openkal.time` | yes | `clock_gettime`, `nanosleep` |
| `openkal.random` | yes | `getentropy` |
| `openkal.fs` | yes | the `*at` functions relative to directory descriptors; two preopens, the working directory and the root |
| `openkal.process` | yes | `fork` and `fexecve`; process groups as units; SIGINT, SIGTERM and SIGHUP taken over as requests to end |
| `openkal.task` | yes | POSIX threads; a word is waited upon through condition variables |
| `openkal.exec` | yes | `mmap` and `mprotect`, never writable and executable at once |
| `openkal.terminal` | yes | `termios` |
| `openkal.timeout` | yes | `poll`; for streams and programs |
| `openkal.map` | yes | `mmap` of a descriptor |
| `openkal.region` | yes | reserved `PROT_NONE` mappings, backed by `mprotect` |
| `openkal.space` | yes | `fork` |
| `openkal.net` | no | not yet written over sockets |
| `openkal.datagram` | no | as `openkal.net` |
| `openkal.prof` | no | no portable source of samples |
| `openkal.debug` | no | no portable source of symbols |

An interface that is not provided is absent, not stubbed. Its names are not
defined, `kal_interfaces()` does not state it, and the manifest's
`[kernel-abi] provides-interfaces` does not list it (SPEC.md clause 6.1). A
program that calls one fails at link naming it, and a loader that binds by the
list leaves it unbound.

Within a provided interface, a property that the words do not claim is answered
with `kal_err_not_supported`, as clause 6.2 requires. These are:

- confinement, a bound lifetime and a one-string command line for a started
program;
- the observable units of 0.24 (`kal_process_job_create` and its companions), a
unit's usage and its limits;
- a rename that does not replace;
- the timeout forms of `accept` and `recv_from`, whose resources belong to the
absent network interfaces.

**A lock belongs to the open file.** POSIX record locks belong to the process, so
a lock is taken through `F_OFD_SETLK` where the system has locks of the open file
description. Elsewhere a lock on the whole file is BSD's `flock`, which belongs to
the open file on every system that has it.

**A request to end is counted.** `kal_process_stop_requested` installs handlers
for SIGINT, SIGTERM and SIGHUP, unless the starter arranged for one of them to be
ignored. Each request adds one to the word and the word is never zero again, as
openkal 0.25 states. A handler may not call the threads library, so it writes to
a pipe, and a context created when the word is armed wakes its waiters.
`kal_process_terminate` sends SIGTERM, which a program may take over.

## How it is examined

openkal's conformance suite runs for every interface the manifest states, on
Linux with glibc (gcc and llvm) and on macOS with libSystem, through the mtest
engine: one case per interface, with each observation as a part of it. The
suite's three-way check compares the manifest, `kal_interfaces()` and the
exported names. Each job writes mtest's report to its summary, and a final job
joins every job's runs.

On Linux the suite reports 329 observations held, none failed, and 12 not
observed: for interfaces not provided, properties not claimed, and the terminal,
which a run without an interactive stream cannot examine.

## License

Apache-2.0.
Loading
Loading