September 29, 2026
2 pull requests merged across 1 repo
Offline-Protocol/offline-protocol-sdk
What
The iOS static library held 25 objects stamped for the newest iOS the release machine's Xcode knew. Every application linking the pod got one linker warning for each:
ld: warning: object file (...curve25519.o) was built for newer 'iOS-simulator' version (26.5) than being linked (14.0)
The build now hands the pod's own deployment target to every compiler, and refuses to package an archive that holds an object built for anything newer.
Why it happened
Two compilers read IPHONEOS_DEPLOYMENT_TARGET, and they fall back differently when it is unset.
| Compiler | Builds | Falls back to |
|---|---|---|
| rustc | the Rust crates | the Rust target's floor, iOS 10.0 |
| cc-rs | the C and assembly in ring and oslog | the version of the installed SDK |
build-uniffi-ios.sh never set the variable.
What changes
- The target is read from the podspec, not written again.
MeshSdk.podspecstays the one place it lives. - A gate reads every object in every slice after the build and before packaging.
- The arm64 simulator slice is held to the target or to 14.0, whichever is newer. No arm64 simulator exists before 14.0, and the toolchain raises the slice to it.
- An archive the gate cannot read fails. That covers an archive with no stamp in it, and one
otoolread part of before giving up. - The SDK's own Rust code is now compiled for iOS 13.0, where it was compiled for 10.0. This is the one effect beyond the warning.
Measured
Minimum OS declared by the objects in each archive, before and after:
| Slice | Before | After |
|---|---|---|
| Device, arm64 | 591 at 10.0, 25 at 26.5 | 390 at 10.0, 229 at 13.0 |
| Simulator, arm64 | 594 at 14.0, 25 at 26.5 | 619 at 14.0 |
| Simulator, x86_64 | not measured | 295 at 10.0, 237 at 13.0 |
Verification
bash scripts/tests/test-ios-min-os.sh: 50 assertions, green under BSD awk, gawk and mawk.- 14 mutants of the gate and the build script, run in scratch copies: all killed.
- The real gate against the real archives: all three pass. Against the archive built before this change: 25 offenders reported.
build-uniffi-ios.shrun end to end: builds, passes the gate, packages, regenerates the bindings with no diff.- shellcheck at
--severity=warningon every script touched.
Not covered
- The gate needs
otool, so CI runs it only in a release. The first real run is the next tag. - No application was built against the new library. The claim that the warnings are gone rests on the stamps, and on a SwiftPM test bundle that linked the new simulator slice with none.
Review
A fresh-context review found no blocker and six things worth fixing. All six are in this version: the gate checks otool's own exit status, the simulator ceiling is computed, the number is read from the podspec instead of mirrored, the docs name both compilers and both crates, and the test covers the cases the first draft missed.
Why
offline-protocol-leaf imported alloc::sync::Arc, which exists only where the processor has pointer-width compare-and-swap. The crate therefore failed to build for ESP32-C3, ESP32-C2 and every Cortex-M0, although all of its dependencies, mls-rs included, already built there. CI gated the Cortex-M33 only, so nothing noticed.
What changes
- One module chooses the store handle.
src/shared.rsselectsalloc::sync::Arcwhere atomics exist andportable_atomic_util::Arcwhere they do not, the same choice mls-rs makes for its own handles. The crate exportsSharedStoreand ashared_storeconstructor. Where atomics existSharedStoreisArc<dyn LeafStore>, so existing callers compile unchanged. - No atomics backend is chosen by the library. On a target without compare-and-swap the firmware links a critical-section implementation.
- A host-side source guard refuses
alloc::syncanywhere butshared.rs, because a host build always has atomics and cannot notice the import. A second test pins each import shape it must catch. - CI:
embedded-corebuilds and lints core, sealed and leaf forriscv32imac-unknown-none-elf(ESP32-C6, H2),riscv32imc-unknown-none-elf(ESP32-C3, C2) andthumbv6m-none-eabi(Cortex-M0). The job's display name is nowEmbedded core (bare metal); no ruleset requires a check by name. - Docs: a support table in the leaf README, a section in ADR 0021, a rule in CLAUDE.md, a note in the footprint README, a CHANGELOG entry.
What this does not do
- Nothing is linked or run on a board. This is a compile result.
- ESP32-C3 and ESP32-C2 still cannot be built on esp-hal. mls-rs 0.56.0 enables portable-atomic's
critical-sectionbackend on those targets, esp-hal 1.2.2 enablesunsafe-assume-single-core, and portable-atomic refuses the pair at compile time. ESP32-C6 and H2 type-check inside an esp-hal graph. Both results were checked by hand and are not in CI. - The Xtensa chips were not tried. The footprint harness still measures a Cortex-M33 only.
Verification
| Check | Result |
|---|---|
Build and clippy -D warnings, core, sealed and leaf, on four targets | Pass |
| The new CI step, extracted from the workflow and run as written | 9 of 9 pass |
Leaf tests, engine leaf_pairing tests, workspace clippy, rustdoc -D warnings, fmt | Pass |
cargo publish --dry-run -p offline-protocol-leaf | Clean |
| Leaf image on the Cortex-M33 | 449.5 KiB, unchanged |
An alloc::sync import put back, in two shapes | Source guard and the ESP32-C3 lint both fail |
Both lockfiles gain one line each (portable-atomic-util under the leaf); the crate was already in the graph through mls-rs.