Skip to content

Compile all features in ci - #1393

Merged
1egoman merged 12 commits into
mainfrom
add-all-features-complication
Sep 10, 2026
Merged

Compile all features in ci#1393
1egoman merged 12 commits into
mainfrom
add-all-features-complication

Conversation

@1egoman

@1egoman 1egoman commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Today, there is nothing verifying that all feature combinations build. This pull request aims to fix this.

Initial approach

Initially, I aimed to use cargo-hack with --feature-powerset to build all feature combinations across all crates. This ended up being far too slow, though:

┌───────────────────────────┬────────┬──────────┬──────────┬───────────┐
│          Target           │ Result │ Baseline │ Powerset │ Job total │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ armv7-linux-androideabi   │ ❌     │ 97s      │ 8.1 min  │ 11.5 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ x86_64-linux-android      │ ❌     │ 111s     │ 9.6 min  │ 13.9 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ aarch64-unknown-linux-gnu │ ✅     │ 92s      │ 9.2 min  │ 14.1 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ aarch64-apple-ios-sim     │ ✅     │ 140s     │ 9.9 min  │ 14.2 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ aarch64-linux-android     │ ❌     │ 123s     │ 10.4 min │ 14.4 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ aarch64-apple-darwin      │ ✅     │ 136s     │ 11.8 min │ 16.2 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ aarch64-apple-ios         │ ✅     │ 164s     │ 12.7 min │ 17.5 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ x86_64-apple-darwin       │ ✅     │ 170s     │ 12.6 min │ 17.5 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ aarch64-pc-windows-msvc   │ ✅     │ 170s     │ 14.5 min │ 20.3 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ x86_64-pc-windows-msvc    │ ✅     │ 215s     │ 15.8 min │ 22.4 min  │
├───────────────────────────┼────────┼──────────┼──────────┼───────────┤
│ x86_64-unknown-linux-gnu  │ ✅     │ 113s     │ 10.9 min │ 24.2 min* │
└───────────────────────────┴────────┴──────────┴──────────┴───────────┘

* That 24.2 min is not real — apt install took 434s on that runner versus 15-38s on every other Linux job. Transient mirror slowness; its true time is ~18 min.

Second approach: targeted feature subset

I want to run this on every commit so I looked a bit at how other popular crates do this - tokio, hyper, etc. They seem to test a fixed set of features each ci run rather than all combinations to keep the ci run time down. However, somewhat suprisingly, it did not:

┌───────────────────────────┬─────────────────────┬───────────┬──────────────┬──────────────┬──────┐
│          Target           │ Full powerset (209) │ Leaf (83) │ Curated (25) │ Hybrid total │  Δ   │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ aarch64-apple-darwin      │ 855s                │ 376s      │ 347s         │ 723s         │ -15% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ x86_64-linux-android      │ 594s                │ 267s      │ 251s         │ 518s         │ -13% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ aarch64-apple-ios-sim     │ 651s                │ 370s      │ 360s         │ 730s         │ +12% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ aarch64-pc-windows-msvc   │ 879s                │ 523s      │ 500s         │ 1023s        │ +16% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ x86_64-pc-windows-msvc    │ 929s                │ 582s      │ 503s         │ 1085s        │ +17% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ x86_64-apple-darwin       │ 716s                │ 441s      │ 440s         │ 881s         │ +23% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ armv7-linux-androideabi   │ 520s                │ 345s      │ 324s         │ 669s         │ +29% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ aarch64-unknown-linux-gnu │ 541s                │ 371s      │ 345s         │ 716s         │ +32% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ x86_64-unknown-linux-gnu  │ 492s                │ 387s      │ 369s         │ 756s         │ +54% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ aarch64-linux-android     │ 461s                │ 365s      │ 344s         │ 709s         │ +54% │
├───────────────────────────┼─────────────────────┼───────────┼──────────────┼──────────────┼──────┤
│ aarch64-apple-ios         │ 475s                │ 404s      │ 409s         │ 813s         │ +71% │
└───────────────────────────┴─────────────────────┴───────────┴──────────────┴──────────────┴──────┘

The big reason why it turned out was that I hadn't realized, but initial test wasn't actually building livekit-ffi or livekit-uniffi. So had those been included, the run time would have been even slower.

Approach three: approach two but omitting livekit-ffi

So after rerunning approach two, only this time without livekit-ffi, the check times went way, way down:

┌───────────────────────────┬───────────────┬─────────────┬──────────────────┐
│          Target           │ Powerset step │ Projected B │   Job total →    │
├───────────────────────────┼───────────────┼─────────────┼──────────────────┤
│ x86_64-pc-windows-msvc    │ 929s          │ ~620s       │ 21.6 → ~16.4 min │
├───────────────────────────┼───────────────┼─────────────┼──────────────────┤
│ aarch64-pc-windows-msvc   │ 879s          │ ~587s       │ 20.4 → ~15.5 min │
├───────────────────────────┼───────────────┼─────────────┼──────────────────┤
│ aarch64-apple-darwin      │ 855s          │ ~571s       │ 19.8 → ~15.1 min │
├───────────────────────────┼───────────────┼─────────────┼──────────────────┤
│ x86_64-apple-darwin       │ 716s          │ ~478s       │ 17.1 → ~13.1 min │
├───────────────────────────┼───────────────┼─────────────┼──────────────────┤
│ aarch64-apple-ios         │ 475s          │ ~317s       │ 11.2 → ~8.6 min  │
├───────────────────────────┼───────────────┼─────────────┼──────────────────┤
│ aarch64-unknown-linux-gnu │ 541s          │ ~361s       │ 13.4 → ~10.4 min │
└───────────────────────────┴───────────────┴─────────────┴──────────────────┘

The slowest (windows) is 16 minutes, which is about as long as the longest ci build takes today. So this should not add any additional time to ci.

This third approach is what I am suggesting and what this pull request contains. All three have pros and cons though, so if others think a different approach is better I'd be interested in hearing your perspective.

TODO

  • Add agents.md note to add "interesting" feature options to ci when introduced
  • Remove add-all-features-complication push trigger

1egoman and others added 5 commits September 1, 2026 17:00
Adds a job that runs `cargo hack --feature-powerset --depth 2 check`
over `livekit`, `livekit-api`, and the ten workspace crates beneath
them: 209 feature combinations in total.

Feature bugs do not show up in the default-feature build — an optional
`dep:` used unconditionally, or a `?/` forward that silently no-ops,
only breaks for callers who pick a particular feature set. Nothing else
in CI exercises those paths today. All 209 combinations currently pass,
so this starts green.

Depth 2 is a deliberate ceiling. It covers pairwise feature interactions,
where nearly all such bugs live, and keeps the job affordable: `livekit`
alone would go from 67 combinations to 232 at depth 3.

Runs on one platform (linux arm64) because feature resolution does not
vary by target — the point is to exercise the feature graph, not the
target matrix. Scheduled nightly rather than per-PR, plus on Cargo.toml
and Cargo.lock changes, which are what can alter feature resolution.

`target/` is deliberately not cached so the reported time reflects a
cold run, and the job prints the powerset time next to a plain
default-feature `cargo check` for comparison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expands the feature-combination job from linux arm64 to the same eleven
targets builds.yml covers, and drops the nightly schedule plus the
Cargo.toml path filter so it runs on every commit.

Measured at 14 min end to end on ubuntu-24.04-arm, which is on par with
the existing build and test jobs, so per-commit is affordable. Matrix
jobs run in parallel, so the cost is the slowest target rather than the
sum of eleven.

One platform was never enough. Feature *resolution* does not vary by
target, but what the resolved features then compile does: cfg-gated
code, the webrtc-sys C++ bridge and the per-platform TLS backends all
differ, so a combination can be green on linux and broken on android or
iOS. That is exactly the class of bug this job exists to catch.

Android goes through cargo-ndk, which supplies the NDK clang the
webrtc-sys C++ bridge needs; it execs `cargo <args>`, so it wraps `hack`
the same way it wraps `build`. fail-fast is off so one broken target
does not hide the other ten, and a concurrency group cancels superseded
runs rather than leaving eleven jobs per push running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three Android targets failed the same 8 of 209 combinations: every
one that enables plain `native-tls`. The cause is openssl-sys, which
cannot find an OpenSSL installation to link against when
cross-compiling to Android and fails its build script outright.

This is not a feature-wiring bug, so there is nothing in the SDK to fix.
Android is a rustls-only platform here — ffi-builds.yml already ships
all three Android targets as `rustls-tls-webpki-roots`, so a
`native-tls` Android build is not a configuration anyone ships.

Excluding the feature takes Android from 209 combinations to 172. It
keeps `native-tls-vendored` covered (33 combinations), since that builds
OpenSSL from source and works, and the excluded combinations are still
exercised on the eight non-Android targets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…werset

