diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a039f29..8c3168f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,9 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 40 env: + # The specification is taken from the branch of the same name where one + # exists, so a change spanning both repositories is reviewed as a whole. + OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }} MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -151,6 +154,54 @@ jobs: echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi + + # ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A + # CHANGE TO ONE. + # + # Another step in this file already clones the specification and + # substitutes it, and that made the arrangement look complete. It was not: + # the steps below reached `openkal' BY VERSION, so a version under review + # --- which by definition is not published --- failed them with + # + # E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the + # synced index ... the index is current, so this name is either + # wrong or not published yet + # + # ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across + # the eight repositories of this ecosystem while one change spanned all of + # them: seven jobs in three repositories had this shape, and every one of + # those repositories ALSO had a job that substituted correctly --- which is + # what made the gap invisible to a check done a repository at a time. + # + # These jobs are green on `main` and can only be green there, because + # there the published version is the one under test. That is the property + # that makes the defect silent: it is not a check that fails, it is a + # check that cannot run at the only time it would have something to say. + - name: Point at the specification's working tree + run: | + set -euo pipefail + git clone --quiet https://github.com/mcpplibs/openkal .spec + if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then + git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH" + echo "the specification is at $OPENKAL_BRANCH" + else + echo "the specification has no $OPENKAL_BRANCH; its default branch is used" + fi + # ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses + # a graph in which one package reaches a dependency by version and + # another by path, so substituting only the root leaves the build + # refusing for a second reason instead of the first. + # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # BSD sed requires an argument to -i that GNU sed refuses. A temporary + # file is the spelling that holds on all three. + subst() { # subst + sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next" + mv "$1.next" "$1" + grep -q "path = \"$2\"" "$1" \ + || { echo "::error::$1 was not substituted"; exit 1; } + } + subst mcpp.toml .spec + subst examples/hello/mcpp.toml ../../.spec - name: Install OVMF and qemu run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 @@ -234,6 +285,9 @@ jobs: run: shell: bash env: + # The specification is taken from the branch of the same name where one + # exists, so a change spanning both repositories is reviewed as a whole. + OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }} MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -376,6 +430,53 @@ jobs: echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi + + # ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A + # CHANGE TO ONE. + # + # Another step in this file already clones the specification and + # substitutes it, and that made the arrangement look complete. It was not: + # the steps below reached `openkal' BY VERSION, so a version under review + # --- which by definition is not published --- failed them with + # + # E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the + # synced index ... the index is current, so this name is either + # wrong or not published yet + # + # ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across + # the eight repositories of this ecosystem while one change spanned all of + # them: seven jobs in three repositories had this shape, and every one of + # those repositories ALSO had a job that substituted correctly --- which is + # what made the gap invisible to a check done a repository at a time. + # + # These jobs are green on `main` and can only be green there, because + # there the published version is the one under test. That is the property + # that makes the defect silent: it is not a check that fails, it is a + # check that cannot run at the only time it would have something to say. + - name: Point at the specification's working tree + run: | + set -euo pipefail + git clone --quiet https://github.com/mcpplibs/openkal .spec + if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then + git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH" + echo "the specification is at $OPENKAL_BRANCH" + else + echo "the specification has no $OPENKAL_BRANCH; its default branch is used" + fi + # ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses + # a graph in which one package reaches a dependency by version and + # another by path, so substituting only the root leaves the build + # refusing for a second reason instead of the first. + # ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and + # BSD sed requires an argument to -i that GNU sed refuses. A temporary + # file is the spelling that holds on all three. + subst() { # subst + sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next" + mv "$1.next" "$1" + grep -q "path = \"$2\"" "$1" \ + || { echo "::error::$1 was not substituted"; exit 1; } + } + subst mcpp.toml .spec - name: The EFI application cross-builds run: | # ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this diff --git a/.gitignore b/.gitignore index 7435482..73453ed 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ target/ .mcpp/ compile_commands.json mcpp.lock + +# A working tree of the specification placed beside the sources. No trailing +# slash: the pattern must match a symbolic link to one as well as a directory. +.spec diff --git a/README.md b/README.md index 2412732..4dac704 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ target = "x86_64-windows-gnu" ldflags = ["-nostdlib", "-Wl,--subsystem,10", "-Wl,-e,efi_main"] [dependencies] -openkal = "0.5.1" -openkal-uefi = "0.1.0" +openkal = "0.9.0" +openkal-uefi = "0.3.0" ``` ## ⚠️ The target is `x86_64-windows-gnu`, and that is not a workaround diff --git a/examples/hello/mcpp.toml b/examples/hello/mcpp.toml index a77ca82..d8116de 100644 --- a/examples/hello/mcpp.toml +++ b/examples/hello/mcpp.toml @@ -21,5 +21,5 @@ ldflags = ["-nostdlib", "-Wl,--subsystem,10", "-Wl,-e,efi_main"] # That is the right refusal --- two forms can name two different trees --- and # it means an example inside a repository follows that repository's own # declaration while a change is in flight. -openkal = "0.8.0" +openkal = "0.9.0" openkal-uefi = { path = "../.." } diff --git a/mcpp.toml b/mcpp.toml index f575049..3face60 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -15,7 +15,7 @@ [package] namespace = "mcpplibs" name = "openkal-uefi" -version = "0.2.0" +version = "0.3.0" description = "An implementation of openkal on UEFI Boot Services, for applications the firmware loads before an operating system exists" license = "Apache-2.0" @@ -32,7 +32,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-uefi" [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" [build] flags = [ diff --git a/src/kal.cpp b/src/kal.cpp index 85b5700..3735f9d 100644 --- a/src/kal.cpp +++ b/src/kal.cpp @@ -45,9 +45,9 @@ efi_system_table* g_st = nullptr; // window rather than a cache. constexpr unsigned kChunk = 128; -kal_io_result write_to(efi_simple_text_output_protocol* out, +kal_intptr write_to(efi_simple_text_output_protocol* out, const unsigned char* p, kal_uintptr n) { - if (!out) return kal_io_result{0, kal_err_io}; + if (!out) return -kal_err_io; efi_char16 buf[kChunk * 2 + 1]; // worst case: every byte becomes CR LF kal_uintptr done = 0; while (done < n) { @@ -63,9 +63,9 @@ kal_io_result write_to(efi_simple_text_output_protocol* out, } buf[w] = 0; if (out->output_string(out, buf) != EFI_SUCCESS) - return kal_io_result{done, kal_err_io}; + return done ? static_cast(done) : -kal_err_io; } - return kal_io_result{done, kal_ok}; + return static_cast(done); } } // namespace @@ -112,13 +112,13 @@ kal_stream kal_stdin (void) { return kal_stream{kStdin}; } kal_stream kal_stdout(void) { return kal_stream{kStdout}; } kal_stream kal_stderr(void) { return kal_stream{kStderr}; } -kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { - if (!g_st) return kal_io_result{0, kal_err_io}; +kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { + if (!g_st) return -kal_err_io; efi_simple_text_output_protocol* out = s.h == kStdout ? g_st->con_out : s.h == kStderr ? (g_st->std_err ? g_st->std_err : g_st->con_out) : nullptr; - if (!out) return kal_io_result{0, kal_err_invalid}; + if (!out) return -kal_err_invalid; return write_to(out, static_cast(buf), n); } @@ -128,9 +128,9 @@ kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) { // loses every key that is not a plain character. Returning "no bytes, no error" // is end of input, which is the truthful reading of a console this // implementation does not attempt to interpret. -kal_io_result kal_stream_read(kal_stream s, void*, kal_uintptr) { - if (s.h != kStdin) return kal_io_result{0, kal_err_invalid}; - return kal_io_result{0, kal_ok}; +kal_intptr kal_stream_read(kal_stream s, void*, kal_uintptr) { + if (s.h != kStdin) return -kal_err_invalid; + return 0; } // The console is unbuffered by this implementation; the bytes were handed to @@ -153,6 +153,14 @@ kal_uintptr kal_stream_props(kal_stream s) { // a stricter alignment is satisfied by over-allocating and storing the original // pointer immediately before the aligned address — the same technique a C // library uses for `aligned_alloc` where the platform lacks one. +// The quantum this environment allocates and protects memory in. +// +// Firmware hands out memory in pages of four kilobytes --- that is the unit of +// `AllocatePages', which is what this allocator draws on --- and it applies no +// protection of its own. The coarser of the two is therefore the page, and a +// caller that rounds to it is never wrong. +kal_uintptr kal_memory_granularity(void) { return 4096; } + void* kal_alloc(kal_uintptr size, kal_uintptr align) { if (!g_st) return nullptr; if (size == 0) size = 1; diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..92f70c4 --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,17 @@ +#include + +// What this implementation says about itself before it is used. Both answers +// are constants; see openkal/version.h for why they belong to no interface. +extern "C" { + +kal_u64 kal_version(void) { return KAL_VERSION; } + +// Firmware supplies a console and a page allocator and nothing else this +// specification has an interface for. `openkal.env' is absent because a program +// started by firmware receives no arguments this implementation can report, and +// reporting none would be a claim rather than an absence. +kal_u64 kal_interfaces(void) { + return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY; +} + +}