From ae3f3afe7da2585bed9e5466d63060213775c46b Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 8 Jun 2026 14:15:13 +0200 Subject: [PATCH 1/5] Support 32-bit ARM Android via a separate Mono bundle .NET 10 defaults Android to the CoreCLR runtime, which only ships 64-bit runtime packs, so the published AAB dropped armeabi-v7a and 32-bit devices became "incompatible" on the Play Store. Build a second Mono/armeabi-v7a bundle alongside the CoreCLR 64-bit one; both go to a single Play listing, version-code-banded (CoreCLR above Mono) so 64-bit devices -- which also report armeabi-v7a support -- always resolve to CoreCLR and only 32-bit-only devices fall through to Mono. - fw-lite.yaml: publish-android is now a coreclr/mono build matrix; create-release stages both bundles under per-arch asset names. - FwLiteMaui.csproj: default RuntimeIdentifiers trimmed to the 64-bit set CoreCLR supports; arm32 builds with -p:UseMonoRuntime=true -p:RuntimeIdentifiers=android-arm. - Troubleshooting dialog shows the process architecture (Arm/Arm64) so you can confirm which bundle a device installed. - Add install-maui-android-release-arm32 task for on-device testing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/fw-lite.yaml | 53 ++++++++++++++++--- Taskfile.yml | 5 ++ backend/FwLite/FwLiteMaui/FwLiteMaui.csproj | 5 +- .../Services/MauiTroubleshootingService.cs | 3 ++ .../Services/ITroubleshootingService.cs | 1 + .../Services/RuntimeInfoHelper.cs | 14 +++++ .../Services/WebTroubleshootingService.cs | 3 ++ backend/FwLite/Taskfile.yml | 10 ++++ .../Services/ITroubleshootingService.ts | 1 + .../troubleshoot/TroubleshootDialog.svelte | 9 +++- 10 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index 97a07232d1..ba0bb38d9a 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -247,10 +247,29 @@ jobs: path: backend/FwLite/artifacts/publish/FwLiteWeb/* publish-android: - name: Publish FW Lite app for Android + name: Publish FW Lite app for Android (${{ matrix.variant }}) needs: [ build-and-test, frontend ] timeout-minutes: 30 runs-on: macos-latest + # Two bundles, one Play listing. .NET 10 defaults Android to CoreCLR, which ships + # 64-bit only; armeabi-v7a (32-bit) devices need the legacy Mono runtime. They can't + # be combined into one AAB (a bundle boots a single runtime), so we build both and + # upload both to the same Play release. The versionCode bands the runtime preference + # above the build number: a 64-bit device's abilist also includes armeabi-v7a, so it + # matches BOTH bundles — the higher-coded CoreCLR band guarantees it never resolves to + # Mono. 32-bit-only devices match only Mono. See create-release for the upload side. + strategy: + fail-fast: false + matrix: + include: + - variant: coreclr + rids: 'android-arm64;android-x64' + runtimeArgs: '' + versionOffset: 100000000 + - variant: mono + rids: 'android-arm' + runtimeArgs: '-p:UseMonoRuntime=true' + versionOffset: 0 steps: - name: Checkout uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 @@ -266,6 +285,10 @@ jobs: - name: Setup Maui run: dotnet workload install maui + - name: Compute Android version code + id: versionCode + shell: bash + run: echo "CODE=$(( ${{ matrix.versionOffset }} + ${{ github.run_number }} ))" >> ${GITHUB_OUTPUT} - name: Decode Android Keystore id: decodeKeystore env: @@ -282,8 +305,9 @@ jobs: KEYSTORE_ALIAS: ${{ vars.FW_LITE_KEYSTORE_UPLOAD_KEY_ALIAS }} run: | dotnet publish -f net10.0-android -p:BuildApple=false --artifacts-path ../artifacts \ + -p:RuntimeIdentifiers='${{ matrix.rids }}' ${{ matrix.runtimeArgs }} \ -p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} \ - -p:ApplicationVersion=${{ github.run_number }} \ + -p:ApplicationVersion=${{ steps.versionCode.outputs.CODE }} \ -p:InformationalVersion=${{ needs.build-and-test.outputs.version }} \ -p:AndroidKeyStore=true \ -p:AndroidSigningKeyStore=${KEYSTORE_PATH} \ @@ -295,10 +319,11 @@ jobs: - name: Upload FWLite App artifacts uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: - name: fw-lite-android + name: fw-lite-android-${{ matrix.variant }} if-no-files-found: error -# path looks like this: backend/FwLite/artifacts/publish/FwLiteMaui/release_net10.0-android/org.sil.fwlitemaui-signed.apk - path: backend/FwLite/artifacts/publish/FwLiteMaui/release_net10.0-android/* + # Single-RID (mono) publishes to release_net10.0-android_android-arm; multi-RID + # (coreclr) to release_net10.0-android. The trailing * tolerates both. + path: backend/FwLite/artifacts/publish/FwLiteMaui/release_net10.0-android*/* publish-win: name: Publish FW Lite app for Windows @@ -399,17 +424,29 @@ jobs: path: fw-lite-web-linux - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: - name: fw-lite-android - path: fw-lite-android + name: fw-lite-android-coreclr + path: fw-lite-android-coreclr + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + name: fw-lite-android-mono + path: fw-lite-android-mono - name: Zip artifacts run: | zip -r fw-lite-portable.zip fw-lite-portable chmod +x fw-lite-web-linux/*/FwLiteWeb fw-lite-web-linux/*/*.sh zip -r fw-lite-web-linux.zip fw-lite-web-linux - - name: Rename Installer + - name: Rename Installer and stage Android bundles run: | mv fw-lite-msix/FwLiteMaui.msixbundle fw-lite-msix/FieldWorksLiteInstaller.msixbundle + # Both variants share the org.sil.FwLiteMaui basename, so rename per arch before + # attaching (release asset names must be unique). Upload BOTH .aab to one Play release. + mkdir -p fw-lite-android + for v in coreclr mono; do + case $v in coreclr) arch=64bit ;; mono) arch=arm32 ;; esac + cp fw-lite-android-$v/*.aab fw-lite-android/FieldWorksLite-$arch.aab + cp fw-lite-android-$v/*.apk fw-lite-android/FieldWorksLite-$arch.apk 2>/dev/null || true + done - name: Create Release uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda #v2.2.1 diff --git a/Taskfile.yml b/Taskfile.yml index 7fb944ce14..8f85a323f7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -152,3 +152,8 @@ tasks: aliases: [android-release-dev] desc: Build a Release "Dev" APK and adb-install it on the connected USB device deps: [fw-lite:install-maui-android-release-dev] + + fw-lite-android-release-arm32: + aliases: [android-release-arm32] + desc: Build & install a Release Mono arm32 (armeabi-v7a) APK on the connected USB device + deps: [fw-lite:install-maui-android-release-arm32] diff --git a/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj b/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj index 69624a297c..273c29db3f 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj +++ b/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj @@ -12,7 +12,10 @@ The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated; either BOTH runtimes must be indicated or ONLY macatalyst-x64. --> - android-arm;android-arm64;android-x86;android-x64 + + android-arm64;android-x64 Exe FwLiteMaui true diff --git a/backend/FwLite/FwLiteMaui/Services/MauiTroubleshootingService.cs b/backend/FwLite/FwLiteMaui/Services/MauiTroubleshootingService.cs index 685a9bbea4..a3f58daefc 100644 --- a/backend/FwLite/FwLiteMaui/Services/MauiTroubleshootingService.cs +++ b/backend/FwLite/FwLiteMaui/Services/MauiTroubleshootingService.cs @@ -22,6 +22,9 @@ public class MauiTroubleshootingService( [JSInvokable] public Task GetCanShare() => Task.FromResult(true); + [JSInvokable] + public Task GetProcessArchitecture() => Task.FromResult(RuntimeInfoHelper.ProcessArchitecture()); + [JSInvokable] public async Task TryOpenDataDirectory() { diff --git a/backend/FwLite/FwLiteShared/Services/ITroubleshootingService.cs b/backend/FwLite/FwLiteShared/Services/ITroubleshootingService.cs index 50be6fad49..3a9813af96 100644 --- a/backend/FwLite/FwLiteShared/Services/ITroubleshootingService.cs +++ b/backend/FwLite/FwLiteShared/Services/ITroubleshootingService.cs @@ -3,6 +3,7 @@ namespace FwLiteShared.Services; public interface ITroubleshootingService { Task GetCanShare(); + Task GetProcessArchitecture(); Task TryOpenDataDirectory(); Task GetDataDirectory(); Task OpenLogFile(); diff --git a/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs b/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs new file mode 100644 index 0000000000..903275f15a --- /dev/null +++ b/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace FwLiteShared.Services; + +public static class RuntimeInfoHelper +{ + // Android ships a separate bundle per ABI (CoreCLR/64-bit vs Mono/armeabi-v7a), so the + // process architecture is a ground-truth fingerprint of which bundle a device installed. + public static string ProcessArchitecture() + { + var bits = Environment.Is64BitProcess ? "64-bit" : "32-bit"; + return $"{RuntimeInformation.ProcessArchitecture} ({bits})"; + } +} diff --git a/backend/FwLite/FwLiteWeb/Services/WebTroubleshootingService.cs b/backend/FwLite/FwLiteWeb/Services/WebTroubleshootingService.cs index 64bd1ec086..89778108dd 100644 --- a/backend/FwLite/FwLiteWeb/Services/WebTroubleshootingService.cs +++ b/backend/FwLite/FwLiteWeb/Services/WebTroubleshootingService.cs @@ -13,6 +13,9 @@ public class WebTroubleshootingService( [JSInvokable] public Task GetCanShare() => Task.FromResult(false); + [JSInvokable] + public Task GetProcessArchitecture() => Task.FromResult(RuntimeInfoHelper.ProcessArchitecture()); + [JSInvokable] public Task GetDataDirectory() { diff --git a/backend/FwLite/Taskfile.yml b/backend/FwLite/Taskfile.yml index c76a165132..fb2d76fa2d 100644 --- a/backend/FwLite/Taskfile.yml +++ b/backend/FwLite/Taskfile.yml @@ -121,6 +121,16 @@ tasks: vars: { FLAVOR: 'Dev' } - adb -d install -r bin/Release/net10.0-android/org.sil.FwLiteMaui.dev-Signed.apk + install-maui-android-release-arm32: + # Production CoreCLR builds are 64-bit only; this builds the separate Mono/armeabi-v7a bundle + # for testing the 32-bit app on real hardware. Output lands under the android-arm RID subfolder. + desc: Build a Release Mono arm32 (armeabi-v7a) "Dev" APK and adb-install it on the connected USB device + dir: ./FwLiteMaui + deps: [ ui:build-viewer ] + cmds: + - dotnet build -f net10.0-android -c Release -t:SignAndroidPackage -p:FwLiteFlavor=Dev -p:UseMonoRuntime=true -p:RuntimeIdentifiers=android-arm -p:RuntimeIdentifier=android-arm -p:AndroidPackageFormat=apk + - adb -d install -r bin/Release/net10.0-android/android-arm/org.sil.FwLiteMaui.dev-Signed.apk + build-mini-lcm-sdk: desc: Builds the sdk, a zip with the FwLiteWeb server with a project and config to run locally dir: ./FwLiteWeb diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/ITroubleshootingService.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/ITroubleshootingService.ts index 9cd162a39e..566504e172 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/ITroubleshootingService.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/ITroubleshootingService.ts @@ -6,6 +6,7 @@ export interface ITroubleshootingService { getCanShare() : Promise; + getProcessArchitecture() : Promise; tryOpenDataDirectory() : Promise; getDataDirectory() : Promise; openLogFile() : Promise; diff --git a/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte b/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte index 46c24b702e..eff8ca9f87 100644 --- a/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte +++ b/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte @@ -27,6 +27,7 @@ const config = useFwLiteConfig(); let projectCode = $state(); let canShare = resource(() => service, async (s) => await s?.getCanShare()); + let architecture = resource(() => service, async (s) => await s?.getProcessArchitecture()); // Mobile platforms keep app data in private storage that no file manager can open. const canOpenDataDirectory = $derived(config.os !== FwLitePlatform.Android && config.os !== FwLitePlatform.iOS); @@ -56,13 +57,19 @@ size="icon-xs" iconProps={{class: 'size-4'}} title={$t`Copy version`} - text={`FieldWorks Lite ${config.appVersion} on ${config.os}`} + text={`FieldWorks Lite ${config.appVersion} on ${config.os}${architecture.current ? ` (${architecture.current})` : ''}`} />

