[Proposal] Declarative native dependency resolution for Apple platforms
- Dominant language
- C#
- Stars
- 2.9k
- Forks
- 576
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 96
Description
### Apple platform
iOS, macOS, Mac Catalyst, and tvOS
### Framework version
net9.0-* (with a staged path for later TFMs)
### Affected platform version
.NET 9+; researched against Xcode 26.6 / Swift 6.3.3 and current Apple, SwiftPM, CocoaPods, NuGet, MSBuild, and dotnet/macios sources as of 2026-08-24.
### Description
## Summary
.NET for Apple platforms should move from independently carrying or acquiring native payloads in each binding/native-interop NuGet toward **one declarative native dependency graph resolved at the final app boundary**.
This is the Apple-platform counterpart to dotnet/android#12495, but Apple mechanisms differ materially from Gradle and should not inherit the Android design mechanically.
The recommended direction is:
1. **Use Swift Package Manager as the package identity and version-resolution authority.** Do not reimplement Swift package semantics in MSBuild/C#.
2. **Add an SDK-owned direct-artifact path** for digest-pinned XCFrameworks, frameworks, static libraries, and artifact bundles from GitHub Releases or vendor HTTPS endpoints.
3. **Do not add first-class CocoaPods restore/build integration.** Provide only an explicit one-shot importer for strictly admissible vendored-binary podspecs.
4. **Keep final app integration in dotnet/macios.** SwiftPM does not perform .NET app linking, framework embedding, resource/privacy-manifest placement, dSYM handling, install-name/rpath repair, code signing, or mixed-provider reconciliation.
5. **Ship a data-only package/project/file contract.** NuGets contribute a versioned JSON manifest through one minimal `buildTransitive` `.props` item. Project references contribute only project-owned manifests through a recursive target.
6. **Resolve once per app and target framework, then materialize per destination/RID.** Multi-RID builds for one TFM share the graph.
7. **Lower resolved physical artifacts into the existing `NativeReference` pipeline.** Extend rather than replace current slice selection, native linking, bundle copy, stripping, dSYM, and signing targets.
8. **Start in report-only, dual-payload mode.** Existing sidecars/embedded/downloaded artifacts remain authoritative until graph selection, locks, offline behavior, and provider reconciliation are proven.
9. **Keep opaque local artifacts as a permanent exception.** NDA-delivered, patched, source-built, relocated, or unavailable artifacts cannot always be reconstructed from a registry/URL.
### Important v1 boundary
SwiftPM is ready to be the **resolution authority**, but materialization has two separate constraints:
- arbitrary source library products are not safe to consume directly: SwiftPM's common `.library(type: .automatic)` produces object/module outputs but no `.a`, `.dylib`, or framework, and there is no documented stable external link-plan export;
- resolved binary artifacts are recorded in versioned `.build/workspace-state.json` and the artifact cache, not in `Package.resolved` or a stable artifact-path export.
V1 should therefore:
- support SwiftPM resolution, registries, mirrors, locks, and conflict analysis;
- support **binary-only product closures** through a pinned-toolchain adapter that validates the workspace-state/export schema and fails closed when it changes;
- support direct digest-pinned artifacts as the stable binary primitive;
- allow source-package requirements in report/conflict analysis;
- require a publisher-built XCFramework or a local `XcodeProject` with no independently resolved Swift packages for graph-coherent source materialization;
- treat existing Xcode projects with their own Swift package graph as explicit opaque/full-trust providers until a managed Xcode adapter can stage the app-selected lock and disable automatic package resolution.
This avoids treating “SwiftPM resolved it” as equivalent to “the final `.app` is linked and distributable.”
## Current-state map
### Modern build chain
The active SDK path is layered: [`dotnet/targets/Xamarin.Shared.Sdk.targets`](https://github.com/dotnet/macios/blob/main/dotnet/targets/Xamarin.Shared.Sdk.targets) imports the per-platform targets under `msbuild/Xamarin.Shared`, which import [`Xamarin.Shared.targets`](https://github.com/dotnet/macios/blob/main/msbuild/Xamarin.Shared/Xamarin.Shared.targets) and, for bindings, [`Xamarin.Shared.ObjCBinding.targets`](https://github.com/dotnet/macios/blob/main/msbuild/Xamarin.Shared/Xamarin.Shared.ObjCBinding.targets). These shared targets are part of the modern SDK path, not a separate legacy-only stack.
### `NativeReference`
`NativeReference` currently represents `.framework`, `.xcframework`/`.xcframework.zip`, `.a`, and `.dylib` inputs, with metadata including:
- `Kind=Framework|Static|Dynamic`
- `ForceLoad`, `SmartLink`
- `Frameworks`, `WeakFrameworks`, `LinkerFlags`
- `IsCxx`, `NeedsGccExceptionHandling`, `LinkWithSwiftSystemLibraries`
- `CopyToAppBundle`
The pipeline adds internal transport metadata such as `RelativePath`, `LinkToExecutable`, and `PublishFolderType`.
Relevant docs:
- [`docs/building-apps/build-items.md`](https://github.com/dotnet/macios/blob/main/docs/building-apps/build-items.md)
- [`docs/native-library-interop.md`](https://github.com/dotnet/macios/blob/main/docs/native-library-interop.md)
### Binding packages/project references
For modern .NET projects, `NoBindingEmbedding` defaults to `true`. A binding build:
1. runs `_PrepareNativeReferences` / [`PrepareNativeReferences.cs`](https://github.com/dotnet/macios/blob/main/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareNativeReferences.cs);
2. runs `_CreateBindingResourcePackage` / [`CreateBindingResourcePackage.cs`](https://github.com/dotnet/macios/blob/main/msbuild/Xamarin.MacDev.Tasks/Tasks/CreateBindingResourcePackage.cs);
3. writes `AssemblyName.resources` or `.resources.zip` beside the managed DLL, containing the native payload and XML manifest;
4. packs that sidecar under `lib//` via `_IncludeBindingResourcesInNuGetPackage`.
At the app boundary, `_ExpandNativeReferences` examines the flattened `@(ReferencePath)` closure. [`ResolveNativeReferences.cs`](https://github.com/dotnet/macios/blob/main/msbuild/Xamarin.MacDev.Tasks/Tasks/ResolveNativeReferences.cs) expands sidecars, selects XCFramework slices, and emits framework/file inputs. This already lets an ordinary class library sit between the app and binding without repacking the native payload.
The deprecated `NoBindingEmbedding=false` path embeds bytes in the managed assembly and relies on `[LinkWith]` inspection by [`tools/common/Assembly.cs`](https://github.com/dotnet/macios/blob/main/tools/common/Assembly.cs) and [`ExtractBindingLibrariesStep.cs`](https://github.com/dotnet/macios/blob/main/tools/dotnet-linker/Steps/ExtractBindingLibrariesStep.cs).
### XCFramework selection
`ResolveNativeReferences.TryResolveXCFramework` reads:
- `AvailableLibraries`
- `LibraryIdentifier`
- `LibraryPath`
- `SupportedPlatform`
- `SupportedPlatformVariant`
- `SupportedArchitectures`
It does not currently model `HeadersPath`, module maps, Swift interfaces, privacy manifests, package identity, minimum deployment targets from Mach-O load commands, transitive `LC_LOAD_DYLIB` requirements, provenance, or signer identity.
### Existing native source build
[`Microsoft.MaciOS.Sdk.Xcode.targets`](https://github.com/dotnet/macios/blob/main/dotnet/targets/Microsoft.MaciOS.Sdk.Xcode.targets) implements `XcodeProject`. `_BuildXcodeProjects` calls `xcodebuild archive`, creates an XCFramework, and emits `NativeReference`. It uses `BUILD_LIBRARY_FOR_DISTRIBUTION=YES`, `SWIFT_INSTALL_OBJC_HEADER=YES`, and `SKIP_INSTALL=NO`.
However, `_BuildXcodeProjects` currently runs early and supplies its own Xcode package cache. An Xcode project containing Swift package references can perform a second independent resolution outside the proposed app graph/lock. That must be detected and either rejected as a managed provider or explicitly treated as opaque.
There is currently no SwiftPM/`Package.swift` integration in this repository.
### Final native pipeline
The final app flow is approximately:
1. `_ExpandNativeReferences`
2. `ComputeFilesToPublish` / ILLink custom steps
3. `ExtractBindingLibrariesStep`, `GatherFrameworksStep`, `ComputeNativeBuildFlagsStep`
4. `_LoadLinkerOutput`
5. `_ComputeFrameworkFilesToPublish` / `_ComputeDynamicLibrariesToPublish`
6. `_ComputeLinkNativeExecutableInputs`
7. `_LinkNativeExecutable` / [`LinkNativeCode.cs`](https://github.com/dotnet/macios/blob/main/msbuild/Xamarin.MacDev.Tasks/Tasks/LinkNativeCode.cs)
8. framework/resource copy, install-name handling, dSYM processing, stripping, and codesigning
Key extension points:
- `dotnet/targets/Xamarin.Shared.Sdk.targets`
- `_ComputeFrameworkFilesToPublish`
- `_CopyDirectoriesToBundle`
- `_ComputeDynamicLibrariesToPublish`
- `_LoadLinkerOutput`
- `_AddSwiftLinkerFlags`
- `_ComputeLinkNativeExecutableInputs`
- `_LinkNativeExecutable`
- `msbuild/Xamarin.Shared/Xamarin.Shared.targets`
- `_ExpandNativeReferences`
- `_CreateBindingResourcePackage`
- `_IncludeBindingResourcesInNuGetPackage`
- `_CollectItemsForPostProcessing`
- `_ComputeXCFrameworkDSyms`
- `_CopyXCFrameworkDSyms`
- `_CodesignAppBundle`
- `msbuild/Xamarin.MacDev.Tasks/Tasks`
- `ResolveNativeReferences.cs`
- `PrepareNativeReferences.cs`
- `CreateBindingResourcePackage.cs`
- `FilterStaticFrameworks.cs`
- `LinkNativeCode.cs`
- `ComputeCodesignItems.cs`
- `InstallNameTool.cs`
- `DSymUtil.cs`
- `SymbolStrip.cs`
- `StripFrameworkHeaders.cs`
## Goals
- Resolve one coherent native package/product graph for each final app TFM.
- Preserve SwiftPM identity, version, registry, mirror, trait, tools-version, and signing semantics.
- Support Swift source-control/registry requirements, SwiftPM binary targets, direct binaries, and declared local providers.
- Separate TFM-qualified graph resolution from destination/RID-specific materialization.
- Preserve request origins through NuGet, project, file, and app declarations.
- Detect version, provider, binary-identity, slice, deployment-target, linkage, embed, and linker conflicts before `clang`, `ld`, `dyld`, codesign, or App Store validation.
- Produce deterministic app locks, verified offline builds, actionable graph output, and native SBOM/provenance.
- Preserve existing packages/apps during migration.
- Avoid SwiftPM/network invocation on no-op and design-time builds.
- Run one resolver for the app graph, not per package/class library.
## Non-goals
- Replacing NuGet restore or combining NuGet and Swift package version spaces.
- Reimplementing SwiftPM's solver, registry protocol, identity, or manifest evaluator.
- Treating CocoaPods as a second graph authority.
- Executing package-authored MSBuild tasks, Ruby, shell phases, `prepare_command`, command plugins, or downloaded post-processing scripts by default.
- Automatically generating managed binding APIs.
- Assuming semantic compatibility from a shared major version.
- Silently raising deployment targets.
- Silently repairing unknown vendor binaries.
- Inferring identity from filenames and suppressing payloads based on inference.
- Guaranteeing arbitrary source Swift package materialization in v1.
- Removing `NativeReference`, `XcodeProject`, or opaque local support.
## Compared options
### SwiftPM
Use SwiftPM for:
- source-control requirements: range, exact, branch, revision;
- registry requirements: range/exact;
- package identity/canonicalization;
- tools-version eligibility;
- traits and graph-wide trait union;
- mirrors and SCM-to-registry transformation;
- `Package.resolved`;
- registry auth, fingerprints, signatures, and signer continuity;
- binary-target URL/checksum validation;
- package graph export.
Do **not** expect SwiftPM to:
- record binary checksums in `Package.resolved`;
- expose a stable product-to-resolved-binary-path or final link-plan export;
- embed/sign app frameworks;
- model all dotnet/macios compile/link/embed/resource/symbol roles;
- preserve/copy dSYMs into the final archive;
- validate final deployment-target compatibility;
- merge NuGet/project/file origins;
- reconcile selected artifacts with legacy providers;
- detect two package identities shipping the same native binary.
### Direct artifact path
Provide an SDK-owned primitive for exact remote artifacts:
```xml
```
Requirements:
- HTTPS only;
- SHA-256 or stronger required;
- reject mutable `latest`/branch URLs by default;
- drop auth on cross-origin redirects;
- hardened extraction (no absolute paths, `..`, escaping symlinks, device files, zip bombs);
- content-addressed cache;
- verify cache hits;
- explicit layout, license, credential identity, mirror, and provenance;
- no post-download scripts.
Keep both:
- `sourceArchiveDigest`: verifies transport bytes;
- canonical `contentDigest`: sorted normalized path/type/mode/symlink/file hash tree used to reconcile repackaged providers.
Archive digest alone cannot identify equivalent content because two packages may ZIP identical XCFramework bytes differently.
### CocoaPods
Do not add first-class runtime integration because:
- CocoaPods is in maintenance mode;
- public Trunk becomes read-only on 2026-12-02;
- Firebase stops publishing pods in 2026-10;
- podspecs outside normalized Trunk JSON are Ruby programs;
- `prepare_command`, script phases, hooks, and plugins make output dependent on arbitrary code;
- integration output is a generated Xcode project, xcconfigs, and shell phases;
- `Podfile.lock` hashes podspec metadata, not payload bytes;
- payload digest/auth behavior is weaker than required.
Provide a developer-invoked importer only for podspecs that:
- use HTTP source with SHA-256;
- contain only `vendored_frameworks`/`vendored_libraries`;
- optionally declare module maps, resources/resource bundles, static framework, system/weak frameworks, libraries, and compiler/link metadata;
- have no source compilation, scripts, hooks, plugins, subspec hierarchy, or dynamic file-tree transformation.
The importer emits a reviewed/check-in manifest; CocoaPods never runs during normal build/CI.
## Proposed public API
Names are provisional.
### Swift package requirement
```xml
```
Registry form:
```xml
```
Requirement forms:
| Requirement | Metadata |
|---|---|
| `UpToNextMajor` | `Version` |
| `UpToNextMinor` | `Version` |
| `Range` | `MinimumVersion`, `MaximumVersion` |
| `Exact` | `Version` |
| `Branch` | `Branch` |
| `Revision` | `Revision` |
Rules:
- `Products` is required for materialization.
- Traits are emitted on generated `Package.Dependency` entries, not passed as root CLI traits.
- Omit the traits API when unused to avoid forcing Swift tools 6.1+.
- Bindings declare `BoundAgainstVersion` and tested compatibility.
- Missing compatibility metadata means exact bound version.
- The SDK rejects mutable branch requirements in locked/release mode unless policy allows them.
- Local package paths never flow from NuGets.
### Local/opaque artifacts
Keep `NativeReference`:
```xml
```
Opaque means:
- never acquire/substitute it;
- never infer a public package identity;
- include it in graph/SBOM/reporting;
- reconcile only explicit identities;
- require app action on collisions.
### App overrides/trust
```xml
```
`AppleNativePackageOverride` is a root constraint, not a Gradle/NuGet-style force. It may narrow/pin only if all SwiftPM requirements remain satisfiable. `UnsafeCompatibilityOverride` bypasses a binding's tested range after successful selection; it does not bypass SwiftPM constraints. A local-fork substitution would be a separate future full-trust feature.
### Modes/locks
```xml
Report
true
true
$(MSBuildProjectDirectory)/apple-native.$(TargetFramework).lock.json
$(MSBuildProjectDirectory)/.apple-native/$(TargetFramework)/Package.resolved
```
Modes:
- `Legacy`: existing behavior only;
- `Report`: analyze/write diagnostics without suppressing/replacing payloads;
- `Resolve`: selected graph owns known-identity providers, while unknown/opaque artifacts remain legacy.
Locks are TFM-qualified. A cross-targeting outer build never writes a lock; multi-RID inner builds share it read-only.
## Manifest and propagation
A versioned JSON manifest distinguishes requirements from providers:
```json
{
"schemaVersion": 1,
"producer": {
"kind": "package",
"id": "Contoso.VendorSDK.Binding",
"version": "3.2.1",
"targetFramework": "net11.0-ios"
},
"requirements": [
{
"id": "swift:vendor-sdk",
"kind": "swiftPackage",
"source": {
"kind": "sourceControl",
"location": "https://github.com/vendor/VendorSDK.git"
},
"requirement": {
"kind": "range",
"lowerBound": "3.2.0",
"upperBound": "4.0.0"
},
"products": [ "VendorCore" ],
"boundAgainst": { "version": "3.2.1", "revision": "..." },
"compatibleSelection": { "lowerBound": "3.2.0", "upperBound": "4.0.0" }
}
],
"providers": [
{
"id": "contoso.vendorsdk.binding/payload",
"satisfies": {
"requirementId": "swift:vendor-sdk",
"version": "3.2.1",
"product": "VendorCore"
},
"kind": "xcframework",
"path": "native/VendorSDK.xcframework.zip",
"contentDigest": "sha256:...",
"sourceArchiveDigest": "sha256:...",
"modified": false,
"binaryIdentity": {
"moduleName": "VendorSDK",
"bundleIdentifier": "com.vendor.sdk"
}
}
]
}
```
Rules:
- requirements state what a consumer needs;
- providers state bytes already supplied;
- providers never alter the graph;
- `modified=true` requires distinct identity/reason/original digest/app acceptance;
- paths are manifest-relative and cannot escape the root;
- newer unsupported schema versions fail with producer identity;
- arbitrary commands are not representable;
- arbitrary linker flags require app approval.
### NuGet
Packages ship:
```text
buildTransitive//.props
buildTransitive//.apple-native.json
```
The conventionally named `.props` contains one inert item and no target/task/script:
```xml
```
`Legacy` gates SDK collection after project evaluation; it must not condition the package `.props`, because `nuget.g.props` is imported before the project body.
`buildTransitive`:
- crosses package and ProjectReference edges by default;
- can be suppressed by `ExcludeAssets=buildTransitive`/matching `PrivateAssets`;
- appears once in the final flattened restore closure;
- must not be re-returned by every referenced project.
### Project references
Add optional recursive target:
```text
GetAppleNativeDependencyManifests
```
It returns only project-origin manifests, with full paths and explicit metadata; forwards configuration/platform/nearest TFM; uses `SkipNonexistentTargets=true`; deduplicates diamonds without hiding divergent content; and is registered for static-graph inner builds. Package-origin manifests are not traversed again.
### File references
Discover `Binding.dll.apple-native.json` beside file-referenced assemblies. Unknown references stay on the legacy path.
## Resolution, materialization, and target flow
```text
NuGet restore / ResolvePackageAssets
-> ResolveReferences / project-reference graph
-> collect package/project/file/app manifests
-> normalize request + apply app constraints
-> resolve Swift package graph once per app + TFM
-> statically classify XcodeProject inputs as managed/opaque
-> materialize binary artifacts per destination/RID
-> emit app-only NativeReference/resource/symbol candidates
-> _ExpandNativeReferences
-> ComputeFilesToPublish / ILLink
-> _LoadLinkerOutput
-> reconcile modern + legacy + Xcode providers
-> _ComputeFrameworkFilesToPublish
-> _ComputeDynamicLibrariesToPublish
-> _ComputeLinkNativeExecutableInputs
-> _LinkNativeExecutable
-> copy resources/frameworks, dSYMs, strip, fix install names, sign
```
Required wiring:
- Add collection/normalization/resolution to `_RunRidSpecificBuildDependsOn`.
- Pass graph/materialization/lock paths through `_RidSpecificProperties`; RID inner builds skip resolution.
- Materialization may feed `_ExpandNativeReferences` only in app/executable builds (`$(_CanOutputAppBundle)==true`, `$(IsBindingProject)!=true`).
- Tag graph-created native inputs and ensure binding `_CreateBindingResourcePackage` sees author-declared inputs only, so libraries never repack the app closure.
- Add reconciliation explicitly after `_LoadLinkerOutput` in both framework and dynamic-library dependency chains.
- Split Xcode checks: inspect project files for package references during normalization; inspect output binary identity during per-RID reconciliation.
- A package-bearing opaque Xcode project is a disclosed exception and may resolve independently per RID; it cannot claim the app lock.
### SwiftPM binary adapter
For a requested v1 product:
1. use `show-dependencies --format json` for resolved package identities/paths;
2. use per-package `describe --type json`/`dump-package` to map product target closures;
3. reject closures containing source, resource, macro, plugin, executable, or system-library targets;
4. adapt the supported `workspace-state.json` schema to acquired binary paths;
5. rely on SwiftPM for archive checksum verification, then verify canonical extracted `contentDigest` against the composite lock;
6. fail closed on unknown state/export versions.
This adapter is explicitly tied to supported Xcode/SwiftPM versions; it is not presented as a stable public SwiftPM API.
### Source packages
Source packages may include Swift/C/C++/Objective-C, resources/`Bundle.module`, macros, build-tool plugins, and automatic/static/dynamic products. Typical automatic libraries produce no external linkable product.
V1 supports graph analysis but requires a publisher-built XCFramework or package-free local Xcode provider for materialization. A later spike may generate an SDK-owned shim package with an explicit static library product, but must prove:
- full transitive object closure;
- resource bundle placement/accessor correctness;
- module/header/interface outputs;
- plugin/macro trust;
- dynamic product handling;
- per-destination symbols and link metadata;
- no hidden PIF/DerivedData contract.
## Destination/native handling
### Slices
| Destination | Platform | Variant | Architectures |
|---|---|---|---|
| iOS device | `ios` | absent | `arm64` |
| iOS simulator | `ios` | `simulator` | `arm64`, `x86_64` |
| Mac Catalyst | `ios` | `maccatalyst` | `arm64`, `x86_64` |
| macOS | `macos` | absent | `arm64`, `x86_64` |
| tvOS device | `tvos` | absent | `arm64` |
| tvOS simulator | `tvos` | `simulator` | `arm64`, `x86_64` |
Rules:
- never `lipo` device and simulator slices;
- inspect legacy fat binaries using `LC_BUILD_VERSION`, not architecture alone;
- require every requested architecture;
- create universal output only within one platform/variant;
- never silently exclude an architecture.
### Deployment target
Inspect each Mach-O slice's `LC_BUILD_VERSION` (fallback legacy `LC_VERSION_MIN_*`) and compare `minos` to the app target:
- macOS `1`
- iOS `2`
- tvOS `3`
- Mac Catalyst `6`
- iOS Simulator `7`
- tvOS Simulator `8`
Fail rather than raising the app target.
### Static/dynamic
- inspect Mach-O/archive type; don't trust extension;
- static frameworks link the binary but may still need resource/privacy wrappers copied;
- dynamic frameworks link/embed/thin/sign;
- reject standalone dylibs on iOS/tvOS;
- allow on macOS/Catalyst with correct layout/rpath/signing, warning that frameworks are more portable;
- treat mergeable XCFrameworks as ordinary dynamic frameworks in v1 and report the lost optimization.
### Modules/resources/privacy
Inventory `HeadersPath`, `Headers`, `Modules/module.modulemap`, Swift modules/interfaces/docs/ABI metadata, generated Objective-C headers, framework resources, standalone `.bundle`s, and `PrivacyInfo.xcprivacy`.
Privacy manifests stay separate in their owning app/framework bundle. Xcode aggregates them into a privacy **report**, not one merged manifest. A bare `.a` cannot carry resources/privacy; use a static framework or companion resource.
### dSYMs/BCSymbolMaps
- preserve vendor dSYMs;
- match Mach-O/dSYM UUIDs;
- copy per-slice symbols through existing XCFramework paths;
- treat BCSymbolMaps as legacy optional payloads (bitcode submission ended with Xcode 14).
### Signing/rpaths/transitive dependencies
Distinguish origin signature from final app embedding signature. Thinning/install-name changes invalidate the original signature; verify origin first, then re-sign final code.
Scan `LC_LOAD_DYLIB`/`LC_RPATH`:
- satisfy SDK/system libraries from selected SDK;
- map third-party install names to exactly one provider;
- fail unresolved `@rpath` or unexpected absolute paths;
- apply recorded repairs before final signing.
## Conflict/deduplication semantics
### Package graph
- key by SwiftPM package identity;
- one selected version per app graph;
- conflicting locations fail unless mirror/SCM-to-registry policy reconciles them;
- SwiftPM resolves ranges;
- selected version must satisfy every binding's declared compatibility unless app accepts unsafe compatibility;
- requested products union;
- traits union according to SwiftPM.
### Direct artifacts
- same ID/version/content digest: collapse;
- same ID/different content digest: supply-chain error;
- same ID/different version: require app constraint/selection;
- version never proves byte identity.
### Physical identity
Use applicable fields:
- module name;
- framework `CFBundleIdentifier`;
- `LC_ID_DYLIB` install name;
- product name;
- platform/variant/architectures;
- signing identity;
- canonical content digest;
- Mach-O UUID;
- static symbol/member inventory when necessary.
Rules:
- identical identity/content: use one provider and retain origins;
- same package/product/version but different content: hard provenance error;
- same module/bundle/install-name with different content: hard collision;
- static and dynamic provider for same product: hard collision unless graph selects one;
- duplicate embedded framework: hard collision;
- unknown artifacts remain legacy and continue to fallback duplicate checks.
Structured linker merging:
- system framework/library: set union;
- weak + strong request: strong wins with origin warning;
- `-ObjC`: global OR;
- force-load: per selected archive, deduped by physical identity;
- rpaths: normalized set; reject absolute build-machine paths;
- `SmartLink=false`: conservative global behavior with origin;
- arbitrary order-sensitive flags: app-owned/explicit approval only.
## Locking, auth, security, and offline
Use two TFM-qualified files:
1. `.apple-native//Package.resolved` for source-control/registry pins.
2. `apple-native..lock.json` for request hash, package pins, products/traits, transport and extracted-content digests, final URL, mirror, provider identity, selected slices/deployment versions, signing/attestation status, license, and resolver/workload version.
The `obj/.../Package.resolved` copy is working state only. Only `UpdateAppleNativeDependencies` copies it and the composite lock back atomically. Normal/locked builds never update checked-in locks.
Security policy:
- HTTPS and digest required;
- credentials referenced by identity and scoped to exact origin;
- no secrets in project/manifest/lock/binlog/diagnostics;
- package/machine mirror policy only; transitive packages cannot redirect;
- strict SwiftPM fingerprint checking;
- registry signature validation on;
- signer-entity checking strict in locked/release mode;
- Swift registry signatures cover source archives/manifests, not arbitrary binary URLs;
- preserve SwiftPM sandbox;
- never pass `--disable-sandbox`, `-skipPackagePluginValidation`, or `-skipMacroValidation`;
- command plugins aren't invoked;
- source build-tool plugins/macros fail by default and require exact package+revision+capability trust;
- noninteractive CI fails when trust is missing.
Offline mode uses only locks/caches and reports the exact missing identity/digest. Design-time builds never contact the network, update locks, or prompt for trust.
## Compatibility/migration
Support simultaneously:
1. declaration-only packages;
2. declaration + packaged provider;
3. current `.resources[.zip]` sidecars;
4. deprecated embedded `[LinkWith]` resources;
5. package-authored download targets;
6. direct `NativeReference`;
7. `XcodeProject` outputs;
8. opaque/patched/source-built artifacts.
Identity discovery precedence:
1. explicit manifest/provider identity;
2. explicit `NativeReference` provenance;
3. publisher/SDK mapping;
4. bundle/module/install-name metadata;
5. checksum similarity;
6. filename.
Only levels 1–3 may suppress payloads. Levels 4–6 improve diagnostics/prove collisions only.
### Dual payload
A modernized package keeps its existing DLL/native sidecar and adds manifest + exact canonical provider content digest.
- old SDK: uses sidecar;
- `Legacy`: same;
- `Report`: resolves/compares, changes nothing;
- `Resolve`: uses sidecar only when selected version/product/content digest match; otherwise fails or acquires according to explicit policy.
Permanent exceptions remain for patched/source-built/private/NDA/local/disappeared upstream artifacts, with identity/digest/license/dependency metadata where possible.
## Incrementality/performance
Stable inputs:
- app declarations/overrides;
- unique manifest hashes;
- relevant `project.assets.json` identity;
- TFM;
- policy identity excluding secrets;
- locks;
- Xcode/Swift/workload/resolver versions.
Outputs:
```text
obj///apple-native/request.json
obj///apple-native/graph.json
obj///apple-native/Package.swift
obj///apple-native/Package.resolved
obj///apple-native/materialized//
obj///apple-native/providers.json
obj///apple-native/link-items/
```
Requirements:
- sorted JSON, no timestamps/avoidable absolute paths;
- generated files in `FileWrites`;
- write-only-when-different;
- stable item-index file because MSBuild `Inputs` doesn't remember prior item-list membership;
- cross-process cache locks;
- read-only enterprise cache + writable delta;
- second no-op build invokes no SwiftPM/resolver process;
- design-time build invokes no network.
## Diagnostics
Reserve a provisional `MT`/`MM` 72xx block (exact allocation requires review):
| Code | Condition |
|---|---|
| `MT/MM7200` | unsupported manifest/schema/export version |
| `MT/MM7201` | incompatible Swift package requirements |
| `MT/MM7202` | conflicting package sources |
| `MT/MM7203` | checksum/provider/provenance mismatch |
| `MT/MM7204` | unapproved source/credential/signer/trust |
| `MT/MM7205` | lock missing/drifted |
| `MT/MM7206` | offline cache miss |
| `MT/MM7207` | no compatible slice |
| `MT/MM7208` | binary requires newer deployment target |
| `MT/MM7209` | selected version outside binding compatibility |
| `MT/MM7210` | duplicate binary identity/different content |
| `MT/MM7211` | static/dynamic or duplicate-symbol collision |
| `MT/MM7212` | unresolved/invalid install-name dependency |
| `MT/MM7213` | unapproved unsafe linker flag/macro/plugin |
| `MT/MM7214` | unsupported source product materialization |
| `MT/MM7215` | invalid/misplaced privacy/resource |
| `MT/MM7216` | dSYM UUID mismatch |
Support targets:
- `AppleNativeDependencyGraph`
- `AppleNativeDependencyInsight`
- `UpdateAppleNativeDependencies`
- `VerifyAppleNativeDependencies`
Every diagnostic includes identity/version, app/package/project/file origins, graph path, destination triple, provider/content digest where safe, remediation/lock command, and no secrets.
## Workstreams
1. **Schema/API:** items, JSON schema, version/override/trust semantics.
2. **Manifest transport:** buildTransitive, recursive project target, file sidecar, pack hooks.
3. **SwiftPM host:** generated root, Package.resolved staging, registry/mirror/security options, graph export, binary artifact adapter.
4. **Direct acquisition:** auth, redirect safety, hardened extraction, CAS cache, signatures/attestations.
5. **Inspection/reconciliation:** XCFramework/artifact metadata, Mach-O, archives/symbols, modules, resources/privacy, providers.
6. **MSBuild integration:** app-only materialization before `_ExpandNativeReferences`, late reconciliation after `_LoadLinkerOutput`, multi-RID propagation.
7. **Link/embed/sign:** flags, rpaths, install names, Swift runtime, resources, dSYMs, codesigning.
8. **Diagnostics/docs:** localized messages, error docs, build items/properties/targets, migration/auth/mirror guidance.
9. **Tests/templates/ecosystem:** task tests, existing XCFramework/binding tests, new XcodeProject tests, templates, `CommunityToolkit/Maui.NativeLibraryInterop` pilot.
10. **Source adapter spike:** aggregate shim only after binary path ships.
Concrete files include:
- `dotnet/targets/Xamarin.Shared.Sdk.props`
- `dotnet/targets/Xamarin.Shared.Sdk.targets`
- `dotnet/targets/Microsoft.MaciOS.Sdk.Xcode.targets`
- `msbuild/Xamarin.Shared/Xamarin.Shared.targets`
- `msbuild/Xamarin.Shared/Xamarin.Shared.ObjCBinding.targets`
- `msbuild/Xamarin.MacDev.Tasks/Tasks/ResolveNativeReferences.cs`
- `PrepareNativeReferences.cs`
- `CreateBindingResourcePackage.cs`
- `FilterStaticFrameworks.cs`
- `LinkNativeCode.cs`
- `ComputeCodesignItems.cs`
- `InstallNameTool.cs`
- `DSymUtil.cs`
- `SymbolStrip.cs`
- `CollectBundleResources.cs`
- `tools/dotnet-linker/Steps/ExtractBindingLibrariesStep.cs`
- `GatherFrameworksStep.cs`
- `ComputeNativeBuildFlagsStep.cs`
- `tools/common/Application.cs`
- `tools/common/Assembly.cs`
- `msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx`
- `docs/website/mtouch-errors.md`
## Validation matrix
Cover at minimum:
- SCM/registry direct/transitive packages; same identity via SCM/registry; version requirements; tools-version eligibility; traits; mirrors; signing/fingerprint drift.
- Binary target ZIP/index; artifact bundle static library; direct immutable release; private endpoint; auth redirect stripping; checksum/cache mismatch; malicious ZIP; cold/warm/offline/locked.
- iOS/tvOS device/simulator; macOS/Catalyst; arm64/x86_64; missing/wrong slice; fat binaries; deployment mismatch.
- static library/framework/dynamic framework/macOS dylib; forbidden iOS/tvOS dylib; Objective-C categories; force-load; weak/strong framework; Swift interfaces/runtime; install-name/rpath; duplicate symbols; mergeable framework ordinary-embed path.
- framework and standalone resources; localized bundle; privacy manifest layouts; matching/mismatched dSYM; origin then app signature.
- direct/transitive package manifests; asset suppression; ProjectReference diamonds; file sidecar; dual payload; legacy sidecar; embedded `[LinkWith]`; downloader; direct/opaque/XcodeProject provider.
- no-op/design-time no network; one graph resolution; multi-RID graph sharing; deterministic locks; parallel cache safety; credential redaction; app extensions.
## Rollout
### Phase 0 — API/inventory
Approve schema, names, trust, TFM lock placement, source boundary, diagnostics, and ecosystem inventory.
**Gate:** contract requires no package-authored executable code.
### Phase 1 — report-only plumbing
Transport manifests, normalize graph, inventory legacy/native providers, change no payload selection.
**Gate:** representative binlogs show identical `ResolvedFileToPublish`, framework/dylib/codesign item sets, and `LinkNativeCode` command lines under `Legacy` vs `Report`.
### Phase 2 — SwiftPM/direct acquisition spike
Implement package graph, registries, locks, direct/binary-target acquisition, destination inspection, offline/no-op behavior.
**Gate:** clean machines produce identical graph/locks; second no-op invokes no resolver; design-time/offline pass under outbound-network denial; unknown workspace-state schema fails closed; multi-RID macOS runs one resolution and one materialization per RID.
### Phase 3 — dual-provider reconciliation
Match selected products to sidecars/local providers; keep legacy fallback; report conflicts; emit native SBOM/provenance.
**Gate:** inference based on bundle metadata/checksum similarity/filename only diagnoses; divergent same-name content fails; only explicit identities suppress.
### Phase 4 — opt-in `Resolve`
Pilot low-risk redistributable binary family, NativeLibraryInterop, and authenticated vendor fixture on all four platforms.
**Gate:** binary-acquisition and propagation/compatibility validation rows pass, including mirror/offline/auth/lock/rollback/opaque fallback.
### Phase 5 — coordinate-only packages
Remove payload only from validated packages; retain rollback version and permanent exceptions.
**Gate:** selected version is in `compatibleSelection`, binding recompiles without new native-signature failures, and device/simulator integration passes for all platforms.
### Phase 6 — source adapter
Only after an aggregate shim/Xcode/SwiftPM spike proves stable products/resources/symbols/trust.
**Gate:** no hidden PIF/DerivedData dependency; one aggregate build per destination; plugin/macro trust explicit.
## Open decisions/highest risks
1. Keep source products publisher-XCFramework/package-free-XcodeProject-only or build aggregate shim?
2. Generated root tools-version policy and inspectability.
3. Two TFM-qualified locks versus embedding SwiftPM pins in one composite lock.
4. Graph scope: all restored refs versus only ILLink-surviving assembly-associated manifests.
5. Exact ProjectReference flow defaults.
6. Required physical binary identity fields.
7. Exact-by-default binding compatibility versus publisher ranges.
8. Swift runtime embedding tool/model.
9. Safe structured linker metadata versus app-approved unsafe flags.
10. Mergeable library future behavior.
11. Required XCFramework signer continuity.
12. CocoaPods importer unverified-adoption policy.
13. Multi-RID orchestration and cache single-flight.
14. `buildTransitive` now versus a future NuGet-native asset type.
15. SwiftPM workspace-state adapter compatibility burden.
## References
### Apple/Xcode
- [Creating a multi-platform binary framework bundle](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle)
- [Distributing binary frameworks as Swift packages](https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages)
- [Placing content in a bundle](https://developer.apple.com/documentation/BundleResources/placing-content-in-a-bundle)
- [Adding privacy manifests](https://developer.apple.com/documentation/BundleResources/adding-a-privacy-manifest-to-your-app-or-third-party-sdk)
- [Describing data use/privacy reports](https://developer.apple.com/documentation/BundleResources/describing-data-use-in-privacy-manifests)
- [Third-party SDK requirements](https://developer.apple.com/support/third-party-SDK-requirements/)
- [Creating a static framework](https://developer.apple.com/documentation/xcode/creating-a-static-framework)
- [Verifying XCFramework origin](https://developer.apple.com/documentation/xcode/verifying-the-origin-of-your-xcframeworks)
- [Mergeable libraries](https://developer.apple.com/documentation/xcode/configuring-your-project-to-use-mergeable-libraries)
- [Framework module issues](https://developer.apple.com/documentation/xcode/identifying-and-addressing-framework-module-issues)
- [Embedding nonstandard code structures](https://developer.apple.com/documentation/xcode/embedding-nonstandard-code-structures-in-a-bundle)
- [Xcode build settings](https://developer.apple.com/documentation/xcode/build-settings-reference)
- [Xcode 26 release notes](https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes)
- [`-ObjC` / categories](https://developer.apple.com/library/archive/qa/qa1490/_index.html)
- [`ld(1)`](https://keith.github.io/xcode-man-pages/ld.1.html)
- [`vtool(1)`](https://keith.github.io/xcode-man-pages/vtool.1.html)
### SwiftPM/Swift
- [SwiftPM docs](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/)
- [Adding dependencies](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/addingdependencies)
- [Resolving versions](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/resolvingpackageversions)
- [Package security](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/packagesecurity)
- [Package registries](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/usingswiftpackageregistry)
- [Plugins](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/plugins)
- [Resources](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/bundlingresources)
- [SE-0272: binary dependencies](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0272-swiftpm-binary-dependencies.md)
- [SE-0291: package collections](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0291-package-collections.md)
- [SE-0292: package registry](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md)
- [SE-0303: plugins](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0303-swiftpm-extensible-build-tools.md)
- [SE-0305: binary target improvements](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0305-swiftpm-binary-target-improvements.md)
- [SE-0378: registry auth](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0378-package-registry-auth.md)
- [SE-0391: registry publishing/signing](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md)
- [`ResolvedPackagesStore.swift`](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackageGraph/ResolvedPackagesStore.swift)
- [`Workspace+BinaryArtifacts.swift`](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/Workspace/Workspace%2BBinaryArtifacts.swift)
- [`ArtifactsArchiveMetadata.swift`](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackageModel/ArtifactsArchiveMetadata.swift)
### CocoaPods/direct distribution
- [CocoaPods support plan](https://blog.cocoapods.org/CocoaPods-Support-Plans/)
- [CocoaPods Trunk/Specs read-only plan](https://blog.cocoapods.org/CocoaPods-Specs-Repo/)
- [Podspec syntax](https://guides.cocoapods.org/syntax/podspec.html)
- [Firebase CocoaPods deprecation](https://firebase.google.com/docs/ios/cocoapods-deprecation)
- [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases)
- [GitHub artifact attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations)
### NuGet/MSBuild
- [NuGet MSBuild props/targets](https://learn.microsoft.com/nuget/concepts/msbuild-props-and-targets)
- [PackageReference assets and locking](https://learn.microsoft.com/nuget/consume-packages/package-references-in-project-files)
- [NuGet security guidance](https://learn.microsoft.com/nuget/concepts/security-best-practices)
- [MSBuild incremental builds](https://learn.microsoft.com/visualstudio/msbuild/incremental-builds)
- [MSBuild task behavior](https://learn.microsoft.com/visualstudio/msbuild/msbuild-task)
- [Static graph spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/static-graph.md)
- [ProjectReference protocol](https://github.com/dotnet/msbuild/blob/main/documentation/ProjectReference-Protocol.md)
### Steps to Reproduce
This is an architecture gap rather than a single product bug. Representative current behavior:
1. Publish or consume two binding/native-interop NuGets that independently embed/download native frameworks or XCFrameworks, with overlapping transitive products or versions.
2. Reference them through direct and transitive NuGet/ProjectReference paths from one iOS, Mac Catalyst, macOS, or tvOS app.
3. Build the app.
4. Observe that there is no app-level package identity/version graph, no native lock spanning the providers, and no coordinate-aware reconciliation before late linker/bundler duplicate/conflict behavior.
5. For source native interop, use `XcodeProject` with Swift package references and observe that Xcode may resolve its own package graph/cache independently of the final .NET app's other native inputs.
### Did you find any workaround?
Current packages can keep embedding `.resources[.zip]` sidecars, use package-authored download targets, ship direct `NativeReference` items, or build local source with `XcodeProject`. Apps can manually align versions and remove duplicates. These remain necessary compatibility paths but do not provide one deterministic app-level graph.
### Relevant log output
N/A — this is a design proposal, not one specific build failure.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading dotnet/targets/Xamarin.Shared.Sdk.targets, msbuild/Xamarin.Shared/Xamarin.Shared.targets, and ResolveNativeReferences.cs to understand the existing NativeReference pipeline. Then inspect Microsoft.MaciOS.Sdk.Xcode.targets and the listed native build tasks; the proposal does not define a bounded implementation or concrete completion test, so its first outcome would need to be a scoped design agreed by maintainers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, swift
- Domain
- build-system, mobile-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100