Skip to content

fix(ios): select an arch when exporting coverage from universal binaries - #6

Merged
mikehardy merged 4 commits into
mainfrom
fix/ios-universal-arch-selection
Sep 24, 2026
Merged

mikehardy merged 4 commits into
mainfrom
fix/ios-universal-arch-selection

Conversation

@mikehardy

@mikehardy mikehardy commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes queue item F3: consumers that build universal (multi-arch) iOS simulator binaries — notably react-native-firebase — see their ios-native Codecov flag stuck at 0%, even with full adoption. RNGMA's tooling already documented the limitation:

"react-native-coverage@0.2.0 cannot export universal simulator LCOV without architecture selection; preserve raw profraw until exporter fix."

Root cause

llvm-cov (export/show/report) cannot read coverage from a universal Mach-O without an -arch selector. A simulator build carrying both arm64 and x86_64 slices therefore exports empty LCOV.

This repo's own e2e never hit the bug because its Xcode Debug build uses the default ONLY_ACTIVE_ARCH=YES → a thin binary (which is why e2e-ios-dynamic sits at ~90%). Consumers producing fat simulator binaries hit the 0% path.

Fix

ios export, ios report, and ios summary now resolve an architecture before invoking llvm-cov:

  1. Thin binary (one slice) → no -arch passed (behavior unchanged).
  2. Universal binary → detect slices via lipo -archs, select the host arch when present, else the first slice.
  3. Explicit override via --arch <arch> (CLI) or ios.arch (config) always wins.

Auto-detection means consumers get the fix for free on upgrade — no config change required. RNFB's rn-coverage-ios-export.js already forwards extra args, so --arch is available if they ever want to pin.

In-repo dogfood / regression guard (now included)

The static e2e cell now builds with ONLY_ACTIVE_ARCH=NO, producing a universal (arm64 + x86_64) simulator binary. This exercises the new arch-selection path in CI here: if the fix ever regresses, that cell's export drops to 0% and the strict assert (exit 2) fails the cell. Added a lipo-based assertion that the static binary really is multi-arch so the guard can't silently degrade to thin. The dynamic (primary) cell stays thin to keep it fast — no new cell, no extra CI cost.

Tests / validation

  • New src/__tests__/arch-selection.test.ts (pure selection logic): thin→none, universal→host, host-absent→first slice, explicit override, blank-ignored. 52 tests / 6 suites pass (was 42/5).
  • yarn typecheck, yarn lint, yarn prepare (build) pass; bash -n + shellcheck -S error clean on the e2e script.
  • lipo-backed detectBinaryArchs verified against a real fat binary.
  • npx @docs.page/cli check → No documentation issues found.

Follow-up (not in this PR)

  • Consumer rollout: once this ships (coordinate version with the release-hygiene PR), bump react-native-coverage in RNFB + RNGMA and confirm ios-native turns non-zero on their Codecov.

@mikehardy
mikehardy force-pushed the fix/ios-universal-arch-selection branch from 5f44027 to 58573f5 Compare September 24, 2026 20:22
llvm-cov cannot read coverage from a universal (multi-arch) Mach-O
without -arch, so a simulator build carrying both arm64 and x86_64
slices exported 0% LCOV. This is why consumers building universal
simulator binaries (e.g. react-native-firebase) saw an ios-native
flag stuck at 0%, while this repo's thin (ONLY_ACTIVE_ARCH=YES) e2e
builds were unaffected.

ios export/report/summary now:
- auto-detect the app binary's slices via 'lipo -archs';
- pass -arch only for fat binaries, choosing the host arch when
  present else the first slice (thin binaries are unchanged);
- accept an override via --arch <arch> or config ios.arch.

Adds unit tests for the selection logic and documents the flag.
@mikehardy
mikehardy force-pushed the fix/ios-universal-arch-selection branch from 58573f5 to f3afbb2 Compare September 24, 2026 20:25
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.25000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.54%. Comparing base (c25c381) to head (a051ea6).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
src/process-ios-native-coverage.ts 81.25% 6 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main       #6      +/-   ##
============================================
+ Coverage     58.85%   59.54%   +0.68%     
  Complexity       15       15              
============================================
  Files            22       22              
  Lines          1016     1048      +32     
  Branches        230      239       +9     
============================================
+ Hits            598      624      +26     
- Misses          346      352       +6     
  Partials         72       72              
Flag Coverage Δ
e2e-android 81.70% <ø> (ø)
e2e-ios-dynamic 90.62% <ø> (ø)
e2e-ios-static 63.07% <ø> (ø)
unit-js 54.08% <81.25%> (+1.46%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Flip the static e2e cell to ONLY_ACTIVE_ARCH=NO so it produces a
universal (arm64 + x86_64) simulator binary, exercising the new
llvm-cov -arch selection in 'rn-coverage ios export'. If the arch fix
regresses, that cell's export drops to 0% and the strict assert fails
it — an in-repo regression guard at no extra CI cost (no new cell).
The dynamic cell stays thin (ONLY_ACTIVE_ARCH=YES) to keep the primary
cell fast. Adds a lipo-based assertion that the static binary is
actually multi-arch so the guard can't silently degrade to thin.
Export resolveLlvmArch/buildArchArgs and add tests for detectBinaryArchs
(mocked lipo), buildArchArgs, and resolveLlvmArch (override/config/auto)
so the new arch-selection lines are covered by unit tests, not only by
the universal e2e cell.
The app and WDA builds targeted "-destination id=<udid>", which
intermittently fails on GitHub runners with "Unable to find a device
matching the provided destination specifier" — a CoreSimulator
availability race, even after the booted UDID is ready. A simulator is
only needed to install and run (simctl + Appium), never to build for the
iphonesimulator SDK, so build with "generic/platform=iOS Simulator"
instead. This is the same device-agnostic build Detox uses in RNFB's
e2e; the specific UDID is still used for install/launch. Complements the
existing 'xcrun simctl list' availability workaround in ci.yml.
@mikehardy
mikehardy merged commit 88c94ed into main Sep 24, 2026
10 checks passed
@mikehardy
mikehardy deleted the fix/ios-universal-arch-selection branch September 24, 2026 21:22
@mikehardy

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 0.2.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant