@@ -113,6 +113,8 @@ jobs:
113113 if [ "${{ github.event.repository.name }}" != "KtsuBuild" ]; then
114114 EXCLUSIONS="$EXCLUSIONS,**/KtsuBuild/**"
115115 fi
116+ # NativeExports.cs is intentionally unsafe C ABI boundary code; exclude from all analysis.
117+ EXCLUSIONS="$EXCLUSIONS,**/NativeExports.cs"
116118 echo "SONAR_EXCLUSIONS=$EXCLUSIONS" >> $GITHUB_ENV
117119
118120 - name : Begin SonarQube
@@ -121,7 +123,7 @@ jobs:
121123 SONAR_TOKEN : ${{ secrets.SONAR_TOKEN }}
122124 shell : powershell
123125 run : |
124- .\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths="coverage/coverage.xml" /d:sonar.coverage.exclusions="**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll" /d:sonar.cs.vstest.reportsPaths="coverage/TestResults/**/*.trx" /d:sonar.exclusions="${{ env.SONAR_EXCLUSIONS }}"
126+ .\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths="coverage/coverage.xml" /d:sonar.coverage.exclusions="**/*Test*.cs,**/*.Tests.cs,**/*.Tests/**/*,**/obj/**/*,**/*.dll,**/NativeExports.cs " /d:sonar.cs.vstest.reportsPaths="coverage/TestResults/**/*.trx" /d:sonar.exclusions="${{ env.SONAR_EXCLUSIONS }}"
125127
126128 - name : Clone KtsuBuild (Latest Tag)
127129 run : |
@@ -154,6 +156,7 @@ jobs:
154156 }
155157
156158 & dotnet run --project "${{ runner.temp }}/KtsuBuild/KtsuBuild.CLI" -- @args
159+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
157160
158161 # Set outputs for downstream jobs
159162 $version = (Get-Content "${{ github.workspace }}/VERSION.md" -Raw).Trim()
@@ -188,6 +191,251 @@ jobs:
188191 ./coverage/*
189192 retention-days : 7
190193
194+ ios-build :
195+ # Compile-only verification that the net10.0-ios stub keeps building on Mac.
196+ # No tests — Apple Simulator boot from CI is a separate problem (see plan §5 Task 2).
197+ # Runs in parallel with `build`; failure here is independent and does not gate release.
198+ name : Build net10.0-ios (macOS)
199+ runs-on : macos-14
200+ timeout-minutes : 20
201+ permissions :
202+ contents : read
203+
204+ steps :
205+ - name : Checkout Repository
206+ uses : actions/checkout@v4
207+ with :
208+ fetch-depth : 1
209+ lfs : true
210+ submodules : recursive
211+ persist-credentials : false
212+
213+ - name : Setup .NET SDK ${{ env.DOTNET_VERSION }}
214+ uses : actions/setup-dotnet@v4
215+ with :
216+ dotnet-version : ${{ env.DOTNET_VERSION }}.x
217+
218+ - name : Install iOS workload
219+ # No caching yet — get the install path working first; caching workload manifests
220+ # is a known-finicky follow-up (manifests need both the file tree and a metadata
221+ # registration to "count" as installed).
222+ run : dotnet workload install ios
223+
224+ # The csproj cross-targets net10.0;net9.0;net8.0;net10.0-ios on macOS AND ktsu.Sdk
225+ # forces RuntimeIdentifiers=win-x64;win-x86;win-arm64;osx-x64;osx-arm64;linux-x64;linux-arm64
226+ # on every project. The combination asks NuGet for the matching
227+ # Microsoft.NETCore.App.Runtime.Mono.<rid> pack at the SDK version — those packs are
228+ # workload-delivered in .NET 10 and no longer on nuget.org, so restore 404s. Windows
229+ # CI gets away with it because the Windows runner has the Mono packs bundled with
230+ # its dotnet install; setup-dotnet on the Mac runner does not. Scoping the restore
231+ # to net10.0-ios alone AND clearing the RID matrix sidesteps the issue — we're only
232+ # compile-checking the IL, no RID-specific output needed.
233+ - name : Restore ImGui.App (net10.0-ios, no RID matrix)
234+ run : dotnet restore ImGui.App/ImGui.App.csproj -p:TargetFrameworks=net10.0-ios -p:RuntimeIdentifiers=
235+
236+ # EnforceCodeStyleInBuild=false bypasses the IDE0055 formatting analyser: the repo's
237+ # working-tree files are LF, .editorconfig demands CRLF, and Windows checkouts get
238+ # auto-converted to CRLF by git's core.autocrlf — macOS checkouts don't, so IDE0055
239+ # fires here on baseline code that's untouched by this PR. The Windows job is the
240+ # authoritative formatter; the macOS job is a compile-check.
241+ - name : Build ImGui.App (net10.0-ios)
242+ run : dotnet build ImGui.App/ImGui.App.csproj -c Release -p:TargetFrameworks=net10.0-ios -p:RuntimeIdentifiers= -p:EnforceCodeStyleInBuild=false --no-restore
243+
244+ ios-simulator :
245+ # Runtime verification: build the headless smoke app AND the curated ImGuiAppDemo.iOS for the
246+ # simulator, boot a sim, launch each, and assert the lifecycle ticks (the app prints a marker and
247+ # exits after N frames via the IMGUIAPP_IOS_SMOKE_FRAMES hook). Runs in parallel; does not gate
248+ # release. Building+launching two apps after a cold cimgui cache approaches the old 30-min cap, so
249+ # the budget is 50 min (a warm-cache run is ~20).
250+ name : iOS Simulator Smoke Test
251+ # The .NET 10 iOS workload (Microsoft.iOS 26.5) requires Xcode 26.5 to build a runnable .app.
252+ # macos-15 only carries up to Xcode 26.3; macos-26 (Tahoe) ships the matching Xcode 26.5+.
253+ # (The compile-only ios-build job needs no Xcode, so it stays on macos-14.)
254+ runs-on : macos-26
255+ timeout-minutes : 50
256+ # HARD GATE: the .NET 10 + Xcode 26 symlinked-developer-dir bug that broke native linking
257+ # (`xcodebuild -find` -> errno=Invalid argument) is worked around by the "Resolve and pin the
258+ # real Xcode" step below (readlink -f + xcode-select/DEVELOPER_DIR pin; dotnet/macios#21762), so
259+ # this runtime smoke test now reliably passes and gates the PR like any other required check.
260+ permissions :
261+ contents : read
262+
263+ steps :
264+ - name : Checkout Repository
265+ uses : actions/checkout@v4
266+ with :
267+ fetch-depth : 1
268+ lfs : true
269+ submodules : recursive
270+ persist-credentials : false
271+
272+ - name : Setup .NET SDK ${{ env.DOTNET_VERSION }}
273+ uses : actions/setup-dotnet@v4
274+ with :
275+ dotnet-version : ${{ env.DOTNET_VERSION }}.x
276+
277+ - name : Install iOS workload
278+ run : dotnet workload install ios
279+
280+ # Diagnostics: this job can only be debugged from CI (no local Mac), so dump the runner's
281+ # actual SDK, workload, iOS runtime-pack, Xcode, and simulator state up front. Never fails.
282+ - name : iOS toolchain diagnostics
283+ run : |
284+ set -x
285+ dotnet --info || true
286+ dotnet workload list || true
287+ echo "--- installed packs ---"
288+ ls -1 "$HOME/.dotnet/packs" 2>/dev/null | grep -i ios || true
289+ ls -1 /usr/local/share/dotnet/packs 2>/dev/null | grep -i ios || true
290+ echo "--- installed Xcodes ---"
291+ ls -d /Applications/Xcode*.app 2>/dev/null || true
292+ echo "--- xcode / sdk ---"
293+ xcodebuild -version || true
294+ xcrun --sdk iphonesimulator --show-sdk-version || true
295+ echo "--- simulator device types ---"
296+ xcrun simctl list devicetypes | grep -i iphone || true
297+ echo "--- simulator runtimes ---"
298+ xcrun simctl list runtimes || true
299+ continue-on-error : true
300+
301+ # ROOT CAUSE: the hosted Xcode_26.5.0.app is a SYMLINK, and on .NET 10 + Xcode 26 hosted runners
302+ # `xcodebuild -find` intermittently fails to resolve the toolchain through a symlinked developer
303+ # dir - dying with `errno=Invalid argument` (NOT "No such file or directory"; the SDK exists).
304+ # The managed build and IL strip never call `xcrun -find` so they pass, but native clang linking
305+ # (and `-find actool`) do, so they die with clang exit 72. Fix: resolve the .app symlink to its
306+ # REAL path with `readlink -f` and pin BOTH xcode-select and DEVELOPER_DIR to it. `readlink -f`
307+ # is a no-op on an already-real path, so this is safe either way; writing DEVELOPER_DIR to
308+ # $GITHUB_ENV overrides any workflow/job-level value for later steps.
309+ # Refs: dotnet/macios#21762, actions/runner-images#13347.
310+ - name : Resolve and pin the real Xcode 26.5 path (symlink workaround)
311+ run : |
312+ set -uxo pipefail
313+ XC=/Applications/Xcode_26.5.app
314+ [ -d "$XC" ] || XC=/Applications/Xcode_26.5.0.app
315+ [ -d "$XC" ] || XC="$(dirname "$(dirname "$(xcode-select -p)")")"
316+ XC="$(readlink -f "$XC")"
317+ echo "Resolved real Xcode: $XC"
318+ sudo xcode-select -switch "$XC/Contents/Developer"
319+ echo "DEVELOPER_DIR=$XC/Contents/Developer" >> "$GITHUB_ENV"
320+ xcodebuild -version
321+ # Informational: confirm the host link tool now resolves through the real path.
322+ xcrun --find install_name_tool || echo "WARN: install_name_tool did not resolve via $XC"
323+
324+ # Hexa.NET.ImGui ships no native cimgui for iOS, so build it from source (Dear ImGui 1.92.2b,
325+ # matching Hexa 2.2.9) into a simulator static lib that ImGui.App statically links via a
326+ # NativeReference. Cached by the build script's hash so it only rebuilds when the recipe changes.
327+ # Runs after the Xcode pin so xcrun resolves the toolchain; before the build so the
328+ # NativeReference's Exists() condition is satisfied when ImGui.App compiles.
329+ - name : Cache native cimgui (iOS simulator)
330+ uses : actions/cache@v4
331+ with :
332+ path : ImGui.App/Platform/iOS/native
333+ key : cimgui-sim-arm64-dylib-imgui1.92.3-${{ hashFiles('scripts/build-cimgui-ios.sh') }}
334+
335+ - name : Build native cimgui for the simulator
336+ run : |
337+ set -euxo pipefail
338+ bash scripts/build-cimgui-ios.sh
339+ # Stash the dylib outside the repo tree: the in-tree native/ dir is gitignored and gets
340+ # wiped (by ktsu.Sdk/dotnet cleaning untracked files) before the embed step runs, so the
341+ # embed step copies from this stable location instead.
342+ cp ImGui.App/Platform/iOS/native/cimgui.dylib "$RUNNER_TEMP/cimgui.dylib"
343+
344+ # The smoke app is Microsoft.NET.Sdk, but it references ImGui.App (ktsu.Sdk), which forces a
345+ # desktop RuntimeIdentifiers matrix whose Mono RID packs 404 on macOS (see the ios-build job).
346+ # Clearing RuntimeIdentifiers and pinning the single simulator RID sidesteps that while still
347+ # producing a runnable .app bundle. The install step provisions the iossimulator-x64 Mono
348+ # runtime, so we target that RID; workload restore pulls the matching Microsoft.iOS runtime pack.
349+ - name : Restore iOS workload packs for the smoke app
350+ run : dotnet workload restore tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj
351+ continue-on-error : true
352+
353+ # The referenced ImGui.App is a *library*; in .NET 10 libraries default to the OLDEST iOS
354+ # platform version (_26.0), whose runtime packs aren't installed on the runner (only the
355+ # latest _26.5 packs are) — hence NU1102 on ImGui.App.csproj. UseFloatingTargetPlatformVersion
356+ # makes the library use the latest platform version (26.5) like the executable, aligning both
357+ # on the installed runtime packs. See https://learn.microsoft.com/dotnet/ios/building-apps/build-properties#usefloatingtargetplatformversion
358+ # ktsu.Sdk pins RuntimeFrameworkVersion=10.0.0 (for the desktop .NETCore.App runtime). On iOS
359+ # that bogus version is inherited by the Apple runtime pack (Microsoft.iOS.Runtime.*, actually
360+ # versioned 26.5.x), causing NU1102. Clearing RuntimeFrameworkVersion lets the iOS workload
361+ # resolve the real pack version. Combined with UseFloatingTargetPlatformVersion (library ->
362+ # latest _26.5 packs, which are the ones installed on the runner).
363+ - name : Restore smoke app (net10.0-ios, simulator RID)
364+ run : dotnet restore tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj -p:TargetFrameworks=net10.0-ios -p:RuntimeIdentifiers= -p:RuntimeFrameworkVersion= -p:UseFloatingTargetPlatformVersion=true -r iossimulator-arm64
365+
366+ - name : Build smoke app (.app for simulator)
367+ run : dotnet build tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj -c Release -f net10.0-ios -r iossimulator-arm64 -p:RuntimeIdentifiers= -p:RuntimeFrameworkVersion= -p:UseFloatingTargetPlatformVersion=true -p:EnforceCodeStyleInBuild=false --no-restore
368+
369+ - name : Locate built .app bundle
370+ id : findapp
371+ run : |
372+ set -euxo pipefail
373+ APP=$(find tests/ImGui.App.iOS.SmokeTest/bin -type d -name "*.app" | head -1)
374+ test -n "$APP"
375+ echo "app=$APP" >> "$GITHUB_OUTPUT"
376+
377+ # Embed cimgui.dylib into the built .app. Hexa.NET.ImGui ships no native cimgui for iOS, and a
378+ # NativeReference did not link it into this Microsoft.NET.Sdk app, so copy the dylib into the
379+ # bundle directly; the runtime resolver dlopens it by bundle path. The simulator does not enforce
380+ # code signing, so an unsigned bundled dylib loads.
381+ - name : Embed cimgui.dylib into the .app bundle
382+ run : cp "$RUNNER_TEMP/cimgui.dylib" "${{ steps.findapp.outputs.app }}/cimgui.dylib"
383+
384+ - name : Boot simulator
385+ run : |
386+ set -euxo pipefail
387+ UDID=$(xcrun simctl create imguiapp-smoke "iPhone 15" 2>/dev/null \
388+ || xcrun simctl create imguiapp-smoke com.apple.CoreSimulator.SimDeviceType.iPhone-15)
389+ echo "SIM_UDID=$UDID" >> "$GITHUB_ENV"
390+ xcrun simctl boot "$UDID"
391+ xcrun simctl bootstatus "$UDID" -b
392+
393+ - name : Install, launch, and assert lifecycle ticked
394+ run : |
395+ set -euxo pipefail
396+ xcrun simctl install "$SIM_UDID" "${{ steps.findapp.outputs.app }}"
397+ # simctl forwards SIMCTL_CHILD_* env vars (prefix stripped) to the launched app.
398+ SIMCTL_CHILD_IMGUIAPP_IOS_SMOKE_FRAMES=30 \
399+ xcrun simctl launch --console-pty --terminate-running-process "$SIM_UDID" dev.ktsu.imguiapp.smoketest 2>&1 | tee /tmp/smoke.log
400+ grep -q "IMGUIAPP_IOS_SMOKE_OK" /tmp/smoke.log
401+
402+ # --- Curated iOS demo (examples/ImGuiAppDemo.iOS) ---
403+ # A richer showcase than the headless smoke app: widgets, Unicode/emoji, a GPU texture, animation,
404+ # and the input/IO surface, all using the iOS-safe non-variadic ImGui subset. It reuses this job's
405+ # booted simulator, cimgui dylib, and the IMGUIAPP_IOS_SMOKE_FRAMES hook (baked into ImGui.App's
406+ # view controller), so the same minimal SDK + restore flags as the smoke app apply.
407+ - name : Restore demo app (net10.0-ios, simulator RID)
408+ run : dotnet restore examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj -p:TargetFrameworks=net10.0-ios -p:RuntimeIdentifiers= -p:RuntimeFrameworkVersion= -p:UseFloatingTargetPlatformVersion=true -r iossimulator-arm64
409+
410+ - name : Build demo app (.app for simulator)
411+ run : dotnet build examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj -c Release -f net10.0-ios -r iossimulator-arm64 -p:RuntimeIdentifiers= -p:RuntimeFrameworkVersion= -p:UseFloatingTargetPlatformVersion=true -p:EnforceCodeStyleInBuild=false --no-restore
412+
413+ - name : Locate built demo .app bundle
414+ id : finddemo
415+ run : |
416+ set -euxo pipefail
417+ APP=$(find examples/ImGuiAppDemo.iOS/bin -type d -name "*.app" | head -1)
418+ test -n "$APP"
419+ echo "app=$APP" >> "$GITHUB_OUTPUT"
420+
421+ - name : Embed cimgui.dylib into the demo .app bundle
422+ run : cp "$RUNNER_TEMP/cimgui.dylib" "${{ steps.finddemo.outputs.app }}/cimgui.dylib"
423+
424+ - name : Install, launch, and assert the demo ticked + uploaded its texture
425+ run : |
426+ set -euxo pipefail
427+ xcrun simctl install "$SIM_UDID" "${{ steps.finddemo.outputs.app }}"
428+ SIMCTL_CHILD_IMGUIAPP_IOS_SMOKE_FRAMES=30 \
429+ xcrun simctl launch --console-pty --terminate-running-process "$SIM_UDID" dev.ktsu.imguiapp.demo 2>&1 | tee /tmp/demo.log
430+ # The lifecycle ticked N frames without crashing in the Metal pipeline...
431+ grep -q "IMGUIAPP_IOS_SMOKE_OK" /tmp/demo.log
432+ # ...and the bundled image decoded (ImageSharp) + uploaded on Metal — PR-2 textures end-to-end.
433+ grep -q "IMGUIAPP_DEMO logo loaded" /tmp/demo.log
434+
435+ - name : Cleanup simulator
436+ if : always()
437+ run : xcrun simctl delete "${SIM_UDID:-imguiapp-smoke}" || true
438+
191439 winget :
192440 name : Update Winget Manifests
193441 needs : build
0 commit comments