1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
name: CI
on:
push:
branches:
pull_request:
branches:
merge_group:
# Every public feature EXCEPT `bare-metal-runtime`. That feature is no-alloc
# and pulls a nightly-only crate feature (`impl_trait_in_assoc_type`), so it
# is mutually exclusive with the alloc features (`std` / `_alloc` /
# `embassy_channels`) and cannot share a `--all-features` build — it gets its
# own nightly job (`bare-metal-runtime`). This list replaces the former
# `--all-features` invocations on the alloc/host lane; keep it in sync when a
# feature is added (or switch to `cargo hack --exclude-features bare-metal-runtime`).
env:
ALLOC_FEATURES: std,tracing,client,client-tokio,server,server-tokio,bare_metal,embassy_channels
# Host/std feature set: `$ALLOC_FEATURES` minus the bare-metal flags
# (`bare_metal` + `embassy_channels`, which implies `bare_metal`). The
# server's runtime caps (`SUBSCRIBERS_PER_GROUP` etc.) share one set of
# consts whose *default* is tight under `bare_metal` and generous
# otherwise, so the std host tests must build WITHOUT `bare_metal` to get
# the generous defaults; the bare-metal-gated tests run separately at the
# tight defaults. The two default regimes cannot be unified into one build.
HOST_FEATURES: std,tracing,client,client-tokio,server,server-tokio
jobs:
check:
name: Format & Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all --check
# `--workspace --all-features` activates every feature on every
# workspace member through cargo's feature unification, which
# gives strong "max coverage" but means that, e.g., a clippy
# regression triggered only by smoltcp's `proto-ipv6` (pulled in
# transitively via the embassy-net adapter under all-features)
# blocks merges on the parent simple-someip crate. The explicit
# per-feature passes below run clippy on `simple-someip` alone
# under the feature combos we actually ship, so a feature-set
# regression surfaces against its responsible feature flag
# rather than as workspace-wide noise.
#
# `$ALLOC_FEATURES` is every feature except `bare-metal-runtime` (which
# is no-alloc + nightly and runs in the `bare-metal-runtime` job); it
# replaces the former `--all-features` pass, which can no longer build
# once a no-alloc-only feature exists.
- run: cargo clippy --workspace --no-default-features --features $ALLOC_FEATURES -- -D warnings -D clippy::pedantic
- run: cargo clippy --no-default-features -- -D warnings -D clippy::pedantic
- run: cargo clippy -p simple-someip --no-default-features --features client,bare_metal -- -D warnings -D clippy::pedantic
- run: cargo clippy -p simple-someip --no-default-features --features server,bare_metal -- -D warnings -D clippy::pedantic
- run: cargo clippy -p simple-someip --no-default-features --features client,server,bare_metal -- -D warnings -D clippy::pedantic
bare-metal-runtime:
name: Bare-metal runtime (nightly)
runs-on: ubuntu-latest
# The `bare-metal-runtime` feature is no-alloc and pulls the nightly-only
# `impl_trait_in_assoc_type` crate feature (embassy's static task pool), so
# it cannot be built on stable or alongside the alloc features the
# `check`/coverage jobs cover. Its own nightly lane: clippy under the
# feature combos it ships, plus a real no_std target build via build-std.
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
with:
components: clippy, rust-src
targets: thumbv7em-none-eabihf
- uses: Swatinem/rust-cache@v2
- run: cargo clippy -p simple-someip --no-default-features --features bare-metal-runtime,client -- -D warnings -D clippy::pedantic
- run: cargo clippy -p simple-someip --no-default-features --features bare-metal-runtime,server -- -D warnings -D clippy::pedantic
# Compile for a real no_std target (no prebuilt core) — the gate that
# proves the runtime stays no_std / no-alloc.
- run: cargo build -Z build-std=core --target thumbv7em-none-eabihf --no-default-features --features bare-metal-runtime,client
- run: cargo build -Z build-std=core --target thumbv7em-none-eabihf --no-default-features --features bare-metal-runtime,server
- name: Doc — bare-metal-runtime
env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --no-deps --no-default-features --features bare-metal-runtime,client
linear-history:
name: Linear PR History
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Check for merge commits
run: |
merge_commits=$(git rev-list --merges origin/main..HEAD)
if [ -n "$merge_commits" ]; then
echo "::error::PR branch contains merge commits. Please rebase instead."
echo "$merge_commits"
exit 1
fi
semver:
name: SemVer Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Scope to the alloc/host feature set: the default `all-features`
# group pulls `bare-metal-runtime`, which is no-alloc + nightly and
# fails to build (and document) alongside the alloc features, so
# rustdoc — and thus the whole check — aborts. The host API is what
# we semver-gate; the bare-metal lane is nightly-only.
- uses: obi1kenobi/cargo-semver-checks-action@v2
with:
feature-group: only-explicit-features
features: std,tracing,client,client-tokio,server,server-tokio,bare_metal,embassy_channels
no_std_target:
# Cross-build for a true no_std target (cortex-m4f, no allocator,
# no std). This is the literal phase-18 gate from
# `bare_metal_plan_v3.md`: phases 4–17 shipped the trait surface
# and no-alloc primitives, but until this job is green the crate
# cannot actually be consumed on cortex-m. Each combination here
# is a separate `cargo build` so a failure surfaces the specific
# feature combo that regressed.
#
# Builds every bare-metal feature combo against the prebuilt
# thumbv7em sysroot, then audits the rlibs for allocator symbols.
# Since PR #124 BOTH `client + bare_metal` and `server +
# bare_metal` must be alloc-free; the build_std_core job below is
# the stronger sysroot-level certification, this audit is the
# cheap stable-toolchain tripwire that names the offending symbol
# when something regresses.
name: no_std target build (thumbv7em-none-eabihf)
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: thumbv7em-none-eabihf
- uses: Swatinem/rust-cache@v2
- name: bare_metal alone
run: cargo build --target thumbv7em-none-eabihf --no-default-features --features bare_metal
- name: server + bare_metal
run: cargo build --target thumbv7em-none-eabihf --no-default-features --features server,bare_metal
- name: client + server + bare_metal
run: cargo build --target thumbv7em-none-eabihf --no-default-features --features client,server,bare_metal
# Each audited combo below is a `cargo clean -p` + build
# immediately followed by its own alloc-symbol audit, so the
# rlib in target/thumbv7em-none-eabihf/debug/ is guaranteed to
# come from exactly that feature set when the audit reads it.
- name: client + bare_metal
run: |
# Invalidate the cargo fingerprint for any prior `simple-someip`
# rlib in this target so the audit step sees an artifact built
# under exactly `client,bare_metal` and not a leftover from
# `bare_metal alone` / `server + bare_metal` / `client + server +
# bare_metal` — `rm -f` of just the rlib does NOT invalidate the
# fingerprint, so the next build would no-op without rewriting
# it. `cargo clean -p` does both in one step.
cargo clean -p simple-someip --target thumbv7em-none-eabihf
cargo build --target thumbv7em-none-eabihf --no-default-features --features client,bare_metal
- name: alloc-symbol audit (client + bare_metal must be alloc-free)
# If `client + bare_metal` ever starts pulling `__rust_alloc`,
# something inside the client engine has regressed onto an
# allocator-bound primitive. Fail loudly so it gets caught in
# the PR rather than discovered downstream. (`server +
# bare_metal` gets the same audit below; the combined
# `client+server` build is covered by the build_std_core job.)
run: |
# Pin to the exact rlib path. `find ... | head -1` was
# nondeterministic and silently picked up stale debug-script
# artifacts. With `cargo clean -p` above, this path is
# guaranteed to be the artifact built by the previous step.
rlib="target/thumbv7em-none-eabihf/debug/libsimple_someip.rlib"
if [ ! -f "$rlib" ]; then
echo "::error::expected rlib not found at $rlib"
ls -la target/thumbv7em-none-eabihf/debug/ || true
exit 1
fi
# No `2>/dev/null` on `nm`: a tool failure (e.g. missing
# binutils, malformed rlib) used to swallow the error and
# report 0 alloc refs, silently letting a regression through.
# `set -o pipefail` plus visible stderr makes that loud.
set -o pipefail
alloc_refs=$(nm -A "$rlib" | grep -c -E '__rust_alloc|__rg_alloc' || true)
echo "client+bare_metal alloc-symbol references: $alloc_refs"
if [ "$alloc_refs" -ne 0 ]; then
echo "::error::client+bare_metal must be alloc-free; found $alloc_refs alloc references."
nm -A "$rlib" | grep -E '__rust_alloc|__rg_alloc' || true
exit 1
fi
- name: server + bare_metal rebuild for audit
run: |
# Same fingerprint-invalidation rationale as the client
# audit above: `cargo clean -p` guarantees the rlib below
# was built under exactly `server,bare_metal`.
cargo clean -p simple-someip --target thumbv7em-none-eabihf
cargo build --target thumbv7em-none-eabihf --no-default-features --features server,bare_metal
- name: alloc-symbol audit (server + bare_metal must be alloc-free)
# Alloc-free since PR #124 (phase 22). A regression here means
# something in the server engine reacquired an allocator-bound
# primitive — fail loudly and name the symbols.
run: |
rlib="target/thumbv7em-none-eabihf/debug/libsimple_someip.rlib"
if [ ! -f "$rlib" ]; then
echo "::error::expected rlib not found at $rlib"
ls -la target/thumbv7em-none-eabihf/debug/ || true
exit 1
fi
set -o pipefail
alloc_refs=$(nm -A "$rlib" | grep -c -E '__rust_alloc|__rg_alloc' || true)
echo "server+bare_metal alloc-symbol references: $alloc_refs"
if [ "$alloc_refs" -ne 0 ]; then
echo "::error::server+bare_metal must be alloc-free; found $alloc_refs alloc references."
nm -A "$rlib" | grep -E '__rust_alloc|__rg_alloc' || true
exit 1
fi
build_std_core:
# Halo's TC4 proxy compiles with `-Zbuild-std=core`: `alloc` is
# absent from the sysroot entirely. The prebuilt-sysroot thumb job
# above SHIPS alloc, so an `extern crate alloc` regression passes
# there and still breaks halo. This job is the halo-certification
# gate (phase 22 / measurement PR 0 — see
# docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md).
name: build-std core gate (no alloc in sysroot)
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
targets: thumbv7em-none-eabihf
- uses: Swatinem/rust-cache@v2
- name: client + bare_metal
run: cargo +nightly build --no-default-features --features client,bare_metal -Zbuild-std=core --target thumbv7em-none-eabihf
- name: server + bare_metal
run: cargo +nightly build --no-default-features --features server,bare_metal -Zbuild-std=core --target thumbv7em-none-eabihf
- name: client + server + bare_metal
run: cargo +nightly build --no-default-features --features client,server,bare_metal -Zbuild-std=core --target thumbv7em-none-eabihf
test:
name: Build, Test & Coverage
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov, cargo-nextest
- run: cargo test --no-default-features
- name: Build matrix — partial feature subsets
run: |
cargo build --no-default-features --features bare_metal
cargo build --no-default-features --features embassy_channels
cargo build --no-default-features --features client
cargo build --no-default-features --features server
cargo build --no-default-features --features client,server
- name: Doc — partial feature subsets (catch unresolved intra-doc links)
env:
RUSTDOCFLAGS: -D warnings
run: |
cargo doc --no-deps --no-default-features --features client
cargo doc --no-deps --no-default-features --features server,bare_metal
# alloc/host lane (bare-metal-runtime docs build in its nightly job)
cargo doc --no-deps --no-default-features --features $ALLOC_FEATURES
- name: No-alloc witness (explicit gate)
run: cargo test --features client,bare_metal --test no_alloc_witness
- name: embassy-net adapter (reference no_std backend)
# The `check` job's `--workspace` clippy checks this crate's *library*
# but never its test target (clippy runs without `--all-targets`, and
# the root-package `cargo test` steps above don't select `-p
# simple-someip-embassy-net`). Its `tests/loopback.rs` host harness
# therefore compiled nowhere in CI and silently bit-rotted against the
# client API. This step builds AND runs it (the loopback tests bridge
# two embassy-net stacks in-process — no TUN, no privilege) so the
# reference adapter stays wired to the current API.
run: cargo test -p simple-someip-embassy-net
- name: SD wire-format conformance (TX direction)
# `tx_announcement_loop_emits_wire_format_offer` is `#[ignore]`'d by
# default because it needs an interface with the `MULTICAST` link
# flag. CI's `lo` lacks it; flip it on, point the test at
# 127.0.0.1, and run just this one test (the rest of the file's
# ignored tests need an external vsomeip docker container — they
# stay skipped).
run: |
sudo ip link set lo multicast on
SIMPLE_SOMEIP_TEST_INTERFACE=127.0.0.1 \
cargo test --features client-tokio,server-tokio \
--test vsomeip_sd_compat \
tx_announcement_loop_emits_wire_format_offer \
-- --ignored --exact --nocapture
# Coverage is split so the std-default and bare-metal-default cap regimes
# are each exercised under their own feature config (they share one set of
# consts and cannot be unified into a single build — see `HOST_FEATURES`):
# 1. the host/std surface at the generous std default caps, and
# 2. the bare-metal-gated test binaries at the tight bare-metal defaults
# (those binaries only build under the full alloc feature set).
# `--no-report` accumulates both into one profile; `report` emits the
# merged lcov. (`bare-metal-runtime` has no host tests — its target build
# is gated in the nightly `bare-metal-runtime` job.) The bare-metal run
# goes first so the host run's `junit.xml` (the comprehensive suite) is
# the one the test-results upload below picks up.
- run: cargo llvm-cov --no-report nextest --no-default-features --features $ALLOC_FEATURES -E 'binary(bare_metal_e2e) | binary(bare_metal_client_local) | binary(static_channels_alloc_witness)'
- run: cargo llvm-cov --no-report nextest --no-default-features --features $HOST_FEATURES
- run: cargo llvm-cov report --lcov --output-path ./target/lcov.info
- name: Upload Coverage report
uses: codecov/codecov-action@v5
with:
report_type: coverage
files: ./target/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Upload test results to Codecov
uses: codecov/codecov-action@v5
with:
report_type: test_results
files: ./target/nextest/default/junit.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
test-windows:
name: Build & Test (Windows)
# Cross-platform socket-bind coverage: the SD discovery path binds a
# wildcard multicast socket alongside an interface-IP unicast socket and
# relies on "most-specific bind wins" to divert unicast SD (see
# `socket_manager::dual_socket_splits_multicast_from_unicast`). That divert
# is the portability risk. No coverage/codecov here — purely the
# OS-portability signal.
#
# Two steps:
# 1. `--no-run` compiles everything (incl. the `tests/` integration suite)
# to prove the crate *builds* on Windows.
# 2. `--lib` runs the library unit tests, which include the dual-socket
# divert assertion — the behavior we actually need to confirm on Windows.
# The `tests/client_server.rs` integration suite is compiled but NOT run
# here: it binds fixed SD ports and is flaky under parallel execution on
# every platform (reproduces on `main`; tracked separately, #84), so running
# it would add cross-platform noise unrelated to this fix.
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Use `$HOST_FEATURES` (no `bare_metal`): this is a std/host portability
# check, and the server cap consts default tight under `bare_metal` —
# building with it would cap the `--lib` server tests to one subscriber
# per group and fail them. The dual-socket divert assertion lives in the
# `--lib` unit tests, which `$HOST_FEATURES` (client) still builds.
# `shell: bash` so `$HOST_FEATURES` expands on the Windows runner
# (default pwsh would not).
- run: cargo test --no-default-features --features $HOST_FEATURES --no-run
shell: bash
- run: cargo test --no-default-features --features $HOST_FEATURES --lib
shell: bash