From 271ef9aec6e78c831f7221e6930b38c9b1f25009 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 01:45:03 +0000 Subject: [PATCH 1/2] ci: make the iOS target framework opt-in and put macOS back in the matrix [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #327. `ImGui.App` widened its target frameworks to include `net10.0-ios` on any macOS host. Every test project references it, so every macOS cell of the test matrix tried to build the iOS head and failed with NETSDK1147 before running a single test — the iOS workload is installed by `ios.yml` and by nothing else. That is why 7 of 42 cells failed on the matrix's first run, all of them macOS, and why macOS was then excluded. This takes option 2 from the issue and fixes the cause rather than compensating for it in CI. The widening is now gated behind `IncludeIosTargets`, which defaults to false, so a macOS host builds exactly what Linux and Windows build. The same gate replaces the host-OS test in `ImGuiAppDemo.iOS` and `ImGui.App.iOS.SmokeTest`, which had the same shape. `ios.yml` opts in at job level on both of its jobs. An environment variable is the mechanism because MSBuild reads it as a property, so it reaches every invocation in the job, including `dotnet workload restore` (which takes no `-p:`) and the smoke and demo builds, which reach `ImGui.App` through a ProjectReference and would otherwise have silently resolved its net10.0 head. macOS returns to the test matrix: `neutral` now expands to Linux, Windows and macOS, and a `macos` platform maps to `macos-latest`. The discovery guard still stops the run on an unhandled platform; its message no longer blames the matrix for an ios-tied project, since the workload is the actual constraint. The UI-suite exclusion now tests for Linux rather than against Windows, so macOS inherits the cheap treatment rather than the 18-minute one — which matters, as macOS runner minutes bill at roughly ten times Linux. Verified: solution builds clean, and `ImGui.App.Tests` passes 343/343. The gate's macOS branch cannot be exercised from a Linux sandbox; property evaluation was checked directly with `dotnet msbuild -getProperty`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N1cVfacJWw1uM42DUmPfUg --- .github/workflows/dotnet.yml | 45 ++++++++++--------- .github/workflows/ios.yml | 14 ++++++ CLAUDE.md | 16 ++++++- ImGui.App/ImGui.App.csproj | 11 ++++- .../ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj | 14 +++--- .../ImGui.App.iOS.SmokeTest.csproj | 14 +++--- 6 files changed, 79 insertions(+), 35 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 732d9b94..2d550ff1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -82,28 +82,28 @@ jobs: # An unrecognized platform must stop the run rather than drop the project. Dropping # it would produce a smaller matrix that still reports success, which is the failure # this design exists to remove. - unknown=$(echo "$projects" | jq -r '[.[] | select(.platform as $p | ["neutral","windows"] | index($p) | not) | .platform] | unique | join(", ")') + unknown=$(echo "$projects" | jq -r '[.[] | select(.platform as $p | ["neutral","windows","macos"] | index($p) | not) | .platform] | unique | join(", ")') if [ -n "$unknown" ]; then echo "::error::Cannot place test project(s) on a runner. Unhandled platform(s): $unknown" - echo "::error::macOS is currently excluded from the matrix, so an ios-tied test project has nowhere to run." + echo "::error::An ios-tied test project needs 'dotnet workload install ios', which this job does not run; the iOS workflow is where iOS builds happen." exit 1 fi - # macOS is deliberately absent from this mapping. A macOS runner builds any project - # whose target frameworks are widened on that host, and in a repo with an iOS head that - # pulls in a target framework needing a workload this job does not install, so every - # macOS cell fails during its build. Restoring the workload on each cell is slow and - # macOS runner minutes are billed at a premium, so the platform is excluded until the - # underlying problem is fixed rather than papered over. An ios-tied test project now - # fails the guard above instead of silently finding no runner. + # macOS is back in this mapping. It was excluded because a macOS runner widened + # ImGui.App's target frameworks to include net10.0-ios, which needs a workload this job + # does not install, so every macOS cell failed during its build with NETSDK1147. That + # widening is now opt-in (IncludeIosTargets, set only by the iOS workflow), so a macOS + # cell builds exactly what Linux and Windows build. An ios-tied test project would still + # have nowhere to run and fails the guard above rather than silently disappearing. matrix=$(echo "$projects" | jq -c ' { include: [ .[] | . as $p | { - neutral: ["ubuntu-latest", "windows-latest"], - windows: ["windows-latest"] + neutral: ["ubuntu-latest", "windows-latest", "macos-latest"], + windows: ["windows-latest"], + macos: ["macos-latest"] }[$p.platform][] | { os: ., @@ -184,7 +184,9 @@ jobs: # # The five UI suites are effectively the whole cost of this job. They run in parallel, so the # slowest of them sets the job's duration, while all nine other test projects finish in about - # thirty-four seconds combined. They are therefore run on Linux only: + # thirty-four seconds combined. They are therefore run on Linux only -- which is also what + # keeps macOS affordable now that it is back in the matrix, since macOS runner minutes are + # billed at roughly ten times Linux: # # * measured per assembly on Windows: 17m28s, 14m32s, 10m05s, 8m20s, 1m01s against ~34s for # everything else, in a job whose test phase took 17m45s. @@ -194,15 +196,18 @@ jobs: # * Linux is the faster host for this work, so it keeps them. # # Only the UI test projects are excluded. The example applications they drive stay in the - # build on both platforms, so a change that breaks one still fails here. + # build on every platform, so a change that breaks one still fails here. + # + # The test is on Linux rather than against Windows, so a platform added to the matrix later + # gets the cheap treatment by default rather than silently inheriting the expensive one. - name: Test shell: bash run: | set -euo pipefail - if [ "${{ runner.os }}" = "Windows" ]; then - ktsubuild test all --workspace "$GITHUB_WORKSPACE" --verbose --exclude "**/*.UITests/*" - else + if [ "${{ runner.os }}" = "Linux" ]; then ktsubuild test all --workspace "$GITHUB_WORKSPACE" --verbose + else + ktsubuild test all --workspace "$GITHUB_WORKSPACE" --verbose --exclude "**/*.UITests/*" fi - name: Upload Coverage @@ -306,8 +311,8 @@ jobs: # Each platform's artifact holds one coverage.xml, already merged across that platform's test # projects by `test all`. The downloads must stay in their own per-artifact directories so - # both survive: flattened, one platform's report would overwrite the other's and the scanner - # would see a single platform's coverage as though it were the whole matrix's. + # they all survive: flattened, one platform's report would overwrite the others' and the + # scanner would see a single platform's coverage as though it were the whole matrix's. - name: Download Coverage if: needs.discover.outputs.has_tests == 'true' uses: actions/download-artifact@v7 @@ -315,8 +320,8 @@ jobs: pattern: coverage-* path: coverage - # The UI suites are excluded from the Windows test job, so their coverage exists only in the - # Linux report, which records the paths that runner checked out to. This job analyses a + # The UI suites run on Linux only, so their coverage exists only in the Linux report, which + # records the paths that runner checked out to. This job analyses a # Windows checkout, and Sonar matches coverage to source files by path, so every entry from # that report was dropped without a word: five suites' worth of coverage, and with it every # file only they exercise, reported as untested. Pointing those paths at this workspace is diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 9ad13a08..bfca845f 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -34,6 +34,16 @@ jobs: permissions: contents: read + # This workflow is the only one that builds the iOS head, and the only one that installs the + # workload it needs. The csprojs default IncludeIosTargets to false, so a macOS host without + # that workload -- every macOS cell of the test matrix -- builds the same target frameworks as + # Linux and Windows instead of failing with NETSDK1147 (#327). MSBuild reads environment + # variables as properties, so setting it at job level opts every dotnet invocation in the job + # in, including the ones that reach ImGui.App through a ProjectReference and would otherwise + # resolve its net10.0 head rather than its net10.0-ios one. + env: + IncludeIosTargets: "true" + steps: - name: Checkout Repository uses: actions/checkout@v5 @@ -114,6 +124,10 @@ jobs: permissions: contents: read + # See the ios-build job: the iOS head is opt-in, and this is the other job that opts in. + env: + IncludeIosTargets: "true" + steps: - name: Checkout Repository uses: actions/checkout@v5 diff --git a/CLAUDE.md b/CLAUDE.md index 89405cc2..3c4c4cb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# CLAUDE.md +# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. @@ -473,6 +473,20 @@ All C# files require this header: ## CI/CD +The iOS target framework is opt-in. `ImGui.App`, `ImGuiAppDemo.iOS` and `ImGui.App.iOS.SmokeTest` +add their `net10.0-ios` head only when `IncludeIosTargets` is `true` **and** the host is macOS; +otherwise `ImGui.App` cross-targets `net10.0;net9.0;net8.0` and the two app projects degrade to a +plain `net10.0` console exe. Only `.github/workflows/ios.yml` opts in, at job level, and it is the +only workflow that runs `dotnet workload install ios`. Without the gate, every macOS build widened +itself to `net10.0-ios` and failed with `NETSDK1147` before reaching a test, which is why macOS was +excluded from the test matrix (#327). If you add a step that has to build the iOS head, set +`IncludeIosTargets` — an environment variable works, MSBuild reads it as a property. + +The test matrix in `dotnet.yml` fans out over Linux, Windows and macOS. The five UI suites run on +Linux only: they are the whole cost of the job, and the CPU rasterizer they drive measures the same +on either host. The `Test` step tests for Linux rather than against Windows, so any platform added +later gets that cheap treatment by default. + Uses `scripts/PSBuild.psm1` PowerShell module for CI pipeline. Version increments are controlled by commit message tags: `[major]`, `[minor]`, `[patch]`, `[pre]`. Auto-generated files (VERSION.md, CHANGELOG.md, LICENSE.md) should not be manually edited. CI runs on Windows, publishes to NuGet, uses SonarQube for analysis. ## Code Quality diff --git a/ImGui.App/ImGui.App.csproj b/ImGui.App/ImGui.App.csproj index 77b410ea..e0f28966 100644 --- a/ImGui.App/ImGui.App.csproj +++ b/ImGui.App/ImGui.App.csproj @@ -1,4 +1,4 @@ - + @@ -8,8 +8,15 @@ true + + + false + net10.0;net9.0;net8.0 - $(TargetFrameworks);net10.0-ios + $(TargetFrameworks);net10.0-ios diff --git a/examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj b/examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj index 5d86e183..1106b667 100644 --- a/examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj +++ b/examples/ImGuiAppDemo.iOS/ImGuiAppDemo.iOS.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,13 +9,15 @@ ktsu.ImGui.Examples.App.iOS ImGuiAppDemoiOS - - net10.0-ios - net10.0 + false + net10.0-ios + net10.0 diff --git a/tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj b/tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj index dd5a5ba4..68106268 100644 --- a/tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj +++ b/tests/ImGui.App.iOS.SmokeTest/ImGui.App.iOS.SmokeTest.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,11 +9,13 @@ ktsu.ImGui.App.iOS.SmokeTest ImGuiAppiOSSmokeTest - - net10.0-ios - net10.0 + + false + net10.0-ios + net10.0 From ea723b730991a3c51804623939b6cc7b2d55d3e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 02:00:31 +0000 Subject: [PATCH 2/2] fix: stop macOS remapping the harness's Ctrl to Super [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS test cell this PR re-enabled found a real platform difference: `Keyboard_PressWithCtrl_ReportsTheModifier` failed on macOS only. Dear ImGui defaults `ConfigMacOSXBehaviors` to true on Apple platforms, and one of those behaviours swaps Ctrl and Super so Cmd drives shortcuts. The harness injects `ImGuiKey.ModCtrl`, which therefore reaches the application as `KeySuper` on macOS and `KeyCtrl` everywhere else, so the same assertion means different things depending on the host. Reproduced on Linux by setting the flag by hand: `ctrl=False super=True`, the exact shape of the macOS failure. Clearing it again in the same session restored `ctrl=True`, which confirms the flag is read per frame and that setting it in `Start` takes effect on a macOS runner. The harness exists to inject input deterministically, so it now pins the behaviour off before the first frame and Ctrl means Ctrl on every platform. A test that wants the macOS mapping can set the flag itself. Adds `Keyboard_PressWithCtrl_DoesNotArriveAsSuper`, which fails if the swap ever comes back — on any platform, not just the one that has it by default. ImGui.App.Testing.Tests: 82 passed. Solution builds clean in Release. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N1cVfacJWw1uM42DUmPfUg --- ImGui.App.Testing/ImGuiAppHarness.cs | 11 +++++++++- .../ImGuiAppHarnessTests.cs | 22 ++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ImGui.App.Testing/ImGuiAppHarness.cs b/ImGui.App.Testing/ImGuiAppHarness.cs index 03f7b28f..0c65e14d 100644 --- a/ImGui.App.Testing/ImGuiAppHarness.cs +++ b/ImGui.App.Testing/ImGuiAppHarness.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.ImGui.App.Testing; @@ -100,6 +100,15 @@ public static ImGuiAppHarness Start(ImGuiAppConfig config, HarnessOptions option Hexa.NET.ImGui.ImGui.GetIO().ConfigFlags |= Hexa.NET.ImGui.ImGuiConfigFlags.DockingEnable; } + // Dear ImGui defaults ConfigMacOSXBehaviors to true on Apple platforms, and one of those + // behaviours swaps Ctrl and Super so that Cmd drives shortcuts. An injected ImGuiKey.ModCtrl + // therefore reaches the application as KeySuper on macOS and as KeyCtrl everywhere else, so + // the same test asserts different things depending on the host. The harness exists to inject + // input deterministically, so it pins the behaviour off and Ctrl means Ctrl on every + // platform. A test that wants the macOS mapping can set the flag itself; it is read afresh + // each frame. + Hexa.NET.ImGui.ImGui.GetIO().ConfigMacOSXBehaviors = false; + // ImGuiController does this for a windowed application, and the harness replaces the // controller. Without it ImGuizmo, ImNodes and ImPlot never learn the ImGui context and an // application that draws with any of them faults inside native code rather than failing as diff --git a/tests/ImGui.App.Testing.Tests/ImGuiAppHarnessTests.cs b/tests/ImGui.App.Testing.Tests/ImGuiAppHarnessTests.cs index 128eafa0..4807be57 100644 --- a/tests/ImGui.App.Testing.Tests/ImGuiAppHarnessTests.cs +++ b/tests/ImGui.App.Testing.Tests/ImGuiAppHarnessTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.ImGui.App.Testing.Tests; @@ -246,6 +246,26 @@ public void Keyboard_PressWithCtrl_ReportsTheModifier() Assert.IsTrue(sawCtrlZ, "Ctrl and Z should arrive together, which is what a shortcut needs."); } + /// + /// Regression test for the macOS Ctrl/Super swap. Dear ImGui defaults + /// ConfigMacOSXBehaviors to true on Apple platforms, which swaps Ctrl and Super so Cmd + /// drives shortcuts; an injected then arrives as + /// KeySuper. The harness pins the behaviour off, so this asserts the modifier lands + /// where the caller asked and nowhere else. + /// + [TestMethod] + public void Keyboard_PressWithCtrl_DoesNotArriveAsSuper() + { + bool sawSuper = false; + using ImGuiAppHarness harness = ImGuiAppHarness.Start( + new ImGuiAppConfig { OnRender = _ => sawSuper |= ImGui.GetIO().KeySuper }, + Window()); + + harness.Keyboard.Press(ImGuiKey.Z, ctrl: true); + + Assert.IsFalse(sawSuper, "Ctrl must not be remapped to Super, which is what macOS behaviours would do."); + } + [TestMethod] public void Keyboard_Type_DeliversEveryCharacterInOrder() {