Configure UM Trace iOS signing and deployment

This commit is contained in:
Brent Perteet
2026-08-21 12:33:11 -05:00
parent 5e87d81bb6
commit 16c2ada09b
7 changed files with 208 additions and 7 deletions

12
.gitignore vendored
View File

@@ -6,6 +6,18 @@
# dotenv files # dotenv files
.env .env
# Local iOS deployment target (contains a personal device UDID).
.ios-device
# Apple signing credentials and provisioning artifacts. Keep these in Keychain or
# an approved secret store; never commit them to the application repository.
*.cer
*.p12
*.p8
*.mobileprovision
*.provisionprofile
*.certSigningRequest
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo

View File

@@ -26,10 +26,10 @@
<NoWarn>$(NoWarn);MVVMTK0045</NoWarn> <NoWarn>$(NoWarn);MVVMTK0045</NoWarn>
<!-- Display name --> <!-- Display name -->
<ApplicationTitle>Field Logger</ApplicationTitle> <ApplicationTitle>UM Trace</ApplicationTitle>
<!-- App Identifier --> <!-- App Identifier -->
<ApplicationId>com.undergroundmagnetics.fieldlogger</ApplicationId> <ApplicationId>com.umagul.trace</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
@@ -46,6 +46,10 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup> </PropertyGroup>
<!-- Apple Developer team used for iOS distribution: W2N8APPQ2C.
Certificate/profile selection stays outside source control and is supplied by
Keychain plus the installed com.umagul.trace provisioning profile at publish time. -->
<!-- SEC-1: the Google Maps Android key is injected at build time, never committed. <!-- SEC-1: the Google Maps Android key is injected at build time, never committed.
Resolution order: (1) MapsApiKey MSBuild property (pass -p:MapsApiKey=... in CI from a Resolution order: (1) MapsApiKey MSBuild property (pass -p:MapsApiKey=... in CI from a
secret), else (2) the MAPS_API_KEY environment variable, else (3) a gitignored secret), else (2) the MAPS_API_KEY environment variable, else (3) a gitignored

View File

@@ -29,8 +29,19 @@
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string> <string>Assets.xcassets/appicon.appiconset</string>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>
<string>Field Logger uses Bluetooth to connect to your Underground Magnetics locating receiver and Maglink RTK GPS receiver.</string> <string>UM Trace uses Bluetooth to connect to your Underground Magnetics locating receiver and MagLink RTK GNSS receiver.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>Field Logger uses your location to display logged points on the map.</string> <string>UM Trace uses your location to display and record georeferenced locate points during an active field session.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>UM Trace uses your location during an active locating session to keep georeferenced field records current while the app is in the background.</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>location</string>
</array>
<!-- The app uses standard, exempt TLS for HTTPS and MQTTS/WSS transport; it does
not implement proprietary or non-standard cryptography. -->
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict> </dict>
</plist> </plist>

View File

@@ -36,8 +36,6 @@ More information: https://aka.ms/maui-privacy-manifest
<string>E174.1</string> <string>E174.1</string>
</array> </array>
</dict> </dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict> <dict>
<key>NSPrivacyAccessedAPIType</key> <key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string> <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
@@ -45,7 +43,7 @@ More information: https://aka.ms/maui-privacy-manifest
<array> <array>
<string>CA92.1</string> <string>CA92.1</string>
</array> </array>
</dict> --> </dict>
</array> </array>
</dict> </dict>
</plist> </plist>

77
doc/iOS_TestFlight.md Normal file
View File

@@ -0,0 +1,77 @@
# UM Trace TestFlight build
## Fixed application identity
- Display name: `UM Trace`
- Bundle ID: `com.umagul.trace`
- Apple team ID: `W2N8APPQ2C`
- App Store Connect SKU: `UM-TRACE-IOS`
- Distribution profile: `UM Trace App Store`
The provisioning profile and all Apple signing files are local-only and covered by
`.gitignore`. Never commit a certificate, private key, App Store Connect API key, `.p12`, or
provisioning profile.
## Required local toolchain
- Xcode 26 at `/Applications/Xcode.app`
- .NET SDK 10.0.100 as the build host
- Workload set 10.0.100 with `maui-ios` and its .NET 9/iOS 26 compatibility pack
- An `Apple Distribution` identity, including its private key, in the login Keychain
- The `UM Trace App Store` provisioning profile installed locally
This repository's local SDK is installed at `~/.dotnet`. Verify it with:
```sh
~/.dotnet/dotnet --info
~/.dotnet/dotnet workload list
```
## Create an archive
Run the release helper from a normal macOS Terminal session. The build number must increase for
every upload to App Store Connect:
```sh
cd /Users/brent/ul-platform/app
./scripts/publish-testflight.sh 1
```
The app remains on its existing .NET 9 target. The .NET 10 SDK host supplies the supported .NET 9
compatibility pack for Xcode 26, avoiding an unrelated Android/Mac Catalyst framework migration.
The script deliberately supplies Xcode through `DEVELOPER_DIR`; it does not change the machine's
global `xcode-select` setting. It restores only the iOS target, builds with the iOS 26 SDK, selects
the installed `UM Trace App Store` profile, and prints the generated `.ipa` path.
Before uploading, verify that this command lists a valid distribution identity:
```sh
security find-identity -v -p codesigning
```
If the identity is absent, import a `.p12` containing the matching private key or create a new
Apple Distribution certificate through Xcode and regenerate the provisioning profile against it.
## Deploy a development build to an iPhone
The local development profile is named `UM Trace Development`. It must include the target phone
and match an `Apple Development` identity for team `W2N8APPQ2C`. Keep the phone unlocked and
connected by USB for the first deployment, trust the Mac when prompted, and enable Developer Mode.
Save the phone UDID in the gitignored `.ios-device` file, then run from a normal macOS Terminal:
```sh
cd /Users/brent/ul-platform/app
./scripts/deploy-ios-device.sh
```
You can instead pass a UDID as the first argument or set `UM_TRACE_DEVICE_UDID`. The helper uses
Xcode 26, builds the `Debug` configuration for `ios-arm64`, signs with the development profile,
installs the app, and launches it on the selected phone.
## Background and privacy declarations
The iOS bundle declares `bluetooth-central` and `location` background modes, Bluetooth and
always/when-in-use location explanations, the MAUI `UserDefaults` required-reason API, and exempt
standard TLS use. Physical-device testing remains required to prove the SRS eight-hour background
BLE/location session; declaring a background mode does not itself guarantee continuous execution.