{$t`Platform`}: {config.os}

+ {#if architecture.current} +

+ {$t`Architecture`}: + {architecture.current} +

+ {/if} {#if service && (canOpenDataDirectory || $isDev)}
From 08ec3515b853d3b2a4d07aff81e0f3c8ecaaec35 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 8 Jun 2026 14:19:57 +0200 Subject: [PATCH 2/5] Extract the new Architecture i18n string The Troubleshoot dialog's $t`Architecture` is a new translatable message; CI fails the build if `pnpm i18n:extract` would add any msgid, so run it and commit the resulting catalog entries (English fallback in en.po, empty in the others). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/viewer/src/locales/en.po | 4 ++++ frontend/viewer/src/locales/es.po | 4 ++++ frontend/viewer/src/locales/fr.po | 4 ++++ frontend/viewer/src/locales/id.po | 4 ++++ frontend/viewer/src/locales/ko.po | 4 ++++ frontend/viewer/src/locales/ms.po | 4 ++++ frontend/viewer/src/locales/sw.po | 4 ++++ frontend/viewer/src/locales/vi.po | 4 ++++ 8 files changed, 32 insertions(+) diff --git a/frontend/viewer/src/locales/en.po b/frontend/viewer/src/locales/en.po index 7678587387..436979a58c 100644 --- a/frontend/viewer/src/locales/en.po +++ b/frontend/viewer/src/locales/en.po @@ -227,6 +227,10 @@ msgstr "Any semantic domain" msgid "Any Ws" msgstr "Any Ws" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "Architecture" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/es.po b/frontend/viewer/src/locales/es.po index 880e5da21e..c822fb0fd5 100644 --- a/frontend/viewer/src/locales/es.po +++ b/frontend/viewer/src/locales/es.po @@ -232,6 +232,10 @@ msgstr "Cualquier dominio semántico" msgid "Any Ws" msgstr "Cualquier W" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/fr.po b/frontend/viewer/src/locales/fr.po index 485d6334c9..01348581f3 100644 --- a/frontend/viewer/src/locales/fr.po +++ b/frontend/viewer/src/locales/fr.po @@ -232,6 +232,10 @@ msgstr "N'importe quel domaine sémantique" msgid "Any Ws" msgstr "Tous les Systèmes d'Ecriture" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/id.po b/frontend/viewer/src/locales/id.po index 215f56abd7..065026748a 100644 --- a/frontend/viewer/src/locales/id.po +++ b/frontend/viewer/src/locales/id.po @@ -232,6 +232,10 @@ msgstr "Domain semantik apa pun" msgid "Any Ws" msgstr "Setiap Ws" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/ko.po b/frontend/viewer/src/locales/ko.po index d84aaca2d1..64f00e437a 100644 --- a/frontend/viewer/src/locales/ko.po +++ b/frontend/viewer/src/locales/ko.po @@ -232,6 +232,10 @@ msgstr "모든 시맨틱 도메인" msgid "Any Ws" msgstr "모든 W" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/ms.po b/frontend/viewer/src/locales/ms.po index 86edeaa7f4..6389e08d1e 100644 --- a/frontend/viewer/src/locales/ms.po +++ b/frontend/viewer/src/locales/ms.po @@ -232,6 +232,10 @@ msgstr "Mana-mana domain semantik" msgid "Any Ws" msgstr "Mana-mana Ws" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/sw.po b/frontend/viewer/src/locales/sw.po index ddc5f77608..3dd55a1c44 100644 --- a/frontend/viewer/src/locales/sw.po +++ b/frontend/viewer/src/locales/sw.po @@ -232,6 +232,10 @@ msgstr "Kikoa chochote cha kumkutania" msgid "Any Ws" msgstr "Ws yoyote" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" diff --git a/frontend/viewer/src/locales/vi.po b/frontend/viewer/src/locales/vi.po index 74bb968e9f..fe6ce41142 100644 --- a/frontend/viewer/src/locales/vi.po +++ b/frontend/viewer/src/locales/vi.po @@ -232,6 +232,10 @@ msgstr "Bất kỳ miền ngữ nghĩa nào" msgid "Any Ws" msgstr "Bất kỳ hệ chữ nào" +#: src/lib/troubleshoot/TroubleshootDialog.svelte +msgid "Architecture" +msgstr "" + #. Confirmation prompt #: src/lib/entry-editor/DeleteDialog.svelte msgid "Are you sure you want to delete {0}?" From 36305d4c1a42fc85332477e4a3329dd8b5be0b54 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 8 Jun 2026 14:52:49 +0200 Subject: [PATCH 3/5] Fix linq2db cctor patcher for trimmer-neutralised cctor; arm32 task + dialog spacing The Mono/arm32 size-optimizing trimmer can reduce SqlTransparentExpression's static ctor to a no-op (nop/ret) before the patcher runs -- the exact shape the patcher itself produces -- which tripped its "stsfld _ctor required" guard and broke the build. - Linq2DbCctorPatcher: accept a no-op cctor as already-neutralised instead of failing; guard the _ctor field directly (the real hazard) and still replace Quote() with a throw in all accepted shapes. Makes the patcher idempotent. - install-maui-android-release-arm32: use -t:Run (full build -> install -> launch graph, targets the USB device) instead of -t:SignAndroidPackage. - TroubleshootDialog: lay the version/Platform/Architecture rows out as flex children (gap-1) so their spacing is uniform. Known related issue (not fixed here, clean builds unaffected): stale .cctor-patched sentinels can make the MSBuild patch target skip a regenerated dll on dirty incremental builds, letting an unpatched dll reach AOT. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../build/Linq2DbCctorPatcher/Program.cs | 21 ++++++++++++++----- backend/FwLite/Taskfile.yml | 11 +++++----- .../troubleshoot/TroubleshootDialog.svelte | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs b/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs index 03e1b416e4..201463a128 100644 --- a/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs +++ b/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs @@ -49,26 +49,37 @@ static int Fail(string message) if (nested is null) return Fail("SqlTransparentExpression nested type not found inside EFCoreMetadataReader."); + // The _ctor field is what makes Quote() dangerous, so its continued existence + // is the real invariant — guard it directly rather than inferring it from the cctor. + if (!nested.Fields.Any(f => f.Name == "_ctor")) + return Fail("SqlTransparentExpression no longer declares the _ctor field; IL shape changed."); + var cctor = nested.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); if (cctor is null || !cctor.HasBody) return Fail("SqlTransparentExpression .cctor not found (or has no body)."); - // Sanity-check the cctor shape: at least one stsfld targeting the _ctor field. - // If upstream renames _ctor or restructures the field init, we want to know. + // The cctor must be in one of two recognised shapes: + // - the original reflection-based init, which assigns _ctor via stsfld (we neutralise it below), or + // - an effective no-op (just `ret`), which the size-optimising trimmer produces by dropping the + // now-unused field init — and which is also what our own stub leaves behind on a re-run. + // Any other shape (real instructions but no stsfld _ctor) is an unrecognised restructure: fail loud. var storesCtorField = cctor.Body.Instructions.Any(ins => ins.OpCode == OpCodes.Stsfld && ins.Operand is FieldReference fr && fr.Name == "_ctor" && fr.DeclaringType.FullName == nested.FullName); - if (!storesCtorField) - return Fail("SqlTransparentExpression .cctor no longer contains a stsfld for the _ctor field; IL shape changed."); + var isNoOp = cctor.Body.Instructions.All(ins => ins.OpCode == OpCodes.Nop || ins.OpCode == OpCodes.Ret); + if (!storesCtorField && !isNoOp) + return Fail("SqlTransparentExpression .cctor has an unrecognised shape (no stsfld for _ctor and not a no-op); IL shape changed."); var quote = nested.Methods.FirstOrDefault(m => m.Name == "Quote" && m.Parameters.Count == 0); if (quote is null || !quote.HasBody) return Fail("SqlTransparentExpression.Quote() not found (or has no body)."); ReplaceBodyWith(cctor, Instruction.Create(OpCodes.Ret)); - Console.WriteLine("Stubbed SqlTransparentExpression .cctor to no-op ret"); + Console.WriteLine(storesCtorField + ? "Stubbed SqlTransparentExpression .cctor to no-op ret" + : "SqlTransparentExpression .cctor already a no-op (trimmer-neutralised); ensured ret"); // Replace Quote() with `throw new NotImplementedException();` so anything that // somehow reaches it fails loud rather than NRE'ing on the now-null _ctor field. diff --git a/backend/FwLite/Taskfile.yml b/backend/FwLite/Taskfile.yml index fb2d76fa2d..1a45549299 100644 --- a/backend/FwLite/Taskfile.yml +++ b/backend/FwLite/Taskfile.yml @@ -122,14 +122,13 @@ tasks: - adb -d install -r bin/Release/net10.0-android/org.sil.FwLiteMaui.dev-Signed.apk install-maui-android-release-arm32: - # Production CoreCLR builds are 64-bit only; this builds the separate Mono/armeabi-v7a bundle - # for testing the 32-bit app on real hardware. Output lands under the android-arm RID subfolder. - desc: Build a Release Mono arm32 (armeabi-v7a) "Dev" APK and adb-install it on the connected USB device + # Production CoreCLR builds are 64-bit only; this builds & runs the separate Mono/armeabi-v7a + # bundle for testing the 32-bit app on real hardware. -t:Run drives the full + # build -> install -> launch graph and targets the connected USB device (AdbTarget=-d). + desc: Build, install & run a Release Mono arm32 (armeabi-v7a) "Dev" APK on the connected USB device dir: ./FwLiteMaui deps: [ ui:build-viewer ] - cmds: - - dotnet build -f net10.0-android -c Release -t:SignAndroidPackage -p:FwLiteFlavor=Dev -p:UseMonoRuntime=true -p:RuntimeIdentifiers=android-arm -p:RuntimeIdentifier=android-arm -p:AndroidPackageFormat=apk - - adb -d install -r bin/Release/net10.0-android/android-arm/org.sil.FwLiteMaui.dev-Signed.apk + cmd: dotnet build -f net10.0-android -c Release -t:Run -p:FwLiteFlavor=Dev -p:UseMonoRuntime=true -p:RuntimeIdentifiers=android-arm -p:RuntimeIdentifier=android-arm -p:AndroidPackageFormat=apk -p:AdbTarget=-d build-mini-lcm-sdk: desc: Builds the sdk, a zip with the FwLiteWeb server with a project and config to run locally diff --git a/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte b/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte index eff8ca9f87..47406c1e30 100644 --- a/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte +++ b/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte @@ -46,7 +46,7 @@
-
+

{$t`FieldWorks Lite version`}: From 8f27f92e3cbc5dce711f7efbcb0bef60bf3f7144 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 8 Jun 2026 15:01:28 +0200 Subject: [PATCH 4/5] Fix arm32 CI: select RIDs in csproj, not via global -p:RuntimeIdentifiers The publish-android matrix passed -p:RuntimeIdentifiers on the command line, which is a global property and broke the SDK's per-RID fan-out: - coreclr leg: the ';' in android-arm64;android-x64 was read as a property separator -> MSB1006 "Switch: android-x64". - mono leg: dependency projects didn't get the singular RuntimeIdentifier -> MSB3030 missing release_android-arm/.dll. Move the RID list into FwLiteMaui.csproj keyed on UseMonoRuntime, and have CI pass only the runtime flag. Also tighten Troubleshoot dialog row spacing (the CopyButton was inflating the version row height). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/fw-lite.yaml | 8 ++++---- backend/FwLite/FwLiteMaui/FwLiteMaui.csproj | 9 ++++++--- .../src/lib/troubleshoot/TroubleshootDialog.svelte | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index ba0bb38d9a..979063e421 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -262,12 +262,13 @@ jobs: fail-fast: false matrix: include: + # RIDs are NOT passed here — the csproj selects them from UseMonoRuntime (coreclr => + # android-arm64;android-x64, mono => android-arm). Passing -p:RuntimeIdentifiers on the + # command line makes it global and breaks the per-RID fan-out (MSB1006 / MSB3030). - variant: coreclr - rids: 'android-arm64;android-x64' runtimeArgs: '' versionOffset: 100000000 - variant: mono - rids: 'android-arm' runtimeArgs: '-p:UseMonoRuntime=true' versionOffset: 0 steps: @@ -304,8 +305,7 @@ jobs: KEYSTORE_PASS: ${{ secrets.FW_LITE_KEYSTORE_PASS }} KEYSTORE_ALIAS: ${{ vars.FW_LITE_KEYSTORE_UPLOAD_KEY_ALIAS }} run: | - dotnet publish -f net10.0-android -p:BuildApple=false --artifacts-path ../artifacts \ - -p:RuntimeIdentifiers='${{ matrix.rids }}' ${{ matrix.runtimeArgs }} \ + dotnet publish -f net10.0-android -p:BuildApple=false --artifacts-path ../artifacts ${{ matrix.runtimeArgs }} \ -p:ApplicationDisplayVersion=${{ needs.build-and-test.outputs.semver-version }} \ -p:ApplicationVersion=${{ steps.versionCode.outputs.CODE }} \ -p:InformationalVersion=${{ needs.build-and-test.outputs.version }} \ diff --git a/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj b/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj index 273c29db3f..dfb14d3980 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj +++ b/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj @@ -13,9 +13,12 @@ either BOTH runtimes must be indicated or ONLY macatalyst-x64. --> - android-arm64;android-x64 + Setting UseMonoRuntime=true builds the 32-bit armeabi-v7a bundle (the legacy runtime still ships an + android-arm pack). Keep the RID list HERE rather than passing -p:RuntimeIdentifiers on the command line: + a global -p: value breaks the SDK's per-RID fan-out (the ';' is read as a property separator -> MSB1006, + and dependency projects don't get the singular RuntimeIdentifier -> MSB3030 missing per-RID output). --> + android-arm64;android-x64 + android-arm Exe FwLiteMaui true diff --git a/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte b/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte index 47406c1e30..7932e62c0d 100644 --- a/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte +++ b/frontend/viewer/src/lib/troubleshoot/TroubleshootDialog.svelte @@ -55,6 +55,7 @@ Date: Mon, 8 Jun 2026 15:32:07 +0200 Subject: [PATCH 5/5] Revert IL patcher change; trim arm32 comments The cctor-patcher tolerance for a trimmer-neutralised (no-op) cctor only mattered for polluted incremental builds. Clean builds (CI) take the normal stsfld path -- the CI failures were always the RID issue (MSB1006/MSB3030), never the patcher -- so the original deliberately-loud guard is fine as-is. Also trim/dedupe the comments added for the arm32 work (the RID/fan-out rationale now lives once, in FwLiteMaui.csproj). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/fw-lite.yaml | 15 +++++-------- backend/FwLite/FwLiteMaui/FwLiteMaui.csproj | 8 +++---- .../build/Linq2DbCctorPatcher/Program.cs | 21 +++++-------------- .../Services/RuntimeInfoHelper.cs | 3 +-- backend/FwLite/Taskfile.yml | 5 ++--- 5 files changed, 16 insertions(+), 36 deletions(-) diff --git a/.github/workflows/fw-lite.yaml b/.github/workflows/fw-lite.yaml index 979063e421..ff227b014b 100644 --- a/.github/workflows/fw-lite.yaml +++ b/.github/workflows/fw-lite.yaml @@ -251,20 +251,15 @@ jobs: needs: [ build-and-test, frontend ] timeout-minutes: 30 runs-on: macos-latest - # Two bundles, one Play listing. .NET 10 defaults Android to CoreCLR, which ships - # 64-bit only; armeabi-v7a (32-bit) devices need the legacy Mono runtime. They can't - # be combined into one AAB (a bundle boots a single runtime), so we build both and - # upload both to the same Play release. The versionCode bands the runtime preference - # above the build number: a 64-bit device's abilist also includes armeabi-v7a, so it - # matches BOTH bundles — the higher-coded CoreCLR band guarantees it never resolves to - # Mono. 32-bit-only devices match only Mono. See create-release for the upload side. + # Two bundles for one Play listing: CoreCLR (64-bit) + Mono (armeabi-v7a) — they can't share an AAB + # (a bundle boots a single runtime). 64-bit devices' abilist also includes armeabi-v7a, so they match + # BOTH bundles; the versionCode bands CoreCLR above Mono so they resolve to CoreCLR and only 32-bit-only + # devices get Mono. See create-release for the upload side. strategy: fail-fast: false matrix: include: - # RIDs are NOT passed here — the csproj selects them from UseMonoRuntime (coreclr => - # android-arm64;android-x64, mono => android-arm). Passing -p:RuntimeIdentifiers on the - # command line makes it global and breaks the per-RID fan-out (MSB1006 / MSB3030). + # RIDs come from the csproj, keyed on UseMonoRuntime (see FwLiteMaui.csproj). - variant: coreclr runtimeArgs: '' versionOffset: 100000000 diff --git a/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj b/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj index dfb14d3980..ef106a0187 100644 --- a/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj +++ b/backend/FwLite/FwLiteMaui/FwLiteMaui.csproj @@ -12,11 +12,9 @@ The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated; either BOTH runtimes must be indicated or ONLY macatalyst-x64. --> - + android-arm64;android-x64 android-arm Exe diff --git a/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs b/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs index 201463a128..03e1b416e4 100644 --- a/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs +++ b/backend/FwLite/FwLiteMaui/build/Linq2DbCctorPatcher/Program.cs @@ -49,37 +49,26 @@ static int Fail(string message) if (nested is null) return Fail("SqlTransparentExpression nested type not found inside EFCoreMetadataReader."); - // The _ctor field is what makes Quote() dangerous, so its continued existence - // is the real invariant — guard it directly rather than inferring it from the cctor. - if (!nested.Fields.Any(f => f.Name == "_ctor")) - return Fail("SqlTransparentExpression no longer declares the _ctor field; IL shape changed."); - var cctor = nested.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); if (cctor is null || !cctor.HasBody) return Fail("SqlTransparentExpression .cctor not found (or has no body)."); - // The cctor must be in one of two recognised shapes: - // - the original reflection-based init, which assigns _ctor via stsfld (we neutralise it below), or - // - an effective no-op (just `ret`), which the size-optimising trimmer produces by dropping the - // now-unused field init — and which is also what our own stub leaves behind on a re-run. - // Any other shape (real instructions but no stsfld _ctor) is an unrecognised restructure: fail loud. + // Sanity-check the cctor shape: at least one stsfld targeting the _ctor field. + // If upstream renames _ctor or restructures the field init, we want to know. var storesCtorField = cctor.Body.Instructions.Any(ins => ins.OpCode == OpCodes.Stsfld && ins.Operand is FieldReference fr && fr.Name == "_ctor" && fr.DeclaringType.FullName == nested.FullName); - var isNoOp = cctor.Body.Instructions.All(ins => ins.OpCode == OpCodes.Nop || ins.OpCode == OpCodes.Ret); - if (!storesCtorField && !isNoOp) - return Fail("SqlTransparentExpression .cctor has an unrecognised shape (no stsfld for _ctor and not a no-op); IL shape changed."); + if (!storesCtorField) + return Fail("SqlTransparentExpression .cctor no longer contains a stsfld for the _ctor field; IL shape changed."); var quote = nested.Methods.FirstOrDefault(m => m.Name == "Quote" && m.Parameters.Count == 0); if (quote is null || !quote.HasBody) return Fail("SqlTransparentExpression.Quote() not found (or has no body)."); ReplaceBodyWith(cctor, Instruction.Create(OpCodes.Ret)); - Console.WriteLine(storesCtorField - ? "Stubbed SqlTransparentExpression .cctor to no-op ret" - : "SqlTransparentExpression .cctor already a no-op (trimmer-neutralised); ensured ret"); + Console.WriteLine("Stubbed SqlTransparentExpression .cctor to no-op ret"); // Replace Quote() with `throw new NotImplementedException();` so anything that // somehow reaches it fails loud rather than NRE'ing on the now-null _ctor field. diff --git a/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs b/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs index 903275f15a..8ef9a1c6fd 100644 --- a/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs +++ b/backend/FwLite/FwLiteShared/Services/RuntimeInfoHelper.cs @@ -4,8 +4,7 @@ namespace FwLiteShared.Services; public static class RuntimeInfoHelper { - // Android ships a separate bundle per ABI (CoreCLR/64-bit vs Mono/armeabi-v7a), so the - // process architecture is a ground-truth fingerprint of which bundle a device installed. + // On Android the process architecture reveals which bundle a device installed (Mono/arm32 vs CoreCLR/64-bit). public static string ProcessArchitecture() { var bits = Environment.Is64BitProcess ? "64-bit" : "32-bit"; diff --git a/backend/FwLite/Taskfile.yml b/backend/FwLite/Taskfile.yml index 1a45549299..b453f88a9b 100644 --- a/backend/FwLite/Taskfile.yml +++ b/backend/FwLite/Taskfile.yml @@ -122,9 +122,8 @@ tasks: - adb -d install -r bin/Release/net10.0-android/org.sil.FwLiteMaui.dev-Signed.apk install-maui-android-release-arm32: - # Production CoreCLR builds are 64-bit only; this builds & runs the separate Mono/armeabi-v7a - # bundle for testing the 32-bit app on real hardware. -t:Run drives the full - # build -> install -> launch graph and targets the connected USB device (AdbTarget=-d). + # Production builds are 64-bit (CoreCLR); this builds & runs the Mono/armeabi-v7a bundle to test + # the 32-bit app on a connected USB device. desc: Build, install & run a Release Mono arm32 (armeabi-v7a) "Dev" APK on the connected USB device dir: ./FwLiteMaui deps: [ ui:build-viewer ]