Splits feature checking by where combinatorial coverage earns its cost,
as an alternative to the full 209-combination powerset:

  - Leaf crates keep the full `--feature-powerset --depth 2` (83
    combinations). Their feature sets are small, so exhaustive coverage
    is cheap, and they are where the `dep:`/`?/` plumbing bugs live.

  - livekit, livekit-api, livekit-ffi and livekit-uniffi get 26
    hand-picked combinations. The powerset over the top crates is where
    the time went — `livekit` alone is 65 of 209 combinations and ~40%
    of wall time — and most of those are combinations nobody can
    select: two TLS backends at once, or an internal `__lk-*` feature
    paired with something unrelated. Every entry here is drawn from
    what the manifests document or what ffi-builds.yml ships.

This also covers livekit-ffi and livekit-uniffi, which the powerset
workflow does not touch at all.

The tradeoff is deliberate and narrows coverage on the top crates: a
feature pair that only breaks in one of the dropped combinations will
not be caught. Both workflows run for now so the numbers can be
compared on the same commit; one should be deleted once that is
settled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erset

Measured cold on a 14-core machine, checking the same crates:

  full powerset (209 combos)                359s
  leaf powerset + curated livekit/api       242s   -33%
  the same, plus livekit-ffi/livekit-uniffi 345s    -4%

The first version of the curated workflow was no faster than the
powerset it was meant to replace — on CI it was slower on 9 of 11
targets. Two reasons, both visible above.

livekit-ffi and livekit-uniffi were the larger one: 103s of a 345s
budget for two crates, because every TLS change rebuilds livekit
underneath livekit-ffi, and the powerset workflow never built them at
all. They move to the nightly job here. Their shipped configurations are
still covered per-commit — builds.yml builds the whole workspace on every
PR, and ffi-builds.yml builds each shipped per-platform TLS variant on
push to main — so what nightly adds back is livekit-ffi's non-default
TLS variants on PRs.

The other was a misread on my part: `livekit` is 65 of 209 combinations
and ~40% of wall time, but those combinations are mostly ~2s cache hits.
The cost attributed to them was the one-time cold build of the
dependency graph, which does not go away when the combinations do — it
just moves into the leaf phase. The leaf-only powerset is already 55-80%
of the full 209.

So: feature-combinations-curated.yml runs per-commit on all eleven
targets (leaf powerset + 17 documented livekit/livekit-api
combinations), and feature-combinations.yml keeps the exhaustive 209
plus the ffi crates, nightly, as the backstop for what the subset skips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/feature-combinations-curated.yml Outdated
Comment on lines +64 to +74
LEAF_PACKAGES: >-
-p livekit-signaling
-p livekit-net
-p livekit-region
-p livekit-protocol
-p livekit-datatrack
-p livekit-data-stream
-p livekit-common
-p libwebrtc
-p webrtc-sys
-p livekit-token

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for reviewers - Is this a good list of "leaf packages"? Is there a better way to determine these programatically? If not, I may also need to add a note to agents.md to keep this list up to date.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Added the AGENTS.md note in cc27308 - still would be interested in alternative solutions though!)

@ladvoc ladvoc Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect when you tried this originally without limiting the package list all examples were also being compiled (since they are members of the workspace). How about doing powerset with the addition of the --ignore-private flag? This excludes any crate with publish = false in its manifest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think moving to the Cargo.toml driven approach, this becomes less of an issue since packages which don't specify either powerset or curated will be excluded, so I will leave it as is.

Comment on lines +213 to +243
# --- livekit -------------------------------------------------------
livekit|
livekit|--no-default-features
livekit|--no-default-features --features native
livekit|--no-default-features --features native,native-tls
livekit|--no-default-features --features native,native-tls-vendored
livekit|--no-default-features --features native,rustls-tls-native-roots
livekit|--no-default-features --features native,rustls-tls-webpki-roots
livekit|--no-default-features --features native,rustls-tls-webpki-roots,glib-main-loop
livekit|--no-default-features --features native,__lk-internal
# --- livekit-api ---------------------------------------------------
livekit-api|
livekit-api|--no-default-features
livekit-api|--no-default-features --features access-token
livekit-api|--no-default-features --features webhooks
livekit-api|--no-default-features --features services,native-tls
livekit-api|--no-default-features --features services,rustls-tls-webpki-roots
livekit-api|--no-default-features --features services,rustls-tls-native-roots
livekit-api|--no-default-features --features signal-client-native,rustls-tls-webpki-roots
livekit-api|--features rustls-tls-webpki-roots
# livekit-ffi and livekit-uniffi are deliberately NOT here. Measured
# cold, their 8 combinations cost 103s of a 345s total — 30% of the
# budget for two crates, because each TLS change rebuilds livekit
# underneath livekit-ffi. Dropping them takes the job from 345s to
# 242s, which is what makes per-commit affordable. Their shipped
# configurations are still covered: builds.yml builds the whole
# workspace (livekit-ffi at default features) on every PR, and
# ffi-builds.yml builds every shipped per-platform TLS variant on
# push to main. The residual gap is livekit-ffi's non-default TLS
# variants on PRs, which the nightly full powerset covers.
EOF

