diff --git a/.github/actions/mtest/action.yml b/.github/actions/mtest/action.yml new file mode 100644 index 0000000..549468c --- /dev/null +++ b/.github/actions/mtest/action.yml @@ -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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1fe325..d3778b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 @@ -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" diff --git a/README.md b/README.md index 47153f2..a6d074c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/mcpp.toml b/mcpp.toml index 5113b59..bc89722 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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" @@ -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 diff --git a/src/fs.cpp b/src/fs.cpp index a438db2..92789ca 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -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) { @@ -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);