50
scripts/deploy-ios-device.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dotnet_bin="${DOTNET_BIN:-${HOME}/.dotnet/dotnet}"
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
codesign_key="${UM_TRACE_DEVELOPMENT_KEY:-Apple Development: Brent Perteet (WZ56HVC6FD)}"
codesign_profile="${UM_TRACE_DEVELOPMENT_PROVISION:-UM Trace Development}"
device_udid="${1:-${UM_TRACE_DEVICE_UDID:-}}"
if [[ -z "${device_udid}" && -f "${repo_dir}/.ios-device" ]]; then
IFS= read -r device_udid < "${repo_dir}/.ios-device"
fi
if [[ -z "${device_udid}" ]]; then
echo "Usage: $0 <device-udid>" >&2
echo "Alternatively, set UM_TRACE_DEVICE_UDID or save the UDID in ${repo_dir}/.ios-device." >&2
exit 2
fi
if [[ ! -x "${dotnet_bin}" ]]; then
echo "Missing .NET SDK at ${dotnet_bin}. Set DOTNET_BIN or install the local SDK." >&2
exit 2
fi
if [[ ! -d "${developer_dir}" ]]; then
echo "Missing Xcode developer directory at ${developer_dir}. Set DEVELOPER_DIR." >&2
exit 2
fi
export DEVELOPER_DIR="${developer_dir}"
"${dotnet_bin}" restore "${repo_dir}/FieldLogger/FieldLogger.csproj" \
-p:TargetFrameworks=net9.0-ios \
-p:RuntimeIdentifier=ios-arm64
# Restoring the MAUI head with a narrowed TargetFrameworks property can rewrite the
# referenced headless library's assets file. Restore that library explicitly before building.
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj"
"${dotnet_bin}" build "${repo_dir}/FieldLogger/FieldLogger.csproj" \
-t:Run \
-p:TargetFrameworks=net9.0-ios \
-f net9.0-ios \
-c Debug \
-p:RuntimeIdentifier=ios-arm64 \
-p:_DeviceName="${device_udid}" \
-p:CodesignKey="${codesign_key}" \
-p:CodesignProvision="${codesign_profile}" \
--no-restore

49
scripts/publish-testflight.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dotnet_bin="${DOTNET_BIN:-${HOME}/.dotnet/dotnet}"
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
build_number="${1:-}"
codesign_key="${UM_TRACE_CODESIGN_KEY:-Apple Distribution}"
codesign_profile="${UM_TRACE_CODESIGN_PROVISION:-UM Trace App Store}"
if [[ ! "${build_number}" =~ ^[0-9]+$ ]]; then
echo "Usage: $0 <numeric-build-number>" >&2
exit 2
fi
if [[ ! -x "${dotnet_bin}" ]]; then
echo "Missing .NET 10 SDK at ${dotnet_bin}. Set DOTNET_BIN or install .NET 10." >&2
exit 2
fi
if [[ ! -d "${developer_dir}" ]]; then
echo "Missing Xcode developer directory at ${developer_dir}. Set DEVELOPER_DIR." >&2
exit 2
fi
export DEVELOPER_DIR="${developer_dir}"
"${dotnet_bin}" restore "${repo_dir}/FieldLogger/FieldLogger.csproj" \
-p:TargetFrameworks=net9.0-ios \
-p:RuntimeIdentifier=ios-arm64
# Restoring the MAUI head with a narrowed TargetFrameworks property can rewrite the
# referenced headless library's assets file. Restore that library explicitly before publish.
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj"
"${dotnet_bin}" publish "${repo_dir}/FieldLogger/FieldLogger.csproj" \
-p:TargetFrameworks=net9.0-ios \
-f net9.0-ios \
-c Release \
-p:RuntimeIdentifier=ios-arm64 \
-p:ArchiveOnBuild=true \
-p:BuildIpa=true \
-p:CodesignKey="${codesign_key}" \
-p:CodesignProvision="${codesign_profile}" \
-p:ApplicationVersion="${build_number}" \
--no-restore
find "${repo_dir}/FieldLogger/bin/Release/net9.0-ios/ios-arm64" \
-type f -name '*.ipa' -print