diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 99ce2c7..9e4b016 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -99,6 +99,8 @@ jobs: - run: nix-build nix/ci.nix -A simulator-all -A ios-lib - name: Run combined simulator test (lifecycle + UI + buttons + scroll) run: nix-build nix/ci.nix -A simulator-all -o result-simulator-all && ./result-simulator-all/bin/test-all-ios + - name: "Guard #216: verify no UDOT/SDOT in iOS device compilation" + run: nix-build nix/ci.nix -A ios-sigill-check - name: Cancel workflow on failure if: failure() continue-on-error: true diff --git a/nix/ci.nix b/nix/ci.nix index 4cbee3a..05ab437 100644 --- a/nix/ci.nix +++ b/nix/ci.nix @@ -31,9 +31,51 @@ let license = lib.licenses.mit; }; }; - } // (if isDarwin then { - ios-lib = import ./ios.nix { inherit sources; }; - watchos-lib = import ./watchos.nix { inherit sources; }; + } // (if isDarwin then let + isAppleSilicon = builtins.currentSystem == "aarch64-darwin"; + iosLib = import ./ios.nix { inherit sources; }; + watchosLib = import ./watchos.nix { inherit sources; }; + lib = import ./lib.nix { inherit sources; }; + # lib with deviceCpu set — only used by ios-sigill-check to prove the fix works + libWithCpuFlag = import ./lib.nix { inherit sources; deviceCpu = "apple-a12"; }; + canary = ../test/ios/sigill_canary.c; + in { + ios-lib = iosLib; + watchos-lib = watchosLib; + + # Issue #216: Verify iOS device C compilation doesn't emit ARMv8.4+ + # instructions (UDOT/SDOT) that crash on pre-A13 devices (A12/A12X). + # Compiles a canary through the same GHC + flags as mkAppleStaticLib. + ios-sigill-check = pkgs.runCommand "ios-sigill-check" {} ( + if isAppleSilicon then '' + echo "=== Disassembly of canary compiled for iOS device (with deviceCpu=apple-a12) ===" + cat ${libWithCpuFlag.compileIOSDeviceC canary} + + # Detect UDOT/SDOT: either as a mnemonic or as a raw .long + # encoding (otool prints .long when it doesn't know the opcode). + # UDOT vector: 0x6E8x94xx, SDOT vector: 0x0E8x94xx + has_dotprod() { + grep -qi 'udot\|sdot' "$1" && return 0 + grep -qE '\.long\s+0x[06]e8[0-9a-f]94' "$1" && return 0 + return 1 + } + + if has_dotprod ${libWithCpuFlag.compileIOSDeviceC canary}; then + echo "" + echo "FAIL: iOS device C compilation emits UDOT/SDOT even with deviceCpu=apple-a12." + echo "These crash on pre-A13 devices (A12/A12X)." + echo "See https://github.com/jappeace/hatter/issues/216" + exit 1 + fi + + echo "" + echo "OK: No UDOT/SDOT detected when deviceCpu=apple-a12 is set." + touch $out + '' else '' + echo "SKIP: not Apple Silicon (${builtins.currentSystem}), UDOT not relevant." + touch $out + '' + ); } else {}); # Emulator/simulator test runners — heavy (include system images), diff --git a/nix/ios-deps.nix b/nix/ios-deps.nix index 1491f33..7c14b7d 100644 --- a/nix/ios-deps.nix +++ b/nix/ios-deps.nix @@ -15,6 +15,7 @@ , consumerCabal2Nix ? null , hpkgs ? (_: _: {}) # consumer haskellPackages overrides , hatterSrc ? null # hatter source tree (builds hatter as a normal dep) +, deviceCpu ? null # optional CPU target for C compilations (issue #216) }: let pkgs = import sources.nixpkgs {}; @@ -41,10 +42,22 @@ let }); } else {}; + # Issue #216: Inject -mcpu into C compilations of Haskell dependencies + # (e.g. sqlite3.c in direct-sqlite) to avoid ARMv8.4+ instructions. + deviceCpuOverride = self: super: + if deviceCpu != null then { + mkDerivation = args: super.mkDerivation (args // { + configureFlags = (args.configureFlags or []) ++ [ + "--ghc-option=-optc-mcpu=${deviceCpu}" + ]; + }); + } else {}; + nativeHaskellPkgs = pkgs.haskellPackages.override { overrides = pkgs.lib.composeManyExtensions [ unwitchOverride hatterOverride + deviceCpuOverride hpkgs ]; }; diff --git a/nix/ios.nix b/nix/ios.nix index dfd50df..6dc9f8f 100644 --- a/nix/ios.nix +++ b/nix/ios.nix @@ -9,11 +9,12 @@ , consumerCabalFile ? null , consumerCabal2Nix ? null , hpkgs ? (_: _: {}) # consumer haskellPackages overrides +, deviceCpu ? null # optional CPU target for device builds (issue #216) }: let - lib = import ./lib.nix { inherit sources; }; + lib = import ./lib.nix { inherit sources deviceCpu; }; iosDeps = import ./ios-deps.nix { - inherit sources consumerCabalFile consumerCabal2Nix hpkgs; + inherit sources consumerCabalFile consumerCabal2Nix hpkgs deviceCpu; hatterSrc = ../.; }; in diff --git a/nix/lib.nix b/nix/lib.nix index 85d7129..cb6e505 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -11,7 +11,7 @@ # Usage: # let lib = import ./lib.nix { sources = import ../npins; }; # in lib.mkAndroidLib { hatterSrc = ../.; mainModule = ../test/ScrollDemoMain.hs; } -{ sources, androidArch ? "aarch64" }: +{ sources, androidArch ? "aarch64", deviceCpu ? null }: let archConfig = { aarch64 = { @@ -72,13 +72,21 @@ let # --- Apple (iOS/watchOS) shared infrastructure --- applePkgs = import sources.nixpkgs {}; appleGhc = applePkgs.haskellPackages.ghc; + + # Issue #216: Constrain instruction set to deviceCpu on device builds + # to prevent ARMv8.4+ instructions (UDOT/SDOT) that crash on pre-A13. + iosDeviceCpuFlag = if deviceCpu != null then "-optc -mcpu=${deviceCpu}" else ""; + iosCFlags = if deviceCpu != null then "-mcpu=${deviceCpu}" else ""; + gmpStatic = applePkgs.gmp.overrideAttrs (old: { dontDisableStatic = true; - }); + } // (if iosCFlags != "" then { + NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or "") + " ${iosCFlags}"; + } else {})); # Apple's libffi (v40) only ships .dylib — no static archive. # Build GNU libffi from source with --enable-static for bundling # into the iOS fat archive (mac2ios patches the platform tag). - libffiStatic = applePkgs.stdenv.mkDerivation { + libffiStatic = applePkgs.stdenv.mkDerivation ({ pname = "libffi-static"; version = "3.5.2"; src = applePkgs.fetchurl { @@ -86,7 +94,9 @@ let hash = "sha256-86MIKiOzfCk6T80QUxR7Nx8v+R+n6hsqUuM1Z2usgtw="; }; configureFlags = [ "--enable-static" "--disable-shared" ]; - }; + } // (if iosCFlags != "" then { + NIX_CFLAGS_COMPILE = "${iosCFlags}"; + } else {})); # ------------------------------------------------------------------------- # Shared data lists — single source of truth for modules, sources, headers @@ -223,6 +233,7 @@ let ghc -staticlib \ -O2 \ + ${if !simulator then iosDeviceCpuFlag else ""} \ -o libHatter.a \ -I${hatterSrc}/include \ -package-db ${crossDeps}/pkgdb \ @@ -247,6 +258,7 @@ let ghc -staticlib \ -O2 \ + ${if !simulator then iosDeviceCpuFlag else ""} \ -o libHatter.a \ -I${hatterSrc}/include \ -optl-lffi \ @@ -703,4 +715,15 @@ in { inherit name; }; + # --------------------------------------------------------------------------- + # compileIOSDeviceC: Compile a C file the same way mkAppleStaticLib does for + # device cbits. Used by ios-sigill-check to verify no UDOT/SDOT appears. + # --------------------------------------------------------------------------- + compileIOSDeviceC = src: applePkgs.runCommand "ios-device-c-obj" { + nativeBuildInputs = [ appleGhc applePkgs.cctools ]; + } '' + ghc -c -O2 ${iosDeviceCpuFlag} -o compiled.o ${src} + otool -tv compiled.o > $out + ''; + } diff --git a/nix/watchos.nix b/nix/watchos.nix index ff671a0..3ebe1e8 100644 --- a/nix/watchos.nix +++ b/nix/watchos.nix @@ -5,11 +5,12 @@ , consumerCabalFile ? null , consumerCabal2Nix ? null , hpkgs ? (_: _: {}) # consumer haskellPackages overrides +, deviceCpu ? null # optional CPU target for device builds (issue #216) }: let - lib = import ./lib.nix { inherit sources; }; + lib = import ./lib.nix { inherit sources deviceCpu; }; iosDeps = import ./ios-deps.nix { - inherit sources consumerCabalFile consumerCabal2Nix hpkgs; + inherit sources consumerCabalFile consumerCabal2Nix hpkgs deviceCpu; hatterSrc = ../.; }; in diff --git a/test/ios/sigill_canary.c b/test/ios/sigill_canary.c new file mode 100644 index 0000000..62b100f --- /dev/null +++ b/test/ios/sigill_canary.c @@ -0,0 +1,18 @@ +// Canary for iOS SIGILL issue #216. +// +// This dot-product loop auto-vectorizes into UDOT (ARMv8.4-A) at -O2 +// on Apple Silicon when clang targets the host CPU. UDOT causes SIGILL +// on pre-A13 devices (A12/A12X lack the instruction). +// +// The iOS build test compiles this with the same toolchain GHC uses and +// checks the disassembly for UDOT. If found, the build would produce +// binaries that crash on older devices. +#include + +int dotProduct(const uint8_t *a, const uint8_t *b, int n) { + int sum = 0; + for (int i = 0; i < n; i++) { + sum += a[i] * b[i]; + } + return sum; +}