Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 45 additions & 3 deletions nix/ci.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
13 changes: 13 additions & 0 deletions nix/ios-deps.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand All @@ -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
];
};
Expand Down
5 changes: 3 additions & 2 deletions nix/ios.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 27 additions & 4 deletions nix/lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -72,21 +72,31 @@ 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 {
url = "https://github.com/libffi/libffi/releases/download/v3.5.2/libffi-3.5.2.tar.gz";
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
Expand Down Expand Up @@ -223,6 +233,7 @@ let

ghc -staticlib \
-O2 \
${if !simulator then iosDeviceCpuFlag else ""} \
-o libHatter.a \
-I${hatterSrc}/include \
-package-db ${crossDeps}/pkgdb \
Expand All @@ -247,6 +258,7 @@ let

ghc -staticlib \
-O2 \
${if !simulator then iosDeviceCpuFlag else ""} \
-o libHatter.a \
-I${hatterSrc}/include \
-optl-lffi \
Expand Down Expand Up @@ -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
'';

}
5 changes: 3 additions & 2 deletions nix/watchos.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions test/ios/sigill_canary.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

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;
}
Loading