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
69 changes: 69 additions & 0 deletions .github/actions/mtest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: mtest
description: >
The mtest engine the conformance suite reports to: the release the workflow 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
# 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"
74 changes: 73 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ env:
# `em++`'s C compiler is `emcc` and not `em` (a C translation unit in the
# conformance suite does not compile without that), and the wasm target rows
# resolve the emsdk payload.
MCPP_VERSION: 2026.11.5.1
MCPP_VERSION: 2026.11.9.1
XLINGS_VERSION: v2026.10.26.1
# The mtest engine the runs are reported with: the conformance suite's cases
# (tools/run-conformance.sh with MTEST set) and `mcpp test`'s store (target/mtest).
MTEST_VERSION: 0.5.2
MTEST_SHA256: be443f8c546b3f4628dcd3f1eb6f45b4d698a7f805232f966349faf8250863c6
XLINGS_NON_INTERACTIVE: '1'

jobs:
Expand Down Expand Up @@ -162,6 +166,8 @@ jobs:
[ -n "$nm_bin" ] || { echo "::error::no llvm-nm in the emsdk payload"; exit 1; }
echo "NM=$nm_bin" >> "$GITHUB_ENV"

- uses: ./.github/actions/mtest

# CONFORMANCE, FOR THE GROUPS THIS IMPLEMENTATION PROVIDES.
#
# The set is named rather than `full`: clause 6.1 says an implementation
Expand Down Expand Up @@ -406,3 +412,69 @@ jobs:
- name: Restore the manifest
if: always()
run: git checkout -- mcpp.toml

- name: This package's test runs, kept for the report
if: always()
run: |
if [ -d target/mtest ]; then cp -R target/mtest "$RUNNER_TEMP/mcpp-test-runs"; fi

# A FAILED RUN, EXPLAINED: through the engine the suite's own text is not in
# the log, so the failing cases are named here with what each observed.
- name: The failing cases, explained
if: failure()
run: |
[ -n "${MTEST:-}" ] && [ -d .spec/conformance/.mtest ] || exit 0
( cd .spec/conformance && "$MTEST" explain ) || true

# THE RUNS OF THIS JOB, REPORTED: the conformance suite's (one case per
# interface, its observations within it) and `mcpp test`'s (one case per
# test program), taken into one store, written as the job's summary and kept
# for the report over every job.
- name: The runs, reported
if: always()
run: |
[ -n "${MTEST:-}" ] || { echo "no mtest engine in this job"; exit 0; }
store="$(cygpath -u "$RUNNER_TEMP" 2> /dev/null || printf '%s' "$RUNNER_TEMP")/mtest-store"
mkdir -p "$store"; here="$PWD"
for from in "$here/.spec/conformance/.mtest" "$RUNNER_TEMP/mcpp-test-runs"; do
if [ -f "$from/index.ndjson" ]; then ( cd "$store" && "$MTEST" store import --from "$from" ); fi
done
[ -f "$store/.mtest/index.ndjson" ] || { echo "no run was kept"; exit 0; }
( cd "$store" && "$MTEST" report --format md --group "$GITHUB_RUN_ID" ) >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@v4
if: always()
with:
name: mtest-${{ github.job }}-${{ strategy.job-index }}
path: ${{ runner.temp }}/mtest-store/.mtest
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7

# ---------------------------------------------------------------------------
# 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 --out "$RUNNER_TEMP/tests.md"
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ The continuous integration gate that already builds a task program under
`-pthread` also asks each of its two contexts for its own region and asserts
that the region contains a local of that context.

## What 0.14 adds (openkal 0.25)

`kal_fs_hard_link` is present and refuses: MEMFS stores one name for each node
(`linkat` reports `EMLINK`), so `KAL_FS_PROP_HARD_LINKS` is not claimed. The
manifest states `requires = ["mcpp:c-abi"]`: an implementation written above a C
library is never placed beneath one that is composed over it.

## Conformance

Measured 2026-10-31 against openkal 0.18.0, `emsdk 6.0.9`, run under node in
Expand Down Expand Up @@ -191,10 +198,10 @@ implementation in its source:

```toml
[dependencies]
openkal = "0.24.0"
openkal = "0.25.0"

[target.'cfg(os = "emscripten")'.dependencies]
openkal-emscripten = "0.13.0"
openkal-emscripten = "0.14.0"
```

## Licence
Expand Down
10 changes: 8 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-emscripten"
version = "0.13.0"
version = "0.14.0"
description = "An implementation of openkal for Emscripten, written ABOVE a C library rather than beneath one, because on this platform there is no kernel to issue calls to."
license = "Apache-2.0"

Expand All @@ -14,11 +14,17 @@ license = "Apache-2.0"
# may be built upon a C library, beneath one, or without one" --- and this is
# the first implementation in the ecosystem to take the first of the three.
provides = ["mcpp:kernel-abi=openkal"]
# WRITTEN ABOVE A C LIBRARY (openkal clause 2), SO IT IS NEVER BENEATH ONE THAT
# NEEDS IT: a C library composed over openkal (openkal-musl) placed above this
# implementation would call itself through it. The requirement states the
# arrangement, and a build tool that checks that the requirements of the layers
# form no cycle refuses the other one before anything is compiled.
requires = ["mcpp:c-abi"]
authors = ["mcpplibs"]
repo = "https://github.com/mcpplibs/openkal-emscripten"

[dependencies]
openkal = "0.24.0"
openkal = "0.25.0"

[build]
# THE DIALECT FLAGS ARE THE SAME AS EVERY OTHER IMPLEMENTATION'S AND THE REASON
Expand Down
16 changes: 16 additions & 0 deletions src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ int kal_fs_preopen(kal_uintptr index, kal_dir* out,
// ATTENUATE a handle with less (0.23, rights.h); READ_ONLY and
// UNCHANGING state what this handle carries.
//
// HARD_LINKS is withheld (0.25): `linkat` reports EMLINK on MEMFS, so no file
// receives a second name, and kal_fs_hard_link says so (below).
//
// LIST_INFO is withheld: the listing gives a name and a kind, and the rest of
// an entry is asked of it (kal_fs_list_next_info).
kal_uintptr kal_fs_props(kal_dir d) {
Expand Down Expand Up @@ -649,6 +652,19 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len,
return kal_ok;
}

// A SECOND NAME FOR A FILE (openkal 0.25). MEMFS stores one name for each
// node (`linkat` reports EMLINK), so KAL_FS_PROP_HARD_LINKS is not claimed and
// the request is refused after its arguments are examined, as clause 6.2 allows
// for an operation a resource's property governs.
int kal_fs_hard_link(kal_dir from, const char* a, kal_uintptr alen,
kal_dir to, const char* b, kal_uintptr blen) {
char na[kMaxName + 1], nb[kMaxName + 1];
if (oke::unpack(from.h) < 0 || oke::unpack(to.h) < 0
|| !cname(a, alen, na) || !cname(b, blen, nb))
return kal_err_invalid;
return kal_err_not_supported;
}

kal_intptr kal_fs_link_read(kal_dir base, const char* name, kal_uintptr len,
char* out, kal_uintptr cap) {
const int b = oke::unpack(base.h);
Expand Down
Loading