From 85da9619d569eacde49a7875b39cae671789fdf4 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Tue, 21 Apr 2026 15:07:45 +0000 Subject: [PATCH] Build hatter as cached cabal package for iOS/watchOS Mirror the Android approach from cross-deps.nix: build hatter via callCabal2nix in ios-deps.nix so the .a and package DB entry are cached. Demo apps and consumers only recompile Main.hs + run_main.c (the per-app entry point) instead of all 22 Hatter modules + 19 C sources from scratch every time. Changes: - ios-deps.nix: add hatterSrc param, hatterOverride (strips exe/test stanzas), compose into overrides, collect hatter .a in deps - ios.nix, watchos.nix: pass hatterSrc = ../. to ios-deps.nix - lib.nix: when crossDeps is provided, mkIOSLib and mkWatchOSLib skip copying Hatter/*.hs and cbits/*.c, only compile run_main.c + Main.hs with -package-db pointing at the cached pkgdb. Standalone fallback (crossDeps == null) keeps current from-source behavior. Prompt: Implement the plan to build hatter as a cached package for iOS/watchOS (like Android) Co-Authored-By: Claude Opus 4.6 --- nix/ios-deps.nix | 28 +++++++++++++-- nix/ios.nix | 1 + nix/lib.nix | 88 +++++++++++++++++++++++++++++++++++++++++++++--- nix/watchos.nix | 1 + 4 files changed, 112 insertions(+), 6 deletions(-) diff --git a/nix/ios-deps.nix b/nix/ios-deps.nix index 9247fb80..1491f335 100644 --- a/nix/ios-deps.nix +++ b/nix/ios-deps.nix @@ -14,6 +14,7 @@ , consumerCabalFile ? null , consumerCabal2Nix ? null , hpkgs ? (_: _: {}) # consumer haskellPackages overrides +, hatterSrc ? null # hatter source tree (builds hatter as a normal dep) }: let pkgs = import sources.nixpkgs {}; @@ -25,8 +26,27 @@ let }) {}; }; + # Build hatter as a regular haskellPackages derivation from local source. + # Executables and tests are stripped to avoid pulling in test-framework deps. + hatterOverride = self: super: + if hatterSrc != null then { + hatter = pkgs.haskell.lib.overrideCabal + (self.callCabal2nix "hatter" hatterSrc {}) + (old: { + postPatch = (old.postPatch or "") + '' + sed -i '/^executable /,$d' hatter.cabal + sed -i '/^test-suite /,$d' hatter.cabal + ''; + doCheck = false; + }); + } else {}; + nativeHaskellPkgs = pkgs.haskellPackages.override { - overrides = pkgs.lib.composeExtensions unwitchOverride hpkgs; + overrides = pkgs.lib.composeManyExtensions [ + unwitchOverride + hatterOverride + hpkgs + ]; }; ghc = nativeHaskellPkgs.ghc; @@ -37,11 +57,15 @@ let haskellPkgs = nativeHaskellPkgs; }; + # When hatterSrc is provided, add the hatter package to the collected deps + # so its .a and .conf are available for linking. + hatterDep = if hatterSrc != null then [ nativeHaskellPkgs.hatter ] else []; + # Hatter's own non-boot dependencies — always included so mkIOSLib's # raw GHC invocation can find them even without a consumer cabal file. hatterOwnDeps = [ nativeHaskellPkgs.unwitch ]; in import ./collect-deps.nix { inherit pkgs ghc ghcPkgCmd; - deps = resolvedDeps ++ hatterOwnDeps; + deps = resolvedDeps ++ hatterDep ++ hatterOwnDeps; } diff --git a/nix/ios.nix b/nix/ios.nix index 4c9e8680..dfd50dff 100644 --- a/nix/ios.nix +++ b/nix/ios.nix @@ -14,6 +14,7 @@ let lib = import ./lib.nix { inherit sources; }; iosDeps = import ./ios-deps.nix { inherit sources consumerCabalFile consumerCabal2Nix hpkgs; + hatterSrc = ../.; }; in lib.mkIOSLib { diff --git a/nix/lib.nix b/nix/lib.nix index 25958ea5..2ef2dcc8 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -552,7 +552,7 @@ in { , simulator ? false , pname ? "hatter-ios" , extraModuleCopy ? "" - , crossDeps ? null # output of ios-deps.nix (lib/, hi/, pkgdb/) + , crossDeps ? null # output of ios-deps.nix (lib/, pkgdb/) }: let iosPkgs = import sources.nixpkgs {}; @@ -584,6 +584,46 @@ in { buildInputs = [ libffiStatic gmpStatic ]; buildPhase = '' + ${if crossDeps != null then '' + # Hatter is pre-built in crossDeps — only compile per-app files. + cp ${mainModule} Main.hs + + # run_main.c is not in cabal c-sources (references per-app ZCMain_main_closure) + mkdir -p cbits + cp ${hatterSrc}/cbits/run_main.c cbits/ + + # Extra module copies (consumer overrides) + ${extraModuleCopy} + + ghc -staticlib \ + -O2 \ + -o libHatter.a \ + -I${hatterSrc}/include \ + -package-db ${crossDeps}/pkgdb \ + -optl-lffi \ + -optl-Wl,-u,_haskellRunMain \ + -optl-Wl,-u,_haskellOnLifecycle \ + -optl-Wl,-u,_haskellRenderUI \ + -optl-Wl,-u,_haskellOnUIEvent \ + -optl-Wl,-u,_haskellOnPermissionResult \ + -optl-Wl,-u,_haskellOnSecureStorageResult \ + -optl-Wl,-u,_haskellOnBleScanResult \ + -optl-Wl,-u,_haskellOnDialogResult \ + -optl-Wl,-u,_haskellOnLocationUpdate \ + -optl-Wl,-u,_haskellOnAuthSessionResult \ + -optl-Wl,-u,_haskellOnPlatformSignInResult \ + -optl-Wl,-u,_haskellOnCameraResult \ + -optl-Wl,-u,_haskellOnVideoFrame \ + -optl-Wl,-u,_haskellOnAudioChunk \ + -optl-Wl,-u,_haskellOnBottomSheetResult \ + -optl-Wl,-u,_haskellOnHttpResult \ + -optl-Wl,-u,_haskellOnNetworkStatusChange \ + -optl-Wl,-u,_haskellLogLocale \ + -optl-Wl,-u,_haskellLogDeviceInfo \ + cbits/run_main.c \ + Main.hs + '' else '' + # Standalone build — compile hatter from source. mkdir -p Hatter cp ${hatterSrc}/src/Hatter/Types.hs Hatter/ cp ${hatterSrc}/src/Hatter/Lifecycle.hs Hatter/ @@ -640,7 +680,6 @@ in { -O2 \ -o libHatter.a \ -I${hatterSrc}/include \ - ${if crossDeps != null then "-package-db ${crossDeps}/pkgdb -i${crossDeps}/hi" else ""} \ -optl-lffi \ -optl-Wl,-u,_haskellRunMain \ -optl-Wl,-u,_haskellOnLifecycle \ @@ -682,6 +721,7 @@ in { cbits/device_info.c \ Main.hs \ Hatter.hs + ''} ''; installPhase = '' @@ -790,7 +830,7 @@ open(sys.argv[1], "w").write(yml) , simulator ? false , pname ? "hatter-watchos" , extraModuleCopy ? "" - , crossDeps ? null # output of ios-deps.nix (lib/, hi/, pkgdb/) + , crossDeps ? null # output of ios-deps.nix (lib/, pkgdb/) }: let iosPkgs = import sources.nixpkgs {}; @@ -821,6 +861,46 @@ open(sys.argv[1], "w").write(yml) buildInputs = [ libffiStatic gmpStatic ]; buildPhase = '' + ${if crossDeps != null then '' + # Hatter is pre-built in crossDeps — only compile per-app files. + cp ${mainModule} Main.hs + + # run_main.c is not in cabal c-sources (references per-app ZCMain_main_closure) + mkdir -p cbits + cp ${hatterSrc}/cbits/run_main.c cbits/ + + # Extra module copies (consumer overrides) + ${extraModuleCopy} + + ghc -staticlib \ + -O2 \ + -o libHatter.a \ + -I${hatterSrc}/include \ + -package-db ${crossDeps}/pkgdb \ + -optl-lffi \ + -optl-Wl,-u,_haskellRunMain \ + -optl-Wl,-u,_haskellOnLifecycle \ + -optl-Wl,-u,_haskellRenderUI \ + -optl-Wl,-u,_haskellOnUIEvent \ + -optl-Wl,-u,_haskellOnPermissionResult \ + -optl-Wl,-u,_haskellOnSecureStorageResult \ + -optl-Wl,-u,_haskellOnBleScanResult \ + -optl-Wl,-u,_haskellOnDialogResult \ + -optl-Wl,-u,_haskellOnLocationUpdate \ + -optl-Wl,-u,_haskellOnAuthSessionResult \ + -optl-Wl,-u,_haskellOnPlatformSignInResult \ + -optl-Wl,-u,_haskellOnCameraResult \ + -optl-Wl,-u,_haskellOnVideoFrame \ + -optl-Wl,-u,_haskellOnAudioChunk \ + -optl-Wl,-u,_haskellOnBottomSheetResult \ + -optl-Wl,-u,_haskellOnHttpResult \ + -optl-Wl,-u,_haskellOnNetworkStatusChange \ + -optl-Wl,-u,_haskellLogLocale \ + -optl-Wl,-u,_haskellLogDeviceInfo \ + cbits/run_main.c \ + Main.hs + '' else '' + # Standalone build — compile hatter from source. mkdir -p Hatter cp ${hatterSrc}/src/Hatter/Types.hs Hatter/ cp ${hatterSrc}/src/Hatter/Lifecycle.hs Hatter/ @@ -877,7 +957,6 @@ open(sys.argv[1], "w").write(yml) -O2 \ -o libHatter.a \ -I${hatterSrc}/include \ - ${if crossDeps != null then "-package-db ${crossDeps}/pkgdb -i${crossDeps}/hi" else ""} \ -optl-lffi \ -optl-Wl,-u,_haskellRunMain \ -optl-Wl,-u,_haskellOnLifecycle \ @@ -919,6 +998,7 @@ open(sys.argv[1], "w").write(yml) cbits/device_info.c \ Main.hs \ Hatter.hs + ''} ''; installPhase = '' diff --git a/nix/watchos.nix b/nix/watchos.nix index 26991eb0..ff671a00 100644 --- a/nix/watchos.nix +++ b/nix/watchos.nix @@ -10,6 +10,7 @@ let lib = import ./lib.nix { inherit sources; }; iosDeps = import ./ios-deps.nix { inherit sources consumerCabalFile consumerCabal2Nix hpkgs; + hatterSrc = ../.; }; in lib.mkWatchOSLib {