Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
471d38b
ci: fail when the vendored core/ snapshots drift from upstream
srpatcha Aug 28, 2026
b0a6422
fix: restore the package import and reconcile the merged backend beha…
Kartikey1306 Aug 28, 2026
e6cb4f9
ci: make the Windows leg of the test matrix able to run at all
Kartikey1306 Aug 28, 2026
49cd368
fix(ninja): escape paths in build statements so Windows builds are valid
Kartikey1306 Aug 28, 2026
842ca8d
ci: add a concurrency group so superseded runs stop holding runners
Kartikey1306 Aug 28, 2026
14928ab
ci: move the test matrix off the retired macos-13 runner image
Kartikey1306 Aug 28, 2026
53ca57f
fix(packages): make the third-party supply chain verifiable, and its …
Kartikey1306 Aug 28, 2026
73e4f43
chore: drop an accidentally committed _build/ directory
Kartikey1306 Aug 28, 2026
10faf65
fix(packages): stop a non-numeric version taking down registry lookup
bobamantra Aug 28, 2026
7a57abd
Fix string termination in node name assignment
Akhil498-coder Aug 28, 2026
9d5831e
Implement test for max-length node name in graph
Akhil498-coder Aug 28, 2026
8e52709
Initialize node name with non-zero data in test
Akhil498-coder Aug 28, 2026
e9a719c
config: reject a non-list packages section instead of dropping it
JoaoMorais03 Aug 29, 2026
527c0bb
fix(deps): resolve sibling eBoot as well as eboot
ShrenikMensinkai Aug 29, 2026
a260ac2
Merge branch 'e68' into HEAD
allanturner1234 Aug 29, 2026
57fff5b
Merge branch 'e69' into HEAD
allanturner1234 Aug 29, 2026
4a307b2
Merge branch 'e73' into HEAD
allanturner1234 Aug 29, 2026
996a028
Merge branch 'e76' into HEAD
allanturner1234 Aug 29, 2026
d844ebf
Merge branch 'e63' into HEAD
allanturner1234 Aug 29, 2026
23dc8c1
feat(build): report flash and RAM footprint after a build
srpatcha Aug 29, 2026
4e900af
feat(cli): add `ebuild doctor` — one-command environment diagnosis
srpatcha Aug 29, 2026
3a46f1e
feat(cli): refuse packages that cannot resolve, and summarise what th…
srpatcha Aug 29, 2026
9693375
feat(cli): add `ebuild package` — assemble the eFirmware .efw image
srpatcha Aug 30, 2026
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
28 changes: 26 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ on:
pull_request:
branches: [master, main]

# Every other workflow in this repo declares a concurrency group; ci.yml,
# the heaviest one, did not. Pushing twice to a PR left the earlier run
# queued, and both competed for the same scarce windows/macos runners --
# three superseded runs sat ahead of the current one for over an hour.
# cancel-in-progress because a superseded commit's result is not wanted.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# ── Test Matrix ───────────────────────────────────────────────────────────
test:
Expand All @@ -17,7 +26,12 @@ jobs:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-22.04, macos-13, windows-2022]
# macos-latest, not macos-13: the macos-13 image is retired, so jobs
# requesting it queue forever and never get a runner. Across four runs
# on this branch the ubuntu-22.04 and windows-2022 jobs all started and
# finished while the three macos-13 jobs sat queued for over two hours.
# Every other workflow in this repo already uses macos-latest.
os: [ubuntu-22.04, macos-latest, windows-2022]
fail-fast: false

steps:
Expand All @@ -42,8 +56,12 @@ jobs:
run: ruff check . --select=E,F,W --ignore=E501
continue-on-error: true

# --exclude: layers/eosuite/ vendors its own tests/ package, so a bare
# `mypy .` sees two modules named "tests" and bails with "Duplicate
# module named 'tests'" before checking anything. continue-on-error hid
# that the type check was doing no work at all.
- name: Type check (mypy)
run: mypy . --ignore-missing-imports --no-strict-optional
run: mypy . --ignore-missing-imports --no-strict-optional --exclude '^layers/'
continue-on-error: true

# Runs the whole tests/ tree. The previous steps ran only tests/unit/
Expand All @@ -57,7 +75,13 @@ jobs:
# coverage policy already lives in codecov.yml; whether to also
# enforce it here is a maintainer decision, so this change leaves
# both of those numbers alone.
#
# shell: bash — the matrix includes windows-2022, where the default
# shell is PowerShell and the backslash line continuations below are a
# syntax error ("Missing expression after unary operator '--'"), so the
# Windows leg of this job failed before pytest ever started.
- name: Run test suite
shell: bash
run: |
python -m pytest tests/ -v --tb=short \
--cov=ebuild --cov-report=xml --cov-report=term-missing \
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/vendor-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Vendored core drift

# core/eos/ and core/eboot/ are snapshots of other repositories in this
# organisation. Fixes merged upstream never reach them, so they drift silently.
# This job makes that drift visible and fails when it grows.
#
# See core/UPSTREAM.yaml for the pins, and ADR-019 in the eos repository for the
# decision to replace the snapshots with real dependencies.

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
paths:
- 'core/**'
- 'scripts/check_vendor_drift.py'
- '.github/workflows/vendor-drift.yml'
schedule:
# Weekly, so upstream moving on its own is noticed even when nothing here changes.
- cron: '0 5 * * 1'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
drift:
name: Compare core/ against pinned upstreams
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Check vendored snapshots
run: python3 scripts/check_vendor_drift.py --list

- name: Explain a failure
if: failure()
run: |
echo "::notice::A file under core/eos or core/eboot now differs from its"
echo "::notice::pinned upstream in a way it did not before."
echo "::notice::"
echo "::notice::Fix it in one of these two ways:"
echo "::notice:: 1. The change belongs upstream — open a PR against"
echo "::notice:: embeddedos-org/eos or embeddedos-org/eBoot, merge it, then"
echo "::notice:: bump the revision in core/UPSTREAM.yaml."
echo "::notice:: 2. The change was unintended — revert it here."
echo "::notice::"
echo "::notice::Raising baseline_drift is not one of the two ways."
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ __pycache__/
*.egg-info/
*.egg
dist/
build/
# Anchored: the bare pattern also matched ebuild/build/, the source package,
# so any new module added there was silently untracked.
/build/
.eggs/
*.whl

Expand Down Expand Up @@ -64,3 +66,4 @@ Desktop.ini
build-*/
node_modules/
target/
_build/
24 changes: 24 additions & 0 deletions core/UPSTREAM.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Upstream pins for the vendored snapshots under core/.
#
# core/eos/ and core/eboot/ are copies of other repositories in this
# organisation, not original source. ADR-019 in the eos repository records the
# decision to replace them with real pinned dependencies. Until that lands, this
# file is the pin and scripts/check_vendor_drift.py enforces it.
#
# baseline_drift is the number of files that already differed from the pinned
# revision when the guard was introduced. It is grandfathered so the guard could
# be merged without blocking work already in flight, and it must only ever go
# down. Reconciling a file means sending the change upstream or reverting it
# here — see ADR-019 decision item 3. Raising this number to make CI pass
# defeats the entire point of the file.

vendored:
- path: core/eos
repository: https://github.com/embeddedos-org/eos.git
revision: 5544c98df94ff460c3a074fb4280c614b784b464
baseline_drift: 44

