From a7e850e0282c6a0e9e8815ed99bcff3a3fadd355 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Thu, 2 Apr 2026 17:32:41 -0400 Subject: [PATCH 01/11] feat(nix): native iOS cross-compilation Cross pkgsets for arm64-apple-ios + simulator via mkCrossPkgs, iOS bindings derivations, and Darwin sandbox fixes for transitive deps. --- flake.nix | 69 ++++++++-------- nix/ios-packages.nix | 106 +++++++++++++++++++++++++ nix/lib/base.nix | 16 +++- nix/lib/default.nix | 4 +- nix/lib/patches.nix | 16 ++++ nix/lib/uniffiGenerate.nix | 1 - nix/package/ios-bindings.nix | 55 +++++++++++++ nix/package/mls_validation_service.nix | 8 ++ 8 files changed, 236 insertions(+), 39 deletions(-) create mode 100644 nix/ios-packages.nix create mode 100644 nix/package/ios-bindings.nix diff --git a/flake.nix b/flake.nix index 1b216e1a1d..b85468ac39 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,8 @@ }; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + # nixpkgs.url = "github:insipx/nixpkgs/insipx/ios-build"; + nixpkgs.url = "git+file:///Users/insipx/code/nixpkgs"; fenix = { url = "github:nix-community/fenix"; inputs = { @@ -17,7 +18,10 @@ flake-parts = { url = "github:hercules-ci/flake-parts"; }; - foundry.url = "github:shazow/foundry.nix/stable"; + foundry = { + url = "github:shazow/foundry.nix/stable"; + inputs.nixpkgs.follows = "nixpkgs"; + }; crane = { url = "github:ipetkov/crane"; }; @@ -46,6 +50,7 @@ ./nix/node-packages.nix ./nix/android-packages.nix ./nix/apps.nix + ./nix/ios-packages.nix ]; perSystem = { @@ -83,37 +88,37 @@ ; wasm-bindings = (pkgs.callPackage ./nix/package/wasm.nix { }).bin; wasm-bindings-test = (pkgs.callPackage ./nix/package/wasm.nix { test = true; }).bin; - } - // lib.optionalAttrs pkgs.stdenv.isDarwin { - # stdenvNoCC is passed to callPackage (for the aggregate derivation). - # This avoids Nix's apple-sdk and cc-wrapper, - # which inject -mmacos-version-min flags that - # conflict with iOS cross-compilation. The builds are impure (__noChroot) - # and use the system Xcode SDK directly via ios-env.nix paths. - ios-libs = - (pkgs.callPackage ./nix/package/ios.nix { - stdenv = pkgs.stdenvNoCC; - }).aggregate; - # iOS bindings - simulator + host macOS only (fast dev/CI builds) - ios-libs-fast = - ( - (pkgs.callPackage ./nix/package/ios.nix { - stdenv = pkgs.stdenvNoCC; - }).mkIos - [ - "aarch64-apple-darwin" - "aarch64-apple-ios-sim" - ] - ).aggregate; - ios-xcframeworks = - (pkgs.callPackage ./nix/package/ios.nix { - stdenv = pkgs.stdenvNoCC; - }).release; - ios-xcframeworks-fast = - (pkgs.callPackage ./nix/package/ios.nix { - stdenv = pkgs.stdenvNoCC; - }).devFast; }; + # // lib.optionalAttrs pkgs.stdenv.isDarwin { + # # stdenvNoCC is passed to callPackage (for the aggregate derivation). + # # This avoids Nix's apple-sdk and cc-wrapper, + # # which inject -mmacos-version-min flags that + # # conflict with iOS cross-compilation. The builds are impure (__noChroot) + # # and use the system Xcode SDK directly via ios-env.nix paths. + # ios-libs = + # (pkgs.callPackage ./nix/package/ios.nix { + # stdenv = pkgs.stdenvNoCC; + # }).aggregate; + # # iOS bindings - simulator + host macOS only (fast dev/CI builds) + # ios-libs-fast = + # ( + # (pkgs.callPackage ./nix/package/ios.nix { + # stdenv = pkgs.stdenvNoCC; + # }).mkIos + # [ + # "aarch64-apple-darwin" + # "aarch64-apple-ios-sim" + # ] + # ).aggregate; + # ios-xcframeworks = + # (pkgs.callPackage ./nix/package/ios.nix { + # stdenv = pkgs.stdenvNoCC; + # }).release; + # ios-xcframeworks-fast = + # (pkgs.callPackage ./nix/package/ios.nix { + # stdenv = pkgs.stdenvNoCC; + # }).devFast; + # }; }; }; } diff --git a/nix/ios-packages.nix b/nix/ios-packages.nix new file mode 100644 index 0000000000..fc9af6538b --- /dev/null +++ b/nix/ios-packages.nix @@ -0,0 +1,106 @@ +{ self, ... }: +{ + perSystem = + { + pkgs, + system, + lib, + ... + }: + let + # Single source of truth: ABI name → target config + iosTargets = { + "aarch64-darwin" = { + config = "arm64-apple-darwin"; + }; + "x86_64-darwin" = { + config = "x86_64-apple-darwin"; + }; + "iphone64" = { + config = "arm64-apple-ios"; + darwinSdkVersion = "26.3"; + darwinMinVersion = "16"; + xcodeVer = "26.3"; + }; + "iphone64-simulator" = { + config = "aarch64-apple-ios-simulator"; + darwinSdkVersion = "26"; + darwinMinVersion = "16"; + xcodeVer = "26.3"; + }; + # "aarch64-ios-macabi" = { + # config = "arm64-apple-darwin"; + # rust.rustcTarget = "aarch64-apple-ios-macabi"; + # }; + # "x86_64-ios-macabi" = { + # config = "x86_64-apple-darwin"; + # rust.rustcTarget = "x86_64-apple-ios-macabi"; + # }; + }; + + # crossPkgs keyed by ABI name (via the `name` field passed to mkCrossPkgs) + crossPkgs = self.lib.mkCrossPkgs system ( + lib.mapAttrsToList (abi: t: t // { name = abi; }) iosTargets + ); + mkIosBindings = p: p.callPackage ./package/ios-bindings.nix; + + # Per-target dylibs keyed by ABI name + iosDylibs = lib.mapAttrs (_: p: (mkIosBindings p { }).dylib) crossPkgs; + + # Swift bindings from host build + inherit (mkIosBindings pkgs { }) swift-bindings; + + fastAbi = + if pkgs.stdenv.hostPlatform.isx86_64 then + "x86_64-darwin" + else if pkgs.stdenv.hostPlatform.isAarch64 then + "aarch64-darwin" + else + throw "Unsupported host architecture for ios-libs-fast"; + + fastDylib = iosDylibs.${fastAbi}; + + ios-libs-fast = pkgs.linkFarm "xmtpv3-ios-fast" [ + { + name = "jniLibs/${fastAbi}/libuniffi_xmtpv3.so"; + path = "${fastDylib}/libuniffi_xmtpv3.so"; + } + { + name = "java/uniffi/xmtpv3/xmtpv3.kt"; + path = "${swift-bindings}/kotlin/uniffi/xmtpv3/xmtpv3.kt"; + } + { + name = "libxmtp-version.txt"; + path = "${swift-bindings}/libxmtp-version.txt"; + } + ]; + + # Aggregate all targets + Swift bindings into a Gradle-ready layout + ios-libs = pkgs.linkFarm "xmtpv3-ios" ( + lib.mapAttrsToList (abi: dylib: { + name = "jniLibs/${abi}/libuniffi_xmtpv3.so"; + path = "${dylib}/libuniffi_xmtpv3.so"; + }) iosDylibs + ++ [ + { + name = "java/uniffi/xmtpv3/xmtpv3.kt"; + path = "${swift-bindings}/kotlin/uniffi/xmtpv3/xmtpv3.kt"; + } + { + name = "libxmtp-version.txt"; + path = "${swift-bindings}/libxmtp-version.txt"; + } + ] + ); + in + { + packages = { + inherit ios-libs ios-libs-fast swift-bindings; + + } + // lib.mapAttrs' (abi: dylib: { + name = "ios-bindings-${abi}"; + value = dylib; + }) iosDylibs; + }; +} diff --git a/nix/lib/base.nix b/nix/lib/base.nix index 8d2f6223c8..2db14d295a 100644 --- a/nix/lib/base.nix +++ b/nix/lib/base.nix @@ -5,7 +5,7 @@ xmtp, zstd, openssl, - sqlite, + sqlcipher, pkg-config, darwin, stdenv, @@ -52,9 +52,8 @@ let buildInputs = [ zstd openssl - sqlite - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ darwin.libiconv ]; + sqlcipher + ]; doCheck = false; # Disable zerocallusedregs hardening which can cause issues with cross-compilation. @@ -71,6 +70,15 @@ let "AWS_LC_SYS_TARGET_CC_${buildPlatformSuffix}" = "cc"; "AWS_LC_SYS_TARGET_CXX_${buildPlatformSuffix}" = "c++"; + # Tell openssl-sys to use the pre-built nixpkgs openssl instead of + # vendoring its own. Without this, openssl-sys falls back to building + # OpenSSL from source via openssl-src, which calls `xcrun` to find a + # compiler and fails in cross-compilation environments. + OPENSSL_NO_VENDOR = "1"; + OPENSSL_DIR = "${openssl.dev}"; + OPENSSL_LIB_DIR = "${openssl.out}/lib"; + OPENSSL_INCLUDE_DIR = "${openssl.dev}/include"; + }; # Make cargo artifacts for a derivation building rust code diff --git a/nix/lib/default.nix b/nix/lib/default.nix index a085b7f956..9c573bb4fb 100644 --- a/nix/lib/default.nix +++ b/nix/lib/default.nix @@ -107,11 +107,11 @@ in t = normalize target; in { - name = t.config; + name = t.name or t.config; value = import inputs.nixpkgs { inherit config overlays; localSystem = system; - crossSystem = t; + crossSystem = removeAttrs t [ "name" ]; }; } ) targets diff --git a/nix/lib/patches.nix b/nix/lib/patches.nix index d35aa96006..1c5a939522 100644 --- a/nix/lib/patches.nix +++ b/nix/lib/patches.nix @@ -47,4 +47,20 @@ } ) + # harfbuzz's test-coretext requires access to macOS font services + # (CTFontManagerRegisterFontsForURLs) which are unavailable in the + # Nix sandbox, causing SIGABRT. Disable the test suite entirely on + # Darwin via the meson 'tests' option. + # + # Pulled into the build graph via: + # remarshal → rich-argparse → rich → pytest-regressions → matplotlib + # → ffmpeg-headless → harfbuzz + ( + final: prev: + prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { + harfbuzz = prev.harfbuzz.overrideAttrs (old: { + mesonFlags = (old.mesonFlags or [ ]) ++ [ "-Dtests=disabled" ]; + }); + } + ) ] diff --git a/nix/lib/uniffiGenerate.nix b/nix/lib/uniffiGenerate.nix index d4a70bf385..d75659f98e 100644 --- a/nix/lib/uniffiGenerate.nix +++ b/nix/lib/uniffiGenerate.nix @@ -31,7 +31,6 @@ stdenv.mkDerivation ( // rec { # Additional overrides we want to explicitly set in this helper # Require the caller to specify cargoArtifacts we can use - pname = "uniffi-bindgen"; dontUnpack = true; # Set the cargo command we will use and pass through the flags buildPhase = diff --git a/nix/package/ios-bindings.nix b/nix/package/ios-bindings.nix new file mode 100644 index 0000000000..32666ca5be --- /dev/null +++ b/nix/package/ios-bindings.nix @@ -0,0 +1,55 @@ +{ + xmtp, + stdenv, + ... +}: +let + inherit (xmtp) base craneLib; + hostPlatform = stdenv.hostPlatform.rust.rustcTarget; + rust-toolchain = p: xmtp.mkToolchain p [ stdenv.hostPlatform.rust.rustcTarget ] [ ]; + rust = craneLib.overrideToolchain rust-toolchain; + version = xmtp.mkVersion rust; + inherit (base) commonArgs bindingsFileset; + + cargoArtifacts = xmtp.base.mkCargoArtifacts rust false null; + + dylib = rust.buildPackage ( + commonArgs + // { + inherit cargoArtifacts version; + pname = "xmtpv3-${hostPlatform}"; + doInstallCargoArtifacts = false; + src = bindingsFileset; + cargoExtraArgs = "-p xmtpv3"; + postFixup = '' + mkdir -p $out + cp $out/lib/libxmtpv3.a $out/libxmtpv3.a + cp $out/lib/libxmtpv3.dylib $out/libxmtpv3.dylib + install_name_tool -id @rpath/libxmtpv3.dylib $out/libxmtpv3.dylib + ''; + } + ); + + swift-bindings = rust.uniffiGenerate { + inherit version; + pname = "xmtpv3-swift"; + language = "swift"; + dylibPath = "${dylib}/libxmtpv3.a"; + doInstallCargoArtifacts = false; + postFixup = '' + mkdir -p $out/swift/include/libxmtp + ls $out/swift + # Organize into expected directory structure for xcframework assembly + cp $out/swift/uniffi/xmtpv3/xmtpv3.swift $out/swift/ + mv $out/swift/uniffi/xmtpv3/xmtpv3FFI.h $out/swift/include/libxmtp/ + mv $out/swift/uniffi/xmtpv3/xmtpv3FFI.modulemap $out/swift/include/libxmtp/module.modulemap + + # Generate version file + echo "Version: ${version}" > $out/libxmtp-version.txt + echo "Date: $(date -u +%Y-%m-%d)" >> $out/libxmtp-version.txt + ''; + }; +in +{ + inherit swift-bindings dylib; +} diff --git a/nix/package/mls_validation_service.nix b/nix/package/mls_validation_service.nix index 9f0ef8f0a4..20b404a933 100644 --- a/nix/package/mls_validation_service.nix +++ b/nix/package/mls_validation_service.nix @@ -2,6 +2,8 @@ xmtp, lib, stdenv, + openssl, + zstd, }: let inherit (lib.fileset) unions; @@ -14,6 +16,12 @@ let specialArgs = lib.optionalAttrs stdenv.hostPlatform.isMusl { RUSTFLAGS = "-C target-feature=+crt-static"; + # mls validation srevice does not need sqlite since it does not + # depend on xmtp_db + buildInputs = [ + openssl + zstd + ]; }; commonArgs = base.commonArgs // specialArgs; From 0de36f30d7f7c60246f502ba821602186bb9b1f4 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Sat, 18 Apr 2026 19:39:46 -0400 Subject: [PATCH 02/11] feat(ios): enable useiOSPrebuilt + add dev/ios-verify Wrap the builder-host Xcode (~94 drvs vs ~2500 source-built); dedupe target config into iosCommon; ios-verify smoke-checks xcframeworks. --- .gitignore | 1 + flake.nix | 5 ++- nix/ios-packages.nix | 82 ++++++++++++++++++------------------ nix/lib/base.nix | 19 ++++----- nix/lib/patches.nix | 46 ++++++++++++++++++++ nix/package/ios-bindings.nix | 8 ++-- 6 files changed, 104 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index c0596b18dc..cec0fb54ba 100644 --- a/.gitignore +++ b/.gitignore @@ -209,3 +209,4 @@ docs/plans # Claude Code .claude/scheduled_tasks.lock /target-linux-amd64 +nixpkgs diff --git a/flake.nix b/flake.nix index b85468ac39..21167c6563 100644 --- a/flake.nix +++ b/flake.nix @@ -7,8 +7,11 @@ }; inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # nixpkgs.url = "github:insipx/nixpkgs/insipx/ios-build"; - nixpkgs.url = "git+file:///Users/insipx/code/nixpkgs"; + # nixpkgs.url = "git+file:///Users/insipx/code/nixpkgs"; + # nixpkgs.url = "git+file:///home/insipx/code/nixpkgs"; + # nixpkgs.url = "git+file:///workspace/nixpkgs?ref=insipx/ios-build"; fenix = { url = "github:nix-community/fenix"; inputs = { diff --git a/nix/ios-packages.nix b/nix/ios-packages.nix index fc9af6538b..5d096aeb9a 100644 --- a/nix/ios-packages.nix +++ b/nix/ios-packages.nix @@ -8,6 +8,12 @@ ... }: let + iosCommon = { + darwinSdkVersion = "26"; + darwinMinVersion = "16"; + xcodeVer = "26.3"; + useiOSPrebuilt = true; # source path — route through apple-sdk / propagate-inputs + }; # Single source of truth: ABI name → target config iosTargets = { "aarch64-darwin" = { @@ -18,16 +24,12 @@ }; "iphone64" = { config = "arm64-apple-ios"; - darwinSdkVersion = "26.3"; - darwinMinVersion = "16"; - xcodeVer = "26.3"; - }; + } + // iosCommon; "iphone64-simulator" = { config = "aarch64-apple-ios-simulator"; - darwinSdkVersion = "26"; - darwinMinVersion = "16"; - xcodeVer = "26.3"; - }; + } + // iosCommon; # "aarch64-ios-macabi" = { # config = "arm64-apple-darwin"; # rust.rustcTarget = "aarch64-apple-ios-macabi"; @@ -58,40 +60,38 @@ else throw "Unsupported host architecture for ios-libs-fast"; - fastDylib = iosDylibs.${fastAbi}; + # Build an xcframework-ready linkFarm from the given ABIs. + mkLibs = + name: abis: + pkgs.linkFarm name ( + lib.concatMap (abi: [ + { + name = "${abi}/libxmtpv3.a"; + path = "${iosDylibs.${abi}}/libxmtpv3.a"; + } + { + name = "${abi}/libxmtpv3.dylib"; + path = "${iosDylibs.${abi}}/libxmtpv3.dylib"; + } + ]) abis + ++ [ + { + name = "swift/xmtpv3.swift"; + path = "${swift-bindings}/swift/xmtpv3.swift"; + } + { + name = "swift/include"; + path = "${swift-bindings}/swift/include"; + } + { + name = "libxmtp-version.txt"; + path = "${swift-bindings}/libxmtp-version.txt"; + } + ] + ); - ios-libs-fast = pkgs.linkFarm "xmtpv3-ios-fast" [ - { - name = "jniLibs/${fastAbi}/libuniffi_xmtpv3.so"; - path = "${fastDylib}/libuniffi_xmtpv3.so"; - } - { - name = "java/uniffi/xmtpv3/xmtpv3.kt"; - path = "${swift-bindings}/kotlin/uniffi/xmtpv3/xmtpv3.kt"; - } - { - name = "libxmtp-version.txt"; - path = "${swift-bindings}/libxmtp-version.txt"; - } - ]; - - # Aggregate all targets + Swift bindings into a Gradle-ready layout - ios-libs = pkgs.linkFarm "xmtpv3-ios" ( - lib.mapAttrsToList (abi: dylib: { - name = "jniLibs/${abi}/libuniffi_xmtpv3.so"; - path = "${dylib}/libuniffi_xmtpv3.so"; - }) iosDylibs - ++ [ - { - name = "java/uniffi/xmtpv3/xmtpv3.kt"; - path = "${swift-bindings}/kotlin/uniffi/xmtpv3/xmtpv3.kt"; - } - { - name = "libxmtp-version.txt"; - path = "${swift-bindings}/libxmtp-version.txt"; - } - ] - ); + ios-libs-fast = mkLibs "xmtpv3-ios-fast" [ fastAbi ]; + ios-libs = mkLibs "xmtpv3-ios" (lib.attrNames iosDylibs); in { packages = { diff --git a/nix/lib/base.nix b/nix/lib/base.nix index 2db14d295a..63645a2379 100644 --- a/nix/lib/base.nix +++ b/nix/lib/base.nix @@ -12,6 +12,8 @@ zlib, pkgsBuildHost, perl, + apple-sdk ? null, + libiconv, }: let # Narrow fileset for buildDepsOnly — only Cargo.toml, Cargo.lock, build.rs, @@ -39,21 +41,21 @@ let # Platform-specific args (like ANDROID_HOME or __noChroot) are added by each derivation. commonArgs = { src = depsFileset; - # strictDeps=true breaks darwin build with ring - strictDeps = if stdenv.buildPlatform.isDarwin then false else true; + strictDeps = true; # these inputs do not get cross compiled nativeBuildInputs = [ pkg-config perl zlib ] - ++ lib.optionals stdenv.buildPlatform.isDarwin [ darwin.libiconv ]; - # these inputs do get cross compiled + ++ lib.optionals stdenv.buildPlatform.isDarwin [ libiconv ]; + buildInputs = [ zstd openssl sqlcipher - ]; + ] + ++ lib.optionals stdenv.buildPlatform.isDarwin [ libiconv ]; doCheck = false; # Disable zerocallusedregs hardening which can cause issues with cross-compilation. @@ -70,15 +72,12 @@ let "AWS_LC_SYS_TARGET_CC_${buildPlatformSuffix}" = "cc"; "AWS_LC_SYS_TARGET_CXX_${buildPlatformSuffix}" = "c++"; - # Tell openssl-sys to use the pre-built nixpkgs openssl instead of - # vendoring its own. Without this, openssl-sys falls back to building - # OpenSSL from source via openssl-src, which calls `xcrun` to find a - # compiler and fails in cross-compilation environments. + # Use nixpkgs' pre-built openssl instead of letting openssl-sys vendor and build from source + # (which calls xcrun for the SDK and fails in cross sandboxes). OPENSSL_NO_VENDOR = "1"; OPENSSL_DIR = "${openssl.dev}"; OPENSSL_LIB_DIR = "${openssl.out}/lib"; OPENSSL_INCLUDE_DIR = "${openssl.dev}/include"; - }; # Make cargo artifacts for a derivation building rust code diff --git a/nix/lib/patches.nix b/nix/lib/patches.nix index 1c5a939522..2d32f5a4ec 100644 --- a/nix/lib/patches.nix +++ b/nix/lib/patches.nix @@ -63,4 +63,50 @@ }); } ) + # zvbi's configure auto-detects X11 and then compiles ntsc-cc.c which + # #includes ; in the Nix Darwin sandbox the probed + # /usr/X11/include has no headers, so the compile fails. Force + # --without-x — libxmtp doesn't need the X utilities. + # + # Pulled in via the same remarshal → matplotlib → ffmpeg-headless chain. + ( + final: prev: + prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { + zvbi = prev.zvbi.overrideAttrs (old: { + configureFlags = (old.configureFlags or [ ]) ++ [ "--without-x" ]; + }); + } + ) + # rsync's 'hardlinks' check-phase test is flaky in the Darwin Nix sandbox + # (macOS filesystem semantics don't match rsync's expected linkage + # behavior). Disable checks on Darwin — not needed by libxmtp. + ( + final: prev: + prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { + rsync = prev.rsync.overrideAttrs (_: { + doCheck = false; + }); + } + ) + # LLVM 21.1.8's BPF BTF tests (func-func-ptr.ll, func-typedef.ll) + # have stale expected output: a 2025-10-20 change to BTF generation + # (llvm/llvm-project#155783, DW_TAG_variant_part support) changed + # emitted offsets, but the test .ll files weren't regenerated. + # FileCheck fails on hardcoded `.long` sequences. Not a miscompile — + # only test-metadata drift, and the BPF backend is unreachable from + # iOS cross-compilation anyway. Delete the two stale tests via + # postPatch so `check-all` passes. + ( + final: prev: + prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { + llvmPackages_21 = prev.llvmPackages_21 // { + libllvm = prev.llvmPackages_21.libllvm.overrideAttrs (old: { + postPatch = (old.postPatch or "") + '' + rm test/CodeGen/BPF/BTF/func-func-ptr.ll + rm test/CodeGen/BPF/BTF/func-typedef.ll + ''; + }); + }; + } + ) ] diff --git a/nix/package/ios-bindings.nix b/nix/package/ios-bindings.nix index 32666ca5be..55b6d6bae4 100644 --- a/nix/package/ios-bindings.nix +++ b/nix/package/ios-bindings.nix @@ -5,7 +5,6 @@ }: let inherit (xmtp) base craneLib; - hostPlatform = stdenv.hostPlatform.rust.rustcTarget; rust-toolchain = p: xmtp.mkToolchain p [ stdenv.hostPlatform.rust.rustcTarget ] [ ]; rust = craneLib.overrideToolchain rust-toolchain; version = xmtp.mkVersion rust; @@ -17,7 +16,7 @@ let commonArgs // { inherit cargoArtifacts version; - pname = "xmtpv3-${hostPlatform}"; + pname = "xmtpv3"; doInstallCargoArtifacts = false; src = bindingsFileset; cargoExtraArgs = "-p xmtpv3"; @@ -40,9 +39,8 @@ let mkdir -p $out/swift/include/libxmtp ls $out/swift # Organize into expected directory structure for xcframework assembly - cp $out/swift/uniffi/xmtpv3/xmtpv3.swift $out/swift/ - mv $out/swift/uniffi/xmtpv3/xmtpv3FFI.h $out/swift/include/libxmtp/ - mv $out/swift/uniffi/xmtpv3/xmtpv3FFI.modulemap $out/swift/include/libxmtp/module.modulemap + mv $out/swift/xmtpv3FFI.h $out/swift/include/libxmtp/ + mv $out/swift/xmtpv3FFI.modulemap $out/swift/include/libxmtp/module.modulemap # Generate version file echo "Version: ${version}" > $out/libxmtp-version.txt From 92d924eea265dd128a1821bca7416df922d90e37 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Wed, 6 May 2026 15:44:48 -0400 Subject: [PATCH 03/11] feat(ios): hermetic xcframework builds xcode-tools bundle + ios-xcframework module (static, dynamic, release, dev outputs); no __noChroot. --- nix/ios-packages.nix | 68 ++- nix/lib/ios-env.nix | 2 +- nix/lib/packages/xcode-tools.nix | 35 ++ nix/package/ios-bindings.nix | 2 +- nix/package/ios-xcframework/default.nix | 24 + nix/package/ios-xcframework/helpers.nix | 78 ++++ nix/package/ios-xcframework/mk-dev.nix | 24 + nix/package/ios-xcframework/mk-dynamic.nix | 107 +++++ nix/package/ios-xcframework/mk-release.nix | 27 ++ nix/package/ios-xcframework/mk-static.nix | 76 ++++ nix/package/ios.nix | 494 --------------------- nix/shells/ios.nix | 4 - sdks/ios/dev/bindings | 4 +- 13 files changed, 437 insertions(+), 508 deletions(-) create mode 100644 nix/lib/packages/xcode-tools.nix create mode 100644 nix/package/ios-xcframework/default.nix create mode 100644 nix/package/ios-xcframework/helpers.nix create mode 100644 nix/package/ios-xcframework/mk-dev.nix create mode 100644 nix/package/ios-xcframework/mk-dynamic.nix create mode 100644 nix/package/ios-xcframework/mk-release.nix create mode 100644 nix/package/ios-xcframework/mk-static.nix delete mode 100644 nix/package/ios.nix diff --git a/nix/ios-packages.nix b/nix/ios-packages.nix index 5d096aeb9a..e57ce4fb1b 100644 --- a/nix/ios-packages.nix +++ b/nix/ios-packages.nix @@ -12,15 +12,20 @@ darwinSdkVersion = "26"; darwinMinVersion = "16"; xcodeVer = "26.3"; - useiOSPrebuilt = true; # source path — route through apple-sdk / propagate-inputs + # Wrap builder-host Xcode (iosSdkPkgs) instead of building the Darwin + # toolchain from source (~94 drvs vs ~2500). Comment out to fall back + # to the hermetic apple-sdk source path. + useiOSPrebuilt = true; }; # Single source of truth: ABI name → target config iosTargets = { "aarch64-darwin" = { config = "arm64-apple-darwin"; + xcodeVer = "26.3"; }; "x86_64-darwin" = { config = "x86_64-apple-darwin"; + xcodeVer = "26.3"; }; "iphone64" = { config = "arm64-apple-ios"; @@ -49,8 +54,7 @@ # Per-target dylibs keyed by ABI name iosDylibs = lib.mapAttrs (_: p: (mkIosBindings p { }).dylib) crossPkgs; - # Swift bindings from host build - inherit (mkIosBindings pkgs { }) swift-bindings; + inherit (mkIosBindings pkgs { }) swift-bindings version; fastAbi = if pkgs.stdenv.hostPlatform.isx86_64 then @@ -60,6 +64,51 @@ else throw "Unsupported host architecture for ios-libs-fast"; + # xcframework derivations are host-side packaging tools — call them through + # the local cross-pkgs so darwin.xcode auto-resolves via crossSystem.xcodeVer. + hostPkgs = crossPkgs.${fastAbi}; + xcode-tools = hostPkgs.callPackage ./lib/packages/xcode-tools.nix { }; + xcframework = hostPkgs.callPackage ./package/ios-xcframework { inherit xcode-tools; }; + + allAbis = lib.attrNames iosDylibs; + fastAbis = [ + fastAbi + "iphone64-simulator" + ]; + + ios-xcframework-static = xcframework.mkStatic { + abis = allAbis; + dylibs = iosDylibs; + swiftBindings = swift-bindings; + inherit version; + }; + ios-xcframework-dynamic = xcframework.mkDynamic { + abis = allAbis; + dylibs = iosDylibs; + swiftBindings = swift-bindings; + inherit version; + }; + ios-xcframework-static-fast = xcframework.mkStatic { + abis = fastAbis; + dylibs = iosDylibs; + swiftBindings = swift-bindings; + inherit version; + }; + + ios-release = xcframework.mkRelease { + static = ios-xcframework-static; + dynamic = ios-xcframework-dynamic; + swiftBindings = swift-bindings; + licenseFile = ../LICENSE; + inherit version; + }; + ios-devFast = xcframework.mkDev { + static = ios-xcframework-static-fast; + dynamic = null; + swiftBindings = swift-bindings; + inherit version; + }; + # Build an xcframework-ready linkFarm from the given ABIs. mkLibs = name: abis: @@ -91,12 +140,19 @@ ); ios-libs-fast = mkLibs "xmtpv3-ios-fast" [ fastAbi ]; - ios-libs = mkLibs "xmtpv3-ios" (lib.attrNames iosDylibs); + ios-libs = mkLibs "xmtpv3-ios" allAbis; in { packages = { - inherit ios-libs ios-libs-fast swift-bindings; - + inherit + ios-libs + ios-libs-fast + swift-bindings + ios-xcframework-static + ios-xcframework-dynamic + ios-release + ios-devFast + ; } // lib.mapAttrs' (abi: dylib: { name = "ios-bindings-${abi}"; diff --git a/nix/lib/ios-env.nix b/nix/lib/ios-env.nix index 2969591576..b8c3ad06b2 100644 --- a/nix/lib/ios-env.nix +++ b/nix/lib/ios-env.nix @@ -1,5 +1,5 @@ # Shared iOS cross-compilation environment configuration. -# Used by both nix/ios.nix (dev shell) and nix/package/ios.nix (build derivation). +# Used by the iOS dev shell (nix/shells/ios.nix). # # All Xcode paths are resolved dynamically at shell time via /usr/bin/xcode-select. # This ensures CI runners using setup-xcode (which installs to versioned paths like diff --git a/nix/lib/packages/xcode-tools.nix b/nix/lib/packages/xcode-tools.nix new file mode 100644 index 0000000000..8d8cadd3a4 --- /dev/null +++ b/nix/lib/packages/xcode-tools.nix @@ -0,0 +1,35 @@ +# Apple toolchain bundle (Xcode + cctools + rcodesign) for hermetic +# xcframework assembly — everything `xcodebuild -create-xcframework` and the +# .framework bundling in nix/package/ios-xcframework need on one PATH. +# +# Must be called through a cross-pkgs whose system config sets xcodeVer +# (ios-packages.nix pins 26.3); otherwise darwin.xcode falls back to the +# nixpkgs default xcode_12_3 and downstream builds break. +{ + lib, + runCommand, + symlinkJoin, + cctools, + rcodesign, + darwin, +}: +let + # Xcode binaries live under Contents/Developer/usr/bin/, not bin/ — lift them + # into a flat bin/ so the standard PATH hook picks them up. + xcode-bins = runCommand "xcode-bins" { } '' + mkdir -p $out/bin + ln -s ${darwin.xcode}/Contents/Developer/usr/bin/* $out/bin/ + ''; +in +symlinkJoin { + name = "xcode-tools"; + paths = [ + cctools + rcodesign + xcode-bins + ]; + meta = { + description = "Apple toolchain (Xcode + cctools + rcodesign) for hermetic xcframework builds"; + platforms = lib.platforms.darwin; + }; +} diff --git a/nix/package/ios-bindings.nix b/nix/package/ios-bindings.nix index 55b6d6bae4..6d4892bd99 100644 --- a/nix/package/ios-bindings.nix +++ b/nix/package/ios-bindings.nix @@ -49,5 +49,5 @@ let }; in { - inherit swift-bindings dylib; + inherit swift-bindings dylib version; } diff --git a/nix/package/ios-xcframework/default.nix b/nix/package/ios-xcframework/default.nix new file mode 100644 index 0000000000..d13c2be2df --- /dev/null +++ b/nix/package/ios-xcframework/default.nix @@ -0,0 +1,24 @@ +# iOS xcframework assembly — static, dynamic, release, and dev outputs. +# +# Hermetic: no __noChroot. mk-static and mk-dynamic depend on xcode-tools +# (Xcode + cctools + rcodesign); xcode-tools is passed in via callPackage from +# ios-packages.nix using a cross-pkgs whose system config sets xcodeVer. +# See docs/specs/2026-05-06-ios-xcframework-redesign.md. +{ + lib, + pkgs, + callPackage, + xcode-tools, +}: +let + helpers = callPackage ./helpers.nix { }; + # Inject xcode-tools and helpers into the callPackage scope so each mk-*.nix + # can request them by name without explicit `inherit` plumbing at every call. + callPackage' = lib.callPackageWith (pkgs // { inherit xcode-tools helpers; }); +in +{ + mkStatic = callPackage' ./mk-static.nix { }; + mkDynamic = callPackage' ./mk-dynamic.nix { }; + mkRelease = callPackage' ./mk-release.nix { }; + mkDev = callPackage' ./mk-dev.nix { }; +} diff --git a/nix/package/ios-xcframework/helpers.nix b/nix/package/ios-xcframework/helpers.nix new file mode 100644 index 0000000000..ec87f03e74 --- /dev/null +++ b/nix/package/ios-xcframework/helpers.nix @@ -0,0 +1,78 @@ +{ lib, writeText }: +rec { + # Classify ABI keys into platform groups for xcframework slicing. + # Each non-empty group becomes one slice in the produced xcframework. + classifyTargets = + abis: + let + deviceAbis = builtins.filter (a: a == "iphone64") abis; + simAbis = builtins.filter (a: a == "iphone64-simulator") abis; + macAbis = builtins.filter (a: lib.hasSuffix "-darwin" a) abis; + in + { + inherit deviceAbis simAbis macAbis; + expectedSlices = lib.count (g: g != [ ]) [ + deviceAbis + simAbis + macAbis + ]; + }; + + # Lipo a list of per-ABI artifacts (.a or .dylib) into a single fat binary. + # Emits empty shell when group is empty. + mkLipoSnippet = + { + group, + ext, + dylibs, + abis, + }: + lib.optionalString (abis != [ ]) '' + mkdir -p $TMPDIR/lipo_${group} + lipo -create \ + ${lib.concatMapStringsSep " " (abi: "${dylibs.${abi}}/libxmtpv3.${ext}") abis} \ + -output $TMPDIR/lipo_${group}/libxmtpv3.${ext} + ''; + + # Build the framework's Info.plist as a structural Nix attrset, rendered via + # lib.generators.toPlist. Keeps escaping and formatting consistent with nixpkgs + # plist conventions instead of an inline heredoc. + mkInfoPlist = + minOSVersion: + writeText "Info.plist" ( + lib.generators.toPlist { escape = true; } { + CFBundleExecutable = "xmtpv3FFI"; + CFBundleIdentifier = "org.xmtp.xmtpv3FFI"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundleName = "xmtpv3FFI"; + CFBundlePackageType = "FMWK"; + CFBundleVersion = "1"; + CFBundleShortVersionString = "1.0"; + MinimumOSVersion = minOSVersion; + } + ); + + # Wrap a (possibly lipo'd) dylib in a .framework bundle for + # xcodebuild -create-xcframework -framework . + mkFrameworkBundle = + { + name, + dylibPath, + minOSVersion, + headerDir, + }: + '' + echo "Building framework bundle: ${name}" + mkdir -p $TMPDIR/${name}/xmtpv3FFI.framework/Headers + mkdir -p $TMPDIR/${name}/xmtpv3FFI.framework/Modules + cp ${dylibPath} $TMPDIR/${name}/xmtpv3FFI.framework/xmtpv3FFI + install_name_tool -id @rpath/xmtpv3FFI.framework/xmtpv3FFI \ + $TMPDIR/${name}/xmtpv3FFI.framework/xmtpv3FFI + cp ${headerDir}/*.h $TMPDIR/${name}/xmtpv3FFI.framework/Headers/ + sed 's/^module /framework module /' \ + ${headerDir}/module.modulemap \ + > $TMPDIR/${name}/xmtpv3FFI.framework/Modules/module.modulemap + cp ${mkInfoPlist minOSVersion} $TMPDIR/${name}/xmtpv3FFI.framework/Info.plist + rcodesign sign $TMPDIR/${name}/xmtpv3FFI.framework + ''; +} diff --git a/nix/package/ios-xcframework/mk-dev.nix b/nix/package/ios-xcframework/mk-dev.nix new file mode 100644 index 0000000000..0e0db72e78 --- /dev/null +++ b/nix/package/ios-xcframework/mk-dev.nix @@ -0,0 +1,24 @@ +{ + lib, + stdenv, +}: +{ + static, + dynamic ? null, + swiftBindings, + version, +}: +stdenv.mkDerivation { + pname = "xmtpv3-ios-xcframeworks-dev"; + inherit version; + dontUnpack = true; + dontFixup = true; + installPhase = '' + mkdir -p $out + cp -r ${static}/LibXMTPSwiftFFI.xcframework $out/ + ${lib.optionalString (dynamic != null) '' + cp -r ${dynamic}/LibXMTPSwiftFFIDynamic.xcframework $out/ + ''} + cp ${swiftBindings}/swift/xmtpv3.swift $out/xmtpv3.swift + ''; +} diff --git a/nix/package/ios-xcframework/mk-dynamic.nix b/nix/package/ios-xcframework/mk-dynamic.nix new file mode 100644 index 0000000000..0bd3c77ae4 --- /dev/null +++ b/nix/package/ios-xcframework/mk-dynamic.nix @@ -0,0 +1,107 @@ +{ + lib, + stdenv, + xcode-tools, + helpers, +}: +{ + abis, + dylibs, + swiftBindings, + version, +}: +let + inherit (helpers.classifyTargets abis) + deviceAbis + simAbis + macAbis + expectedSlices + ; + headerDir = "${swiftBindings}/swift/include/libxmtp"; +in +stdenv.mkDerivation { + pname = "xmtpv3-dynamic-xcframework"; + inherit version; + dontUnpack = true; + dontFixup = true; + nativeBuildInputs = [ xcode-tools ]; + installPhase = '' + set -euo pipefail + echo "=== Building dynamic xcframework ===" + + ${helpers.mkLipoSnippet { + group = "sim"; + ext = "dylib"; + inherit dylibs; + abis = simAbis; + }} + ${helpers.mkLipoSnippet { + group = "macos"; + ext = "dylib"; + inherit dylibs; + abis = macAbis; + }} + + ${lib.optionalString (deviceAbis != [ ]) ( + helpers.mkFrameworkBundle { + name = "fw_ios"; + dylibPath = "${dylibs.iphone64}/libxmtpv3.dylib"; + minOSVersion = "14.0"; + inherit headerDir; + } + )} + ${lib.optionalString (simAbis != [ ]) ( + helpers.mkFrameworkBundle { + name = "fw_sim"; + dylibPath = "$TMPDIR/lipo_sim/libxmtpv3.dylib"; + minOSVersion = "14.0"; + inherit headerDir; + } + )} + ${lib.optionalString (macAbis != [ ]) ( + helpers.mkFrameworkBundle { + name = "fw_mac"; + dylibPath = "$TMPDIR/lipo_macos/libxmtpv3.dylib"; + minOSVersion = "11.0"; + inherit headerDir; + } + )} + + XCF_ARGS="" + ${lib.optionalString (deviceAbis != [ ]) '' + XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_ios/xmtpv3FFI.framework" + ''} + ${lib.optionalString (simAbis != [ ]) '' + XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_sim/xmtpv3FFI.framework" + ''} + ${lib.optionalString (macAbis != [ ]) '' + XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_mac/xmtpv3FFI.framework" + ''} + + mkdir -p $out + xcodebuild -create-xcframework \ + $XCF_ARGS \ + -output $out/LibXMTPSwiftFFIDynamic.xcframework + + echo "Validating dynamic xcframework..." + FOUND=0 + for fw in $out/LibXMTPSwiftFFIDynamic.xcframework/*/xmtpv3FFI.framework; do + test -d "$fw" || continue + FOUND=$((FOUND + 1)) + test -f "$fw/xmtpv3FFI" || { echo "FAIL: Missing binary in $fw"; exit 1; } + test -f "$fw/Info.plist" || { echo "FAIL: Missing Info.plist in $fw"; exit 1; } + test -d "$fw/Headers" || { echo "FAIL: Missing Headers in $fw"; exit 1; } + test -f "$fw/Headers/xmtpv3FFI.h" || { echo "FAIL: Missing xmtpv3FFI.h in $fw"; exit 1; } + test -f "$fw/Modules/module.modulemap" || { echo "FAIL: Missing modulemap in $fw"; exit 1; } + head -1 "$fw/Modules/module.modulemap" | grep -q "^framework module xmtpv3FFI" || \ + { echo "FAIL: modulemap missing 'framework module' prefix in $fw"; exit 1; } + INSTALL_NAME=$(otool -D "$fw/xmtpv3FFI" | tail -1) + echo "$INSTALL_NAME" | grep -q "@rpath/xmtpv3FFI.framework/xmtpv3FFI" || \ + { echo "FAIL: Bad install name '$INSTALL_NAME' in $fw"; exit 1; } + echo " dynamic OK: $(basename $(dirname $fw))" + done + [ "$FOUND" -ge ${toString expectedSlices} ] || \ + { echo "FAIL: Expected >= ${toString expectedSlices} dynamic slices, found $FOUND"; exit 1; } + echo "Dynamic xcframework validation passed ($FOUND slices)" + ''; +} diff --git a/nix/package/ios-xcframework/mk-release.nix b/nix/package/ios-xcframework/mk-release.nix new file mode 100644 index 0000000000..f1d1947b23 --- /dev/null +++ b/nix/package/ios-xcframework/mk-release.nix @@ -0,0 +1,27 @@ +{ + stdenv, +}: +{ + static, + dynamic, + swiftBindings, + licenseFile, + version, +}: +stdenv.mkDerivation { + pname = "xmtpv3-ios-xcframeworks"; + inherit version; + dontUnpack = true; + dontFixup = true; + installPhase = '' + mkdir -p $out/LibXMTPSwiftFFI/Sources/LibXMTP + cp -r ${static}/LibXMTPSwiftFFI.xcframework $out/LibXMTPSwiftFFI/ + cp ${swiftBindings}/swift/xmtpv3.swift $out/LibXMTPSwiftFFI/Sources/LibXMTP/ + cp ${licenseFile} $out/LibXMTPSwiftFFI/LICENSE + + mkdir -p $out/LibXMTPSwiftFFIDynamic/Sources/LibXMTP + cp -r ${dynamic}/LibXMTPSwiftFFIDynamic.xcframework $out/LibXMTPSwiftFFIDynamic/ + cp ${swiftBindings}/swift/xmtpv3.swift $out/LibXMTPSwiftFFIDynamic/Sources/LibXMTP/ + cp ${licenseFile} $out/LibXMTPSwiftFFIDynamic/LICENSE + ''; +} diff --git a/nix/package/ios-xcframework/mk-static.nix b/nix/package/ios-xcframework/mk-static.nix new file mode 100644 index 0000000000..c65566326d --- /dev/null +++ b/nix/package/ios-xcframework/mk-static.nix @@ -0,0 +1,76 @@ +{ + lib, + stdenv, + xcode-tools, + helpers, +}: +{ + abis, + dylibs, + swiftBindings, + version, +}: +let + inherit (helpers.classifyTargets abis) + deviceAbis + simAbis + macAbis + expectedSlices + ; + headerDir = "${swiftBindings}/swift/include/libxmtp"; +in +stdenv.mkDerivation { + pname = "xmtpv3-static-xcframework"; + inherit version; + dontUnpack = true; + dontFixup = true; + nativeBuildInputs = [ xcode-tools ]; + installPhase = '' + set -euo pipefail + echo "=== Building static xcframework ===" + + ${helpers.mkLipoSnippet { + group = "sim"; + ext = "a"; + inherit dylibs; + abis = simAbis; + }} + ${helpers.mkLipoSnippet { + group = "macos"; + ext = "a"; + inherit dylibs; + abis = macAbis; + }} + + XCF_ARGS="" + ${lib.optionalString (deviceAbis != [ ]) '' + XCF_ARGS="$XCF_ARGS -library ${dylibs.iphone64}/libxmtpv3.a -headers ${headerDir}" + ''} + ${lib.optionalString (simAbis != [ ]) '' + XCF_ARGS="$XCF_ARGS -library $TMPDIR/lipo_sim/libxmtpv3.a -headers ${headerDir}" + ''} + ${lib.optionalString (macAbis != [ ]) '' + XCF_ARGS="$XCF_ARGS -library $TMPDIR/lipo_macos/libxmtpv3.a -headers ${headerDir}" + ''} + + mkdir -p $out + xcodebuild -create-xcframework \ + $XCF_ARGS \ + -output $out/LibXMTPSwiftFFI.xcframework + + echo "Validating static xcframework..." + FOUND=0 + for slice in $out/LibXMTPSwiftFFI.xcframework/*/; do + [ -d "$slice/Headers" ] || continue + FOUND=$((FOUND + 1)) + test -f "$slice/Headers/xmtpv3FFI.h" || { echo "FAIL: Missing xmtpv3FFI.h in $slice"; exit 1; } + test -f "$slice/Headers/module.modulemap" || { echo "FAIL: Missing modulemap in $slice"; exit 1; } + head -1 "$slice/Headers/module.modulemap" | grep -q "module xmtpv3FFI" || \ + { echo "FAIL: Bad modulemap in $slice"; exit 1; } + echo " static OK: $(basename $slice)" + done + [ "$FOUND" -ge ${toString expectedSlices} ] || \ + { echo "FAIL: Expected >= ${toString expectedSlices} static slices, found $FOUND"; exit 1; } + echo "Static xcframework validation passed ($FOUND slices)" + ''; +} diff --git a/nix/package/ios.nix b/nix/package/ios.nix deleted file mode 100644 index 63345c3bd6..0000000000 --- a/nix/package/ios.nix +++ /dev/null @@ -1,494 +0,0 @@ -# iOS cross-compilation package derivation. -# Builds static and dynamic libraries for 4 targets + Swift bindings, cacheable in Cachix. -# Assembles xcframeworks (static + dynamic) from built targets. -# Uses shared config from nix/lib/ios-env.nix and nix/lib/mobile-common.nix. -# -# Key derivations: -# - Per-target static+dynamic libraries (impure — need Xcode SDK) -# - Swift bindings (impure — runs uniffi-bindgen) -# - Aggregate ios-libs (pure — symlinks to all targets) -# - Static xcframework assembly (impure — needs Xcode for lipo + xcodebuild) -# - Dynamic xcframework assembly (impure — needs Xcode for .framework wrapping) -# - Release aggregate (pure — zip-ready directory layout) -# - Dev aggregate (pure — bare xcframeworks for Package.swift detection) -# -{ - lib, - xmtp, - stdenv, - ... -}: -let - inherit (xmtp) iosEnv base; - # override craneLib rust toolchain with given stdenv - # https://crane.dev/API.html#mklib - craneLib = xmtp.craneLib.overrideScope ( - _: _: { - inherit stdenv; - } - ); - ffi-uniffi-bindgen = "${xmtp.ffi-uniffi-bindgen}/bin/ffi-uniffi-bindgen"; - # Rust toolchain with iOS/macOS cross-compilation targets. - # No clippy/rustfmt — this is a build-only toolchain (the dev shell adds those). - # overrideToolchain tells crane to use our custom fenix-based toolchain instead - # of the default nixpkgs rustc. - rust-toolchain = p: xmtp.mkToolchain p iosEnv.iosTargets [ ]; - rust = craneLib.overrideToolchain rust-toolchain; - - # Extract version once for use throughout the file - version = xmtp.mkVersion rust; - - # Inherit shared config - inherit (base) commonArgs bindingsFileset; - - # Build static (.a) and dynamic (.dylib) libraries for a single cross-compilation target. - # - # Uses crane's two-phase approach: - # 1. buildDepsOnly — compiles all dependencies, outputs cargo artifacts. - # Cache key: Cargo.lock + Cargo.toml files + build.rs files. - # This is the expensive step (~30-60 min) that benefits from Cachix. - # 2. buildPackage — compiles project source using cached dep artifacts. - # Uses the full source fileset and inherits cargoArtifacts from phase 1. - # - # IMPROVEMENT: buildDepsOnly could include an Xcode existence check in its - # buildPhaseCargoCommand to provide a clearer error message if Xcode is missing, - # rather than failing deep in the cc-rs build with cryptic SDK path errors. - # Per-target dep cache — rebuilds only when Cargo.lock, Cargo.toml, or build.rs change. - # Keyed by target triple. Impure (__noChroot) because build scripts need Xcode SDK. - # Shared between buildTarget and swiftBindings (for aarch64-apple-darwin). - mkDepsForTarget = - target: - let - envSetup = iosEnv.envSetup target; - in - xmtp.base.mkCargoArtifacts rust false { - pname = "xmtpv3-deps-${target}"; - CARGO_BUILD_TARGET = target; - __noChroot = true; - cargoExtraArgs = "--target ${target} -p xmtpv3"; - buildPhaseCargoCommand = '' - ${envSetup} - cargo build --locked --release --target ${target} -p xmtpv3 - ''; - }; - - buildTarget = - target: - let - cargoArtifacts = mkDepsForTarget target; - in - # Phase 2: Build project source using cached dep artifacts. - rust.buildPackage ( - commonArgs - // { - inherit cargoArtifacts version; - CARGO_BUILD_TARGET = target; - __noChroot = true; - pname = "xmtpv3-${target}"; - doInstallCargoArtifacts = false; - src = bindingsFileset; - cargoExtraArgs = "--target ${target} -p xmtpv3"; - # preBuild works here (unlike buildDepsOnly) because buildPackage doesn't - # need to replace source files, so crane leaves the build hooks intact. - preBuild = iosEnv.envSetup target; - # Override crane's default installPhase which expects a binary executable. - # We produce a static library (.a) and dynamic library (.dylib), so we copy them directly. - installPhaseCommand = '' - mkdir -p $out/${target} - cp target/${target}/release/libxmtpv3.a $out/${target}/ - cp target/${target}/release/libxmtpv3.dylib $out/${target}/ - install_name_tool -id @rpath/libxmtpv3.dylib $out/${target}/libxmtpv3.dylib - ''; - } - ); - - # Native host env setup for Swift bindings (builds for macOS host, not iOS). - nativeEnvSetup = iosEnv.envSetup "aarch64-apple-darwin"; - - # Swift bindings derivation. - # Builds libxmtpv3 for the native macOS host, then runs uniffi-bindgen to - # generate Swift bindings (.swift file + C header + modulemap). - # Impure because it needs the Xcode SDK for the native host build. - # The generated files are platform-independent — they work with any target's .a file. - swiftBindings = rust.buildPackage ( - commonArgs - // { - pname = "xmtpv3-swift-bindings"; - __noChroot = true; - src = bindingsFileset; - inherit version; - cargoArtifacts = mkDepsForTarget "aarch64-apple-darwin"; - cargoExtraArgs = "-p xmtpv3"; - CARGO_BUILD_TARGET = "aarch64-apple-darwin"; - buildPhaseCargoCommand = '' - ${nativeEnvSetup} - cargo build --release --target aarch64-apple-darwin -p xmtpv3 - ''; - # Prevent crane from trying to find and install cargo binaries from $out/bin. - # This derivation produces generated source files, not executables. - doNotPostBuildInstallCargoBinaries = true; - postBuild = '' - ${nativeEnvSetup} - # Generate Swift bindings using uniffi-bindgen. - # This runs the ffi-uniffi-bindgen binary (built above) against the compiled - # static library to extract the FFI interface and produce: - # - xmtpv3.swift: Swift source with all public API types and functions - # - xmtpv3FFI.h: C header for the FFI layer - # - xmtpv3FFI.modulemap: Clang module map (renamed to module.modulemap) - ${ffi-uniffi-bindgen} generate \ - --library target/aarch64-apple-darwin/release/libxmtpv3.a \ - --out-dir $TMPDIR/swift-out \ - --language swift - ''; - installPhaseCommand = '' - # Organize into expected directory structure for xcframework assembly - mkdir -p $out/swift/include/libxmtp - cp $TMPDIR/swift-out/xmtpv3.swift $out/swift/ - mv $TMPDIR/swift-out/xmtpv3FFI.h $out/swift/include/libxmtp/ - mv $TMPDIR/swift-out/xmtpv3FFI.modulemap $out/swift/include/libxmtp/module.modulemap - ''; - } - ); - - # Function to build a specific set of targets (mirrors mkAndroid in android.nix) - mkIos = - targetList: - let - selectedTargets = lib.genAttrs targetList buildTarget; - - selectedAggregate = stdenv.mkDerivation { - pname = "xmtpv3-ios-libs"; - inherit version; - dontUnpack = true; - doInstallCargoArtifacts = false; - installPhase = '' - mkdir -p $out/swift - ${lib.concatMapStringsSep "\n" (target: '' - mkdir -p $out/${target} - ln -s ${selectedTargets.${target}}/${target}/libxmtpv3.a $out/${target}/libxmtpv3.a - ln -s ${selectedTargets.${target}}/${target}/libxmtpv3.dylib $out/${target}/libxmtpv3.dylib - '') targetList} - ln -s ${swiftBindings}/swift/xmtpv3.swift $out/swift/xmtpv3.swift - ln -s ${swiftBindings}/swift/include $out/swift/include - ''; - }; - in - { - targets = selectedTargets; - inherit swiftBindings; - aggregate = selectedAggregate; - }; - - # Classify targets into platform groups for xcframework assembly. - # Each non-empty group produces one platform slice in the xcframework. - # Currently the only device target is aarch64-apple-ios; if more are added - # they would need lipo like sim/macOS groups. - classifyTargets = - targetList: - let - device = builtins.filter (t: t == "aarch64-apple-ios") targetList; - sim = builtins.filter (t: lib.hasSuffix "-ios-sim" t) targetList; - mac = builtins.filter (t: lib.hasSuffix "-darwin" t) targetList; - in - { - deviceTargets = device; - simTargets = sim; - macTargets = mac; - expectedSlices = - (if device != [ ] then 1 else 0) + (if sim != [ ] then 1 else 0) + (if mac != [ ] then 1 else 0); - }; - - # Shell preamble for xcframework derivations: resolves Xcode and adds - # toolchain binaries (lipo, otool, install_name_tool) and /usr/bin (codesign) to PATH. - xcframeworkEnvSetup = '' - ${iosEnv.envSetup "aarch64-apple-darwin"} - export PATH="$_XCODE_DEV/Toolchains/XcodeDefault.xctoolchain/usr/bin:/usr/bin:$PATH" - ''; - - # Assemble a static xcframework from per-target .a files. - # Takes the target list, built target derivations, and swift bindings. - # Produces $out/LibXMTPSwiftFFI.xcframework/ - mkStaticXcframework = - targetList: selectedTargets: swiftBindings: - let - inherit (classifyTargets targetList) - deviceTargets - simTargets - macTargets - expectedSlices - ; - headerDir = "${swiftBindings}/swift/include/libxmtp"; - in - stdenv.mkDerivation { - pname = "xmtpv3-static-xcframework"; - inherit version; - __noChroot = true; - dontUnpack = true; - dontFixup = true; - installPhase = '' - ${xcframeworkEnvSetup} - set -euo pipefail - - echo "=== Building static xcframework ===" - - # lipo sim .a files into fat lib - ${lib.optionalString (builtins.length simTargets > 0) '' - mkdir -p $TMPDIR/lipo_sim - lipo -create \ - ${lib.concatMapStringsSep " " (t: "${selectedTargets.${t}}/${t}/libxmtpv3.a") simTargets} \ - -output $TMPDIR/lipo_sim/libxmtpv3.a - ''} - - # lipo macOS .a files into fat lib - ${lib.optionalString (builtins.length macTargets > 0) '' - mkdir -p $TMPDIR/lipo_macos - lipo -create \ - ${lib.concatMapStringsSep " " (t: "${selectedTargets.${t}}/${t}/libxmtpv3.a") macTargets} \ - -output $TMPDIR/lipo_macos/libxmtpv3.a - ''} - - # Build xcodebuild args - XCF_ARGS="" - ${lib.optionalString (deviceTargets != [ ]) '' - XCF_ARGS="$XCF_ARGS -library ${ - selectedTargets.${"aarch64-apple-ios"} - }/aarch64-apple-ios/libxmtpv3.a -headers ${headerDir}" - ''} - ${lib.optionalString (simTargets != [ ]) '' - XCF_ARGS="$XCF_ARGS -library $TMPDIR/lipo_sim/libxmtpv3.a -headers ${headerDir}" - ''} - ${lib.optionalString (macTargets != [ ]) '' - XCF_ARGS="$XCF_ARGS -library $TMPDIR/lipo_macos/libxmtpv3.a -headers ${headerDir}" - ''} - - mkdir -p $out - xcodebuild -create-xcframework \ - $XCF_ARGS \ - -output $out/LibXMTPSwiftFFI.xcframework - - # === Validation === - echo "Validating static xcframework..." - FOUND=0 - for slice in $out/LibXMTPSwiftFFI.xcframework/*/; do - [ -d "$slice/Headers" ] || continue - FOUND=$((FOUND + 1)) - test -f "$slice/Headers/xmtpv3FFI.h" || { echo "FAIL: Missing xmtpv3FFI.h in $slice"; exit 1; } - test -f "$slice/Headers/module.modulemap" || { echo "FAIL: Missing modulemap in $slice"; exit 1; } - head -1 "$slice/Headers/module.modulemap" | grep -q "module xmtpv3FFI" || \ - { echo "FAIL: Bad modulemap in $slice"; exit 1; } - echo " static OK: $(basename $slice)" - done - [ "$FOUND" -ge ${toString expectedSlices} ] || \ - { echo "FAIL: Expected >= ${toString expectedSlices} static slices, found $FOUND"; exit 1; } - echo "Static xcframework validation passed ($FOUND slices)" - - chmod -R u+w $out - ''; - }; - - # Assemble a dynamic xcframework from per-target .dylib files. - # Takes the target list, built target derivations, and swift bindings. - # Produces $out/LibXMTPSwiftFFIDynamic.xcframework/ - mkDynamicXcframework = - targetList: selectedTargets: swiftBindings: - let - inherit (classifyTargets targetList) - deviceTargets - simTargets - macTargets - expectedSlices - ; - headerDir = "${swiftBindings}/swift/include/libxmtp"; - - # Shell snippet to wrap a dylib in a .framework bundle. - # minOSVersion is required by App Store validation for embedded frameworks. - mkFrameworkBundle = name: dylibPath: minOSVersion: '' - echo "Building framework bundle: ${name}" - mkdir -p $TMPDIR/${name}/xmtpv3FFI.framework/Headers - mkdir -p $TMPDIR/${name}/xmtpv3FFI.framework/Modules - cp ${dylibPath} $TMPDIR/${name}/xmtpv3FFI.framework/xmtpv3FFI - install_name_tool -id @rpath/xmtpv3FFI.framework/xmtpv3FFI \ - $TMPDIR/${name}/xmtpv3FFI.framework/xmtpv3FFI - cp ${headerDir}/*.h $TMPDIR/${name}/xmtpv3FFI.framework/Headers/ - sed 's/^module /framework module /' \ - ${headerDir}/module.modulemap \ - > $TMPDIR/${name}/xmtpv3FFI.framework/Modules/module.modulemap - /usr/libexec/PlistBuddy \ - -c "Add :CFBundleExecutable string xmtpv3FFI" \ - -c "Add :CFBundleIdentifier string org.xmtp.xmtpv3FFI" \ - -c "Add :CFBundleInfoDictionaryVersion string 6.0" \ - -c "Add :CFBundleName string xmtpv3FFI" \ - -c "Add :CFBundlePackageType string FMWK" \ - -c "Add :CFBundleVersion string 1" \ - -c "Add :CFBundleShortVersionString string 1.0" \ - -c "Add :MinimumOSVersion string ${minOSVersion}" \ - $TMPDIR/${name}/xmtpv3FFI.framework/Info.plist - codesign --force --sign - $TMPDIR/${name}/xmtpv3FFI.framework - ''; - in - stdenv.mkDerivation { - pname = "xmtpv3-dynamic-xcframework"; - inherit version; - __noChroot = true; - dontUnpack = true; - dontFixup = true; - installPhase = '' - ${xcframeworkEnvSetup} - set -euo pipefail - - echo "=== Building dynamic xcframework ===" - - # lipo sim .dylib files into fat dylib - ${lib.optionalString (builtins.length simTargets > 0) '' - mkdir -p $TMPDIR/lipo_sim - lipo -create \ - ${lib.concatMapStringsSep " " (t: "${selectedTargets.${t}}/${t}/libxmtpv3.dylib") simTargets} \ - -output $TMPDIR/lipo_sim/libxmtpv3.dylib - ''} - - # lipo macOS .dylib files into fat dylib - ${lib.optionalString (builtins.length macTargets > 0) '' - mkdir -p $TMPDIR/lipo_macos - lipo -create \ - ${lib.concatMapStringsSep " " (t: "${selectedTargets.${t}}/${t}/libxmtpv3.dylib") macTargets} \ - -output $TMPDIR/lipo_macos/libxmtpv3.dylib - ''} - - # Build .framework bundles per platform - ${lib.optionalString (deviceTargets != [ ]) ( - mkFrameworkBundle "fw_ios" "${ - selectedTargets.${"aarch64-apple-ios"} - }/aarch64-apple-ios/libxmtpv3.dylib" "14.0" - )} - ${lib.optionalString (simTargets != [ ]) ( - mkFrameworkBundle "fw_sim" "$TMPDIR/lipo_sim/libxmtpv3.dylib" "14.0" - )} - ${lib.optionalString (macTargets != [ ]) ( - mkFrameworkBundle "fw_mac" "$TMPDIR/lipo_macos/libxmtpv3.dylib" "11.0" - )} - - # Build xcodebuild args - XCF_ARGS="" - ${lib.optionalString (deviceTargets != [ ]) '' - XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_ios/xmtpv3FFI.framework" - ''} - ${lib.optionalString (simTargets != [ ]) '' - XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_sim/xmtpv3FFI.framework" - ''} - ${lib.optionalString (macTargets != [ ]) '' - XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_mac/xmtpv3FFI.framework" - ''} - - mkdir -p $out - xcodebuild -create-xcframework \ - $XCF_ARGS \ - -output $out/LibXMTPSwiftFFIDynamic.xcframework - - # === Validation === - echo "Validating dynamic xcframework..." - FOUND=0 - for fw in $out/LibXMTPSwiftFFIDynamic.xcframework/*/xmtpv3FFI.framework; do - test -d "$fw" || continue - FOUND=$((FOUND + 1)) - test -f "$fw/xmtpv3FFI" || { echo "FAIL: Missing binary in $fw"; exit 1; } - test -f "$fw/Info.plist" || { echo "FAIL: Missing Info.plist in $fw"; exit 1; } - test -d "$fw/Headers" || { echo "FAIL: Missing Headers in $fw"; exit 1; } - test -f "$fw/Headers/xmtpv3FFI.h" || { echo "FAIL: Missing xmtpv3FFI.h in $fw"; exit 1; } - test -f "$fw/Modules/module.modulemap" || { echo "FAIL: Missing modulemap in $fw"; exit 1; } - head -1 "$fw/Modules/module.modulemap" | grep -q "^framework module xmtpv3FFI" || \ - { echo "FAIL: modulemap missing 'framework module' prefix in $fw"; exit 1; } - INSTALL_NAME=$(otool -D "$fw/xmtpv3FFI" | tail -1) - echo "$INSTALL_NAME" | grep -q "@rpath/xmtpv3FFI.framework/xmtpv3FFI" || \ - { echo "FAIL: Bad install name '$INSTALL_NAME' in $fw"; exit 1; } - echo " dynamic OK: $(basename $(dirname $fw))" - done - [ "$FOUND" -ge ${toString expectedSlices} ] || \ - { echo "FAIL: Expected >= ${toString expectedSlices} dynamic slices, found $FOUND"; exit 1; } - echo "Dynamic xcframework validation passed ($FOUND slices)" - - chmod -R u+w $out - ''; - }; - - # Aggregate for release: zip-ready directories with xcframeworks + Sources + LICENSE. - mkRelease = - { - static, - dynamic, - swiftBindings, - licenseFile, - }: - stdenv.mkDerivation { - pname = "xmtpv3-ios-xcframeworks"; - inherit version; - dontUnpack = true; - installPhase = '' - # Static zip contents - mkdir -p $out/LibXMTPSwiftFFI/Sources/LibXMTP - cp -r ${static}/LibXMTPSwiftFFI.xcframework $out/LibXMTPSwiftFFI/ - cp ${swiftBindings}/swift/xmtpv3.swift $out/LibXMTPSwiftFFI/Sources/LibXMTP/ - cp ${licenseFile} $out/LibXMTPSwiftFFI/LICENSE - - # Dynamic zip contents - mkdir -p $out/LibXMTPSwiftFFIDynamic/Sources/LibXMTP - cp -r ${dynamic}/LibXMTPSwiftFFIDynamic.xcframework $out/LibXMTPSwiftFFIDynamic/ - cp ${swiftBindings}/swift/xmtpv3.swift $out/LibXMTPSwiftFFIDynamic/Sources/LibXMTP/ - cp ${licenseFile} $out/LibXMTPSwiftFFIDynamic/LICENSE - - chmod -R u+w $out - ''; - }; - - # Aggregate for dev: bare xcframeworks at paths Package.swift expects. - mkDev = - { - static, - dynamic ? null, - swiftBindings, - }: - stdenv.mkDerivation { - pname = "xmtpv3-ios-xcframeworks-dev"; - inherit version; - dontUnpack = true; - installPhase = '' - mkdir -p $out - cp -r ${static}/LibXMTPSwiftFFI.xcframework $out/ - ${lib.optionalString (dynamic != null) '' - cp -r ${dynamic}/LibXMTPSwiftFFIDynamic.xcframework $out/ - ''} - # Include generated Swift bindings for dev script to copy to SDK source tree - cp ${swiftBindings}/swift/xmtpv3.swift $out/xmtpv3.swift - chmod -R u+w $out - ''; - }; - -in -{ - inherit swiftBindings mkIos; - # Default: all targets (for backward compat) - inherit (mkIos iosEnv.iosTargets) targets aggregate; - # Release: zip-ready directories for CI - release = - let - ios = mkIos iosEnv.iosTargets; - in - mkRelease { - static = mkStaticXcframework iosEnv.iosTargets ios.targets swiftBindings; - dynamic = mkDynamicXcframework iosEnv.iosTargets ios.targets swiftBindings; - inherit swiftBindings; - licenseFile = ../../LICENSE; - }; - # Dev fast: simulator + macOS only, static only - devFast = - let - fastTargets = [ - "aarch64-apple-darwin" - "aarch64-apple-ios-sim" - ]; - ios = mkIos fastTargets; - in - mkDev { - static = mkStaticXcframework fastTargets ios.targets swiftBindings; - inherit swiftBindings; - }; -} diff --git a/nix/shells/ios.nix b/nix/shells/ios.nix index 1dd446e6f9..1d93472772 100644 --- a/nix/shells/ios.nix +++ b/nix/shells/ios.nix @@ -2,10 +2,6 @@ # Provides the environment for `cargo build --target ` from the CLI. # Uses shared config from nix/lib/ios-env.nix. # -# Relationship to nix/package/ios.nix: -# - This file: interactive dev shell (`nix develop .#ios`) -# - package/ios.nix: CI/release build derivation (`nix build .#ios-libs`) -# Both use ios-env.nix for shared cross-compilation config. { stdenv, darwin, diff --git a/sdks/ios/dev/bindings b/sdks/ios/dev/bindings index 7cad2cb902..f69acedd02 100755 --- a/sdks/ios/dev/bindings +++ b/sdks/ios/dev/bindings @@ -16,7 +16,7 @@ fi if [[ "${1:-}" == "--release" ]]; then echo "Building iOS xcframeworks (all targets)..." - nix build "${ROOT}#ios-xcframeworks" --out-link "${ROOT}/bindings/mobile/build/release" + nix build "${ROOT}#ios-release" --out-link "${ROOT}/bindings/mobile/build/release" rm -rf "$SWIFT_OUT" mkdir -p "$SWIFT_OUT" cp -rL "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFI/LibXMTPSwiftFFI.xcframework" "$SWIFT_OUT/" @@ -28,7 +28,7 @@ else echo "Building iOS xcframeworks (simulator + macOS only)..." # Materialize the xcframework instead of symlinking --- rm -rf "$SWIFT_OUT" - nix build "${ROOT}#ios-xcframeworks-fast" \ + nix build "${ROOT}#ios-devFast" \ --out-link "${ROOT}/bindings/mobile/build/nix-fast" mkdir -p "$SWIFT_OUT" cp -rL "${ROOT}/bindings/mobile/build/nix-fast/LibXMTPSwiftFFI.xcframework" "$SWIFT_OUT/" From bb6802d63557776fdfe508ec45a41824d4968f52 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Wed, 10 Jun 2026 13:11:41 -0400 Subject: [PATCH 04/11] chore(nix): apply iOS nixpkgs branch as patch; CA the iOS SDK Cross pkgsets apply the upstream iOS branch onto nixos-unstable via fetchpatch2 (re-pin hash after branch pushes; URL in nix/lib/default.nix). SDK output content-addressed on the from-scratch path. --- flake.nix | 36 ++---------------------------------- nix/lib/default.nix | 19 ++++++++++++++++++- nix/lib/patches.nix | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/flake.nix b/flake.nix index 21167c6563..5e521e1a9a 100644 --- a/flake.nix +++ b/flake.nix @@ -7,11 +7,9 @@ }; inputs = { + # Cross pkgsets apply the iOS branch (NixOS/nixpkgs#512100) as a patch + # on top — see nixpkgs-patched in nix/lib/default.nix. nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - # nixpkgs.url = "github:insipx/nixpkgs/insipx/ios-build"; - # nixpkgs.url = "git+file:///Users/insipx/code/nixpkgs"; - # nixpkgs.url = "git+file:///home/insipx/code/nixpkgs"; - # nixpkgs.url = "git+file:///workspace/nixpkgs?ref=insipx/ios-build"; fenix = { url = "github:nix-community/fenix"; inputs = { @@ -92,36 +90,6 @@ wasm-bindings = (pkgs.callPackage ./nix/package/wasm.nix { }).bin; wasm-bindings-test = (pkgs.callPackage ./nix/package/wasm.nix { test = true; }).bin; }; - # // lib.optionalAttrs pkgs.stdenv.isDarwin { - # # stdenvNoCC is passed to callPackage (for the aggregate derivation). - # # This avoids Nix's apple-sdk and cc-wrapper, - # # which inject -mmacos-version-min flags that - # # conflict with iOS cross-compilation. The builds are impure (__noChroot) - # # and use the system Xcode SDK directly via ios-env.nix paths. - # ios-libs = - # (pkgs.callPackage ./nix/package/ios.nix { - # stdenv = pkgs.stdenvNoCC; - # }).aggregate; - # # iOS bindings - simulator + host macOS only (fast dev/CI builds) - # ios-libs-fast = - # ( - # (pkgs.callPackage ./nix/package/ios.nix { - # stdenv = pkgs.stdenvNoCC; - # }).mkIos - # [ - # "aarch64-apple-darwin" - # "aarch64-apple-ios-sim" - # ] - # ).aggregate; - # ios-xcframeworks = - # (pkgs.callPackage ./nix/package/ios.nix { - # stdenv = pkgs.stdenvNoCC; - # }).release; - # ios-xcframeworks-fast = - # (pkgs.callPackage ./nix/package/ios.nix { - # stdenv = pkgs.stdenvNoCC; - # }).devFast; - # }; }; }; } diff --git a/nix/lib/default.nix b/nix/lib/default.nix index 9c573bb4fb..e00bef1d36 100644 --- a/nix/lib/default.nix +++ b/nix/lib/default.nix @@ -99,6 +99,23 @@ in let hostPkgs = mkHostPkgs system; overlays = baseOverlays ++ [ (xmtpOverlay hostPkgs) ]; + # iOS cross-compilation support not yet in nixpkgs, applied as a + # patch from the open PR (NixOS/nixpkgs#512100, the insipx/ios-build + # branch). Remove once the PR merges. + # + # The pull/NNN.patch URL tracks the PR head: every push to the + # branch changes the patch content, so rebuild once after a push — + # the hash-mismatch error prints the new hash to paste over `hash`. + nixpkgs-patched = hostPkgs.applyPatches { + name = "nixpkgs-ios-cross"; + src = inputs.nixpkgs; + patches = [ + (hostPkgs.fetchpatch2 { + url = "https://github.com/NixOS/nixpkgs/pull/512100.patch"; + hash = lib.fakeHash; + }) + ]; + }; in lib.listToAttrs ( map ( @@ -108,7 +125,7 @@ in in { name = t.name or t.config; - value = import inputs.nixpkgs { + value = import nixpkgs-patched { inherit config overlays; localSystem = system; crossSystem = removeAttrs t [ "name" ]; diff --git a/nix/lib/patches.nix b/nix/lib/patches.nix index 2d32f5a4ec..b3b0c0e900 100644 --- a/nix/lib/patches.nix +++ b/nix/lib/patches.nix @@ -109,4 +109,25 @@ }; } ) + # Content-address the iOS SDK extracted from Xcode (from-scratch + # apple-sdk path only; no-op for useiOSPrebuilt). The SDK output is + # keyed on its contents, not on darwin.xcode's store hash — Apple + # ships minor Xcode bundle revisions whose .xip hash drifts while the + # embedded iPhoneOS.sdk stays byte-identical. Without CA every such + # drift rebuilds the SDK and the entire iOS chain behind it. Requires + # ca-derivations on the local daemon + Darwin builders. Upstream + # nixpkgs keeps the derivation input-addressed by default, so this + # stays an overlay. + ( + final: prev: + prev.lib.optionalAttrs prev.stdenv.hostPlatform.isiOS { + apple-sdk = prev.apple-sdk.overrideAttrs (old: { + src = old.src.overrideAttrs (_: { + __contentAddressed = true; + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + }); + }); + } + ) ] From 2a500c53b36af64c3cb5948f662ee5617c287ee5 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Wed, 10 Jun 2026 13:11:55 -0400 Subject: [PATCH 05/11] feat(ios): auto-import Xcode into /nix/store just ios build imports /Applications/Xcode_.app when missing (one-time, Darwin only); CI verifies xmtp-cache-apple already did it. --- nix/lib/default.nix | 2 +- sdks/ios/CLAUDE.md | 16 +++++++++ sdks/ios/dev/.ensure-xcode | 69 ++++++++++++++++++++++++++++++++++++++ sdks/ios/dev/bindings | 6 ++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100755 sdks/ios/dev/.ensure-xcode diff --git a/nix/lib/default.nix b/nix/lib/default.nix index e00bef1d36..836dfa4ac1 100644 --- a/nix/lib/default.nix +++ b/nix/lib/default.nix @@ -112,7 +112,7 @@ in patches = [ (hostPkgs.fetchpatch2 { url = "https://github.com/NixOS/nixpkgs/pull/512100.patch"; - hash = lib.fakeHash; + hash = "sha256-QBeYgZHwCcd/vmXjkxewBhQrXT4Gv46Xh6z8e0JYxiY="; }) ]; }; diff --git a/sdks/ios/CLAUDE.md b/sdks/ios/CLAUDE.md index c25f189b35..56b9422f47 100644 --- a/sdks/ios/CLAUDE.md +++ b/sdks/ios/CLAUDE.md @@ -22,6 +22,22 @@ nix develop .#ios just ios build ``` +### First-time setup: Xcode in the Nix store + +On macOS, iOS builds need Xcode imported into /nix/store (Apple's license +forbids redistributing it via binary caches). `just ios build` handles this +automatically on first run (~5 min) if `/Applications/Xcode_26.3.app` +exists. Install it with: + +```bash +brew install xcodes +xcodes install 26.3 +``` + +The version must match `xcodeVer` in `nix/ios-packages.nix`. Override with +`XCODE_VERSION` / `XCODE_APP` env vars if your install lives elsewhere. +CI imports Xcode via the xmtp-cache-apple action instead. + ## Development Commands All commands run through justfile recipes with Nix: diff --git a/sdks/ios/dev/.ensure-xcode b/sdks/ios/dev/.ensure-xcode new file mode 100755 index 0000000000..e99b6ec07b --- /dev/null +++ b/sdks/ios/dev/.ensure-xcode @@ -0,0 +1,69 @@ +#!/bin/bash +# Ensure Xcode is materialized in /nix/store for the iOS cross-compile. +# +# The nixpkgs iOS toolchain (darwin.xcode_ via requireFile) is +# fixed-output: its store path is computable at eval time, but the content +# must be imported manually — Apple's license forbids redistribution, so no +# binary cache can serve it. +# +# Local dev: imports /Applications/Xcode_.app on first run (~5 min). +# Install it with `xcodes install ` if missing. +# CI: the xmtp-cache-apple action pre-imports Xcode; this script +# only verifies and fails fast with a pointer if it didn't. +# +# XCODE_VERSION must match xcodeVer in nix/ios-packages.nix (and the +# LOCAL examples.nix bump on the nixpkgs branch). + +set -euo pipefail +source "$(dirname "$0")/.setup" + +XCODE_VERSION="${XCODE_VERSION:-26.3}" +XCODE_APP="${XCODE_APP:-/Applications/Xcode_${XCODE_VERSION}.app}" +XCODE_ATTR="xcode_${XCODE_VERSION//./_}" + +expected=$(NIXPKGS_ALLOW_UNFREE=1 nix eval --impure --raw \ + --inputs-from "$ROOT" "nixpkgs#darwin.${XCODE_ATTR}.outPath") + +if nix path-info "$expected" >/dev/null 2>&1; then + exit 0 +fi + +if [[ "${CI:-}" == "true" ]]; then + echo "ERROR: Xcode missing from Nix store in CI: $expected" >&2 + echo "The xmtp-cache-apple action should import it before this step." >&2 + exit 1 +fi + +if [[ ! -d "$XCODE_APP" ]]; then + echo "Xcode ${XCODE_VERSION} not found at ${XCODE_APP}." + echo "⚠️ Installing via xcodes prompts for your Apple ID credentials" + echo " and downloads ~5 GB from Apple." + read -p "Install Xcode ${XCODE_VERSION} now? (y/N) " -n 1 -r; echo + [[ $REPLY =~ ^[Yy]$ ]] || exit 1 + if command -v xcodes >/dev/null 2>&1; then + xcodes install "${XCODE_VERSION}" + else + nix run nixpkgs#xcodes -- install "${XCODE_VERSION}" + fi + # xcodes may name the bundle Xcode-.app; pick up whatever landed. + if [[ ! -d "$XCODE_APP" ]]; then + XCODE_APP=$(ls -d /Applications/Xcode*"${XCODE_VERSION}"*.app 2>/dev/null | head -1) + fi + if [[ -z "$XCODE_APP" || ! -d "$XCODE_APP" ]]; then + echo "ERROR: xcodes finished but no Xcode ${XCODE_VERSION} bundle found in /Applications." >&2 + exit 1 + fi +fi + +echo "Importing ${XCODE_APP} into /nix/store (one-time per Xcode version, ~5 min)…" +got=$(nix store add --name Xcode.app "$XCODE_APP") + +if [[ "$got" != "$expected" ]]; then + echo "WARN: imported $got" >&2 + echo " but the flake expects $expected" >&2 + echo " Your Xcode bundle differs from the hash pinned in nixpkgs" >&2 + echo " (pkgs/os-specific/darwin/xcode/default.nix). Re-download via" >&2 + echo " xcodes, or update the pinned hash for ${XCODE_ATTR}." >&2 + exit 1 +fi +echo "Imported: $got" diff --git a/sdks/ios/dev/bindings b/sdks/ios/dev/bindings index f69acedd02..60f155354d 100755 --- a/sdks/ios/dev/bindings +++ b/sdks/ios/dev/bindings @@ -1,6 +1,12 @@ #!/bin/bash source "$(dirname "$0")/.setup" +# Darwin only: Linux workstations build via remote Darwin builders, which +# hold their own Xcode import. +if [[ "$(uname -s)" == "Darwin" ]]; then + "$(dirname "$0")/.ensure-xcode" +fi + SWIFT_OUT="${ROOT}/bindings/mobile/build/swift" # --- Preflight sanity check on the nix binary cache --- From 09605fbf8b0b6ef9e238fecf86fc7df48895a8f6 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Wed, 10 Jun 2026 17:26:29 -0400 Subject: [PATCH 06/11] remove unnecessary patches --- nix/lib/patches.nix | 63 --------------------------------------------- 1 file changed, 63 deletions(-) diff --git a/nix/lib/patches.nix b/nix/lib/patches.nix index b3b0c0e900..eea283639b 100644 --- a/nix/lib/patches.nix +++ b/nix/lib/patches.nix @@ -46,69 +46,6 @@ }); } ) - - # harfbuzz's test-coretext requires access to macOS font services - # (CTFontManagerRegisterFontsForURLs) which are unavailable in the - # Nix sandbox, causing SIGABRT. Disable the test suite entirely on - # Darwin via the meson 'tests' option. - # - # Pulled into the build graph via: - # remarshal → rich-argparse → rich → pytest-regressions → matplotlib - # → ffmpeg-headless → harfbuzz - ( - final: prev: - prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { - harfbuzz = prev.harfbuzz.overrideAttrs (old: { - mesonFlags = (old.mesonFlags or [ ]) ++ [ "-Dtests=disabled" ]; - }); - } - ) - # zvbi's configure auto-detects X11 and then compiles ntsc-cc.c which - # #includes ; in the Nix Darwin sandbox the probed - # /usr/X11/include has no headers, so the compile fails. Force - # --without-x — libxmtp doesn't need the X utilities. - # - # Pulled in via the same remarshal → matplotlib → ffmpeg-headless chain. - ( - final: prev: - prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { - zvbi = prev.zvbi.overrideAttrs (old: { - configureFlags = (old.configureFlags or [ ]) ++ [ "--without-x" ]; - }); - } - ) - # rsync's 'hardlinks' check-phase test is flaky in the Darwin Nix sandbox - # (macOS filesystem semantics don't match rsync's expected linkage - # behavior). Disable checks on Darwin — not needed by libxmtp. - ( - final: prev: - prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { - rsync = prev.rsync.overrideAttrs (_: { - doCheck = false; - }); - } - ) - # LLVM 21.1.8's BPF BTF tests (func-func-ptr.ll, func-typedef.ll) - # have stale expected output: a 2025-10-20 change to BTF generation - # (llvm/llvm-project#155783, DW_TAG_variant_part support) changed - # emitted offsets, but the test .ll files weren't regenerated. - # FileCheck fails on hardcoded `.long` sequences. Not a miscompile — - # only test-metadata drift, and the BPF backend is unreachable from - # iOS cross-compilation anyway. Delete the two stale tests via - # postPatch so `check-all` passes. - ( - final: prev: - prev.lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin { - llvmPackages_21 = prev.llvmPackages_21 // { - libllvm = prev.llvmPackages_21.libllvm.overrideAttrs (old: { - postPatch = (old.postPatch or "") + '' - rm test/CodeGen/BPF/BTF/func-func-ptr.ll - rm test/CodeGen/BPF/BTF/func-typedef.ll - ''; - }); - }; - } - ) # Content-address the iOS SDK extracted from Xcode (from-scratch # apple-sdk path only; no-op for useiOSPrebuilt). The SDK output is # keyed on its contents, not on darwin.xcode's store hash — Apple From d7011266af2109a4687d9f4d1cecda74475e5c7b Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Wed, 10 Jun 2026 17:43:52 -0400 Subject: [PATCH 07/11] fix(ios): address review feedback sqlcipher kept for musl dep-builds; DEVELOPER_DIR hook for xcodebuild; Android drops OPENSSL_* pins; reproducible version file; Linux hides unbuildable iOS outputs; plist min-version fixes; workflows derive xcode-path and materialize Xcode via xmtp-cache-apple. --- .github/actions/setup-ios-xcode/action.yml | 41 +++++++++++++ .github/workflows/lint-ios.yml | 1 + .github/workflows/release-ios.yml | 3 +- .github/workflows/test-ios.yml | 4 +- flake.nix | 2 +- nix/ios-packages.nix | 54 ++++++++++------- nix/js.nix | 3 + nix/lib/base.nix | 9 ++- nix/lib/default.nix | 14 ++--- nix/lib/packages/xcode-tools.nix | 25 ++++++-- nix/lib/shell-common.nix | 9 +++ nix/package/android.nix | 6 ++ nix/package/ios-bindings.nix | 5 +- nix/package/ios-xcframework/helpers.nix | 70 +++++++++++++++------- nix/package/ios-xcframework/mk-dynamic.nix | 30 ++++++++-- nix/package/ios-xcframework/mk-static.nix | 4 ++ nix/package/mls_validation_service.nix | 6 +- nix/package/nextest.nix | 3 + nix/package/xdbg.nix | 5 ++ nix/package/xnet-cli.nix | 4 ++ nix/shells/android.nix | 4 +- sdks/ios/dev/.ensure-xcode | 63 +++++++++++++------ sdks/ios/dev/bindings | 9 ++- 23 files changed, 283 insertions(+), 91 deletions(-) create mode 100644 .github/actions/setup-ios-xcode/action.yml diff --git a/.github/actions/setup-ios-xcode/action.yml b/.github/actions/setup-ios-xcode/action.yml new file mode 100644 index 0000000000..8846ef3416 --- /dev/null +++ b/.github/actions/setup-ios-xcode/action.yml @@ -0,0 +1,41 @@ +name: Setup iOS Xcode +description: > + Materialize the pinned Xcode at its /nix/store path via xmtp-cache-apple + (Apple's license forbids serving Xcode from a binary cache). Single source + of truth for the Xcode version and store path used by the iOS workflows — + keep in sync with xcodeVer in nix/ios-packages.nix. + +runs: + using: composite + steps: + # The local bundle is only needed for the cold-cache bootstrap import; + # warm runs restore a NAR and never touch it. A runner with no local + # Xcode still works warm — and on a cold cache it fails in the + # bootstrap step with a clear missing-bundle cause (nothing can + # conjure Xcode there anyway). + - name: Resolve Xcode bundle path + id: xcode-path + shell: bash + run: | + p="" + for c in /Applications/Xcode_26.3*.app /Applications/Xcode-26.3*.app /Applications/Xcode.app; do + [ -d "$c" ] || continue + v=$(defaults read "$c/Contents/version" CFBundleShortVersionString 2>/dev/null || true) + case "$v" in 26.3|26.3.*) p="$c"; break ;; esac + done + if [ -z "$p" ]; then + echo "::warning::No local Xcode 26.3 bundle found; OK on a warm cache, the cold-cache bootstrap will fail." + p=/Applications/Xcode_26.3.app + fi + echo "path=$p" >> "$GITHUB_OUTPUT" + - uses: xmtplabs/xmtp-cache-apple@v1.0.3 + with: + xcode-path: ${{ steps.xcode-path.outputs.path }} + # requireFile fixed-output path: moves only if the pinned sha256 for + # xcode_26_3 changes, never with nixpkgs/flake.lock bumps. + xcode-nix-path: /nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app + # Point non-nix tool invocations (raw xcodebuild/swift steps) at the + # pinned Xcode too; nix builds get this via the xcode-tools setup hook. + - name: Export DEVELOPER_DIR + shell: bash + run: echo "DEVELOPER_DIR=/nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app/Contents/Developer" >> "$GITHUB_ENV" diff --git a/.github/workflows/lint-ios.yml b/.github/workflows/lint-ios.yml index 9808551dce..892a2ecae1 100644 --- a/.github/workflows/lint-ios.yml +++ b/.github/workflows/lint-ios.yml @@ -13,6 +13,7 @@ jobs: with: github-token: ${{ github.token }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} + - uses: ./.github/actions/setup-ios-xcode - uses: taiki-e/install-action@just - name: Lint iOS SDK run: just ios lint diff --git a/.github/workflows/release-ios.yml b/.github/workflows/release-ios.yml index 829fbdd626..a013a12814 100644 --- a/.github/workflows/release-ios.yml +++ b/.github/workflows/release-ios.yml @@ -48,6 +48,7 @@ jobs: with: github-token: ${{ github.token }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} + - uses: ./.github/actions/setup-ios-xcode - name: Compute version id: version run: | @@ -76,7 +77,7 @@ jobs: echo "base-version=$BASE" >> "$GITHUB_OUTPUT" echo "Computed version: $VERSION" - name: Build xcframeworks - run: nix build .#ios-xcframeworks --out-link build/ios + run: nix build .#ios-release --out-link build/ios - name: Package and checksum id: checksum diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml index c36042894b..48498dfc98 100644 --- a/.github/workflows/test-ios.yml +++ b/.github/workflows/test-ios.yml @@ -52,9 +52,7 @@ jobs: github-token: ${{ github.token }} cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} with-warpbuild-cache: "false" - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "26" + - uses: ./.github/actions/setup-ios-xcode - uses: taiki-e/install-action@just - name: Build and test env: diff --git a/flake.nix b/flake.nix index 5e521e1a9a..d595c91a25 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,7 @@ }; inputs = { - # Cross pkgsets apply the iOS branch (NixOS/nixpkgs#512100) as a patch + # Cross pkgsets apply the upstream iOS branch as a patch # on top — see nixpkgs-patched in nix/lib/default.nix. nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; fenix = { diff --git a/nix/ios-packages.nix b/nix/ios-packages.nix index e57ce4fb1b..3186e04bfd 100644 --- a/nix/ios-packages.nix +++ b/nix/ios-packages.nix @@ -64,11 +64,14 @@ else throw "Unsupported host architecture for ios-libs-fast"; - # xcframework derivations are host-side packaging tools — call them through - # the local cross-pkgs so darwin.xcode auto-resolves via crossSystem.xcodeVer. - hostPkgs = crossPkgs.${fastAbi}; - xcode-tools = hostPkgs.callPackage ./lib/packages/xcode-tools.nix { }; - xcframework = hostPkgs.callPackage ./package/ios-xcframework { inherit xcode-tools; }; + # xcframework derivations are host-side packaging tools — call them + # through NATIVE pkgs (substitutable stdenv) and resolve Xcode + # explicitly. A cross pkgset with xcodeVer would rebuild its whole + # bootstrap from source just to run lipo/xcodebuild. + xcode-tools = pkgs.callPackage ./lib/packages/xcode-tools.nix { + inherit (iosCommon) xcodeVer; + }; + xcframework = pkgs.callPackage ./package/ios-xcframework { inherit xcode-tools; }; allAbis = lib.attrNames iosDylibs; fastAbis = [ @@ -86,6 +89,10 @@ abis = allAbis; dylibs = iosDylibs; swiftBindings = swift-bindings; + iosMinVersion = iosCommon.darwinMinVersion + ".0"; + # Pinned constant (matches the darwin toolchain default) so the + # advertised floor doesn't vary with the build host's architecture. + macMinVersion = "14.0"; inherit version; }; ios-xcframework-static-fast = xcframework.mkStatic { @@ -139,24 +146,29 @@ ] ); - ios-libs-fast = mkLibs "xmtpv3-ios-fast" [ fastAbi ]; + ios-libs-fast = mkLibs "xmtpv3-ios-fast" fastAbis; ios-libs = mkLibs "xmtpv3-ios" allAbis; in { - packages = { - inherit - ios-libs - ios-libs-fast - swift-bindings - ios-xcframework-static - ios-xcframework-dynamic - ios-release - ios-devFast - ; - } - // lib.mapAttrs' (abi: dylib: { - name = "ios-bindings-${abi}"; - value = dylib; - }) iosDylibs; + # iOS packaging needs darwin.xcode — only exposed on Darwin hosts. + # Linux workstations build via .#packages.aarch64-darwin. on a + # remote Darwin builder. + packages = lib.optionalAttrs pkgs.stdenv.isDarwin ( + { + inherit + ios-libs + ios-libs-fast + swift-bindings + ios-xcframework-static + ios-xcframework-dynamic + ios-release + ios-devFast + ; + } + // lib.mapAttrs' (abi: dylib: { + name = "ios-bindings-${abi}"; + value = dylib; + }) iosDylibs + ); }; } diff --git a/nix/js.nix b/nix/js.nix index 296591bb90..a6169eac68 100644 --- a/nix/js.nix +++ b/nix/js.nix @@ -4,6 +4,7 @@ mktemp, buf, curl, + git, geckodriver, corepack, pkg-config, @@ -24,6 +25,8 @@ mkShell { mktemp buf curl + # eval-time builtins.fetchGit needs a git matching the nix glibc + git geckodriver # playwright version here must match that in package.json EXACTLY for integration tests to work playwright diff --git a/nix/lib/base.nix b/nix/lib/base.nix index 63645a2379..6cf81e2a02 100644 --- a/nix/lib/base.nix +++ b/nix/lib/base.nix @@ -55,7 +55,9 @@ let openssl sqlcipher ] - ++ lib.optionals stdenv.buildPlatform.isDarwin [ libiconv ]; + # target-side: Rust Apple target specs emit -liconv, so gate on the + # platform the artifact runs on, not where it is built. + ++ lib.optionals stdenv.hostPlatform.isDarwin [ libiconv ]; doCheck = false; # Disable zerocallusedregs hardening which can cause issues with cross-compilation. @@ -101,6 +103,10 @@ let // overrides' ); + # Runtime search path for natively-built binaries: OPENSSL_NO_VENDOR links + # openssl (and sqlcipher/zstd) dynamically, but rustc records no rpath. + runtimeLibPath = lib.makeLibraryPath commonArgs.buildInputs; + in { inherit @@ -108,5 +114,6 @@ in bindingsFileset commonArgs mkCargoArtifacts + runtimeLibPath ; } diff --git a/nix/lib/default.nix b/nix/lib/default.nix index 836dfa4ac1..d333570f22 100644 --- a/nix/lib/default.nix +++ b/nix/lib/default.nix @@ -100,19 +100,19 @@ in hostPkgs = mkHostPkgs system; overlays = baseOverlays ++ [ (xmtpOverlay hostPkgs) ]; # iOS cross-compilation support not yet in nixpkgs, applied as a - # patch from the open PR (NixOS/nixpkgs#512100, the insipx/ios-build - # branch). Remove once the PR merges. + # patch from the upstream iOS branch. Remove once it merges. # - # The pull/NNN.patch URL tracks the PR head: every push to the - # branch changes the patch content, so rebuild once after a push — - # the hash-mismatch error prints the new hash to paste over `hash`. + # Pinned by commit SHAs (immutable compare URL): pushes to the + # branch never affect this build. To pick up new branch commits, + # bump the head SHA, set hash = lib.fakeHash, rebuild once, and + # paste the printed hash. nixpkgs-patched = hostPkgs.applyPatches { name = "nixpkgs-ios-cross"; src = inputs.nixpkgs; patches = [ (hostPkgs.fetchpatch2 { - url = "https://github.com/NixOS/nixpkgs/pull/512100.patch"; - hash = "sha256-QBeYgZHwCcd/vmXjkxewBhQrXT4Gv46Xh6z8e0JYxiY="; + url = "https://github.com/NixOS/nixpkgs/compare/2f51ad37d9416828be4be7f48e7617b01cdf0641...insipx:nixpkgs:4f8f394b62476598617a085acda285902d5998ce.patch"; + hash = "sha256-QzRz77CLCzm+xKAezYctSL7hHQF2IBKt6iqMOD2avg8="; }) ]; }; diff --git a/nix/lib/packages/xcode-tools.nix b/nix/lib/packages/xcode-tools.nix index 8d8cadd3a4..bcfe128f4d 100644 --- a/nix/lib/packages/xcode-tools.nix +++ b/nix/lib/packages/xcode-tools.nix @@ -2,9 +2,9 @@ # xcframework assembly — everything `xcodebuild -create-xcframework` and the # .framework bundling in nix/package/ios-xcframework need on one PATH. # -# Must be called through a cross-pkgs whose system config sets xcodeVer -# (ios-packages.nix pins 26.3); otherwise darwin.xcode falls back to the -# nixpkgs default xcode_12_3 and downstream builds break. +# Callable from native pkgs: the Xcode version is an explicit argument +# (resolving via crossSystem.xcodeVer would force a shadow cross pkgset +# whose stdenv rebuilds the world from source just for packaging tools). { lib, runCommand, @@ -12,22 +12,37 @@ cctools, rcodesign, darwin, + xcodeVer, }: let + xcode = darwin."xcode_${lib.replaceStrings [ "." ] [ "_" ] xcodeVer}"; # Xcode binaries live under Contents/Developer/usr/bin/, not bin/ — lift them # into a flat bin/ so the standard PATH hook picks them up. xcode-bins = runCommand "xcode-bins" { } '' mkdir -p $out/bin - ln -s ${darwin.xcode}/Contents/Developer/usr/bin/* $out/bin/ + ln -s ${xcode}/Contents/Developer/usr/bin/* $out/bin/ + ''; + # Only the cctools we actually invoke — the full package would put raw + # ar/ld/nm/strip on PATH ahead of the stdenv's wrapped toolchain. + cctools-bins = runCommand "cctools-bins" { } '' + mkdir -p $out/bin + ln -s ${cctools}/bin/lipo ${cctools}/bin/install_name_tool ${cctools}/bin/otool $out/bin/ ''; in symlinkJoin { name = "xcode-tools"; paths = [ - cctools + cctools-bins rcodesign xcode-bins ]; + # xcodebuild resolves its developer dir via xcode-select/DEVELOPER_DIR, + # not the binary's location — point it at our Xcode, not the sandbox's. + postBuild = '' + mkdir -p $out/nix-support + echo 'export DEVELOPER_DIR="${xcode}/Contents/Developer"' \ + > $out/nix-support/setup-hook + ''; meta = { description = "Apple toolchain (Xcode + cctools + rcodesign) for hermetic xcframework builds"; platforms = lib.platforms.darwin; diff --git a/nix/lib/shell-common.nix b/nix/lib/shell-common.nix index 2d0ed2dc34..9b3f09df74 100644 --- a/nix/lib/shell-common.nix +++ b/nix/lib/shell-common.nix @@ -31,6 +31,7 @@ gh, jq, curl, + git, git-cliff, graphite-cli, toxiproxy, @@ -69,6 +70,10 @@ in { # Core Rust build environment: env vars, hardening, native deps, LD_LIBRARY_PATH rustBase = xmtp.base.commonArgs // { + # nix-built git: shells export LD_LIBRARY_PATH with nix openssl/zlib + # (new glibc), which breaks the runner's system git during eval-time + # builtins.fetchGit (crane git deps). Keep a matching git on PATH. + nativeBuildInputs = (xmtp.base.commonArgs.nativeBuildInputs or [ ]) ++ [ git ]; env = { OPENSSL_DIR = "${openssl.dev}"; OPENSSL_LIB_DIR = "${lib.getLib openssl}/lib"; @@ -156,6 +161,10 @@ in gh jq curl + # nix-built git: the shell's LD_LIBRARY_PATH puts nix openssl/zlib + # (glibc 2.42) in front of the runner's system git, which then fails + # with GLIBC_ABI_DT_X86_64_PLT loader errors during crane git fetches. + git git-cliff # changelog generation + print-only version oracle (release flow) graphite-cli toxiproxy diff --git a/nix/package/android.nix b/nix/package/android.nix index 56f29c73cc..d849fc7210 100644 --- a/nix/package/android.nix +++ b/nix/package/android.nix @@ -15,6 +15,12 @@ let # set buildInputs to empty to force the android build to link against libraries in the NDK sysroot instead # of nix library path. This avoids compiling C dependencies by using android-native libs and having to patch nixpkgs packages. buildInputs = [ ]; + # commonArgs pins openssl-sys to nixpkgs' host OpenSSL; Android must + # vendor instead (NDK sysroot has no OpenSSL). Nulls drop the vars. + OPENSSL_NO_VENDOR = null; + OPENSSL_DIR = null; + OPENSSL_LIB_DIR = null; + OPENSSL_INCLUDE_DIR = null; }; cargoArtifacts = xmtp.base.mkCargoArtifacts rust false specialArgs; diff --git a/nix/package/ios-bindings.nix b/nix/package/ios-bindings.nix index 6d4892bd99..d1a7076f8d 100644 --- a/nix/package/ios-bindings.nix +++ b/nix/package/ios-bindings.nix @@ -42,9 +42,10 @@ let mv $out/swift/xmtpv3FFI.h $out/swift/include/libxmtp/ mv $out/swift/xmtpv3FFI.modulemap $out/swift/include/libxmtp/module.modulemap - # Generate version file + # Generate version file. Commit date, not wall clock — keeps the + # output reproducible across days. echo "Version: ${version}" > $out/libxmtp-version.txt - echo "Date: $(date -u +%Y-%m-%d)" >> $out/libxmtp-version.txt + echo "Date: ${xmtp.gitCommitDate}" >> $out/libxmtp-version.txt ''; }; in diff --git a/nix/package/ios-xcframework/helpers.nix b/nix/package/ios-xcframework/helpers.nix index ec87f03e74..48ed143532 100644 --- a/nix/package/ios-xcframework/helpers.nix +++ b/nix/package/ios-xcframework/helpers.nix @@ -38,41 +38,69 @@ rec { # lib.generators.toPlist. Keeps escaping and formatting consistent with nixpkgs # plist conventions instead of an inline heredoc. mkInfoPlist = - minOSVersion: + minOSVersion: isMacOS: writeText "Info.plist" ( - lib.generators.toPlist { escape = true; } { - CFBundleExecutable = "xmtpv3FFI"; - CFBundleIdentifier = "org.xmtp.xmtpv3FFI"; - CFBundleInfoDictionaryVersion = "6.0"; - CFBundleName = "xmtpv3FFI"; - CFBundlePackageType = "FMWK"; - CFBundleVersion = "1"; - CFBundleShortVersionString = "1.0"; - MinimumOSVersion = minOSVersion; - } + lib.generators.toPlist { escape = true; } ( + { + CFBundleExecutable = "xmtpv3FFI"; + CFBundleIdentifier = "org.xmtp.xmtpv3FFI"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundleName = "xmtpv3FFI"; + CFBundlePackageType = "FMWK"; + CFBundleVersion = "1"; + CFBundleShortVersionString = "1.0"; + } + # macOS frameworks declare LSMinimumSystemVersion; iOS uses + # MinimumOSVersion. + // ( + if isMacOS then { LSMinimumSystemVersion = minOSVersion; } else { MinimumOSVersion = minOSVersion; } + ) + ) ); # Wrap a (possibly lipo'd) dylib in a .framework bundle for - # xcodebuild -create-xcframework -framework . + # xcodebuild -create-xcframework -framework . iOS frameworks are + # flat; macOS frameworks require the versioned Versions/A layout with + # top-level symlinks and Info.plist under Resources/. mkFrameworkBundle = { name, dylibPath, minOSVersion, headerDir, + isMacOS ? false, }: + let + fw = "$TMPDIR/${name}/xmtpv3FFI.framework"; + contentDir = if isMacOS then "${fw}/Versions/A" else fw; + plistDest = if isMacOS then "${contentDir}/Resources/Info.plist" else "${fw}/Info.plist"; + dylibId = + if isMacOS then + "@rpath/xmtpv3FFI.framework/Versions/A/xmtpv3FFI" + else + "@rpath/xmtpv3FFI.framework/xmtpv3FFI"; + in '' echo "Building framework bundle: ${name}" - mkdir -p $TMPDIR/${name}/xmtpv3FFI.framework/Headers - mkdir -p $TMPDIR/${name}/xmtpv3FFI.framework/Modules - cp ${dylibPath} $TMPDIR/${name}/xmtpv3FFI.framework/xmtpv3FFI - install_name_tool -id @rpath/xmtpv3FFI.framework/xmtpv3FFI \ - $TMPDIR/${name}/xmtpv3FFI.framework/xmtpv3FFI - cp ${headerDir}/*.h $TMPDIR/${name}/xmtpv3FFI.framework/Headers/ + mkdir -p ${contentDir}/Headers ${contentDir}/Modules + ${lib.optionalString isMacOS "mkdir -p ${contentDir}/Resources"} + cp ${dylibPath} ${contentDir}/xmtpv3FFI + # store copies are read-only; install_name_tool and rcodesign both + # rewrite the binary in place + chmod u+w ${contentDir}/xmtpv3FFI + install_name_tool -id ${dylibId} ${contentDir}/xmtpv3FFI + cp ${headerDir}/*.h ${contentDir}/Headers/ sed 's/^module /framework module /' \ ${headerDir}/module.modulemap \ - > $TMPDIR/${name}/xmtpv3FFI.framework/Modules/module.modulemap - cp ${mkInfoPlist minOSVersion} $TMPDIR/${name}/xmtpv3FFI.framework/Info.plist - rcodesign sign $TMPDIR/${name}/xmtpv3FFI.framework + > ${contentDir}/Modules/module.modulemap + cp ${mkInfoPlist minOSVersion isMacOS} ${plistDest} + ${lib.optionalString isMacOS '' + ln -s A ${fw}/Versions/Current + ln -s Versions/Current/xmtpv3FFI ${fw}/xmtpv3FFI + ln -s Versions/Current/Headers ${fw}/Headers + ln -s Versions/Current/Modules ${fw}/Modules + ln -s Versions/Current/Resources ${fw}/Resources + ''} + rcodesign sign ${fw} ''; } diff --git a/nix/package/ios-xcframework/mk-dynamic.nix b/nix/package/ios-xcframework/mk-dynamic.nix index 0bd3c77ae4..6ed7a0539f 100644 --- a/nix/package/ios-xcframework/mk-dynamic.nix +++ b/nix/package/ios-xcframework/mk-dynamic.nix @@ -9,6 +9,10 @@ dylibs, swiftBindings, version, + # Advertised minimum iOS version — thread from iosCommon.darwinMinVersion + # so the plists never drift from what the binaries are built with. + iosMinVersion, + macMinVersion, }: let inherit (helpers.classifyTargets abis) @@ -25,6 +29,10 @@ stdenv.mkDerivation { dontUnpack = true; dontFixup = true; nativeBuildInputs = [ xcode-tools ]; + # xcodebuild dlopens plugins linked against /Library/Developer + # PrivateFrameworks (installed by Xcode's first-launch on the builder); + # bind just that path into the sandbox instead of going __noChroot. + __impureHostDeps = [ "/Library/Developer/PrivateFrameworks" ]; installPhase = '' set -euo pipefail echo "=== Building dynamic xcframework ===" @@ -46,7 +54,7 @@ stdenv.mkDerivation { helpers.mkFrameworkBundle { name = "fw_ios"; dylibPath = "${dylibs.iphone64}/libxmtpv3.dylib"; - minOSVersion = "14.0"; + minOSVersion = iosMinVersion; inherit headerDir; } )} @@ -54,7 +62,7 @@ stdenv.mkDerivation { helpers.mkFrameworkBundle { name = "fw_sim"; dylibPath = "$TMPDIR/lipo_sim/libxmtpv3.dylib"; - minOSVersion = "14.0"; + minOSVersion = iosMinVersion; inherit headerDir; } )} @@ -62,7 +70,8 @@ stdenv.mkDerivation { helpers.mkFrameworkBundle { name = "fw_mac"; dylibPath = "$TMPDIR/lipo_macos/libxmtpv3.dylib"; - minOSVersion = "11.0"; + minOSVersion = macMinVersion; + isMacOS = true; inherit headerDir; } )} @@ -88,16 +97,25 @@ stdenv.mkDerivation { for fw in $out/LibXMTPSwiftFFIDynamic.xcframework/*/xmtpv3FFI.framework; do test -d "$fw" || continue FOUND=$((FOUND + 1)) + # macOS slices use the versioned bundle layout (Versions/A + symlinks, + # Info.plist under Resources/); iOS slices are flat. + if [ -d "$fw/Versions" ]; then + PLIST="$fw/Resources/Info.plist" + WANT_ID="@rpath/xmtpv3FFI.framework/Versions/A/xmtpv3FFI" + else + PLIST="$fw/Info.plist" + WANT_ID="@rpath/xmtpv3FFI.framework/xmtpv3FFI" + fi test -f "$fw/xmtpv3FFI" || { echo "FAIL: Missing binary in $fw"; exit 1; } - test -f "$fw/Info.plist" || { echo "FAIL: Missing Info.plist in $fw"; exit 1; } + test -f "$PLIST" || { echo "FAIL: Missing Info.plist in $fw"; exit 1; } test -d "$fw/Headers" || { echo "FAIL: Missing Headers in $fw"; exit 1; } test -f "$fw/Headers/xmtpv3FFI.h" || { echo "FAIL: Missing xmtpv3FFI.h in $fw"; exit 1; } test -f "$fw/Modules/module.modulemap" || { echo "FAIL: Missing modulemap in $fw"; exit 1; } head -1 "$fw/Modules/module.modulemap" | grep -q "^framework module xmtpv3FFI" || \ { echo "FAIL: modulemap missing 'framework module' prefix in $fw"; exit 1; } INSTALL_NAME=$(otool -D "$fw/xmtpv3FFI" | tail -1) - echo "$INSTALL_NAME" | grep -q "@rpath/xmtpv3FFI.framework/xmtpv3FFI" || \ - { echo "FAIL: Bad install name '$INSTALL_NAME' in $fw"; exit 1; } + echo "$INSTALL_NAME" | grep -q "$WANT_ID" || \ + { echo "FAIL: Bad install name '$INSTALL_NAME' in $fw (want $WANT_ID)"; exit 1; } echo " dynamic OK: $(basename $(dirname $fw))" done [ "$FOUND" -ge ${toString expectedSlices} ] || \ diff --git a/nix/package/ios-xcframework/mk-static.nix b/nix/package/ios-xcframework/mk-static.nix index c65566326d..b73b190d6b 100644 --- a/nix/package/ios-xcframework/mk-static.nix +++ b/nix/package/ios-xcframework/mk-static.nix @@ -25,6 +25,10 @@ stdenv.mkDerivation { dontUnpack = true; dontFixup = true; nativeBuildInputs = [ xcode-tools ]; + # xcodebuild dlopens plugins linked against /Library/Developer + # PrivateFrameworks (installed by Xcode's first-launch on the builder); + # bind just that path into the sandbox instead of going __noChroot. + __impureHostDeps = [ "/Library/Developer/PrivateFrameworks" ]; installPhase = '' set -euo pipefail echo "=== Building static xcframework ===" diff --git a/nix/package/mls_validation_service.nix b/nix/package/mls_validation_service.nix index 20b404a933..7e86892761 100644 --- a/nix/package/mls_validation_service.nix +++ b/nix/package/mls_validation_service.nix @@ -3,6 +3,7 @@ lib, stdenv, openssl, + sqlcipher, zstd, }: let @@ -16,10 +17,11 @@ let specialArgs = lib.optionalAttrs stdenv.hostPlatform.isMusl { RUSTFLAGS = "-C target-feature=+crt-static"; - # mls validation srevice does not need sqlite since it does not - # depend on xmtp_db + # The binary doesn't use xmtp_db, but cargoArtifacts deps-builds the + # whole workspace (incl. libsqlite3-sys), so sqlcipher must stay. buildInputs = [ openssl + sqlcipher zstd ]; }; diff --git a/nix/package/nextest.nix b/nix/package/nextest.nix index 59fbdbc74f..c372798c0e 100644 --- a/nix/package/nextest.nix +++ b/nix/package/nextest.nix @@ -31,6 +31,9 @@ let nativeBuildInputs = xmtp.base.commonArgs.nativeBuildInputs ++ [ cargo-llvm-cov ]; + # Test executables link the nix openssl/sqlcipher dynamically + # (OPENSSL_NO_VENDOR) and cargo doesn't rpath — resolve at run time. + LD_LIBRARY_PATH = xmtp.base.runtimeLibPath; }; cargoArtifacts = xmtp.base.mkCargoArtifacts rust false ( diff --git a/nix/package/xdbg.nix b/nix/package/xdbg.nix index 5dc7a83417..71a4c9cd5c 100644 --- a/nix/package/xdbg.nix +++ b/nix/package/xdbg.nix @@ -29,5 +29,10 @@ rust.buildPackage ( version = xmtp.mkVersion rust; pname = "xdbg"; cargoExtraArgs = "-p xdbg"; + # Record the dynamic-openssl rpath so the binary runs outside a dev + # shell (`nix run`, cross-version/cross-talk tests). + postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' + patchelf --add-rpath ${base.runtimeLibPath} $out/bin/xdbg + ''; } ) diff --git a/nix/package/xnet-cli.nix b/nix/package/xnet-cli.nix index 84a64b8361..54df818e8b 100644 --- a/nix/package/xnet-cli.nix +++ b/nix/package/xnet-cli.nix @@ -29,5 +29,9 @@ rust.buildPackage ( NIX_GIT_COMMIT_DATE = xmtp.gitCommitDate; pname = "xnet-cli"; cargoExtraArgs = "-p xnet-cli"; + # Record the dynamic-openssl rpath so the binary runs outside a dev shell. + postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' + patchelf --add-rpath ${base.runtimeLibPath} $out/bin/xnet-cli + ''; } ) diff --git a/nix/shells/android.nix b/nix/shells/android.nix index 6d94825013..1e63237ffa 100644 --- a/nix/shells/android.nix +++ b/nix/shells/android.nix @@ -37,7 +37,9 @@ mkShell ( zlib ]; - inherit (base.commonArgs) nativeBuildInputs; + # rustBase, not commonArgs: carries the nix git needed for eval-time + # builtins.fetchGit under the shell's LD_LIBRARY_PATH. + inherit (shellCommon.rustBase) nativeBuildInputs; buildInputs = base.commonArgs.buildInputs diff --git a/sdks/ios/dev/.ensure-xcode b/sdks/ios/dev/.ensure-xcode index e99b6ec07b..e2fa21913e 100755 --- a/sdks/ios/dev/.ensure-xcode +++ b/sdks/ios/dev/.ensure-xcode @@ -6,36 +6,65 @@ # must be imported manually — Apple's license forbids redistribution, so no # binary cache can serve it. # -# Local dev: imports /Applications/Xcode_.app on first run (~5 min). -# Install it with `xcodes install ` if missing. +# Local dev: imports the matching /Applications/Xcode*.app on first run +# (~5 min). Installs via `xcodes` (with consent) if missing. # CI: the xmtp-cache-apple action pre-imports Xcode; this script # only verifies and fails fast with a pointer if it didn't. # -# XCODE_VERSION must match xcodeVer in nix/ios-packages.nix (and the -# LOCAL examples.nix bump on the nixpkgs branch). +# XCODE_VERSION must match xcodeVer in nix/ios-packages.nix. set -euo pipefail source "$(dirname "$0")/.setup" +# Escape hatch for Macs that delegate builds to remote Darwin builders +# (which hold their own Xcode import) — no local Xcode needed. +[[ "${XMTP_SKIP_ENSURE_XCODE:-}" == "1" ]] && exit 0 + XCODE_VERSION="${XCODE_VERSION:-26.3}" -XCODE_APP="${XCODE_APP:-/Applications/Xcode_${XCODE_VERSION}.app}" XCODE_ATTR="xcode_${XCODE_VERSION//./_}" -expected=$(NIXPKGS_ALLOW_UNFREE=1 nix eval --impure --raw \ - --inputs-from "$ROOT" "nixpkgs#darwin.${XCODE_ATTR}.outPath") +# Find an /Applications/Xcode*.app whose version matches (covers Xcode.app, +# Xcode_.app, and xcodes' Xcode-.app naming). Echoes the path. +find_xcode_bundle() { + local candidate ver + for candidate in /Applications/Xcode*.app; do + [[ -d "$candidate" ]] || continue + ver=$(defaults read "$candidate/Contents/version" CFBundleShortVersionString 2>/dev/null || true) + if [[ "$ver" == "$XCODE_VERSION" || "$ver" == "$XCODE_VERSION".* ]]; then + echo "$candidate" + return 0 + fi + done + return 1 +} + +# Cache the expected-path eval (~2-5s per run). Keyed on version + +# flake.lock checksum so a nixpkgs bump invalidates the cache. +lock_sum=$(cksum "$ROOT/flake.lock" | cut -d' ' -f1) +cache_file="${XDG_CACHE_HOME:-$HOME/.cache}/xmtp/xcode-store-path-${XCODE_VERSION}-${lock_sum}" +if [[ -r "$cache_file" ]]; then + expected=$(<"$cache_file") +else + expected=$(NIXPKGS_ALLOW_UNFREE=1 nix eval --impure --raw \ + --inputs-from "$ROOT" "nixpkgs#darwin.${XCODE_ATTR}.outPath") + mkdir -p "$(dirname "$cache_file")" + printf '%s' "$expected" > "$cache_file" +fi if nix path-info "$expected" >/dev/null 2>&1; then exit 0 fi -if [[ "${CI:-}" == "true" ]]; then +if [[ -n "${CI:-}" ]]; then echo "ERROR: Xcode missing from Nix store in CI: $expected" >&2 echo "The xmtp-cache-apple action should import it before this step." >&2 exit 1 fi +XCODE_APP="${XCODE_APP:-$(find_xcode_bundle || true)}" + if [[ ! -d "$XCODE_APP" ]]; then - echo "Xcode ${XCODE_VERSION} not found at ${XCODE_APP}." + echo "Xcode ${XCODE_VERSION} not found in /Applications." echo "⚠️ Installing via xcodes prompts for your Apple ID credentials" echo " and downloads ~5 GB from Apple." read -p "Install Xcode ${XCODE_VERSION} now? (y/N) " -n 1 -r; echo @@ -45,11 +74,8 @@ if [[ ! -d "$XCODE_APP" ]]; then else nix run nixpkgs#xcodes -- install "${XCODE_VERSION}" fi - # xcodes may name the bundle Xcode-.app; pick up whatever landed. + XCODE_APP=$(find_xcode_bundle || true) if [[ ! -d "$XCODE_APP" ]]; then - XCODE_APP=$(ls -d /Applications/Xcode*"${XCODE_VERSION}"*.app 2>/dev/null | head -1) - fi - if [[ -z "$XCODE_APP" || ! -d "$XCODE_APP" ]]; then echo "ERROR: xcodes finished but no Xcode ${XCODE_VERSION} bundle found in /Applications." >&2 exit 1 fi @@ -59,11 +85,12 @@ echo "Importing ${XCODE_APP} into /nix/store (one-time per Xcode version, ~5 min got=$(nix store add --name Xcode.app "$XCODE_APP") if [[ "$got" != "$expected" ]]; then - echo "WARN: imported $got" >&2 - echo " but the flake expects $expected" >&2 - echo " Your Xcode bundle differs from the hash pinned in nixpkgs" >&2 - echo " (pkgs/os-specific/darwin/xcode/default.nix). Re-download via" >&2 - echo " xcodes, or update the pinned hash for ${XCODE_ATTR}." >&2 + echo "ERROR: imported ${XCODE_APP} as $got" >&2 + echo " but the flake expects $expected" >&2 + echo " The bundle's contents differ from the hash pinned in nixpkgs" >&2 + echo " (pkgs/os-specific/darwin/xcode/default.nix) even though its" >&2 + echo " version matches. Re-download via xcodes, or update the" >&2 + echo " pinned hash for ${XCODE_ATTR}." >&2 exit 1 fi echo "Imported: $got" diff --git a/sdks/ios/dev/bindings b/sdks/ios/dev/bindings index 60f155354d..7369940aee 100755 --- a/sdks/ios/dev/bindings +++ b/sdks/ios/dev/bindings @@ -3,8 +3,13 @@ source "$(dirname "$0")/.setup" # Darwin only: Linux workstations build via remote Darwin builders, which # hold their own Xcode import. +ATTR_PREFIX="" if [[ "$(uname -s)" == "Darwin" ]]; then "$(dirname "$0")/.ensure-xcode" +else + # iOS packages are only exposed on Darwin systems; from Linux, target + # the Darwin outputs explicitly (built on remote Darwin builders). + ATTR_PREFIX="packages.aarch64-darwin." fi SWIFT_OUT="${ROOT}/bindings/mobile/build/swift" @@ -22,7 +27,7 @@ fi if [[ "${1:-}" == "--release" ]]; then echo "Building iOS xcframeworks (all targets)..." - nix build "${ROOT}#ios-release" --out-link "${ROOT}/bindings/mobile/build/release" + nix build "${ROOT}#${ATTR_PREFIX}ios-release" --out-link "${ROOT}/bindings/mobile/build/release" rm -rf "$SWIFT_OUT" mkdir -p "$SWIFT_OUT" cp -rL "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFI/LibXMTPSwiftFFI.xcframework" "$SWIFT_OUT/" @@ -34,7 +39,7 @@ else echo "Building iOS xcframeworks (simulator + macOS only)..." # Materialize the xcframework instead of symlinking --- rm -rf "$SWIFT_OUT" - nix build "${ROOT}#ios-devFast" \ + nix build "${ROOT}#${ATTR_PREFIX}ios-devFast" \ --out-link "${ROOT}/bindings/mobile/build/nix-fast" mkdir -p "$SWIFT_OUT" cp -rL "${ROOT}/bindings/mobile/build/nix-fast/LibXMTPSwiftFFI.xcframework" "$SWIFT_OUT/" From bb90eae260338cbbf615c96ffbb01fbc51df912f Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Thu, 2 Jul 2026 11:38:42 -0400 Subject: [PATCH 08/11] chore(nix): bump iOS nixpkgs patch to latest staging rebase Same stack applied to the same nixos-unstable input; existing cross drvs are unchanged. --- nix/lib/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/lib/default.nix b/nix/lib/default.nix index d333570f22..a142838c8c 100644 --- a/nix/lib/default.nix +++ b/nix/lib/default.nix @@ -111,8 +111,8 @@ in src = inputs.nixpkgs; patches = [ (hostPkgs.fetchpatch2 { - url = "https://github.com/NixOS/nixpkgs/compare/2f51ad37d9416828be4be7f48e7617b01cdf0641...insipx:nixpkgs:4f8f394b62476598617a085acda285902d5998ce.patch"; - hash = "sha256-QzRz77CLCzm+xKAezYctSL7hHQF2IBKt6iqMOD2avg8="; + url = "https://github.com/NixOS/nixpkgs/compare/af9204149566f50f1c12f1b9522b1d606a404f0c...insipx:nixpkgs:e550a4f3a5e0fa9c1c7b06519a5369873b90b4e4.patch"; + hash = "sha256-qkT2/kVBr9I+WeMSJ4l6aiDmrA/mz/+8snW5bCbc76Y="; }) ]; }; From 7d49823e4002496ebc844eeca63d09699f928e9a Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Thu, 2 Jul 2026 11:42:56 -0400 Subject: [PATCH 09/11] feat(ios): assemble xcframeworks without xcodebuild An xcframework is one directory per slice plus an Info.plist manifest; write it directly with cctools + rcodesign. xcodebuild -create-xcframework dlopens host Xcode first-launch frameworks (CoreDevice -> MobileDevice), which cannot be bound into the build sandbox. Drops the xcode-tools bundle and the __impureHostDeps escape hatch. --- .github/actions/setup-ios-xcode/action.yml | 2 +- nix/ios-packages.nix | 11 +-- nix/lib/packages/xcode-tools.nix | 50 ------------- nix/package/ios-xcframework/default.nix | 15 ++-- nix/package/ios-xcframework/helpers.nix | 56 +++++++++++++- nix/package/ios-xcframework/mk-dynamic.nix | 85 ++++++++++++++++------ nix/package/ios-xcframework/mk-static.nix | 79 ++++++++++++++------ 7 files changed, 184 insertions(+), 114 deletions(-) delete mode 100644 nix/lib/packages/xcode-tools.nix diff --git a/.github/actions/setup-ios-xcode/action.yml b/.github/actions/setup-ios-xcode/action.yml index 8846ef3416..afd7129528 100644 --- a/.github/actions/setup-ios-xcode/action.yml +++ b/.github/actions/setup-ios-xcode/action.yml @@ -35,7 +35,7 @@ runs: # xcode_26_3 changes, never with nixpkgs/flake.lock bumps. xcode-nix-path: /nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app # Point non-nix tool invocations (raw xcodebuild/swift steps) at the - # pinned Xcode too; nix builds get this via the xcode-tools setup hook. + # pinned Xcode; nix builds resolve it through the flake instead. - name: Export DEVELOPER_DIR shell: bash run: echo "DEVELOPER_DIR=/nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app/Contents/Developer" >> "$GITHUB_ENV" diff --git a/nix/ios-packages.nix b/nix/ios-packages.nix index 3186e04bfd..3428c21149 100644 --- a/nix/ios-packages.nix +++ b/nix/ios-packages.nix @@ -64,14 +64,9 @@ else throw "Unsupported host architecture for ios-libs-fast"; - # xcframework derivations are host-side packaging tools — call them - # through NATIVE pkgs (substitutable stdenv) and resolve Xcode - # explicitly. A cross pkgset with xcodeVer would rebuild its whole - # bootstrap from source just to run lipo/xcodebuild. - xcode-tools = pkgs.callPackage ./lib/packages/xcode-tools.nix { - inherit (iosCommon) xcodeVer; - }; - xcframework = pkgs.callPackage ./package/ios-xcframework { inherit xcode-tools; }; + # xcframework derivations are host-side packaging (lipo/plist/sign) — + # call them through NATIVE pkgs so their stdenv stays substitutable. + xcframework = pkgs.callPackage ./package/ios-xcframework { }; allAbis = lib.attrNames iosDylibs; fastAbis = [ diff --git a/nix/lib/packages/xcode-tools.nix b/nix/lib/packages/xcode-tools.nix deleted file mode 100644 index bcfe128f4d..0000000000 --- a/nix/lib/packages/xcode-tools.nix +++ /dev/null @@ -1,50 +0,0 @@ -# Apple toolchain bundle (Xcode + cctools + rcodesign) for hermetic -# xcframework assembly — everything `xcodebuild -create-xcframework` and the -# .framework bundling in nix/package/ios-xcframework need on one PATH. -# -# Callable from native pkgs: the Xcode version is an explicit argument -# (resolving via crossSystem.xcodeVer would force a shadow cross pkgset -# whose stdenv rebuilds the world from source just for packaging tools). -{ - lib, - runCommand, - symlinkJoin, - cctools, - rcodesign, - darwin, - xcodeVer, -}: -let - xcode = darwin."xcode_${lib.replaceStrings [ "." ] [ "_" ] xcodeVer}"; - # Xcode binaries live under Contents/Developer/usr/bin/, not bin/ — lift them - # into a flat bin/ so the standard PATH hook picks them up. - xcode-bins = runCommand "xcode-bins" { } '' - mkdir -p $out/bin - ln -s ${xcode}/Contents/Developer/usr/bin/* $out/bin/ - ''; - # Only the cctools we actually invoke — the full package would put raw - # ar/ld/nm/strip on PATH ahead of the stdenv's wrapped toolchain. - cctools-bins = runCommand "cctools-bins" { } '' - mkdir -p $out/bin - ln -s ${cctools}/bin/lipo ${cctools}/bin/install_name_tool ${cctools}/bin/otool $out/bin/ - ''; -in -symlinkJoin { - name = "xcode-tools"; - paths = [ - cctools-bins - rcodesign - xcode-bins - ]; - # xcodebuild resolves its developer dir via xcode-select/DEVELOPER_DIR, - # not the binary's location — point it at our Xcode, not the sandbox's. - postBuild = '' - mkdir -p $out/nix-support - echo 'export DEVELOPER_DIR="${xcode}/Contents/Developer"' \ - > $out/nix-support/setup-hook - ''; - meta = { - description = "Apple toolchain (Xcode + cctools + rcodesign) for hermetic xcframework builds"; - platforms = lib.platforms.darwin; - }; -} diff --git a/nix/package/ios-xcframework/default.nix b/nix/package/ios-xcframework/default.nix index d13c2be2df..2812ab82b0 100644 --- a/nix/package/ios-xcframework/default.nix +++ b/nix/package/ios-xcframework/default.nix @@ -1,20 +1,21 @@ # iOS xcframework assembly — static, dynamic, release, and dev outputs. # -# Hermetic: no __noChroot. mk-static and mk-dynamic depend on xcode-tools -# (Xcode + cctools + rcodesign); xcode-tools is passed in via callPackage from -# ios-packages.nix using a cross-pkgs whose system config sets xcodeVer. +# Hermetic: the xcframework bundle is assembled by hand (one directory per +# slice plus an Info.plist manifest) instead of via +# `xcodebuild -create-xcframework`, which dlopens host Xcode first-launch +# frameworks and so can't run inside the build sandbox. Only cctools +# (lipo/install_name_tool/otool) and rcodesign are needed — no Xcode. # See docs/specs/2026-05-06-ios-xcframework-redesign.md. { lib, pkgs, callPackage, - xcode-tools, }: let helpers = callPackage ./helpers.nix { }; - # Inject xcode-tools and helpers into the callPackage scope so each mk-*.nix - # can request them by name without explicit `inherit` plumbing at every call. - callPackage' = lib.callPackageWith (pkgs // { inherit xcode-tools helpers; }); + # Inject helpers into the callPackage scope so each mk-*.nix can request + # it by name without explicit `inherit` plumbing at every call. + callPackage' = lib.callPackageWith (pkgs // { inherit helpers; }); in { mkStatic = callPackage' ./mk-static.nix { }; diff --git a/nix/package/ios-xcframework/helpers.nix b/nix/package/ios-xcframework/helpers.nix index 48ed143532..f12a69f06f 100644 --- a/nix/package/ios-xcframework/helpers.nix +++ b/nix/package/ios-xcframework/helpers.nix @@ -18,6 +18,54 @@ rec { ]; }; + # Map an ABI key to its Mach-O architecture name. + abiArch = abi: if lib.hasPrefix "x86_64" abi then "x86_64" else "arm64"; + + # Slice metadata mirroring xcodebuild's LibraryIdentifier convention: + # -[_…][-], archs sorted alphabetically. + mkSlice = + { + platform, + abis, + variant ? null, + }: + let + archs = lib.naturalSort (lib.unique (map abiArch abis)); + in + { + inherit platform variant archs; + id = lib.concatStringsSep "-" ( + [ + platform + (lib.concatStringsSep "_" archs) + ] + ++ lib.optional (variant != null) variant + ); + }; + + # Top-level xcframework manifest — the only thing + # `xcodebuild -create-xcframework` adds beyond copying slices into place. + mkXCFrameworkPlist = + slices: + writeText "xcframework-Info.plist" ( + lib.generators.toPlist { escape = true; } { + AvailableLibraries = map ( + s: + { + BinaryPath = s.binaryPath; + LibraryIdentifier = s.id; + LibraryPath = s.libraryPath; + SupportedArchitectures = s.archs; + SupportedPlatform = s.platform; + } + // lib.optionalAttrs (s ? headersPath) { HeadersPath = s.headersPath; } + // lib.optionalAttrs (s.variant != null) { SupportedPlatformVariant = s.variant; } + ) slices; + CFBundlePackageType = "XFWK"; + XCFrameworkFormatVersion = "1.0"; + } + ); + # Lipo a list of per-ABI artifacts (.a or .dylib) into a single fat binary. # Emits empty shell when group is empty. mkLipoSnippet = @@ -58,10 +106,10 @@ rec { ) ); - # Wrap a (possibly lipo'd) dylib in a .framework bundle for - # xcodebuild -create-xcframework -framework . iOS frameworks are - # flat; macOS frameworks require the versioned Versions/A layout with - # top-level symlinks and Info.plist under Resources/. + # Wrap a (possibly lipo'd) dylib in a .framework bundle, one per + # xcframework slice. iOS frameworks are flat; macOS frameworks require + # the versioned Versions/A layout with top-level symlinks and + # Info.plist under Resources/. mkFrameworkBundle = { name, diff --git a/nix/package/ios-xcframework/mk-dynamic.nix b/nix/package/ios-xcframework/mk-dynamic.nix index 6ed7a0539f..fbfc06e1b5 100644 --- a/nix/package/ios-xcframework/mk-dynamic.nix +++ b/nix/package/ios-xcframework/mk-dynamic.nix @@ -1,7 +1,8 @@ { lib, stdenv, - xcode-tools, + cctools, + rcodesign, helpers, }: { @@ -22,17 +23,61 @@ let expectedSlices ; headerDir = "${swiftBindings}/swift/include/libxmtp"; + + slices = + lib.optional (deviceAbis != [ ]) ( + helpers.mkSlice { + platform = "ios"; + abis = deviceAbis; + } + // { + srcFw = "$TMPDIR/fw_ios/xmtpv3FFI.framework"; + } + ) + ++ lib.optional (simAbis != [ ]) ( + helpers.mkSlice { + platform = "ios"; + abis = simAbis; + variant = "simulator"; + } + // { + srcFw = "$TMPDIR/fw_sim/xmtpv3FFI.framework"; + } + ) + ++ lib.optional (macAbis != [ ]) ( + helpers.mkSlice { + platform = "macos"; + abis = macAbis; + } + // { + srcFw = "$TMPDIR/fw_mac/xmtpv3FFI.framework"; + } + ); + + infoPlist = helpers.mkXCFrameworkPlist ( + map ( + s: + s + // { + libraryPath = "xmtpv3FFI.framework"; + binaryPath = + if s.platform == "macos" then + "xmtpv3FFI.framework/Versions/A/xmtpv3FFI" + else + "xmtpv3FFI.framework/xmtpv3FFI"; + } + ) slices + ); in stdenv.mkDerivation { pname = "xmtpv3-dynamic-xcframework"; inherit version; dontUnpack = true; dontFixup = true; - nativeBuildInputs = [ xcode-tools ]; - # xcodebuild dlopens plugins linked against /Library/Developer - # PrivateFrameworks (installed by Xcode's first-launch on the builder); - # bind just that path into the sandbox instead of going __noChroot. - __impureHostDeps = [ "/Library/Developer/PrivateFrameworks" ]; + nativeBuildInputs = [ + cctools + rcodesign + ]; installPhase = '' set -euo pipefail echo "=== Building dynamic xcframework ===" @@ -76,25 +121,21 @@ stdenv.mkDerivation { } )} - XCF_ARGS="" - ${lib.optionalString (deviceAbis != [ ]) '' - XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_ios/xmtpv3FFI.framework" - ''} - ${lib.optionalString (simAbis != [ ]) '' - XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_sim/xmtpv3FFI.framework" - ''} - ${lib.optionalString (macAbis != [ ]) '' - XCF_ARGS="$XCF_ARGS -framework $TMPDIR/fw_mac/xmtpv3FFI.framework" - ''} - - mkdir -p $out - xcodebuild -create-xcframework \ - $XCF_ARGS \ - -output $out/LibXMTPSwiftFFIDynamic.xcframework + # Assembled by hand: an xcframework is one directory per slice plus a + # manifest. `xcodebuild -create-xcframework` does the same job but + # dlopens host Xcode first-launch frameworks, which can't be sandboxed. + # cp -R (not -rL) so the macOS bundle's Versions/ symlinks survive. + XCF=$out/LibXMTPSwiftFFIDynamic.xcframework + ${lib.concatMapStrings (s: '' + mkdir -p $XCF/${s.id} + cp -R ${s.srcFw} $XCF/${s.id}/ + '') slices} + cp ${infoPlist} $XCF/Info.plist echo "Validating dynamic xcframework..." + test -f $XCF/Info.plist || { echo "FAIL: Missing xcframework Info.plist"; exit 1; } FOUND=0 - for fw in $out/LibXMTPSwiftFFIDynamic.xcframework/*/xmtpv3FFI.framework; do + for fw in $XCF/*/xmtpv3FFI.framework; do test -d "$fw" || continue FOUND=$((FOUND + 1)) # macOS slices use the versioned bundle layout (Versions/A + symlinks, diff --git a/nix/package/ios-xcframework/mk-static.nix b/nix/package/ios-xcframework/mk-static.nix index b73b190d6b..555a2b8590 100644 --- a/nix/package/ios-xcframework/mk-static.nix +++ b/nix/package/ios-xcframework/mk-static.nix @@ -1,7 +1,7 @@ { lib, stdenv, - xcode-tools, + cctools, helpers, }: { @@ -18,17 +18,55 @@ let expectedSlices ; headerDir = "${swiftBindings}/swift/include/libxmtp"; + + slices = + lib.optional (deviceAbis != [ ]) ( + helpers.mkSlice { + platform = "ios"; + abis = deviceAbis; + } + // { + srcLib = "${dylibs.iphone64}/libxmtpv3.a"; + } + ) + ++ lib.optional (simAbis != [ ]) ( + helpers.mkSlice { + platform = "ios"; + abis = simAbis; + variant = "simulator"; + } + // { + srcLib = "$TMPDIR/lipo_sim/libxmtpv3.a"; + } + ) + ++ lib.optional (macAbis != [ ]) ( + helpers.mkSlice { + platform = "macos"; + abis = macAbis; + } + // { + srcLib = "$TMPDIR/lipo_macos/libxmtpv3.a"; + } + ); + + infoPlist = helpers.mkXCFrameworkPlist ( + map ( + s: + s + // { + libraryPath = "libxmtpv3.a"; + binaryPath = "libxmtpv3.a"; + headersPath = "Headers"; + } + ) slices + ); in stdenv.mkDerivation { pname = "xmtpv3-static-xcframework"; inherit version; dontUnpack = true; dontFixup = true; - nativeBuildInputs = [ xcode-tools ]; - # xcodebuild dlopens plugins linked against /Library/Developer - # PrivateFrameworks (installed by Xcode's first-launch on the builder); - # bind just that path into the sandbox instead of going __noChroot. - __impureHostDeps = [ "/Library/Developer/PrivateFrameworks" ]; + nativeBuildInputs = [ cctools ]; installPhase = '' set -euo pipefail echo "=== Building static xcframework ===" @@ -46,27 +84,24 @@ stdenv.mkDerivation { abis = macAbis; }} - XCF_ARGS="" - ${lib.optionalString (deviceAbis != [ ]) '' - XCF_ARGS="$XCF_ARGS -library ${dylibs.iphone64}/libxmtpv3.a -headers ${headerDir}" - ''} - ${lib.optionalString (simAbis != [ ]) '' - XCF_ARGS="$XCF_ARGS -library $TMPDIR/lipo_sim/libxmtpv3.a -headers ${headerDir}" - ''} - ${lib.optionalString (macAbis != [ ]) '' - XCF_ARGS="$XCF_ARGS -library $TMPDIR/lipo_macos/libxmtpv3.a -headers ${headerDir}" - ''} - - mkdir -p $out - xcodebuild -create-xcframework \ - $XCF_ARGS \ - -output $out/LibXMTPSwiftFFI.xcframework + # Assembled by hand: an xcframework is one directory per slice plus a + # manifest. `xcodebuild -create-xcframework` does the same job but + # dlopens host Xcode first-launch frameworks, which can't be sandboxed. + XCF=$out/LibXMTPSwiftFFI.xcframework + ${lib.concatMapStrings (s: '' + mkdir -p $XCF/${s.id}/Headers + cp ${s.srcLib} $XCF/${s.id}/libxmtpv3.a + cp -r ${headerDir}/. $XCF/${s.id}/Headers/ + '') slices} + cp ${infoPlist} $XCF/Info.plist echo "Validating static xcframework..." + test -f $XCF/Info.plist || { echo "FAIL: Missing xcframework Info.plist"; exit 1; } FOUND=0 - for slice in $out/LibXMTPSwiftFFI.xcframework/*/; do + for slice in $XCF/*/; do [ -d "$slice/Headers" ] || continue FOUND=$((FOUND + 1)) + test -f "$slice/libxmtpv3.a" || { echo "FAIL: Missing libxmtpv3.a in $slice"; exit 1; } test -f "$slice/Headers/xmtpv3FFI.h" || { echo "FAIL: Missing xmtpv3FFI.h in $slice"; exit 1; } test -f "$slice/Headers/module.modulemap" || { echo "FAIL: Missing modulemap in $slice"; exit 1; } head -1 "$slice/Headers/module.modulemap" | grep -q "module xmtpv3FFI" || \ From 9abb617b63717c221d5d030dd464da6b61bb3d7b Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Thu, 2 Jul 2026 11:51:04 -0400 Subject: [PATCH 10/11] fix(ci): tolerate missing store Xcode on runners Runner Xcode bundles are pruned images and never hash to the pinned requireFile path; bindings substitute from xmtp.cachix and swift steps fall back to the version-verified runner bundle. --- .github/actions/setup-ios-xcode/action.yml | 15 +++++++++++++-- sdks/ios/dev/.ensure-xcode | 8 +++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-ios-xcode/action.yml b/.github/actions/setup-ios-xcode/action.yml index afd7129528..3d1c2b4744 100644 --- a/.github/actions/setup-ios-xcode/action.yml +++ b/.github/actions/setup-ios-xcode/action.yml @@ -28,14 +28,25 @@ runs: p=/Applications/Xcode_26.3.app fi echo "path=$p" >> "$GITHUB_OUTPUT" + # On a NAR-cache miss the action imports the runner bundle, which is a + # pruned image that never hashes to the pin — its export step then fails. + # Tolerate that: builds substitute from xmtp.cachix and swift steps use + # the runner bundle via the DEVELOPER_DIR fallback below. - uses: xmtplabs/xmtp-cache-apple@v1.0.3 + continue-on-error: true with: xcode-path: ${{ steps.xcode-path.outputs.path }} # requireFile fixed-output path: moves only if the pinned sha256 for # xcode_26_3 changes, never with nixpkgs/flake.lock bumps. xcode-nix-path: /nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app # Point non-nix tool invocations (raw xcodebuild/swift steps) at the - # pinned Xcode; nix builds resolve it through the flake instead. + # pinned store Xcode when it materialized, else at the version-verified + # runner bundle — runner images are pruned and never hash to the pin. - name: Export DEVELOPER_DIR shell: bash - run: echo "DEVELOPER_DIR=/nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app/Contents/Developer" >> "$GITHUB_ENV" + run: | + dev=/nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app + if ! nix path-info "$dev" >/dev/null 2>&1; then + dev="${{ steps.xcode-path.outputs.path }}" + fi + echo "DEVELOPER_DIR=$dev/Contents/Developer" >> "$GITHUB_ENV" diff --git a/sdks/ios/dev/.ensure-xcode b/sdks/ios/dev/.ensure-xcode index e2fa21913e..b0b2fb7a75 100755 --- a/sdks/ios/dev/.ensure-xcode +++ b/sdks/ios/dev/.ensure-xcode @@ -56,9 +56,11 @@ if nix path-info "$expected" >/dev/null 2>&1; then fi if [[ -n "${CI:-}" ]]; then - echo "ERROR: Xcode missing from Nix store in CI: $expected" >&2 - echo "The xmtp-cache-apple action should import it before this step." >&2 - exit 1 + # Runner Xcode bundles are pruned images that never hash to the pinned + # store path. Proceed anyway: the bindings substitute from xmtp.cachix, + # and a cache miss fails the nix build with the missing path regardless. + echo "WARNING: pinned Xcode not in Nix store ($expected); relying on the binary cache." >&2 + exit 0 fi XCODE_APP="${XCODE_APP:-$(find_xcode_bundle || true)}" From bd52923b5285f64faf0ba0dacc4ceef410e94fca Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Thu, 2 Jul 2026 12:21:43 -0400 Subject: [PATCH 11/11] fix(ios): ship self-contained artifacts Static OpenSSL for shipped Apple binaries (nix-store dylib refs crash on device); floors stay 14/11, asserted against Package.swift/podspec at eval; validation checks LC_BUILD_VERSION and store-path leaks; symlink-safe release packaging; shared slice/openssl helpers. --- .github/actions/setup-ios-xcode/action.yml | 10 ++- .github/workflows/release-ios.yml | 11 ++- flake.lock | 36 +++------ nix/ios-packages.nix | 33 ++++++-- nix/lib/base.nix | 17 +++-- nix/lib/ios-env.nix | 16 ++-- nix/package/ios-bindings.nix | 31 +++++++- nix/package/ios-xcframework/helpers.nix | 59 ++++++++++++++ nix/package/ios-xcframework/mk-dynamic.nix | 89 ++++++---------------- nix/package/ios-xcframework/mk-static.nix | 52 +++---------- sdks/ios/dev/.ensure-xcode | 4 +- sdks/ios/dev/bindings | 6 +- 12 files changed, 202 insertions(+), 162 deletions(-) diff --git a/.github/actions/setup-ios-xcode/action.yml b/.github/actions/setup-ios-xcode/action.yml index 3d1c2b4744..e60caa7d57 100644 --- a/.github/actions/setup-ios-xcode/action.yml +++ b/.github/actions/setup-ios-xcode/action.yml @@ -28,6 +28,9 @@ runs: p=/Applications/Xcode_26.3.app fi echo "path=$p" >> "$GITHUB_OUTPUT" + # requireFile fixed-output path: moves only if the pinned sha256 for + # xcode_26_3 changes, never with nixpkgs/flake.lock bumps. + echo "nix-path=/nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app" >> "$GITHUB_OUTPUT" # On a NAR-cache miss the action imports the runner bundle, which is a # pruned image that never hashes to the pin — its export step then fails. # Tolerate that: builds substitute from xmtp.cachix and swift steps use @@ -36,17 +39,16 @@ runs: continue-on-error: true with: xcode-path: ${{ steps.xcode-path.outputs.path }} - # requireFile fixed-output path: moves only if the pinned sha256 for - # xcode_26_3 changes, never with nixpkgs/flake.lock bumps. - xcode-nix-path: /nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app + xcode-nix-path: ${{ steps.xcode-path.outputs.nix-path }} # Point non-nix tool invocations (raw xcodebuild/swift steps) at the # pinned store Xcode when it materialized, else at the version-verified # runner bundle — runner images are pruned and never hash to the pin. - name: Export DEVELOPER_DIR shell: bash run: | - dev=/nix/store/x9hdz5mfp44i9b05sswp271jdv68r8vx-Xcode.app + dev="${{ steps.xcode-path.outputs.nix-path }}" if ! nix path-info "$dev" >/dev/null 2>&1; then + echo "::warning::Pinned Xcode not in the store (expected on a cold NAR cache); swift steps use the runner bundle." dev="${{ steps.xcode-path.outputs.path }}" fi echo "DEVELOPER_DIR=$dev/Contents/Developer" >> "$GITHUB_ENV" diff --git a/.github/workflows/release-ios.yml b/.github/workflows/release-ios.yml index a013a12814..abae31ca78 100644 --- a/.github/workflows/release-ios.yml +++ b/.github/workflows/release-ios.yml @@ -82,12 +82,15 @@ jobs: - name: Package and checksum id: checksum run: | - cp -rL build/ios/LibXMTPSwiftFFI LibXMTPSwiftFFI + # cp -R + zip -y: the macOS dynamic framework's Versions/ symlinks + # are part of the signed bundle — dereferencing them post-signing + # invalidates the rcodesign signature. + mkdir LibXMTPSwiftFFI && cp -R build/ios/LibXMTPSwiftFFI/. LibXMTPSwiftFFI/ chmod -R u+w LibXMTPSwiftFFI - (cd LibXMTPSwiftFFI && zip -qr ../LibXMTPSwiftFFI.zip .) - cp -rL build/ios/LibXMTPSwiftFFIDynamic LibXMTPSwiftFFIDynamic + (cd LibXMTPSwiftFFI && zip -qry ../LibXMTPSwiftFFI.zip .) + mkdir LibXMTPSwiftFFIDynamic && cp -R build/ios/LibXMTPSwiftFFIDynamic/. LibXMTPSwiftFFIDynamic/ chmod -R u+w LibXMTPSwiftFFIDynamic - (cd LibXMTPSwiftFFIDynamic && zip -qr ../LibXMTPSwiftFFIDynamic.zip .) + (cd LibXMTPSwiftFFIDynamic && zip -qry ../LibXMTPSwiftFFIDynamic.zip .) echo "checksum=$(shasum -a 256 LibXMTPSwiftFFI.zip | awk '{ print $1 }')" >> "$GITHUB_OUTPUT" echo "dynamic-checksum=$(shasum -a 256 LibXMTPSwiftFFIDynamic.zip | awk '{ print $1 }')" >> "$GITHUB_OUTPUT" - name: Create or update GitHub Release diff --git a/flake.lock b/flake.lock index 0f58a85772..e61d6c539b 100644 --- a/flake.lock +++ b/flake.lock @@ -72,7 +72,9 @@ "foundry": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { "lastModified": 1778233390, @@ -91,16 +93,18 @@ }, "nixpkgs": { "locked": { - "lastModified": 1666753130, - "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", + "lastModified": 1782723713, + "narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", + "rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8", "type": "github" }, "original": { - "id": "nixpkgs", - "type": "indirect" + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs-lib": { @@ -119,22 +123,6 @@ } }, "nixpkgs_2": { - "locked": { - "lastModified": 1782723713, - "narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { "locked": { "lastModified": 1770107345, "narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=", @@ -156,7 +144,7 @@ "fenix": "fenix", "flake-parts": "flake-parts", "foundry": "foundry", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs", "rust-manifest": "rust-manifest", "treefmt-nix": "treefmt-nix" } @@ -192,7 +180,7 @@ }, "treefmt-nix": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1780220602, diff --git a/nix/ios-packages.nix b/nix/ios-packages.nix index 3428c21149..1882d18916 100644 --- a/nix/ios-packages.nix +++ b/nix/ios-packages.nix @@ -10,22 +10,29 @@ let iosCommon = { darwinSdkVersion = "26"; - darwinMinVersion = "16"; + # Deployment floor, not SDK version — asserted below to match the + # floors advertised in Package.swift and the podspec. + darwinMinVersion = "14"; xcodeVer = "26.3"; # Wrap builder-host Xcode (iosSdkPkgs) instead of building the Darwin # toolchain from source (~94 drvs vs ~2500). Comment out to fall back # to the hermetic apple-sdk source path. useiOSPrebuilt = true; }; + # macOS deployment floor for the mac slices, same contract as + # iosCommon.darwinMinVersion. + macMinVersion = "11.0"; # Single source of truth: ABI name → target config iosTargets = { "aarch64-darwin" = { config = "arm64-apple-darwin"; xcodeVer = "26.3"; + darwinMinVersion = macMinVersion; }; "x86_64-darwin" = { config = "x86_64-apple-darwin"; xcodeVer = "26.3"; + darwinMinVersion = macMinVersion; }; "iphone64" = { config = "arm64-apple-ios"; @@ -54,7 +61,23 @@ # Per-target dylibs keyed by ABI name iosDylibs = lib.mapAttrs (_: p: (mkIosBindings p { }).dylib) crossPkgs; - inherit (mkIosBindings pkgs { }) swift-bindings version; + # The native build only feeds uniffi bindings generation — it never + # ships, so skip the from-source static openssl. + inherit (mkIosBindings pkgs { staticOpenssl = false; }) swift-bindings version; + + # The floors advertised to SPM/CocoaPods must match what the binaries + # are built with (LC_BUILD_VERSION minos); catch drift at eval time. + iosMinVersion = + assert lib.assertMsg (lib.hasInfix ".iOS(.v${iosCommon.darwinMinVersion})" ( + builtins.readFile "${self}/Package.swift" + )) "Package.swift .iOS floor doesn't match darwinMinVersion ${iosCommon.darwinMinVersion}"; + assert lib.assertMsg (lib.hasInfix "deployment_target = '${iosCommon.darwinMinVersion}.0'" ( + builtins.readFile "${self}/sdks/ios/XMTP.podspec" + )) "XMTP.podspec deployment_target doesn't match darwinMinVersion ${iosCommon.darwinMinVersion}"; + assert lib.assertMsg (lib.hasInfix ".macOS(.v${lib.versions.major macMinVersion})" ( + builtins.readFile "${self}/Package.swift" + )) "Package.swift .macOS floor doesn't match macMinVersion ${macMinVersion}"; + iosCommon.darwinMinVersion + ".0"; fastAbi = if pkgs.stdenv.hostPlatform.isx86_64 then @@ -84,11 +107,7 @@ abis = allAbis; dylibs = iosDylibs; swiftBindings = swift-bindings; - iosMinVersion = iosCommon.darwinMinVersion + ".0"; - # Pinned constant (matches the darwin toolchain default) so the - # advertised floor doesn't vary with the build host's architecture. - macMinVersion = "14.0"; - inherit version; + inherit iosMinVersion macMinVersion version; }; ios-xcframework-static-fast = xcframework.mkStatic { abis = fastAbis; diff --git a/nix/lib/base.nix b/nix/lib/base.nix index 6cf81e2a02..bee91c2661 100644 --- a/nix/lib/base.nix +++ b/nix/lib/base.nix @@ -74,12 +74,18 @@ let "AWS_LC_SYS_TARGET_CC_${buildPlatformSuffix}" = "cc"; "AWS_LC_SYS_TARGET_CXX_${buildPlatformSuffix}" = "c++"; - # Use nixpkgs' pre-built openssl instead of letting openssl-sys vendor and build from source - # (which calls xcrun for the SDK and fails in cross sandboxes). + } + # Use nixpkgs' pre-built openssl instead of letting openssl-sys vendor and build from source + # (which calls xcrun for the SDK and fails in cross sandboxes). + // opensslEnv openssl; + + # openssl-sys env for a given openssl package — shared by the dynamic + # default above and per-package static overrides (e.g. iOS artifacts). + opensslEnv = openssl': { OPENSSL_NO_VENDOR = "1"; - OPENSSL_DIR = "${openssl.dev}"; - OPENSSL_LIB_DIR = "${openssl.out}/lib"; - OPENSSL_INCLUDE_DIR = "${openssl.dev}/include"; + OPENSSL_DIR = "${openssl'.dev}"; + OPENSSL_LIB_DIR = "${openssl'.out}/lib"; + OPENSSL_INCLUDE_DIR = "${openssl'.dev}/include"; }; # Make cargo artifacts for a derivation building rust code @@ -114,6 +120,7 @@ in bindingsFileset commonArgs mkCargoArtifacts + opensslEnv runtimeLibPath ; } diff --git a/nix/lib/ios-env.nix b/nix/lib/ios-env.nix index b8c3ad06b2..7c7be5252f 100644 --- a/nix/lib/ios-env.nix +++ b/nix/lib/ios-env.nix @@ -58,14 +58,16 @@ let # We detect this by checking if the result starts with /nix/ and skip it. resolveXcode = '' _XCODE_DEV="${defaultDeveloperDir}" - # Unset DEVELOPER_DIR before querying xcode-select, because: - # 1. Nix's apple-sdk sets DEVELOPER_DIR to a Nix store path - # 2. xcode-select -p honors DEVELOPER_DIR over the system setting - # 3. We want the system setting (set by setup-xcode on CI) - if DEVELOPER_DIR= /usr/bin/xcode-select -p &>/dev/null; then + # An explicitly exported non-Nix DEVELOPER_DIR wins — CI's setup action + # points it at the version-verified Xcode, which may differ from the + # runner's xcode-select default. + if [[ -n "''${DEVELOPER_DIR:-}" && "''${DEVELOPER_DIR}" != /nix/* ]]; then + _XCODE_DEV="$DEVELOPER_DIR" + # Otherwise query xcode-select with DEVELOPER_DIR unset — Nix's + # apple-sdk hook sets it to a store path that xcode-select would echo + # back, and a store apple-sdk lacks the real Xcode toolchain. + elif DEVELOPER_DIR= /usr/bin/xcode-select -p &>/dev/null; then _XCODE_SELECT=$(DEVELOPER_DIR= /usr/bin/xcode-select -p) - # Skip Nix store paths — inside build sandboxes, xcode-select returns - # a Nix apple-sdk path that lacks the real Xcode toolchain. if [[ "$_XCODE_SELECT" != /nix/* ]]; then _XCODE_DEV="$_XCODE_SELECT" fi diff --git a/nix/package/ios-bindings.nix b/nix/package/ios-bindings.nix index d1a7076f8d..d58dc4f44a 100644 --- a/nix/package/ios-bindings.nix +++ b/nix/package/ios-bindings.nix @@ -1,6 +1,12 @@ { + lib, xmtp, stdenv, + openssl, + # Shipped artifacts (xcframework inputs) link OpenSSL statically — + # consumers' devices have no /nix/store. The native bindings-generation + # build never ships, so it keeps the cheaper dynamic default. + staticOpenssl ? true, ... }: let @@ -10,10 +16,22 @@ let version = xmtp.mkVersion rust; inherit (base) commonArgs bindingsFileset; - cargoArtifacts = xmtp.base.mkCargoArtifacts rust false null; + opensslStatic = openssl.override { static = true; }; + opensslArgs = lib.optionalAttrs staticOpenssl ( + base.opensslEnv opensslStatic + // { + buildInputs = lib.remove openssl commonArgs.buildInputs ++ [ opensslStatic ]; + OPENSSL_STATIC = "1"; + } + ); + + cargoArtifacts = xmtp.base.mkCargoArtifacts rust false opensslArgs; + + targetPrefix = stdenv.cc.targetPrefix or ""; dylib = rust.buildPackage ( commonArgs + // opensslArgs // { inherit cargoArtifacts version; pname = "xmtpv3"; @@ -25,6 +43,17 @@ let cp $out/lib/libxmtpv3.a $out/libxmtpv3.a cp $out/lib/libxmtpv3.dylib $out/libxmtpv3.dylib install_name_tool -id @rpath/libxmtpv3.dylib $out/libxmtpv3.dylib + '' + + lib.optionalString staticOpenssl '' + # Shipped artifacts must be self-contained: a /nix/store load + # command is a dyld crash on device, and undefined OpenSSL symbols + # in the archive push an unresolvable link burden onto consumers. + if ${targetPrefix}otool -L $out/libxmtpv3.dylib | grep '/nix/store/'; then + echo "FAIL: libxmtpv3.dylib links nix store libraries"; exit 1 + fi + if ${targetPrefix}nm -u $out/libxmtpv3.a 2>/dev/null | grep -qE '_SSL_|_CRYPTO_malloc|_EVP_'; then + echo "FAIL: libxmtpv3.a has undefined OpenSSL symbols"; exit 1 + fi ''; } ); diff --git a/nix/package/ios-xcframework/helpers.nix b/nix/package/ios-xcframework/helpers.nix index f12a69f06f..5c30c2fdde 100644 --- a/nix/package/ios-xcframework/helpers.nix +++ b/nix/package/ios-xcframework/helpers.nix @@ -43,6 +43,65 @@ rec { ); }; + # The xcframework slice list for a set of ABIs. `srcFor` maps a group key + # ("device" | "sim" | "macos") to that slice's source artifact path. + mkSlices = + abis: srcFor: + let + inherit (classifyTargets abis) deviceAbis simAbis macAbis; + in + lib.optional (deviceAbis != [ ]) ( + mkSlice { + platform = "ios"; + abis = deviceAbis; + } + // { + group = "device"; + src = srcFor "device"; + } + ) + ++ lib.optional (simAbis != [ ]) ( + mkSlice { + platform = "ios"; + abis = simAbis; + variant = "simulator"; + } + // { + group = "sim"; + src = srcFor "sim"; + } + ) + ++ lib.optional (macAbis != [ ]) ( + mkSlice { + platform = "macos"; + abis = macAbis; + } + // { + group = "macos"; + src = srcFor "macos"; + } + ); + + # Device and simulator arm64 share an arch string; only LC_BUILD_VERSION + # tells them apart. 1=macOS 2=iOS 7=iOS-simulator (numeric or symbolic + # depending on the otool flavor). + checkPlatformSnippet = + slice: path: + let + want = + if slice.platform == "macos" then + "1|MACOS" + else if slice.variant != null then + "7|IOSSIMULATOR" + else + "2|IOS"; + in + '' + GOT_PLAT=$(otool -l ${path} | awk '/LC_BUILD_VERSION/{f=1} f && $1=="platform"{print $2; exit}') + echo "$GOT_PLAT" | grep -qxE '${want}' || \ + { echo "FAIL: ${slice.id} has platform '$GOT_PLAT' (want ${want})"; exit 1; } + ''; + # Top-level xcframework manifest — the only thing # `xcodebuild -create-xcframework` adds beyond copying slices into place. mkXCFrameworkPlist = diff --git a/nix/package/ios-xcframework/mk-dynamic.nix b/nix/package/ios-xcframework/mk-dynamic.nix index fbfc06e1b5..4f20edb2db 100644 --- a/nix/package/ios-xcframework/mk-dynamic.nix +++ b/nix/package/ios-xcframework/mk-dynamic.nix @@ -16,43 +16,19 @@ macMinVersion, }: let - inherit (helpers.classifyTargets abis) - deviceAbis - simAbis - macAbis - expectedSlices - ; + inherit (helpers.classifyTargets abis) simAbis macAbis; headerDir = "${swiftBindings}/swift/include/libxmtp"; - slices = - lib.optional (deviceAbis != [ ]) ( - helpers.mkSlice { - platform = "ios"; - abis = deviceAbis; - } - // { - srcFw = "$TMPDIR/fw_ios/xmtpv3FFI.framework"; - } - ) - ++ lib.optional (simAbis != [ ]) ( - helpers.mkSlice { - platform = "ios"; - abis = simAbis; - variant = "simulator"; - } - // { - srcFw = "$TMPDIR/fw_sim/xmtpv3FFI.framework"; - } - ) - ++ lib.optional (macAbis != [ ]) ( - helpers.mkSlice { - platform = "macos"; - abis = macAbis; - } - // { - srcFw = "$TMPDIR/fw_mac/xmtpv3FFI.framework"; - } - ); + slices = helpers.mkSlices abis (group: "$TMPDIR/fw_${group}/xmtpv3FFI.framework"); + + # Source dylib for each slice's framework bundle: the device slice ships + # the store artifact directly, sim/mac ship the lipo'd fat binary. + srcDylib = + group: + if group == "device" then + "${dylibs.iphone64}/libxmtpv3.dylib" + else + "$TMPDIR/lipo_${group}/libxmtpv3.dylib"; infoPlist = helpers.mkXCFrameworkPlist ( map ( @@ -95,45 +71,30 @@ stdenv.mkDerivation { abis = macAbis; }} - ${lib.optionalString (deviceAbis != [ ]) ( - helpers.mkFrameworkBundle { - name = "fw_ios"; - dylibPath = "${dylibs.iphone64}/libxmtpv3.dylib"; - minOSVersion = iosMinVersion; - inherit headerDir; - } - )} - ${lib.optionalString (simAbis != [ ]) ( - helpers.mkFrameworkBundle { - name = "fw_sim"; - dylibPath = "$TMPDIR/lipo_sim/libxmtpv3.dylib"; - minOSVersion = iosMinVersion; - inherit headerDir; - } - )} - ${lib.optionalString (macAbis != [ ]) ( + ${lib.concatMapStrings ( + s: helpers.mkFrameworkBundle { - name = "fw_mac"; - dylibPath = "$TMPDIR/lipo_macos/libxmtpv3.dylib"; - minOSVersion = macMinVersion; - isMacOS = true; + name = "fw_${s.group}"; + dylibPath = srcDylib s.group; + minOSVersion = if s.platform == "macos" then macMinVersion else iosMinVersion; + isMacOS = s.platform == "macos"; inherit headerDir; } - )} + ) slices} - # Assembled by hand: an xcframework is one directory per slice plus a - # manifest. `xcodebuild -create-xcframework` does the same job but - # dlopens host Xcode first-launch frameworks, which can't be sandboxed. + # Assembled by hand — see default.nix for why xcodebuild is avoided. # cp -R (not -rL) so the macOS bundle's Versions/ symlinks survive. XCF=$out/LibXMTPSwiftFFIDynamic.xcframework ${lib.concatMapStrings (s: '' mkdir -p $XCF/${s.id} - cp -R ${s.srcFw} $XCF/${s.id}/ + cp -R ${s.src} $XCF/${s.id}/ '') slices} cp ${infoPlist} $XCF/Info.plist echo "Validating dynamic xcframework..." - test -f $XCF/Info.plist || { echo "FAIL: Missing xcframework Info.plist"; exit 1; } + ${lib.concatMapStrings ( + s: helpers.checkPlatformSnippet s "$XCF/${s.id}/xmtpv3FFI.framework/xmtpv3FFI" + ) slices} FOUND=0 for fw in $XCF/*/xmtpv3FFI.framework; do test -d "$fw" || continue @@ -159,8 +120,8 @@ stdenv.mkDerivation { { echo "FAIL: Bad install name '$INSTALL_NAME' in $fw (want $WANT_ID)"; exit 1; } echo " dynamic OK: $(basename $(dirname $fw))" done - [ "$FOUND" -ge ${toString expectedSlices} ] || \ - { echo "FAIL: Expected >= ${toString expectedSlices} dynamic slices, found $FOUND"; exit 1; } + [ "$FOUND" -ge ${toString (lib.length slices)} ] || \ + { echo "FAIL: Expected >= ${toString (lib.length slices)} dynamic slices, found $FOUND"; exit 1; } echo "Dynamic xcframework validation passed ($FOUND slices)" ''; } diff --git a/nix/package/ios-xcframework/mk-static.nix b/nix/package/ios-xcframework/mk-static.nix index 555a2b8590..51cda26a71 100644 --- a/nix/package/ios-xcframework/mk-static.nix +++ b/nix/package/ios-xcframework/mk-static.nix @@ -11,43 +11,13 @@ version, }: let - inherit (helpers.classifyTargets abis) - deviceAbis - simAbis - macAbis - expectedSlices - ; + inherit (helpers.classifyTargets abis) simAbis macAbis; headerDir = "${swiftBindings}/swift/include/libxmtp"; - slices = - lib.optional (deviceAbis != [ ]) ( - helpers.mkSlice { - platform = "ios"; - abis = deviceAbis; - } - // { - srcLib = "${dylibs.iphone64}/libxmtpv3.a"; - } - ) - ++ lib.optional (simAbis != [ ]) ( - helpers.mkSlice { - platform = "ios"; - abis = simAbis; - variant = "simulator"; - } - // { - srcLib = "$TMPDIR/lipo_sim/libxmtpv3.a"; - } - ) - ++ lib.optional (macAbis != [ ]) ( - helpers.mkSlice { - platform = "macos"; - abis = macAbis; - } - // { - srcLib = "$TMPDIR/lipo_macos/libxmtpv3.a"; - } - ); + slices = helpers.mkSlices abis ( + group: + if group == "device" then "${dylibs.iphone64}/libxmtpv3.a" else "$TMPDIR/lipo_${group}/libxmtpv3.a" + ); infoPlist = helpers.mkXCFrameworkPlist ( map ( @@ -84,19 +54,17 @@ stdenv.mkDerivation { abis = macAbis; }} - # Assembled by hand: an xcframework is one directory per slice plus a - # manifest. `xcodebuild -create-xcframework` does the same job but - # dlopens host Xcode first-launch frameworks, which can't be sandboxed. + # Assembled by hand — see default.nix for why xcodebuild is avoided. XCF=$out/LibXMTPSwiftFFI.xcframework ${lib.concatMapStrings (s: '' mkdir -p $XCF/${s.id}/Headers - cp ${s.srcLib} $XCF/${s.id}/libxmtpv3.a + cp ${s.src} $XCF/${s.id}/libxmtpv3.a cp -r ${headerDir}/. $XCF/${s.id}/Headers/ '') slices} cp ${infoPlist} $XCF/Info.plist echo "Validating static xcframework..." - test -f $XCF/Info.plist || { echo "FAIL: Missing xcframework Info.plist"; exit 1; } + ${lib.concatMapStrings (s: helpers.checkPlatformSnippet s "$XCF/${s.id}/libxmtpv3.a") slices} FOUND=0 for slice in $XCF/*/; do [ -d "$slice/Headers" ] || continue @@ -108,8 +76,8 @@ stdenv.mkDerivation { { echo "FAIL: Bad modulemap in $slice"; exit 1; } echo " static OK: $(basename $slice)" done - [ "$FOUND" -ge ${toString expectedSlices} ] || \ - { echo "FAIL: Expected >= ${toString expectedSlices} static slices, found $FOUND"; exit 1; } + [ "$FOUND" -ge ${toString (lib.length slices)} ] || \ + { echo "FAIL: Expected >= ${toString (lib.length slices)} static slices, found $FOUND"; exit 1; } echo "Static xcframework validation passed ($FOUND slices)" ''; } diff --git a/sdks/ios/dev/.ensure-xcode b/sdks/ios/dev/.ensure-xcode index b0b2fb7a75..b5926eab51 100755 --- a/sdks/ios/dev/.ensure-xcode +++ b/sdks/ios/dev/.ensure-xcode @@ -8,8 +8,8 @@ # # Local dev: imports the matching /Applications/Xcode*.app on first run # (~5 min). Installs via `xcodes` (with consent) if missing. -# CI: the xmtp-cache-apple action pre-imports Xcode; this script -# only verifies and fails fast with a pointer if it didn't. +# CI: the xmtp-cache-apple action pre-imports Xcode; if it +# couldn't, warn and defer to the binary cache. # # XCODE_VERSION must match xcodeVer in nix/ios-packages.nix. diff --git a/sdks/ios/dev/bindings b/sdks/ios/dev/bindings index 7369940aee..c0d0d20327 100755 --- a/sdks/ios/dev/bindings +++ b/sdks/ios/dev/bindings @@ -30,8 +30,10 @@ if [[ "${1:-}" == "--release" ]]; then nix build "${ROOT}#${ATTR_PREFIX}ios-release" --out-link "${ROOT}/bindings/mobile/build/release" rm -rf "$SWIFT_OUT" mkdir -p "$SWIFT_OUT" - cp -rL "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFI/LibXMTPSwiftFFI.xcframework" "$SWIFT_OUT/" - cp -rL "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFIDynamic/LibXMTPSwiftFFIDynamic.xcframework" "$SWIFT_OUT/" + # cp -R (not -rL): the macOS dynamic framework's Versions/ symlinks are + # part of the signed bundle; dereferencing invalidates the signature. + cp -R "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFI/LibXMTPSwiftFFI.xcframework" "$SWIFT_OUT/" + cp -R "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFIDynamic/LibXMTPSwiftFFIDynamic.xcframework" "$SWIFT_OUT/" chmod -R u+w "$SWIFT_OUT" cp -f "${ROOT}/bindings/mobile/build/release/LibXMTPSwiftFFI/Sources/LibXMTP/xmtpv3.swift" \ "${ROOT}/sdks/ios/Sources/XMTPiOS/Libxmtp/xmtpv3.swift"