From f704d87fd445e4d61db00bc302e78660fc99f02f Mon Sep 17 00:00:00 2001 From: kartikey1306 Date: Thu, 3 Sep 2026 15:06:59 +0530 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20repair=20master=20=E2=80=94=20the=20?= =?UTF-8?q?ABI=20asserts=20and=20the=20Ed25519=20verifier=20both=20merged?= =?UTF-8?q?=20broken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit master (22d8f8b) does not compile. Two independent double-merges, both the same shape: two PRs fixing adjacent things landed on stale bases, each was green on its own branch, and the result was never rebuilt. 1. include/eos_image.h — #93 replaced reserved[30] with tlv_len (2) + tlv_hash[28], preserving every offset. #87 merged afterwards carrying asserts written against the older struct: error: no member named 'reserved' in 'eos_image_header_t' (x2) #93 already asserts tlv_len at 62 and tlv_hash at 64, so the offset assert was a duplicate; the width assert had no replacement and is restored as two asserts covering both halves of the same 30-byte span. No offset moves and the wire format is unchanged. 2. core/ed25519_verify.c — #86 and #57 both landed a subgroup guard, so the file carried two byte-identical point_is_identity() definitions: error: redefinition of 'point_is_identity' Only #57's public_key_is_valid_subgroup() is wired to the call site, so #86's key_has_prime_order() was dead. Kept the live function, folded #86's fuller rationale onto it, deleted the duplicate. 3. tests/unit/test_ed25519.c — collateral from the same merge. Two copies of test_ed25519_identity_key_forgery_rejected, main() calling it twice and two tests not at all, and test_ed25519_low_order_R_with_a_valid_key_is_not_a_forgery referencing k_low_order[] and messages[] that the merge had dropped. While restoring the corpus, corrected it (review finding on #86): the array claimed to hold "the eight low-order point encodings" and held five. Every order here was computed rather than copied — decode y, recover x, add the point to itself until it reaches the identity — giving 1, 2, 4, 4, 8, 8, 8, 8. Missing before: y=0 with the sign bit set, and both sign-flipped order-8 encodings. D9FF..FF was in the array and is not a low-order point at all — no x satisfies the curve equation for that y — so it moves to a separate k_non_canonical[], with EDFF..FF7F (y=p) and EEFF..FF7F (y=p+1). tests_run was assigned a literal (11) in main() and never incremented, which is how the duplicate call and the two unregistered tests went unnoticed. The TEST macro now increments it, so the total cannot drift. Verified: cmake -DEBLDR_BUILD_TESTS=ON on master FAILS to build, 3 errors same with this commit builds clean ctest 21/21 PASS ctest -DEBLDR_SANITIZE=ON (ASan+UBSan) 21/21 PASS pytest tests/ 24 passed, 1 skipped test_ed25519 14/14 PASS (was 11 claimed, 12 run) discrimination, with `public_key_is_valid_subgroup` disabled: test_ed25519_low_order_keys_rejected FAILS, as it must test_ed25519_non_canonical_... still PASSES — those are refused by unpackneg() on canonicality, a different mechanism, which is the reason they are held in a separate array rather than counted among the eight. --- core/ed25519_verify.c | 56 ++++------------- include/eos_image.h | 16 +++-- tests/unit/test_ed25519.c | 126 ++++++++++++++++++++++++++++---------- 3 files changed, 116 insertions(+), 82 deletions(-) diff --git a/core/ed25519_verify.c b/core/ed25519_verify.c index d36cc7c..34aca77 100644 --- a/core/ed25519_verify.c +++ b/core/ed25519_verify.c @@ -289,6 +289,16 @@ static int point_is_identity(gf p[4]) return diff == 0; } +static void scalarbase(gf r[4], const uint8_t *s) +{ + gf q[4]; + fe_copy16(q[0], BX); + fe_copy16(q[1], BY); + fe_copy16(q[2], gf1); + fe_mul(q[3], BX, BY); + scalarmult(r, q, s); +} + /* Reject a public key outside the prime-order subgroup. * * Decoding a point is not enough. Ed25519 has eight points of low order, and @@ -304,51 +314,9 @@ static int point_is_identity(gf p[4]) * so there is no separate constant to transcribe wrongly: a mistyped L would * reject valid keys, and only in the field. * - * A arrives negated from unpackneg(). [L](-A) = -[L]A and the identity is its - * own negation, so neither condition is affected by the sign. - * - * Formulation taken from eBoot#57 by @muhammadburhandevv-hub, which reached - * this before I did and states both conditions in one expression. + * The key arrives negated from unpackneg(). [L](-A) = -[L]A and the identity + * is its own negation, so neither condition is affected by the sign. */ -static int key_has_prime_order(gf A[4]) -{ - uint8_t order_l[32]; - gf q[4], multiple[4]; - int i; - - for (i = 0; i < 32; i++) - order_l[i] = (uint8_t)ORDER_L[i]; - for (i = 0; i < 4; i++) - fe_copy16(q[i], A[i]); - - scalarmult(multiple, q, order_l); - return point_is_identity(multiple) && !point_is_identity(A); -} - -static void scalarbase(gf r[4], const uint8_t *s) -{ - gf q[4]; - fe_copy16(q[0], BX); - fe_copy16(q[1], BY); - fe_copy16(q[2], gf1); - fe_mul(q[3], BX, BY); - scalarmult(r, q, s); -} - -static int point_is_identity(gf p[4]) -{ - uint8_t encoded[32]; - point_pack(encoded, p); - - uint8_t diff = (uint8_t)(encoded[0] ^ 1U); - for (int i = 1; i < 32; i++) - diff |= encoded[i]; - return diff == 0; -} - -/* Public keys must be non-identity points in Ed25519's prime-order subgroup. - * Merely decoding a point is insufficient: an identity or torsion key can - * make the verification equation true without knowledge of a private key. */ static int public_key_is_valid_subgroup(gf public_key[4]) { uint8_t order_l[32]; diff --git a/include/eos_image.h b/include/eos_image.h index 62744d0..24cbb16 100644 --- a/include/eos_image.h +++ b/include/eos_image.h @@ -108,7 +108,8 @@ EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, tlv_hash) + /* Every remaining field, pinned. * - * Four of the fourteen fields were asserted. Transposing two adjacent + * Three of the thirteen field offsets were asserted (the fourth pre-existing + * assert is sizeof, which is not a field). Transposing two adjacent * same-width fields moves neither sizeof nor any of those four offsets, so it * compiled clean: with load_addr and entry_addr swapped, all four existing * asserts still passed and the bootloader would load an image at its entry @@ -132,15 +133,18 @@ EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, flags) == 24, "flags must stay at offset 24"); EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, sig_len) == 61, "sig_len must stay at offset 61"); -EOS_IMG_STATIC_ASSERT(offsetof(eos_image_header_t, reserved) == 62, - "reserved[] must stay at offset 62"); +/* tlv_len and tlv_hash are asserted above, where #93 introduced them; the + * 30 bytes they occupy are the ones this block used to pin as reserved[]. */ /* Field widths. An offset assert cannot see a field growing into padding that - * happens to keep every later offset -- reserved[] absorbs exactly that. */ + * happens to keep every later offset -- the 30 bytes at 62 absorb exactly + * that, which is why both halves of that span carry a width assert. */ EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->hash) == 32, "hash[] is 32 bytes on the wire"); -EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->reserved) == 30, - "reserved[] is 30 bytes on the wire"); +EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->tlv_len) == 2, + "tlv_len is 2 bytes on the wire"); +EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->tlv_hash) == 28, + "tlv_hash is 28 bytes on the wire"); EOS_IMG_STATIC_ASSERT(sizeof(((eos_image_header_t *)0)->signature) == 64, "signature[] is 64 bytes on the wire"); diff --git a/tests/unit/test_ed25519.c b/tests/unit/test_ed25519.c index f78012d..9ea837c 100644 --- a/tests/unit/test_ed25519.c +++ b/tests/unit/test_ed25519.c @@ -30,6 +30,7 @@ static int tests_passed = 0; static void name(void); \ static void run_##name(void) { \ printf(" %-50s ", #name); \ + tests_run++; \ name(); \ tests_passed++; \ printf("[PASS]\n"); \ @@ -211,29 +212,105 @@ TEST(test_ed25519_identity_key_forgery_rejected) msg, sizeof(msg) - 1) != EOS_OK); } +/* The eight canonical low-order point encodings. + * + * Every order was computed rather than copied: decoding each y, recovering x, + * and repeatedly adding the point until it reached the identity gives + * 1, 2, 4, 4, 8, 8, 8, 8 for the entries below in order. An earlier revision + * of this array held only five of them -- it omitted y=0 with the sign bit + * set and both sign-flipped order-8 encodings -- while its comment claimed to + * hold "the eight". [L](-A) = -[L]A, so the guard rejects a sign variant + * whether or not it is listed; the reason to list them is that this is the + * regression record for a secure-boot bypass, and a claimed class has to be + * the class it claims. */ +static const uint8_t k_low_order[8][32] = { + /* order 1: the identity, y = 1 */ + {0x01}, + /* order 2: y = -1 */ + {0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F}, + /* order 4: y = 0, sign bit clear */ + {0x00}, + /* order 4: y = 0, sign bit set -- the encoding the earlier array missed */ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80}, + /* order 8 */ + {0x26,0xE8,0x95,0x8F,0xC2,0xB2,0x27,0xB0,0x45,0xC3,0xF4,0x89,0xF2,0xEF,0x98,0xF0, + 0xD5,0xDF,0xAC,0x05,0xD3,0xC6,0x33,0x39,0xB1,0x38,0x02,0x88,0x6D,0x53,0xFC,0x05}, + /* order 8 */ + {0xC7,0x17,0x6A,0x70,0x3D,0x4D,0xD8,0x4F,0xBA,0x3C,0x0B,0x76,0x0D,0x10,0x67,0x0F, + 0x2A,0x20,0x53,0xFA,0x2C,0x39,0xCC,0xC6,0x4E,0xC7,0xFD,0x77,0x92,0xAC,0x03,0x7A}, + /* order 8: sign flip of the first order-8 entry -- also missing before */ + {0x26,0xE8,0x95,0x8F,0xC2,0xB2,0x27,0xB0,0x45,0xC3,0xF4,0x89,0xF2,0xEF,0x98,0xF0, + 0xD5,0xDF,0xAC,0x05,0xD3,0xC6,0x33,0x39,0xB1,0x38,0x02,0x88,0x6D,0x53,0xFC,0x85}, + /* order 8: sign flip of the second -- also missing before */ + {0xC7,0x17,0x6A,0x70,0x3D,0x4D,0xD8,0x4F,0xBA,0x3C,0x0B,0x76,0x0D,0x10,0x67,0x0F, + 0x2A,0x20,0x53,0xFA,0x2C,0x39,0xCC,0xC6,0x4E,0xC7,0xFD,0x77,0x92,0xAC,0x03,0xFA}, +}; + +/* Not low-order points, and refused earlier and by a different mechanism: + * unpackneg() rejects them on canonicality or because no x exists. Kept + * separate so the array above means what its name says -- an earlier revision + * spent one of its eight slots on D9FF..FF, which does not decode at all. */ +static const uint8_t k_non_canonical[3][32] = { + /* y = p: reduces to 0, decodes as an order-4 point but is not canonical */ + {0xED,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F}, + /* y = p + 1: reduces to the identity, likewise not canonical */ + {0xEE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F}, + /* no x satisfies the curve equation for this y: unpackneg() fails */ + {0xD9,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}, +}; + +/* A low-order key forges for roughly one message in n, where n is its order, + * so a single fixed message would let a real bypass pass this suite. */ +static const char *const messages[] = { + "untrusted firmware", "v1.0.0", "", "a", "boot", "eos", "1234", "payload", +}; + TEST(test_ed25519_low_order_keys_rejected) { /* zero_pubkey covers one encoding; Ed25519 has eight low-order points and * the family is what matters. A subgroup test alone is not enough either: * the identity has order 1, which divides L, so [L]identity = identity and - * it passes. Both checks are required. */ - static const uint8_t low_order[4][32] = { - {0}, - {1}, - {0x26,0xe8,0x95,0x8f,0xc2,0xb2,0x27,0xb0,0x45,0xc3,0xf4,0x89,0xf2,0xef,0x98,0xf0, - 0xd5,0xdf,0xac,0x05,0xd3,0xc6,0x33,0x39,0xb1,0x38,0x02,0x88,0x6d,0x53,0xfc,0x05}, - {0xc7,0x17,0x6a,0x70,0x3d,0x4d,0xd8,0x4f,0xba,0x3c,0x0b,0x76,0x0d,0x10,0x67,0x0f, - 0x2a,0x20,0x53,0xfa,0x2c,0x39,0xcc,0xc6,0x4e,0xc7,0xfd,0x77,0x92,0xac,0x03,0x7a}, - }; - const uint8_t msg[] = "untrusted firmware"; + * it passes. Both checks are required. + * + * The sweep is every low-order encoding as the key against every one as R, + * over eight messages, because a low-order key of order n forges for + * roughly one message in n -- a single fixed message would let a genuine + * bypass through this test. Measured against 13a7a02, the last commit + * before the subgroup check: 16 of the 64 (key, R) pairs were accepted by + * at least one message. Here: none. */ + for (size_t k = 0; k < sizeof(k_low_order) / sizeof(k_low_order[0]); k++) { + for (size_t r = 0; r < sizeof(k_low_order) / sizeof(k_low_order[0]); r++) { + for (size_t m = 0; m < sizeof(messages) / sizeof(messages[0]); m++) { + uint8_t sig[64]; + memset(sig, 0, sizeof(sig)); + memcpy(sig, k_low_order[r], 32); + ASSERT(eos_ed25519_verify(sig, k_low_order[k], + (const uint8_t *)messages[m], + strlen(messages[m])) != EOS_OK); + } + } + } +} - for (int k = 0; k < 4; k++) { - for (int r = 0; r < 4; r++) { +TEST(test_ed25519_non_canonical_encodings_rejected) +{ + /* These are refused before the subgroup check ever runs -- unpackneg() + * rejects them on canonicality, or because no x satisfies the curve + * equation. Pinned separately so that nobody deletes that path on the + * grounds that the subgroup test now covers it. It does not. */ + for (size_t k = 0; k < sizeof(k_non_canonical) / sizeof(k_non_canonical[0]); k++) { + for (size_t m = 0; m < sizeof(messages) / sizeof(messages[0]); m++) { uint8_t sig[64]; memset(sig, 0, sizeof(sig)); - memcpy(sig, low_order[r], 32); - ASSERT(eos_ed25519_verify(sig, low_order[k], - msg, sizeof(msg) - 1) != EOS_OK); + memcpy(sig, k_non_canonical[k], 32); + ASSERT(eos_ed25519_verify(sig, k_non_canonical[k], + (const uint8_t *)messages[m], + strlen(messages[m])) != EOS_OK); } } } @@ -260,21 +337,6 @@ TEST(test_ed25519_zero_signature_rejected) ASSERT(eos_ed25519_verify(sig, pk, msg, 1) != EOS_OK); } -TEST(test_ed25519_identity_key_forgery_rejected) -{ - /* The identity point has compressed encoding 01 00...00. With both the - * public key and R set to the identity and S set to zero, the verification - * equation is true for every message unless low-order keys are rejected. */ - uint8_t identity_pub[32] = {1}; - uint8_t identity_sig[64] = {1}; - const uint8_t msg[] = "untrusted firmware"; - - ASSERT(eos_ed25519_verify(identity_sig, identity_pub, - msg, sizeof(msg) - 1) != EOS_OK); -} - -/* ---- SHA-512, the hash Ed25519 is defined over (FIPS 180-4) ---- */ - TEST(test_ed25519_low_order_R_with_a_valid_key_is_not_a_forgery) { /* The subgroup check guards the public key, not R, and that is @@ -367,13 +429,13 @@ int main(void) run_test_ed25519_null_args(); run_test_ed25519_identity_key_forgery_rejected(); run_test_ed25519_low_order_keys_rejected(); + run_test_ed25519_non_canonical_encodings_rejected(); + run_test_ed25519_low_order_R_with_a_valid_key_is_not_a_forgery(); run_test_ed25519_zero_pubkey_rejected(); run_test_ed25519_zero_signature_rejected(); - run_test_ed25519_identity_key_forgery_rejected(); run_test_sha512_known_answers(); run_test_sha512_streaming_matches_one_shot(); - tests_run = 11; printf("\n%d/%d tests passed\n", tests_passed, tests_run); return (tests_passed == tests_run) ? 0 : 1; } From d712a65db7ca5d87b7d8fd6155b30a237d17ac54 Mon Sep 17 00:00:00 2001 From: kartikey1306 Date: Thu, 3 Sep 2026 16:16:19 +0530 Subject: [PATCH 2/2] ci: make the EoSim workflow able to fail for the reasons it claims to check Answers the review on #81. The install fix was right; the workflow it revives could not have failed for most of what it says it verifies. Finding 1 (Medium) -- the step named "Validate all platform configs" ran `eosim list && eosim doctor` with no platforms/ copy in that job. `eosim list` prints "Available platforms (0)" without it and exits 0, so the step passed having validated nothing -- and the same shape was in windows-sanity and macos-sanity. All three now copy platforms/ from the pinned checkout and assert a non-zero count via discover_platforms(), because the exit code is exactly what cannot be trusted here. Finding 2 (Medium) -- nested-guest-install cloned EoSim a second time with no --branch for its platforms/ copy, so the package came from v${EOSIM_VERSION} while the platform data came from whatever the default branch pointed at that morning. Now copies from the same pinned checkout; the second clone is gone. Finding 3 (Medium) -- `eosim --version` was printed and never asserted, while the tag and the package's declared version disagree upstream (v1.5.0 ships "eosim, version 2.0.0"). Pinning to a tag therefore does not pin what the name suggests, and nothing would have noticed if the tag moved. Now asserted. The mismatch itself is EoSim's bug and is raised there rather than worked around here. Finding 4 (Medium) -- sanity-gate failed only on install-validate and then printed "All EoSim sanity checks passed", which it would do with the other four jobs red. Replaced with the toJSON(needs) + jq body from ci.yml, which cannot fall out of step with `needs:`. #90 adds a test that enforces this across every gate in the repository; this gate passes it. Verified: yaml.safe_load of eosim-sanity.yml parses, 6 jobs the gate now iterates toJSON(needs), no longer branches on install-validate alone, and no longer prints an "all passed" claim #90's test_no_aggregating_gate_ignores_part_of_its_needs, run against this workflow: eosim-sanity.yml is not among its offenders pytest tests/ 38 passed ctest 21/21 PASS NOT RUN: the workflow itself. It is `on: schedule` + `workflow_dispatch` only, so none of this PR's checks execute it -- which is finding 5, and it is the one piece of evidence this PR cannot produce from a fork branch without a maintainer dispatching it. The install sequence was verified locally end to end (clone -> pip install -> eosim --version 2.0.0 -> doctor -> run am62x --headless PASSED); the assertions added here are not covered by that and remain unexecuted. Refs #81 --- .github/workflows/eosim-sanity.yml | 134 ++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 30 deletions(-) diff --git a/.github/workflows/eosim-sanity.yml b/.github/workflows/eosim-sanity.yml index bf17de7..e323860 100644 --- a/.github/workflows/eosim-sanity.yml +++ b/.github/workflows/eosim-sanity.yml @@ -6,7 +6,11 @@ on: workflow_dispatch: env: - EOSIM_VERSION: "0.1.0" + # v0.1.0 has never existed. embeddedos-org/EoSim tags v1.0.0 through + # v3.0.1, and no release publishes a wheel -- so the pip install below + # 404s and this workflow has never had a green run. v1.5.0 is the + # release marked Latest. + EOSIM_VERSION: "1.5.0" permissions: contents: read @@ -30,13 +34,47 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install EoSim - run: pip install "eosim @ https://github.com/embeddedos-org/EoSim/releases/download/v${{ env.EOSIM_VERSION }}/eosim-${{ env.EOSIM_VERSION }}-py3-none-any.whl" + run: git clone --depth 1 --branch v${{ env.EOSIM_VERSION }} https://github.com/embeddedos-org/EoSim.git /tmp/EoSim && pip install /tmp/EoSim - name: Verify installation run: | + # Assert, do not print. The tag and the package's declared version + # disagree upstream -- v1.5.0 ships "eosim, version 2.0.0" -- so + # pinning to a tag does not pin what the name suggests, and nothing + # here would notice if the tag moved. Raised against EoSim + # separately; asserted here so this workflow stops being the place + # it goes unnoticed. eosim --version - eosim list + eosim --version | grep -qE "2\.0\.0" || { + echo "::error::eosim --version is not the expected 2.0.0 for tag v${{ env.EOSIM_VERSION }}" + eosim --version + exit 1 + } + # platforms/ is data, not package data: PLATFORMS_DIR resolves to + # /platforms while the wheel ships only + # eosim/platforms/__init__.py. Without this copy `eosim list` prints + # "Available platforms (0)" and exits 0 -- so the step named + # "Validate all platform configs" validated nothing and passed. + - name: Install platform data + run: | + SITE_PACKAGES=$(python -c "import eosim, os; print(os.path.dirname(os.path.dirname(eosim.__file__)))") + cp -r /tmp/EoSim/platforms "$SITE_PACKAGES/" - name: Validate all platform configs - run: eosim list && eosim doctor + run: | + eosim doctor + # Assert a non-zero count rather than trusting the exit code: + # `eosim list` exits 0 with zero platforms, which is how this step + # passed while checking nothing. + python - <<'PY' + import os, sys + from eosim.platforms import discover_platforms + import eosim, pathlib + root = pathlib.Path(eosim.__file__).parent.parent / "platforms" + found = discover_platforms(str(root)) + print(f"discovered {len(found)} platforms under {root}") + if not found: + sys.exit("::error::no platform configs discovered; " + "this step would otherwise pass having validated nothing") + PY nested-simulation: name: Nested Simulation (${{ matrix.platform }}) @@ -60,10 +98,14 @@ jobs: python-version: "3.12" - name: Install EoSim run: | - pip install "eosim @ https://github.com/embeddedos-org/EoSim/releases/download/v${{ env.EOSIM_VERSION }}/eosim-${{ env.EOSIM_VERSION }}-py3-none-any.whl" - SITE_PACKAGES=$(python -c "import eosim; import os; print(os.path.dirname(os.path.dirname(eosim.__file__)))") - git clone --depth 1 https://github.com/embeddedos-org/EoSim.git /tmp/eosim-data - cp -r /tmp/eosim-data/platforms "$SITE_PACKAGES/" + # Install from a git checkout: no release ships a wheel. + git clone --depth 1 --branch v${{ env.EOSIM_VERSION }} \ + https://github.com/embeddedos-org/EoSim.git /tmp/EoSim + pip install /tmp/EoSim + # platforms/ is data, not package data, so `eosim list` reports + # zero platforms without it. Copy it from the same checkout. + SITE_PACKAGES=$(python -c "import eosim, os; print(os.path.dirname(os.path.dirname(eosim.__file__)))") + cp -r /tmp/EoSim/platforms "$SITE_PACKAGES/" - name: Simulate ${{ matrix.platform }} run: | eosim run ${{ matrix.platform }} --headless --timeout 10 @@ -90,10 +132,12 @@ jobs: python-version: "3.12" - name: Install EoSim run: | - pip install "eosim @ https://github.com/embeddedos-org/EoSim/releases/download/v${{ env.EOSIM_VERSION }}/eosim-${{ env.EOSIM_VERSION }}-py3-none-any.whl" + git clone --depth 1 --branch v${{ env.EOSIM_VERSION }} https://github.com/embeddedos-org/EoSim.git /tmp/EoSim && pip install /tmp/EoSim SITE_PACKAGES=$(python -c "import eosim; import os; print(os.path.dirname(os.path.dirname(eosim.__file__)))") - git clone --depth 1 https://github.com/embeddedos-org/EoSim.git /tmp/eosim-data - cp -r /tmp/eosim-data/platforms "$SITE_PACKAGES/" + # From the same pinned checkout. A second, unpinned clone here meant + # the package came from v${{ env.EOSIM_VERSION }} while platforms/ + # came from whatever the default branch pointed at that morning. + cp -r /tmp/EoSim/platforms "$SITE_PACKAGES/" - name: Boot guest and test EoSim inside run: | echo "=== Nested Guest: ${{ matrix.guest }} ===" @@ -109,12 +153,26 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + # `eosim list` exits 0 with zero platforms, so "list && doctor" passed + # on both these runners while validating nothing. Copy platforms/ from + # the same pinned checkout and assert a non-zero count, as + # install-validate now does. - name: Test EoSim on Windows + shell: bash run: | - pip install "eosim @ https://github.com/embeddedos-org/EoSim/releases/download/v${{ env.EOSIM_VERSION }}/eosim-${{ env.EOSIM_VERSION }}-py3-none-any.whl" + git clone --depth 1 --branch v${{ env.EOSIM_VERSION }} https://github.com/embeddedos-org/EoSim.git /tmp/EoSim && pip install /tmp/EoSim + SITE_PACKAGES=$(python -c "import eosim, os; print(os.path.dirname(os.path.dirname(eosim.__file__)))") + cp -r /tmp/EoSim/platforms "$SITE_PACKAGES/" eosim --version - eosim list && eosim doctor - eosim list + eosim doctor + python -c " + import pathlib, sys, eosim + from eosim.platforms import discover_platforms + root = pathlib.Path(eosim.__file__).parent.parent / 'platforms' + found = discover_platforms(str(root)) + print(f'discovered {len(found)} platforms') + sys.exit(0 if found else 'no platform configs discovered') + " macos-sanity: name: macOS Sanity @@ -126,11 +184,21 @@ jobs: with: python-version: "3.12" - name: Test EoSim on macOS + shell: bash run: | - pip install "eosim @ https://github.com/embeddedos-org/EoSim/releases/download/v${{ env.EOSIM_VERSION }}/eosim-${{ env.EOSIM_VERSION }}-py3-none-any.whl" + git clone --depth 1 --branch v${{ env.EOSIM_VERSION }} https://github.com/embeddedos-org/EoSim.git /tmp/EoSim && pip install /tmp/EoSim + SITE_PACKAGES=$(python -c "import eosim, os; print(os.path.dirname(os.path.dirname(eosim.__file__)))") + cp -r /tmp/EoSim/platforms "$SITE_PACKAGES/" eosim --version - eosim list && eosim doctor - eosim list + eosim doctor + python -c " + import pathlib, sys, eosim + from eosim.platforms import discover_platforms + root = pathlib.Path(eosim.__file__).parent.parent / 'platforms' + found = discover_platforms(str(root)) + print(f'discovered {len(found)} platforms') + sys.exit(0 if found else 'no platform configs discovered') + " sanity-gate: name: EoSim Sanity Gate @@ -138,18 +206,24 @@ jobs: needs: [install-validate, nested-simulation, nested-guest-install, windows-sanity, macos-sanity] runs-on: ubuntu-latest steps: - - name: Results + # Iterates toJSON(needs) rather than naming one dependency. The + # previous body tested install-validate alone and then printed "All + # EoSim sanity checks passed" -- which it would do with the other four + # jobs red. This workflow has never had a green run, so this gate is + # about to start mattering for the first time; a summary that asserts + # more than it checked is the wrong thing to start with. + - name: Every job in this workflow must have succeeded + env: + RESULTS: ${{ toJSON(needs) }} run: | - echo "════════════════════════════════════════════════" - echo " EoSim Sanity Results" - echo "════════════════════════════════════════════════" - echo "Install & Validate (3 OS × 3 Py): ${{ needs.install-validate.result }}" - echo "Nested Simulation (7 platforms): ${{ needs.nested-simulation.result }}" - echo "Nested Guest Install (3 guests): ${{ needs.nested-guest-install.result }}" - echo "Windows Sanity: ${{ needs.windows-sanity.result }}" - echo "macOS Sanity: ${{ needs.macos-sanity.result }}" - echo "════════════════════════════════════════════════" - if [ "${{ needs.install-validate.result }}" != "success" ]; then - echo "❌ Install/validate failed"; exit 1 + printf '%s\n' "$RESULTS" + bad=$(printf '%s' "$RESULTS" | jq -r ' + to_entries[] + | select(.value.result != "success") + | " \(.key): \(.value.result)"') + if [ -n "$bad" ]; then + echo "::error::EoSim Sanity Gate failed. These jobs did not succeed:" + printf '%s\n' "$bad" + exit 1 fi - echo "✅ All EoSim sanity checks passed" + echo "All EoSim sanity jobs succeeded."