- path: core/eboot
repository: https://github.com/embeddedos-org/eBoot.git
revision: 39b09253450b3789b66f64ff0465b03b7910295d
baseline_drift: 46
1 change: 1 addition & 0 deletions core/eos/core/src/graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ EosResult eos_graph_add_node(EosGraph *g, const char *name, EosNodeType type,
int id = g->node_count;
EosNode *n = &g->nodes[id];
strncpy(n->name, name, EOS_MAX_NAME - 1);
n->name[EOS_MAX_NAME-1] = '\0';
n->type = type;
n->build_type = build_type;
n->status = EOS_NODE_PENDING;
Expand Down
40 changes: 40 additions & 0 deletions core/eos/tests/test_graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,46 @@ static void test_graph_add_nodes(void) {

ASSERT(g.node_count == 3, "graph has 3 nodes");
}
static void test_graph_max_length_node_name(void) {
printf("test_graph_max_length_node_name:\n");

EosGraph g;
eos_graph_init(&g);

char name[EOS_MAX_NAME];

/* Create a name that fills the buffer except for the null terminator. */
memset(name, 'A', EOS_MAX_NAME - 1);
name[EOS_MAX_NAME - 1] = '\0';

/*
* Fill the destination with non-zero data so the test can detect
* a missing null terminator even though eos_graph_init() zeroes
* the graph initially.
*/
memset(g.nodes[0].name, 'X', EOS_MAX_NAME);

int id;
EosResult r = eos_graph_add_node(
&g,
name,
EOS_NODE_PACKAGE,
EOS_BUILD_CMAKE,
&id
);

ASSERT(r == EOS_OK, "maximum-length node name can be added");

ASSERT(
g.nodes[id].name[EOS_MAX_NAME - 1] == '\0',
"node name is null terminated"
);

ASSERT(
eos_graph_find_node(&g, name) == id,
"maximum-length node name can be found"
);
}
static void test_graph_find_node(void) {
printf("test_graph_find_node:\n");
EosGraph g;
Expand Down Expand Up @@ -168,6 +207,7 @@ int main(void) {
test_graph_init();
test_graph_add_nodes();
test_graph_find_node();
test_graph_max_length_node_name();
test_topological_sort_linear();
test_topological_sort_diamond();
test_cycle_detection();
Expand Down
17 changes: 17 additions & 0 deletions docs/book/book.md
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,23 @@ Request: freertos >= 10.5, mbedtls ^3.0
+-------------------+
```

#### Version ordering

A request without a version resolves to the highest version of that package in
the registry. `version` is a free-form string — the recipe format does not
require dotted integers — so the ordering is defined as follows:

| Rule | Example |
|---|---|
| A leading `v` or `V` is ignored | `v2.9.3` ranks with `2.9.3` |
| All-digit components compare numerically | `1.10.0` > `1.9.0` |
| Any other component compares as text, below any numeric one | `1.x` < `1.0` |
| A `-` or `+` suffix ranks below the same version without one | `3.6.0-rc1` < `3.6.0` |

Every version string has a place in this order, including ones that carry no
numbers at all (`main`), so one unusual recipe cannot break lookup for the
packages around it.

---

## Chapter 13: SDK Generation
Expand Down
56 changes: 31 additions & 25 deletions ebuild/build/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@

ALL_BACKENDS = {"cmake", "make", "meson", "cargo", "kbuild", "ninja"}

#: Backends this dispatcher actually drives. "ninja" is ebuild's own backend --
#: the CLI invokes NinjaBackend directly and never routes it through here.
DISPATCHED_BACKENDS = {"cmake", "make", "meson", "cargo", "kbuild"}


class UnknownBackendError(ValueError, RuntimeError):
"""Raised when a backend reaches the dispatcher that it cannot drive.

Subclasses both ValueError and RuntimeError: callers treat an unrecognized
backend name as a bad argument, while the CLI treats a backend it failed to
route (notably "ninja") as a routing failure. Silently doing nothing here is
what made `ebuild build` report "Build completed successfully" without ever
running a compiler.
"""


def _unknown_backend(backend: str, step: str) -> UnknownBackendError:
return UnknownBackendError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(DISPATCHED_BACKENDS))}. "
"ebuild's own 'ninja' backend is invoked directly by the CLI and is "
f"not dispatched here, so it cannot be {step} through BackendDispatcher."
)


def detect_backend(source_dir: Path) -> str:
"""Auto-detect the build system from project files.
Expand Down Expand Up @@ -100,7 +124,7 @@ def configure(
dry_run: If True, log commands instead of executing them.

Raises:
ValueError: If the backend is not recognized.
UnknownBackendError: If the backend is not one this dispatcher drives.
"""
config = config or {}
self.build_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -121,23 +145,11 @@ def configure(
elif backend == "cargo":
pass # Cargo does not have a separate configure step

elif backend in ("make", "kbuild", "ninja"):
elif backend in ("make", "kbuild"):
pass # No separate configure step

else:
raise ValueError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}"
)

else:
raise RuntimeError(
f"BackendDispatcher cannot configure backend '{backend}'. "
"This dispatcher only handles cmake, meson, and cargo "
"(make/kbuild need no configure step). ebuild's own ninja "
"backend is invoked directly and requires 'targets' in "
"build.yaml -- add targets or choose another backend."
)
raise _unknown_backend(backend, "configured")

def build(
self,
Expand All @@ -154,7 +166,7 @@ def build(
dry_run: If True, log commands instead of executing them.

Raises:
ValueError: If the backend is not recognized.
UnknownBackendError: If the backend is not one this dispatcher drives.
"""
config = config or {}

Expand Down Expand Up @@ -185,10 +197,7 @@ def build(
_run_or_log(cmd, dry_run)

else:
raise ValueError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}"
)
raise _unknown_backend(backend, "built")

def clean(
self,
Expand All @@ -203,7 +212,7 @@ def clean(
dry_run: If True, log commands instead of executing them.

Raises:
ValueError: If the backend is not recognized.
UnknownBackendError: If the backend is not one this dispatcher drives.
"""
if backend == "cmake":
_run_or_log(
Expand Down Expand Up @@ -237,7 +246,4 @@ def clean(
check=False,
)
else:
raise ValueError(
f"Unknown build backend '{backend}'. "
f"Supported backends: {', '.join(sorted(ALL_BACKENDS))}"
)
raise _unknown_backend(backend, "cleaned")
Loading
Loading