diff --git a/clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj b/clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj
index 2835b5d..606854d 100644
--- a/clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj
+++ b/clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj
@@ -15,6 +15,7 @@
Info.plist
full
$(NoWarn);XCODE_27_0_PREVIEW
+ $(WarningsNotAsErrors);IL2104
true
iossimulator-arm64
ios-arm64
diff --git a/clients/apple/build-ios-device.sh b/clients/apple/build-ios-device.sh
index 6433989..2bad783 100755
--- a/clients/apple/build-ios-device.sh
+++ b/clients/apple/build-ios-device.sh
@@ -21,9 +21,12 @@ done
project="$root/clients/apple/VoiceCat.iOS/VoiceCat.iOS.csproj"
"$root/scripts/build-native-ios.sh"
"$dotnet_host" restore "$project" --locked-mode -p:VoiceCatIosStatic=true
-"$dotnet_host" restore "$project" --locked-mode -r ios-arm64 --no-dependencies
+device_lock="obj/packages.device.lock.json"
+"$dotnet_host" restore "$project" -r ios-arm64 --no-dependencies \
+ -p:RestorePackagesWithLockFile=true -p:RestoreLockedMode=false -p:NuGetLockFilePath="$device_lock"
-set -- build "$project" -c "$configuration" -r ios-arm64 --no-restore -m:1
+set -- build "$project" -c "$configuration" -r ios-arm64 --no-restore -m:1 \
+ -p:NuGetLockFilePath="$device_lock"
if [ -n "${VOICECAT_CODESIGN_KEY:-}" ]; then set -- "$@" -p:CodesignKey="$VOICECAT_CODESIGN_KEY"; fi
if [ -n "${VOICECAT_CODESIGN_PROVISION:-}" ]; then set -- "$@" -p:CodesignProvision="$VOICECAT_CODESIGN_PROVISION"; fi
"$dotnet_host" "$@"
diff --git a/docs/ios-deploy.md b/docs/ios-deploy.md
index 03c6d0a..28c951f 100644
--- a/docs/ios-deploy.md
+++ b/docs/ios-deploy.md
@@ -35,6 +35,16 @@ clients/apple/deploy-ios-device.sh \
The verified bundle is staged at `dist/ios-managed-device/VoiceCat.iOS.app`. Omit `--no-build`
to build and deploy in one command. Add `--console` to attach the launch to device logs.
+The device build first validates the checked-in managed dependency locks, then generates
+`obj/packages.device.lock.json` for the single `ios-arm64` app restore. This keeps a preceding
+simulator restore from making the device build fail with `NU1004`; root-level generated lock
+files under `clients/apple/VoiceCat.iOS` are not inputs to the device build.
+
+.NET's iOS linker reports `IL2104` aggregate warnings for the pinned BouncyCastle and
+Google.Protobuf assemblies. The iOS project leaves those aggregate warnings visible but does not
+promote them to errors. All other warnings remain subject to the repository-wide
+`TreatWarningsAsErrors` policy.
+
Confirm that the process stayed alive:
```bash
@@ -81,15 +91,30 @@ them after runtime changes, then verify that dependency versions did not change:
--force-evaluate
```
+The device script should not require this command merely because a simulator was built first. If
+it does, confirm that it is using `obj/packages.device.lock.json` for its `ios-arm64` restore.
+
+### Command-line signing identity appears missing
+
+Xcode being signed in and `security find-identity -v -p codesigning` returning no identities can
+be a shell sandbox or keychain-access artifact. Run the identity check from an ordinary Terminal
+session before changing certificates. A successful build prints the selected Apple Development
+identity, provisioning profile, bundle ID, and app ID before linking.
+
### Developer disk image cannot be mounted
CoreDevice errors 10003 or 12040 mean the phone locked. Unlock it, keep the display awake, and
rerun deployment with `--no-build`.
+CoreDevice can also install the app successfully and then reject only the launch with error
+10002 and reason `Locked`. In that case, unlock the phone and rerun the same `--no-build`
+deployment; rebuilding is unnecessary. Occasional CoreDeviceService initialization timeouts while
+listing devices or processes do not imply that an already-confirmed install or launch failed.
+
## Verified hardware result
-On 2026-09-19, the Debug build completed with .NET 10.0.401 and Xcode 27.0. The host and
+On 2026-09-21, the Debug build completed with .NET 10.0.401 and Xcode 27.0. The host and
ReplayKit extension were signed under team `FJV8L966W4`, installed wirelessly on Talon's iPhone,
-launched as `me.iamtalon.voicecat`, and remained in the device process list. Audio,
+and launched as `me.iamtalon.voicecat`; the running UI was confirmed on the device. Audio,
background/lock behavior, Bluetooth, ReplayKit, ScreenCaptureKit, and VoiceOver remain separate
manual hardware gates.
diff --git a/tests/VoiceCat.Tests/PublishServerScriptTests.cs b/tests/VoiceCat.Tests/PublishServerScriptTests.cs
index 53b01a3..87992ba 100644
--- a/tests/VoiceCat.Tests/PublishServerScriptTests.cs
+++ b/tests/VoiceCat.Tests/PublishServerScriptTests.cs
@@ -39,6 +39,20 @@ public class PublishServerScriptTests
Assert.Contains("com.apple.security.cs.allow-jit", entitlements);
}
+ [Fact]
+ public async Task IosDeviceBuildUsesAnIntermediateDeviceLockAndScopesKnownTrimWarnings()
+ {
+ string root = FindRoot();
+ string script = await File.ReadAllTextAsync(Path.Combine(root, "clients", "apple", "build-ios-device.sh"));
+ string project = await File.ReadAllTextAsync(Path.Combine(
+ root, "clients", "apple", "VoiceCat.iOS", "VoiceCat.iOS.csproj"));
+
+ Assert.Contains("NuGetLockFilePath=\"$device_lock\"", script);
+ Assert.Contains("RestoreLockedMode=false", script);
+ Assert.Contains("$(WarningsNotAsErrors);IL2104", project);
+ Assert.DoesNotContain("false", project);
+ }
+
[Fact]
public async Task DefaultPublishTargetsWindowsAndLinuxWhileRuntimeCanSelectOne()
{