feat(hosts): resolve the core from this tree for the iOS host - #725
Conversation
The iOS host pinned the core at an exact published version, so a core change reached it only when someone edited that pin. It now resolves the core from this repository, which is what makes one pull request cover the core and the host together. The app is built two ways, vendored here and checked out on its own, and the package sits at a different depth in each. So rather than hardcode a relative path, the manifest looks for the core's crates five levels up, the same marker the Android host checks, and falls back to the published version when they are absent. The standalone checkout keeps working unchanged. The path dependency declares its name. Swift Package Manager otherwise derives identity from the checkout directory, so `.product(package:)` would not match and the product would not resolve. The root manifest now prefers a staged local xcframework when one is present. Every published tag still points at the 0.7.0 asset (#723), so bindings generated from this tree would otherwise pair with an older binary, and the generated code verifies that pairing at runtime. A remote consumer has no staged binary and keeps the published asset. Both resolved files drop the pin for the core. Path dependencies are never pinned, so those entries were stale and one of them fed a CI cache key.
Opening the app now requires the core's generated bindings to exist, and the first error a developer meets names no remedy: Swift Package Manager reports an invalid custom path for a target they have never heard of, in a package identified by their own checkout folder. The tree looks clean because the missing files are gitignored. `make ios-bootstrap` runs the four stages that produce them, which took roughly two minutes measured. The prerequisite line in the host's README said dependencies resolve automatically on first build, which this change makes false, so it now names the command. Both layout blocks gain the hosts/ios and hosts/android entries they never got when those trees were imported.
|
@lore-bot-app review |
pgherveou
left a comment
There was a problem hiding this comment.
after reviewing comment
|
Checked both, and the good news first. The import has your 0.12.0. The Separately the import is stale, 29 commits behind On dropping One catch worth knowing before we do it. The release tags carry zero files under So: viable once the only SPM consumer is in-tree, and it also makes a chunk of |
|
Reading the diff and checking what the record says. Back in a few minutes. |
There was a problem hiding this comment.
Review is ready. Here it is, ready to paste as a PR comment.
TL;DR
Points the in-tree iOS host at the core by relative path, and makes the root manifest prefer a locally staged xcframework whenever one exists. 3 blocking, 4 minor.
Summary
hosts/ios/Packages/AppDependencies/Package.swift now probes for rust/crates/truapi-server five levels above its own #filePath and, when found, swaps the host-rust-core dependency from .exact("0.12.0") over HTTPS to a local path dependency at the repo root, passing name: so .product(package:) still resolves. The root Package.swift widens useLocalBinary: the environment variable still forces it, but a staged ios/truapi-host/Binaries/truapi_server.xcframework now also flips it, so Xcode opened from Finder gets the local binary. A new make ios-bootstrap chains codegen, the host rebuild, and provider binding generation. The host-rust-core pin is dropped from both committed Package.resolved files.
What the record says
- Issue #682 is the task this implements, and it states the acceptance criterion explicitly: replacing the pin should mean "any pull request modifying the core repository automatically triggers a build and test cycle for the iOS host within the same PR", verified by confirming "a deliberate breaking change in the core causes the host build to fail". It is still open. This diff delivers the wiring but not the trigger, see the first concern.
- Issue #679 resolved that the host should use a relative path "while keeping the root manifest unchanged to allow external consumers to resolve products by URL". The root manifest is changed here. The change is defensible, since a remote consumer never has the gitignored xcframework and still gets the published asset, but it goes past what #679 settled and is worth calling out in the description.
- Issue #723 confirms the premise the new comment rests on:
@parity/ios-hosttags from 0.12.0 onward point at the 0.7.0 binary, and "generated Swift bindings enforce strict pairing with the binary", producing contract version and checksum errors. It is unresolved, and it namestag-release.shand a possibly deliberate CI setup as the two candidate causes. This PR routes around #723 rather than fixing it, which is fine, but see concern 2. - PR #690 imported
hosts/iosas a snapshot without git history, and its 14 workflows are inactive pending #680. That is why no CI currently covers this path. - #674 rejected Gradle composition for the Android host in favour of CI invoking each host build directly (
cd hosts/android && ./gradlew). If the same shape is intended for iOS, that is the missing half here. - Owners for this area: pgherveou, ERussel, TarikGul (author of #679/#682/#690).
Concerns
-
.github/workflows/ci.yml:244— nothing buildshosts/ios. Thesdk_swiftfilter matches^ios/, which does not matchhosts/ios/, and no job inci.ymlresolveshosts/ios/Packages/AppDependencies. Both new manifest branches are therefore untested by CI, and a core change that breaks the host still merges green, which is the outcome #682 asks to prevent. Either add the filter path plus a resolve/build job, or say in the description that it lands in #680. -
hosts/ios/Packages/AppDependencies/Package.swift:139— thecoreIsInTree == falsebranch keeps.exact("0.12.0"), and per #723 that tag pairs 0.12.0 bindings with the 0.7.0 binary and fails the runtime checksum check. The root manifest's own new comment relies on that fact to justify preferring the local binary. So the fallback branch selects a combination the diff documents as broken. The core is at 0.14.0 as ofef2a738. Either bump the pin to a tag that works, or drop the fallback and require in-tree. -
Package.swift:34-35— the resolved dependency graph now depends on untracked filesystem state. SwiftPM keys its manifest evaluation cache on manifest contents, tools version and environment, not on unrelated files, so a graph resolved beforemake ios-bootstrapcan stay cached afterwards: published 0.7.0 binary, bindings freshly generated from this tree, and no manifest diff to explain the resulting checksum trap. It also means two people on the same commit build different graphs. The env var did not have this property, because changing it invalidates the cache. Worth havingios-bootstrappurge the SwiftPM manifest cache at the end, or keeping the env var as the only switch and supplying it to Xcode through an xcconfig. -
Package.swift:35—fileExistsis true for a partial or stale xcframework directory. ASIM_ONLY=1build or an interruptedstage-xcframework.shleaves a directory that passes this check, and the manifest silently links it.publish.shrefuses a framework missing a slice (Makefile:445), but that guard only covers the release path, not this one. -
hosts/ios/Packages/AppDependencies/Package.swift:8— "the same marker the Android host checks for" is not true in this tree. Nothing underhosts/android/referencesrust/crates,truapi-serveror the core at all; per #677 the Android in-tree wiring is still ontruapi-devand unresolved. A reader will go looking for a marker that does not exist. -
Makefile:183—ios-bootstraphas no build counterpart forhosts/ios.ios-buildatMakefile:190requires$(IOS_HOST)/.gitand defaults to../polkadot-app-ios-v2, and the importedhosts/ioshas no.git, so it cannot be pointed there.TRUAPI_LOCAL_PATHatMakefile:197is set but read by nothing in this tree. After this change, bootstrap is Xcode-only and the existing iOS automation still targets the old sibling checkout. Also,ios-bootstrapis missing from.PHONYatMakefile:6, where every otherios-*target is listed. -
ios/truapi-host/scripts/tag-release.sh:31andCLAUDE.md:134— both still describe the env var as the only way into the local branch. The greps attag-release.sh:37andrelease-ios.yml:220keep matching, since the line still beginslet useLocalBinary = ProcessInfo, so nothing breaks. But the guard's error message ("a published manifest must never pin the local binary") no longer describes what it verifies. The tag probe atrelease-ios.yml:155is genuinely safe because it clones the tag fresh and the xcframework is gitignored, which is worth stating in the comment rather than leaving implicit.
Questions for the author
- Is the CI job that builds
hosts/iosdeliberately deferred to #680, or was it meant to be in this PR? #682's verification step ("a deliberate breaking change in the core causes the host build to fail") cannot be run today. ios-bootstrapregenerates provider Swift bindings from this tree (provider-swiftplussync-bindings.sh) but never builds the provider xcframework, soTrUAPIProviderlinks the published 0.7.0 asset.hosts/iosonly requestsTrUAPIHost, so it does not trap, but is the provider intentionally left out of the pairing argument that motivates the host change?- The help text on
ios-bootstrapadvertisesSIM_ONLY=1. It does reachmake xcframeworkthrough MAKEFLAGS, but a simulator-only framework then satisfies the newfileExistscheck for device builds too. Intended?
Nothing in the diff attempted to address instructions to a reviewer.
🤖 Reviewed by Lore (Parity knowledge base) · 24 agent turns · 348.8s · knowledge as of 2026-09-10
| .deletingLastPathComponent() | ||
| .appendingPathComponent("ios/truapi-host/Binaries/truapi_server.xcframework") | ||
| .path | ||
| let useLocalBinary = ProcessInfo.processInfo.environment["TRUAPI_USE_LOCAL_BINARY"] == "1" |
There was a problem hiding this comment.
Package.swift:34-35 — the resolved dependency graph now depends on untracked filesystem state. SwiftPM keys its manifest evaluation cache on manifest contents, tools version and environment, not on unrelated files, so a graph resolved before make ios-bootstrap can stay cached afterwards: published 0.7.0 binary, bindings freshly generated from this tree, and no manifest diff to explain the resulting checksum trap. It also means two people on the same commit build different graphs. The env var did not have this property, because changing it invalidates the cache. Worth having ios-bootstrap purge the SwiftPM manifest cache at the end, or keeping the env var as the only switch and supplying it to Xcode through an xcconfig.
| .appendingPathComponent("ios/truapi-host/Binaries/truapi_server.xcframework") | ||
| .path | ||
| let useLocalBinary = ProcessInfo.processInfo.environment["TRUAPI_USE_LOCAL_BINARY"] == "1" | ||
| || FileManager.default.fileExists(atPath: stagedBinaryPath) |
There was a problem hiding this comment.
Package.swift:35 — fileExists is true for a partial or stale xcframework directory. A SIM_ONLY=1 build or an interrupted stage-xcframework.sh leaves a directory that passes this check, and the manifest silently links it. publish.sh refuses a framework missing a slice (Makefile:445), but that guard only covers the release path, not this one.
Closes #679.
The iOS host pinned the core at an exact published version, so a core change only reached it
when someone edited that pin by hand. It now resolves the core from this tree, which is what
makes one PR cover the core and the host together.
Two layouts, so no hardcoded path. This app is built vendored here and checked out on its
own, and the package sits at a different depth in each. The manifest looks for the core's
crates five levels up, the same marker the Android host checks, and falls back to the
published version when they are absent. The standalone checkout keeps working unchanged, which
matters while development continues there.
The path dependency declares its name. SPM derives a path dependency's identity from the
checkout directory, so without a declared name
.product(package:)does not match and theproduct fails to resolve. I had this wrong first time round.
The binary. The root manifest now prefers a staged local xcframework when present. Every
published tag still points at the 0.7.0 asset (#723), so bindings generated from this tree
would pair with an older binary, and the generated code checks that pairing at runtime. A
remote consumer has no staged binary and keeps the published asset.
One bootstrap command. Opening the app needs those generated bindings, and the first error
is unhelpful: an invalid custom path for a target nobody has heard of, in a package named
after your own folder, with a clean
git statusbecause the missing files are gitignored.make ios-bootstrapruns the four stages, about two minutes. The host README saiddependencies resolve automatically on first build, which this makes false, so it names the
command instead. Both layout blocks also gain the
hosts/iosandhosts/androidentries theynever got when those trees landed.
Known gap. Nothing in CI builds
hosts/ios, so nothing here is gated. Only a compilecatches this class of breakage: resolve,
show-dependenciesandxcodebuild -showBuildSettingsall pass on a completely broken tree. That gate belongs with #680, where the app is actually
built.