-
Notifications
You must be signed in to change notification settings - Fork 3
650 lines (558 loc) · 26.8 KB
/
Copy pathci.yml
File metadata and controls
650 lines (558 loc) · 26.8 KB
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
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
# Cancel outdated runs on the same PR — only latest commit matters
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# CI builds are always from a cache-restored or cold state; incremental
# compilation only adds overhead and bloats the rust-cache snapshots.
CARGO_INCREMENTAL: 0
jobs:
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Format check
run: cargo fmt --all -- --check
licenses:
name: License Consistency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: License consistency check
run: ./scripts/check-license-consistency.sh
# crates.io renders a crate's README from its archive and a published
# version is immutable, so a crate that ships without one stays bare on
# the registry until the next release. Cargo infers the README silently
# and omits it just as silently, so nothing else catches the miss.
- name: Crate README presence
run: ./scripts/check-crate-readmes.sh
# The NIP-44 vectors are a crypto conformance fixture vendored into the
# tree. Pinning them to the checksum the spec publishes is what stops a
# green suite from being achieved by editing the vectors.
- name: NIP-44 vector integrity
run: ./scripts/check-nip44-vectors.sh
# The NIP-44 vectors above are vendored, so a checksum the upstream spec
# publishes is what pins them. The spec vectors in this repository have no
# such external publisher: they are pinned instead by being regenerable from
# a second implementation that never reads the crates under test.
#
# Without this job the Rust tests still compare code against the vector
# files, but nothing compares the vector files against anything, so a red
# test could be turned green by editing an expectation -- exactly the move
# every vector file's header forbids. Regenerating and diffing makes that
# edit impossible without also editing the generator, and that diff reads as
# what it is: a change to the specified wire format.
spec-vectors:
name: Spec Vector Provenance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
# Standard library only, by design: the generator is a conformance
# artifact, and a dependency is a second thing that has to be trusted to
# compute a wire format correctly.
- name: Regenerate the spec vectors and fail on any difference
run: python3 tools/spec-vectors/generate.py --check
# The License Consistency job catches the three THIRD-PARTY-NOTICES.md copies
# drifting from each other, but a dependency bump leaves all three identical
# and uniformly stale — regenerating after Cargo.lock changes was manual
# discipline with nothing to catch a miss. Mirror the python-bindings job's
# regenerate-and-diff pattern: rerun cargo-about and fail on any diff.
#
# cargo-about consults clearlydefined.io for license texts, so in rare cases
# an upstream data change can surface here as drift with no Cargo.lock change;
# the fix is the same either way — regenerate and commit.
notices:
name: Third-Party Notices Drift
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
# The cargo-about version is pinned in the regeneration script (output
# formatting changes across releases would read as false drift); read it
# from there so this job can never disagree with local regeneration.
- name: Read pinned cargo-about version
id: pin
run: |
version="$(sed -n 's/^CARGO_ABOUT_VERSION="\(.*\)"$/\1/p' scripts/generate-third-party-notices.sh)"
test -n "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache cargo-about binary
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-about
key: cargo-about-${{ steps.pin.outputs.version }}-${{ runner.os }}
# `--features cli` is required: without it the install exits 0 but ships
# no binary.
- name: Install cargo-about
run: |
if ! cargo about --version 2>/dev/null | grep -qFx "cargo-about ${{ steps.pin.outputs.version }}"; then
cargo install cargo-about --version "${{ steps.pin.outputs.version }}" --features cli --locked --force
fi
- name: Regenerate third-party notices
run: ./scripts/generate-third-party-notices.sh
- name: Fail on stale THIRD-PARTY-NOTICES.md
run: |
if ! git diff --exit-code -- \
THIRD-PARTY-NOTICES.md \
bindings/react-native/THIRD-PARTY-NOTICES.md \
bindings/python/THIRD-PARTY-NOTICES.md; then
echo "::error::THIRD-PARTY-NOTICES.md is stale — the shipped-binary dependency graph changed without regenerating attribution."
echo "Run scripts/generate-third-party-notices.sh and commit the result."
exit 1
fi
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-clippy
- name: Clippy
run: cargo clippy --workspace --locked -- -D warnings
# The messaging-only build. `data` is a default-on feature so that a
# native consumer can drop the CRDT engine and its ~1.5 MB, and every
# other gate in this file builds default features only — without this
# step the opt-out path we promise crates.io consumers would never be
# compiled at all, and would rot behind a `#[cfg]` nobody exercises.
- name: Clippy (no default features)
# No `--all-targets`, matching the workspace job above: the lint set
# includes `unwrap_used = deny`, which test code across the workspace
# does not satisfy, so linting test targets fails on main today.
run: cargo clippy --package offline-protocol --no-default-features --locked -- -D warnings
docs:
name: Rustdoc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-docs
# Gates the *public* doc surface, so no --document-private-items: a
# public item linking to a private one is precisely the breakage this
# catches (the link silently resolves to nothing for every reader who
# is not building the crate themselves).
- name: Build docs
run: cargo doc --workspace --no-deps --locked
env:
RUSTDOCFLAGS: -D warnings
msrv:
name: MSRV (Rust 1.87)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.87.0
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-msrv
- name: Check workspace on MSRV
run: cargo check --workspace --all-targets --locked
embedded-core:
name: Embedded core (thumbv8m)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: thumbv8m.main-none-eabihf
components: clippy, llvm-tools
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-embedded
# The gate. `offline-protocol-core` and `offline-protocol-sealed` are the
# two crates a constrained leaf node links, and nothing else in this file
# compiles either without `std`. Without these steps the no_std
# configuration rots back to std-only the first time someone adds a
# `use std::` to a file that has no reason to look bare-metal-specific,
# and the failure surfaces on someone's hardware rather than here.
- name: Build core for bare metal
run: >
cargo build --package offline-protocol-core
--no-default-features --locked --target thumbv8m.main-none-eabihf
- name: Clippy core for bare metal
run: >
cargo clippy --package offline-protocol-core
--no-default-features --locked --target thumbv8m.main-none-eabihf
-- -D warnings
# Publish isolation. Core declares seven dependencies locally rather than
# inheriting them, which is what makes `default-features = false` take
# effect; the cost is that a missing `std` re-add is invisible inside the
# workspace, where another member's features paper over it, and only
# fails when cargo builds the crate on its own at release time. Selecting
# the single package reproduces that isolation here instead.
- name: Build core standalone with default features
run: cargo build --package offline-protocol-core --locked
# The sealed layer: the envelope codec, address derivation and canonical
# signing payloads a leaf shares with the engine (ADR 0022). It carries
# the only crypto dependency in the bare-metal build, and the same two
# traps as core: local dependency declarations that must keep
# `default-features = false`, and a `std` feature that is invisible to
# lose inside the workspace.
- name: Build the sealed layer for bare metal
run: >
cargo build --package offline-protocol-sealed
--no-default-features --locked --target thumbv8m.main-none-eabihf
- name: Clippy the sealed layer for bare metal
run: >
cargo clippy --package offline-protocol-sealed
--no-default-features --locked --target thumbv8m.main-none-eabihf
-- -D warnings
- name: Build the sealed layer standalone with default features
run: cargo build --package offline-protocol-sealed --locked
# The leaf node itself. `bare-metal-rng` selects getrandom's custom
# backend, which the target needs because getrandom has none for it and
# refuses to compile otherwise; the crate still registers no
# implementation, because that is the firmware's TRNG to wire up.
#
# This step is the only thing that catches an mls-rs error formatted with
# `{}`: mls-rs implements `Display` on its error only under `std`, so the
# host build accepts what the device build rejects.
- name: Build the leaf node for bare metal
run: >
cargo build --package offline-protocol-leaf
--no-default-features --features bare-metal-rng
--locked --target thumbv8m.main-none-eabihf
- name: Clippy the leaf node for bare metal
run: >
cargo clippy --package offline-protocol-leaf
--no-default-features --features bare-metal-rng
--locked --target thumbv8m.main-none-eabihf
-- -D warnings
- name: Build the leaf node standalone with default features
run: cargo build --package offline-protocol-leaf --locked
# `embedded-footprint` has its own workspace, so the repo's Clippy job
# does not reach it. The leaf configurations are linted individually
# because each is a separate link with a different feature set.
- name: Clippy the footprint harness
working-directory: tools/embedded-footprint
run: |
for features in "" "leaf" "leaf-full"; do
cargo clippy --release --locked --target thumbv8m.main-none-eabihf \
${features:+--features "$features"} -- -D warnings
done
# Report-only: the size is information, not a threshold. A number that
# fails a build invites tuning the number.
- name: Measure embedded footprint
run: ./tools/embedded-footprint/measure.sh >> "$GITHUB_STEP_SUMMARY"
mls-interop:
name: MLS interop (phone <-> leaf)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-mls-interop
workspaces: tools/mls-interop
# The workspace Clippy and Format jobs stop at the SDK's workspace
# boundary, and this crate has its own. Without this step it is the only
# Rust in the repo that nothing lints.
- name: Clippy
working-directory: tools/mls-interop
run: cargo clippy --locked --all-targets -- -D warnings
# The other half of ADR 0021. `embedded-footprint` proves a leaf's MLS
# stack fits; this proves it is talking to the phone rather than only to
# itself. Both library versions are pinned, so this fails when a bump
# changes behaviour, which is the point: an interop claim is about two
# specific versions.
#
# The step 0 lines are negative controls: each restores one mls-rs
# default and requires the phone to refuse the result, except 0.3, which
# pins a cap OpenMLS declares and never applies. If any of them flips,
# read the harness README before touching anything.
- name: Phone and leaf MLS stacks interoperate
working-directory: tools/mls-interop
run: cargo run --release --locked
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
release_target: x86_64-unknown-linux-gnu
- os: macos-latest
release_target: aarch64-apple-darwin
- os: windows-latest
release_target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.release_target }}
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-test
- name: Test
run: cargo test --workspace --locked
- name: Build release cdylib
run: cargo build --release --locked --target ${{ matrix.release_target }} --package offline-protocol-uniffi
security:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: EmbarkStudios/cargo-deny-action@v2
python-bindings:
name: Python Bindings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Static — no bindgen, no toolchain — so a regression in the
# single-entry-point rule fails in seconds rather than after the install
# and build below. The drift gate at the end of this job only catches a
# partial regeneration that someone actually committed; this catches the
# script that would produce one.
- name: Guard the shared binding generator
run: |
shellcheck --severity=warning \
scripts/generate-bindings.sh \
scripts/tests/test-generate-bindings.sh
bash scripts/tests/test-generate-bindings.sh
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-python-bindings
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: bindings/python/pyproject.toml
# Keep in sync with the `uniffi` pin in crates/offline-protocol-uniffi/Cargo.toml;
# scripts/generate-bindings.sh fails the build if the two disagree.
- name: Install uniffi-bindgen
run: cargo install uniffi --version 0.30.0 --features cli --locked
# Delete the generated files first, so the drift gate below proves they
# were *written* and not merely unchanged. `git diff --exit-code` on a
# path nothing wrote to reports no diff, which makes "up to date" and
# "never regenerated" indistinguishable — so a mistyped output path in
# the generator would sail through this job green while every UDL change
# afterwards shipped stale bindings. Deleting turns that into a diff.
#
# Only the generated members: `java/` also holds hand-written
# com/offlineprotocol/**, and the Python package dir is mostly
# hand-written too. `git diff` restores nothing, so if a later step fails
# the checkout is discarded with the runner.
- name: Remove generated bindings so regeneration must replace them
run: |
rm -rf bindings/react-native/ios/Generated \
bindings/react-native/android/src/main/java/uniffi
rm -f bindings/python/offline_protocol_sdk/offline_protocol.py
# Regenerate before building anything. Bindgen reads the UDL and nothing
# else, so putting this ahead of the cdylib compile means drift surfaces
# in seconds rather than after a full debug build — and a broken desktop
# build stops costing us Swift/Kotlin drift coverage it never needed.
- name: Regenerate all UniFFI bindings
run: bash scripts/generate-bindings.sh
# One regeneration, one gate: the three files are one artifact set off one
# UDL, so checking them separately would let a half-updated commit pass
# the half of the gate it happened to satisfy.
- name: Fail on stale bindings
run: |
if ! git diff --exit-code -- \
bindings/python/offline_protocol_sdk/offline_protocol.py \
bindings/react-native/ios/Generated/ \
bindings/react-native/android/src/main/java/uniffi/; then
echo "::error::Committed UniFFI bindings are out of date."
echo "Run: bash scripts/generate-bindings.sh and commit all three."
exit 1
fi
# Builds the desktop cdylib pytest loads. It re-runs the generator on the
# way out, which is idempotent and is what keeps the one-entry-point rule
# true for the script developers actually invoke.
- name: Build the desktop library
run: bash bindings/python/scripts/build-desktop.sh
- name: Install Python binding
run: pip install -e 'bindings/python[dev]'
- name: Run pytest
run: pytest bindings/python/tests/ -v
# release.yml runs only on a `v*` tag, so every line of it used to reach
# production untested — the packaging step's first real execution would be
# during a release, with a half-built asset set as the failure mode. The
# logic lives in scripts/package-release-assets.sh precisely so it can be
# driven here against a fixture tree mirroring what the download steps
# produce, failure paths included.
release-packaging:
name: Release Packaging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# --severity=warning, not the default: this gates on real defects
# (unquoted expansions, masked exit codes) without failing the release
# pipeline over style preferences on a shellcheck version bump.
- name: Shellcheck the packaging scripts
run: |
shellcheck --severity=warning \
scripts/package-release-assets.sh \
scripts/tests/test-package-release-assets.sh
- name: Package release assets against a fixture tree
run: bash scripts/tests/test-package-release-assets.sh
react-native-typecheck:
name: React Native Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Node version and the install/build pair below deliberately mirror the
# npm-publish job in release.yml. `npm run build` (tsc) had been reachable
# *only* from that job, which runs immediately before `npm publish` — so a
# type error or a tsconfig incompatibility surfaced during a release
# instead of on the PR that caused it. A dependabot bump to TypeScript 7
# (#178) passed all of CI for exactly this reason while failing to build.
#
# This job does not run scripts/prepare-npm.sh: that validates the
# pre-built iOS XCFramework and Android .so binaries, which don't exist
# outside a release run. The packaging invariants that need no binaries
# are asserted directly below instead.
#
# Node 24 because release.yml is pinned there: it publishes to npm with
# trusted publishing, which needs the npm >= 11.5.1 that Node 24 bundles.
# Keeping the two in lockstep is the whole point of this job, so this
# version tracks that one rather than being chosen independently.
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
cache-dependency-path: bindings/react-native/package-lock.json
# iOS autolinking invariants (#312). None of these need a native binary,
# and every one of them failed *silently* before that fix: React Native
# resolves a dependency's podspec by globbing "*.podspec" in the package
# root without recursing, so a podspec under ios/ is invisible and the
# dependency is skipped with no error anywhere — the consumer only finds
# out at runtime, from a linking error whose remedy is "run pod install".
- name: Verify iOS autolinking invariants
working-directory: bindings/react-native
run: |
test -f MeshSdk.podspec || {
echo "::error::MeshSdk.podspec must stay at the package root — autolinking does not recurse into ios/"
exit 1
}
if compgen -G "ios/*.podspec" > /dev/null; then
echo "::error::A podspec under ios/ is invisible to React Native autolinking; keep it at the package root"
exit 1
fi
node -e '
const ios = require("./react-native.config.js")?.dependency?.platforms?.ios;
if (ios === null || ios === undefined) {
console.error("::error::react-native.config.js disables iOS autolinking (dependency.platforms.ios is " + ios + ")");
process.exit(1);
}
'
node -e '
if (!(require("./package.json").files || []).includes("MeshSdk.podspec")) {
console.error("::error::package.json files[] must ship MeshSdk.podspec, or the published package has no podspec at its root");
process.exit(1);
}
'
echo "iOS autolinking invariants OK"
- name: Install dependencies
working-directory: bindings/react-native
run: npm ci
- name: Typecheck TypeScript
working-directory: bindings/react-native
run: npm run build
# Behavior, not types: the TypeScript layer is the one part of the bridge
# neither the Rust text-guards (which read source as text) nor the
# Android/iOS harnesses (which cover the native halves) can exercise.
# See bindings/react-native/js-ci-harness/README.md.
- name: React Native JS behavior tests
working-directory: bindings/react-native
run: npm run test:js
android-unit-tests:
name: Android Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
# ubuntu-latest ships an Android SDK, but provision it explicitly so the
# toolchain (and license acceptance) doesn't drift out from under us.
- uses: android-actions/setup-android@v4
- uses: gradle/actions/setup-gradle@v6
with:
gradle-version: "8.9"
# Runs the RN Android module's JVM unit tests (InboundFragmentBuffer,
# OutboundFragmentQueue, the GATT CCCD/long-read helpers, MeshController,
# ...) via a standalone harness, since the module can't build on its own.
# See bindings/react-native/android-ci-harness/README.md.
- name: Android unit tests
working-directory: bindings/react-native/android-ci-harness
run: gradle :offlineprotocol:testDebugUnitTest --no-daemon --stacktrace
ios-unit-tests:
name: iOS Unit Tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
# Runs the RN iOS module's standalone policy-class suites (the relay
# translator/rate-limiter/presence/in-flight-tracker/timestamp helpers
# that mirror the Android module) via the SwiftPM harness in
# bindings/react-native/ios/Package.swift, since the pod can't build
# on its own outside an app workspace.
- name: iOS unit tests
run: swift test --package-path bindings/react-native/ios
# The largest bridge files are on Package.swift's `exclude:` list (they
# need CoreBluetooth / MultipeerConnectivity / React / the generated
# UniFFI module, none of which the SwiftPM harness can supply), so
# `swift test` above compiles none of them and every change to them has
# historically shipped with zero compile coverage. They CAN be
# type-checked directly against the iphoneos SDK plus the checked-in
# Generated/ modulemap, which at least catches API drift and syntax
# breakage before a release.
# `-typecheck` emits no binary; warnings do not fail the build.
#
# BleManager / WifiDirectManager / ReticulumManager joined this list in
# #300: only their mesh/ and ble/ collaborators were missing, and adding
# those three sources is enough to typecheck all of them. The one file
# still uncovered is OfflineProtocolModule.swift, which needs real React
# headers — see ios/BRIDGE_MAINTENANCE.md for the local symlink-farm
# recipe to check it by hand when you touch it.
- name: iOS bridge typecheck (files excluded from the SwiftPM harness)
working-directory: bindings/react-native/ios
run: |
xcrun --sdk iphoneos swiftc -typecheck \
-target arm64-apple-ios15.1 \
-sdk "$(xcrun --sdk iphoneos --show-sdk-path)" \
-Xcc -fmodule-map-file=Generated/offline_protocolFFI.modulemap \
-Xcc -IGenerated \
InternetManager.swift NostrManager.swift TransportManager.swift \
BleManager.swift WifiDirectManager.swift ReticulumManager.swift \
mesh/Mesh.swift ble/MeshConnectionRegistry.swift \
ble/BleDiscoveryBootstrapPolicy.swift \
PeripheralRestorationAgeOutPolicy.swift \
BleMessageNotificationPolicy.swift \
Generated/offline_protocol.swift ForcedPresenceCheckQueue.swift \
AddressDeclarationPolicy.swift \
GatewayAttachPolicy.swift GatewayVerdictTracker.swift \
InboundFragmentBuffer.swift OutboundFragmentQueue.swift \
PeerIdentityBinding.swift RelayAnswerPrefixes.swift \
RelayControlOpTranslator.swift RelayGroupSnapshotBridge.swift \
RelayRateLimiter.swift LegacyRelayMessage.swift MonotonicClock.swift \
NostrQueryTracker.swift \
PresenceWatchPolicy.swift RecipientInFlightTracker.swift \
SocketGenerationTracker.swift SupersededLatchPolicy.swift \
RelayTimestamps.swift WriteStallWatchdog.swift