Reach the phones, and link where loading is not allowed - #6
Merged
Conversation
The packages carried six desktop runtimes and nothing else. They now carry
Android and iOS as well, which are not the same kind of addition.
Android needs nothing from the consumer and nothing new from the binding. The
.so is resolved through the ordinary NuGet runtime-identifier mechanism and
packed into the apk, so the existing net8.0 assembly serves it unchanged. Only
the 64-bit ABIs ship: Google Play has required 64-bit for years, and Box3D
disables NEON on armv7, which has no divide or square root, so armeabi-v7a
would be a slower scalar build for devices that cannot be published to anyway.
iOS cannot work that way at all. Apple does not allow an application to load a
dynamic library that is not a signed framework in its bundle, so Box3D is
shipped as a static archive in an xcframework and linked into the application
by a .targets file the package carries. The symbols are therefore already in
the main image by the time a P/Invoke runs, which is why the binding names
__Internal there - and naming it conditionally needs a target framework of its
own. That framework is net10.0-ios rather than net8.0-ios because .NET 8's and
9's mobile workloads are out of support and the SDK refuses to build them
(NETSDK1202). Every other platform is still served by net8.0.
Consequences worth stating rather than discovering:
- Building this repository now needs the .NET 10 SDK and the iOS workload,
because dotnet build builds every framework a project declares. The test,
lint and aot jobs install it; docs.yml instead builds the one framework
docfx.json already asked for.
- Consumers on iOS need .NET 10. Nobody else does.
build-native.ps1 now takes the target from the runtime identifier instead of
the host, since the two stopped being the same thing, and gives each target its
own CMake tree - a shared one caches the first target's toolchain and either
fails on the second or, worse, produces a binary for the wrong target under the
right name. It finds the NDK and the SDK's bundled CMake and Ninja on its own,
and strips the Android output, which takes it from about 6 MB to under 900 KB
per ABI.
Both platforms are verified against the packed .nupkg rather than the
repository, because the repository cannot show either failure. A real Android
application is built and its apk opened to confirm libbox3d.so is inside it,
and a real iOS application is built and its executable checked for Box3D's
symbols, which is what proves the static archive survived the link. Neither
runs a simulation on a device, and the platform table says "in the apk" and
"linked" rather than "yes".
Two things that would have shipped broken in silence:
- pack gathers package files from the outer build of a multi-targeting
project, where every framework-specific property reads empty. Conditioning
the iOS build files on TargetPlatformIdentifier was true whenever the inner
build was inspected by hand and false during the pack that mattered, so
they were simply absent from the package.
- .gitignore ignores [Bb]uild/, which covered the very directory NuGet's
convention puts consumer build files in. The iOS .targets would never have
been committed, and the package would have looked complete while no iOS
application could link against it.
The README and both package descriptions now also state that this project was
built with AI assistance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The iOS target framework was added to the packable projects unconditionally, which quietly made building this repository depend on the .NET 10 SDK and the iOS workload - and there is no Linux host pack for Apple's toolchain, so "dotnet workload install ios" does not install something limited there, it fails. Every ubuntu job in CI failed on it, starting with lint. The framework is now opt-in through Box3DTargetApple, off by default. Building, testing and formatting are net8.0 with the .NET 8 SDK, exactly as before. The two jobs that produce packages turn it on and run on macOS, where the workload exists; the iOS half is still verified end to end, by installing the resulting package into a real iOS application and checking Box3D is linked into it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xcodebuild copies each -library argument into the framework under the name it arrived with, so the merged simulator archive landed as libbox3d-simulator.a next to the device's libbox3d.a. The framework is valid either way and links either way, but the two variants no longer answer to the same file name, and anything matching on it sees one and not the other - the package inspection did exactly that and failed, which is what it is for. The merge now happens in a directory of its own so the output can keep the name libbox3d.a, and that directory is removed afterwards rather than a single file whose name the script had to know. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The iOS consumer job built the application on macos-14 and got no further than the SDK's own version gate: .NET for iOS 26.5 requires Xcode 26.6, and macos-14 tops out at 15.4. Nothing about the package was wrong - the restore resolved it for iOS and reported it compatible - so the check never reached the thing it exists to check. It runs on macos-26 now, whose default Xcode is 26.6, and selects the newest Xcode the image carries rather than trusting the default to be it. The job that builds Box3D for iOS stays on macos-14 deliberately. It needs CMake and clang and nothing else, 15.4 has both, and a static archive built against an older SDK links perfectly well into an application built with a newer one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pointing xcode-select at the newest /Applications/Xcode_*.app was meant to avoid depending on the image's default. It made things worse: those side-by-side installs are not all complete, the one it picked had no macOS SDK under it, and actool failed to run at all - a failure invented entirely by the step meant to prevent one. macos-26 already defaults to Xcode 26.6, which is the version this workload asks for. The step now only reports what it is using, which is what was actually wanted from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The consumer is meant to be an ordinary application that has never heard of Box3D.NET, and it was not one: it is created under artifacts/, inside the repository, so it inherited Directory.Build.props from the root along with TreatWarningsAsErrors, the analyzers and the documentation gate. The iOS template ships AppDelegate and SceneDelegate, CA1711 objects to both names, and the build failed on this repository's own style rules before it ever got near the package. An empty Directory.Build.props and .targets in the working directory end MSBuild's search upwards. The desktop script avoids the same trap by setting ImportDirectoryBuildProps in a project it writes itself; this one cannot, because the project comes from a workload template. Android passed only because its template happens not to trip a rule. It gets the same treatment rather than the same luck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The consumer application built and linked cleanly and then the symbol check failed, which on its own does not say whether Box3D was left out of the link or whether nm simply cannot see it. Guessing between those from a red X is what a second CI run costs, so the check now gathers the answer before it fails. It asks the question two ways. Whether the linker was handed libbox3d.a at all, which is what the package's .targets is responsible for and is visible only in the build log; and whether b3 symbols are in the executable, which is what ForceLoad is responsible for. Either one is proof the archive reached the application, and reporting both says which half to look at. On failure it dumps the symbol count, the bundle contents and every build-log line naming a native reference. That a clean build proves nothing here is the point: a P/Invoke to __Internal is resolved by Mono at run time, so an application whose archive never reached the linker builds, installs and launches exactly like a correct one, and fails on the first physics call - on a device nobody in CI has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It passed by the weaker of its two halves: the linker was handed Box3D's archive, and zero b3 symbols were visible in the executable. The README claimed the symbols were confirmed present, which is not what happened, and a claim this repository cannot back is worse than a narrower one it can. So the wording now matches the check, and the check can now tell the two reasons for an empty symbol list apart. A stripped release binary defines no symbols at all and proves nothing either way, which is the normal case and is reported as such. A binary that kept its symbol table with no b3 in it is something else entirely - the archive reached the linker and was dropped, which is exactly what ForceLoad exists to prevent - and that now fails instead of passing quietly alongside the harmless case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The consumer wrote a file that used Box3D and left it unreachable: nothing in the template's AppDelegate refers to Box3DUse, so the trimmer dropped it, mtouch found no __Internal P/Invoke to keep a symbol for, and -dead_strip removed every object -force_load had just pulled out of libbox3d.a. The build succeeded, the archive reached the linker, and the executable came out with 61752 symbols and not one of Box3D's. Insert the call into Main instead, ahead of UIApplication.Main, which is what an application using this package actually looks like. The failure path now reads mtouch-symbols.list before it throws, because "the symbols are missing" has two different causes and they need different fixes: nothing asked for them, or something asked and the link dropped them anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Editing the entry point was the wrong place to hang this on: no file in the iOS template matches UIApplication.Main( any more, so the guard fired and the job failed on the anchor rather than on the package. The workload writes that file and it has changed spelling once already. A module initializer does not depend on the template's shape. Published with TrimMode=full, Box3DUse survives in the application assembly and Box3D.NET.Native keeps its b3 entry points; with the initializer removed and nothing else changed, Box3DUse is gone and Box3D.NET.Native is not in the publish output at all. That is the trimming this check kept walking into. The entry point edit stays as a second anchor, best effort now, and lists the sources it searched when it finds nowhere to insert. The symbol check at the end is the judge either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check searched the binary for b3 and found 968 of them, all managed: an AOT-compiled method carries its parameter types in its mangled name, so b3BodyId and b3ShapeDef turn up inside names like _Box3D_NET_Box3D_Body__ctor_Box3D_Native_b3BodyId. Those come from the managed assembly and would be in the executable whether or not libbox3d.a survived the link, which is the one thing this job exists to establish. Count the two apart. A defined symbol whose name starts _b3 came out of the archive; _Box3D_NET_ ones say the trimmer kept Box3D's C#. Passing now needs a native symbol, and the managed count separates the two ways this has failed: no managed symbols means the trimmer took Box3D before mtouch could ask for anything, managed but no native means the archive was dropped at the link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Android and iOS to the packages, which were desktop-only.
Android needs nothing new from the binding: the
.sois resolved through the ordinary NuGet runtime-identifier mechanism and packed into the apk, so the existingnet8.0assembly serves it unchanged. Onlyarm64-v8aandx86_64ship — Play has required 64-bit for years, and Box3D falls back to a scalar path on armv7.iOS cannot work that way. Apple does not allow an application to load a dynamic library that is not a signed framework, so Box3D ships as a static archive in an
xcframeworkand is linked in by a.targetsfile the package carries. The binding names__Internalthere, which needs a target framework of its own —net10.0-ios, because .NET 8's and 9's mobile workloads are out of support and the SDK refusesnet8.0-iosoutright (NETSDK1202).What this costs
iosworkload, sincedotnet buildbuilds every declared framework. Thetest,lintandaotjobs install it;docs.ymlinstead builds the single frameworkdocfx.jsonalready asked for.Verification
Against the packed
.nupkg, not the repository, because the repository cannot show either failure mode:.apkopened to confirmlibbox3d.sois inside it;Neither runs a simulation on a device, and the platform table says
in the apkandlinkedrather thanyes.Two silent failures found on the way
packgathers package files from the outer build of a multi-targeting project, where framework-specific properties read empty. Conditioning the iOS build files onTargetPlatformIdentifierwas true whenever the inner build was inspected by hand and false during the pack that mattered — the files were simply missing from the package..gitignoreignores[Bb]uild/, which covers exactly the directory NuGet's convention puts consumer build files in. The iOS.targetswould never have been committed, and the package would have looked complete while no iOS application could link against it.Also states in the README and in both package descriptions that this project was built with AI assistance.
🤖 Generated with Claude Code