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.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/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.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() { 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