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 @@ -27,8 +27,12 @@ on:
default: ""
env:
MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }}
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 @@ -178,6 +182,8 @@ jobs:
esac
mcpp toolchain list

- uses: ./.github/actions/mtest

- name: Every interface, every kind of examination
run: |
bash .spec/tools/run-conformance.sh openkal-macos . full,optional
Expand Down Expand Up @@ -249,6 +255,11 @@ jobs:
test "$missing" -eq 0
done

- 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

# WHAT THIS SYSTEM'S SANDBOX REFUSED, AND WHAT IT ENDED, when a test
# failed: a confined program's refusals are the system's log's, and a
# program it ended leaves a report; neither reaches the test's output.
Expand Down Expand Up @@ -443,3 +454,64 @@ jobs:
fi
rm -f src/probe.cpp
echo "a dependence upon a C library is visible to the check"

# 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"
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ written on the kernel's own calls.

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

[target.'cfg(os = "macos")'.dependencies]
openkal-macos = "0.22.1"
openkal-macos = "0.23.0"
```

Its purpose is as much to test the specification as to be used. A specification
Expand Down Expand Up @@ -183,6 +183,20 @@ same image parses as fast on Linux arm64. Since 0.17.1 it is the same kernel ide
this system's library keeps for each thread (`pthread_threadid_np`, a name no C
library in this ecosystem defines).

## What 0.23 adds (openkal 0.25)

**A second name for a file** (`kal_fs_hard_link`) is `linkat` with no flags, so a
final link receives the name itself. `KAL_FS_PROP_HARD_LINKS` is claimed on APFS
and HFS+, which store several names for one file, and not on a network volume,
whose answer depends on its server. A directory is refused with
`kal_err_is_directory` before the call, because the kernel's own answer (`EPERM`)
names a permission; two volumes are refused by the kernel with `EXDEV`, which is
reported as `kal_err_not_supported`.

**The requests to end are counted.** The handler adds one to the stop word for each
request, and one where the count would wrap to zero, in a single compare-and-exchange,
so two handlers that interrupt one another each count.

## What 0.21 adds (openkal 0.23)

**A region at an address.** `kal_exec_alloc_at` passes the address as a hint,
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-macos"
version = "0.22.1"
version = "0.23.0"
description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used."
license = "Apache-2.0"

Expand Down Expand Up @@ -61,7 +61,7 @@ provides-interfaces = [
]

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

[build]
# The flags are attached to this package's own sources rather than to the whole
Expand Down Expand Up @@ -113,3 +113,9 @@ default = []
# library defines --- which is the only name reachable from a program that
# defines every ordinary one.
standalone = { defines = ["OKM_STANDALONE"] }

# The tests are written with the mtest library (mcpp-community/mtest): each file
# declares its cases, and a test that starts a copy of itself declares what the
# copy does as a `child'.
[dev-dependencies]
mcpplibs.mtest = "0.5.2"
31 changes: 30 additions & 1 deletion src/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ kal_uintptr volume_props(int fd) {
// Both implement `renameatx_np' with RENAME_EXCL (0.17), and both store a
// socket bound to a name.
if (named("apfs") || named("hfs"))
return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS
return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_HARD_LINKS
| KAL_FS_PROP_EXECUTABLE
| KAL_FS_PROP_RENAME_NO_REPLACE | KAL_FS_PROP_LOCAL_ENDPOINT;

Expand Down Expand Up @@ -1060,6 +1060,35 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len,
return kal_ok;
}

// A SECOND NAME FOR A FILE (0.25) is `linkat' with no flags, which gives the
// node the name denotes another name and does not follow a final link. Claimed
// on the system's own formats (APFS, HFS+), which store several names for one
// file; a network volume's answer depends on its server and is not claimed. A
// directory is refused before the call, because the kernel refuses its second
// name with EPERM, which reads as a permission. Two volumes are refused by the
// kernel with EXDEV, which is reported as kal_err_not_supported: an operation
// the pair cannot perform.
int kal_fs_hard_link(kal_dir from, const char* a, kal_uintptr alen,
kal_dir to, const char* b, kal_uintptr blen) {
const int f = okm::unpack(from.h), t2 = okm::unpack(to.h);
if (f < 0 || t2 < 0 || !okm::acceptable(a, alen) || !okm::acceptable(b, blen)) return kal_err_invalid;
if ((volume_props(t2) & KAL_FS_PROP_HARD_LINKS) == 0) return kal_err_not_supported;
okm::terminated ta(a, alen), tb(b, blen);
if (!ta.ok || !tb.ok) return kal_err_invalid;
if (refused_write(t2)) return kal_err_permission;
okm::kstat64 st{};
const okm_long s = okm::sys(okm::nr_fstatat64, f, reinterpret_cast<okm_long>(ta.buf),
reinterpret_cast<okm_long>(&st), okm::at_symlink_nofollow);
if (okm::failed(s)) return okm::translate(s);
if ((okm::stat_mode(st) & okm::s_ifmt) == okm::s_ifdir) return kal_err_is_directory;
const okm_long r = okm::sys(okm::nr_linkat, f, reinterpret_cast<okm_long>(ta.buf),
t2, reinterpret_cast<okm_long>(tb.buf), 0);
if (r == -okm::e_xdev) return kal_err_not_supported;
if (okm::failed(r)) return okm::translate(r);
wrote(t2, b, blen);
return kal_ok;
}

// THE NAME WITH EVERY LINK FOLLOWED (0.23): the node opened for events only
// (O_EVTONLY, which needs no permission to read it and follows every link), and
// the kernel's name for that descriptor taken (F_GETPATH), as this system's own
Expand Down
12 changes: 11 additions & 1 deletion src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,17 @@ void stop_handler(int signo) {
int none = 0;
__atomic_compare_exchange_n(&g_stop_signal, &none, signo, false,
__ATOMIC_RELAXED, __ATOMIC_RELAXED);
__atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE);
// EACH REQUEST IS COUNTED (openkal 0.25), and the count skips zero when it
// wraps, so the word is never zero again once a request has arrived. One
// exchange both counts and skips zero, so handlers of two signals that
// interrupt one another each count, at the wrap as everywhere else; an add
// followed by a fix-up could lose the second count between the two.
kal_u32 seen = __atomic_load_n(&g_stop_word, __ATOMIC_RELAXED), next;
do {
next = seen + 1u;
if (next == 0) next = 1u;
} while (!__atomic_compare_exchange_n(&g_stop_word, &seen, next, false,
__ATOMIC_RELEASE, __ATOMIC_RELAXED));
// Woken through the same operation `kal_task_wake' performs, issued as the
// raw call because a handler may not enter code that takes a lock. Waking
// ALL of them: any number of contexts may be waiting upon this one word, and
Expand Down
2 changes: 1 addition & 1 deletion src/sys.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ enum : okm_long {
nr_getdirentries64 = 344, nr_bsdthread_terminate = 361,
nr_thread_selfid = 372,
nr_openat = 463, nr_renameat = 465, nr_faccessat = 466,
nr_fstatat64 = 470, nr_unlinkat = 472, nr_readlinkat = 473,
nr_fstatat64 = 470, nr_linkat = 471, nr_unlinkat = 472, nr_readlinkat = 473,
nr_symlinkat = 474, nr_fstatfs64 = 346, nr_sysctl = 202,
nr_mkdirat = 475,
// The descriptor limit, read when the descriptors a process holds cannot be
Expand Down
45 changes: 19 additions & 26 deletions tests/conformance_env_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
// `_NSGetExecutablePath' and `realpath' are the system's own, declared without
// its headers as tests/conformance_spawn.cpp declares them: this test starts
// itself again by its name.
#include <string>
#include <vector>
import openkal.env;
import openkal.time;
import openkal.stream;
import openkal.process;
import openkal.fs;
import mcpplibs.mtest;

using namespace mcpplibs::mtest;

extern "C" int _NSGetExecutablePath(char* buf, unsigned* size);
extern "C" char* realpath(const char* path, char* resolved);

namespace {
int failures = 0;

// ASKED BEFORE THIS IMPLEMENTATION'S INITIALISER RAN (0.21.3). This format runs
// initialisers in link order, and this test's objects precede the package's: a
Expand All @@ -38,20 +42,7 @@ kal_u64 processor_ns() {
return ok ? info.cpu_ns : ~0ull;
}

void check(bool ok, const char* what) {
if (ok) return;
++failures;
const char pre[] = "FAIL: ";
kal::write(kal::err(), pre, sizeof(pre) - 1);
kal_uintptr n = 0; while (what[n]) ++n;
kal::write(kal::err(), what, n);
kal::write(kal::err(), "\n", 1);
}

bool is(const char* a, const char* b) {
while (*a && *a == *b) { ++a; ++b; }
return *a == *b;
}
void check(bool ok, const char* what) { expect(ok) << what; }

// The granularity another start of this program states, or -1.
kal_duration granularity_elsewhere() {
Expand All @@ -62,12 +53,14 @@ kal_duration granularity_elsewhere() {
kal_dir d{}; char n[8]; kal_uintptr l = 0;
if (kal_fs_preopen(i, &d, n, sizeof n, &l) == kal_ok && l == 1 && n[0] == '/') root = d;
}
const char* argv[2] = { "conformance_env_time", "--granularity" };
const kal_uintptr lens[2] = { 20, 13 };
const std::vector<std::string> command = child_command("granularity");
std::vector<const char*> argv;
std::vector<kal_uintptr> lens;
for (const auto& a : command) { argv.push_back(a.c_str()); lens.push_back(a.size()); }
const kal_spawn how{ root, kal::fs::working(), nullptr, nullptr, 0, 0 };
kal_process p{};
kal_uintptr n = 0; while (self[1 + n]) ++n;
if (kal_process_spawn(&how, self + 1, n, argv, lens, 2, nullptr, nullptr, 0, nullptr, &p) != kal_ok)
if (kal_process_spawn(&how, self + 1, n, argv.data(), lens.data(), argv.size(), nullptr, nullptr, 0, nullptr, &p) != kal_ok)
return ~0ull;
int status = -1, terminated = -1;
kal_process_wait(p, &status, &terminated);
Expand All @@ -76,12 +69,13 @@ kal_duration granularity_elsewhere() {
}
}

int main(int argc, char** argv) {
// The other start of granularity_elsewhere: its granularity as its status.
if (argc > 1 && is(argv[1], "--granularity")) {
const kal_duration g = kal::time::granularity();
return g < 250 ? static_cast<int>(g) : 250;
}
// The other start of granularity_elsewhere: its granularity as its status.
child granularity{"granularity", [](std::vector<std::string>) {
const kal_duration g = kal::time::granularity();
return g < 250 ? static_cast<int>(g) : 250;
}};

UTest conformance_env_time_test{"conformance_env_time", [] {
// A program always receives the name it was started with, even where the
// environment has none, in which case it is empty rather than absent.
check(kal::env::arg_count() >= 1, "at least the program name is present");
Expand Down Expand Up @@ -134,5 +128,4 @@ int main(int argc, char** argv) {

const char ok[] = "openkal-macos: env and time conformance\n";
kal::write(kal::out(), ok, sizeof(ok) - 1);
return failures == 0 ? 0 : 1;
}
}};
Loading
Loading