@1egoman 1egoman Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers - here is the list of "interesting" feature combinations currently being tested.

I could probably cut this down even further? access-token should be no longer used.

1egoman and others added 2 commits September 2, 2026 13:34
The curated per-commit workflow is the better trade, so the nightly
209-combination job is not worth the runner time. Verified green on all
eleven targets (run 33651960004) at 7.7-17.4 min per job, with the
feature-checking steps themselves down ~30% against the full powerset.

Removing it gives up two things, both deliberately:

  - The exhaustive backstop. A feature pair that only breaks in one of
    the combinations the curated list skips is now caught nowhere. The
    header comment records the one-liner to run the full 209 by hand
    when a feature bug needs chasing.

  - Feature-combination coverage for livekit-ffi and livekit-uniffi,
    which only ever lived in this workflow. Their shipped configurations
    are still built — builds.yml builds the workspace on every PR, and
    ffi-builds.yml builds each shipped per-platform TLS variant on push
    to main — so what is lost is livekit-ffi's non-default TLS variants
    on PRs. That cost 103s of a 345s budget to cover, which is what
    kept it out of the per-commit job in the first place.

Also folds the rationale that lived in the deleted file (the Android
openssl-sys exclusion) into the surviving workflow rather than leaving
the comment pointing at a file that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both halves of feature-combinations-curated.yml are maintained by hand,
and neither fails loudly when it goes stale:

  - A new feature on livekit or livekit-api is invisible to the job
    until it is added to the curated COMBOS list, because those two
    crates get a hand-picked list rather than a powerset.
  - A new workspace crate is invisible until it is added to
    LEAF_PACKAGES, because cargo-hack only varies the features of
    packages named with -p. The crate still gets built as a dependency,
    which makes it easy to assume it is covered when it is not.

Also records the constraints that are not obvious from reading the
workflow: why depth stays at 2, why openssl-sys features have to be
excluded on Android, why livekit-ffi and livekit-uniffi are left out,
and that --print-command-list enumerates combinations without building.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@1egoman
1egoman marked this pull request as ready for review September 2, 2026 18:50
@1egoman
1egoman requested a review from ladvoc as a code owner September 2, 2026 18:50
devin-ai-integration[bot]

This comment was marked as resolved.

Both from review feedback on #1393, and both real gaps.

livekit-token-source is a published crate (0.1.2) whose five features
are bare pass-throughs to livekit-net — exactly the forwarding this job
exists to check — and it was unchecked. It was missed because the scope
was framed as "livekit and livekit-api plus what they depend on", and
livekit-token-source is a sibling rather than a dependency: nothing in
the SDK path pulls it in, only examples/token_source. The header now
states the rule as every publishable workspace crate that has features,
and calls out the sibling case, so the next such crate is not missed the
same way. Leaf combinations go from 83 to 104, which costs 9s: the
combinations are TLS variants already compiled elsewhere in the job.

livekit-api's `services,native-tls-vendored` was the one TLS backend of
the four not covered, while the livekit equivalent was — an asymmetric
omission rather than a deliberate one. `services` is what enables
dep:reqwest, so this is the combination where the
`reqwest?/native-tls-vendored` forward actually becomes live. Costs 1s,
since livekit already compiles vendored OpenSSL earlier in the run.

Deliberately not fixing yuv-sys, whose `jpeg` feature is also unchecked:
it would pull the libyuv C build into a job that is otherwise pure
cargo check, needs system libjpeg on all eleven targets, and is not on
the client SDK path.

Verified both locally before pushing: token-source's 21 combinations and
the new livekit-api combination all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/feature-combinations-curated.yml Outdated
@1egoman 1egoman added the internal to tag changes that don't require changelog documentation label Sep 9, 2026
jq.exe uses native windows line endings which the script wasn't taking
into account properly.
@1egoman
1egoman force-pushed the add-all-features-complication branch 2 times, most recently from e9c27d1 to 7486053 Compare September 9, 2026 20:35
@1egoman

1egoman commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

I've had to do some fighting with ci to get it to NOT update protobufs. I don't want this change to include any rust, as it should be purely a ci / Cargo.toml configuration change and not have a changeset.

Therefore because I cancelled the "generate protobufs" job, ci won't pass when I merge this, but it's a false negative.

@1egoman
1egoman merged commit 8a85181 into main Sep 10, 2026
35 of 36 checks passed
@1egoman
1egoman deleted the add-all-features-complication branch September 10, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal to tag changes that don't require changelog documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants