diff --git a/.github/actions/setup-ios-xcode/action.yml b/.github/actions/setup-ios-xcode/action.yml new file mode 100644 index 0000000000..e60caa7d57 --- /dev/null +++ b/.github/actions/setup-ios-xcode/action.yml @@ -0,0 +1,54 @@ +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" + # 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 + # 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 }} + 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="${{ 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/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..abae31ca78 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,17 +77,20 @@ 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 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/.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/.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.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/flake.nix b/flake.nix index 1b216e1a1d..d595c91a25 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,8 @@ }; inputs = { + # 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 = { url = "github:nix-community/fenix"; @@ -17,7 +19,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 +51,7 @@ ./nix/node-packages.nix ./nix/android-packages.nix ./nix/apps.nix + ./nix/ios-packages.nix ]; perSystem = { @@ -83,36 +89,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/ios-packages.nix b/nix/ios-packages.nix new file mode 100644 index 0000000000..1882d18916 --- /dev/null +++ b/nix/ios-packages.nix @@ -0,0 +1,188 @@ +{ self, ... }: +{ + perSystem = + { + pkgs, + system, + lib, + ... + }: + let + iosCommon = { + darwinSdkVersion = "26"; + # 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"; + } + // iosCommon; + "iphone64-simulator" = { + config = "aarch64-apple-ios-simulator"; + } + // iosCommon; + # "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; + + # 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 + "x86_64-darwin" + else if pkgs.stdenv.hostPlatform.isAarch64 then + "aarch64-darwin" + else + throw "Unsupported host architecture for ios-libs-fast"; + + # 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 = [ + 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 iosMinVersion macMinVersion 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: + 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 = mkLibs "xmtpv3-ios-fast" fastAbis; + ios-libs = mkLibs "xmtpv3-ios" allAbis; + in + { + # 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 8d2f6223c8..bee91c2661 100644 --- a/nix/lib/base.nix +++ b/nix/lib/base.nix @@ -5,13 +5,15 @@ xmtp, zstd, openssl, - sqlite, + sqlcipher, pkg-config, darwin, stdenv, zlib, pkgsBuildHost, perl, + apple-sdk ? null, + libiconv, }: let # Narrow fileset for buildDepsOnly — only Cargo.toml, Cargo.lock, build.rs, @@ -39,22 +41,23 @@ 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 - sqlite + sqlcipher ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ darwin.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. @@ -71,6 +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). + // 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"; }; # Make cargo artifacts for a derivation building rust code @@ -94,6 +109,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 @@ -101,5 +120,7 @@ in bindingsFileset commonArgs mkCargoArtifacts + opensslEnv + runtimeLibPath ; } diff --git a/nix/lib/default.nix b/nix/lib/default.nix index a085b7f956..a142838c8c 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 upstream iOS branch. Remove once it merges. + # + # 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/compare/af9204149566f50f1c12f1b9522b1d606a404f0c...insipx:nixpkgs:e550a4f3a5e0fa9c1c7b06519a5369873b90b4e4.patch"; + hash = "sha256-qkT2/kVBr9I+WeMSJ4l6aiDmrA/mz/+8snW5bCbc76Y="; + }) + ]; + }; in lib.listToAttrs ( map ( @@ -107,11 +124,11 @@ in t = normalize target; in { - name = t.config; - value = import inputs.nixpkgs { + name = t.name or t.config; + value = import nixpkgs-patched { inherit config overlays; localSystem = system; - crossSystem = t; + crossSystem = removeAttrs t [ "name" ]; }; } ) targets diff --git a/nix/lib/ios-env.nix b/nix/lib/ios-env.nix index 2969591576..7c7be5252f 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 @@ -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/lib/patches.nix b/nix/lib/patches.nix index d35aa96006..eea283639b 100644 --- a/nix/lib/patches.nix +++ b/nix/lib/patches.nix @@ -46,5 +46,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"; + }); + }); + } + ) ] 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/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/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 new file mode 100644 index 0000000000..d58dc4f44a --- /dev/null +++ b/nix/package/ios-bindings.nix @@ -0,0 +1,83 @@ +{ + 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 + inherit (xmtp) base craneLib; + rust-toolchain = p: xmtp.mkToolchain p [ stdenv.hostPlatform.rust.rustcTarget ] [ ]; + rust = craneLib.overrideToolchain rust-toolchain; + version = xmtp.mkVersion rust; + inherit (base) commonArgs bindingsFileset; + + 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"; + 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 + '' + + 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 + ''; + } + ); + + 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 + mv $out/swift/xmtpv3FFI.h $out/swift/include/libxmtp/ + mv $out/swift/xmtpv3FFI.modulemap $out/swift/include/libxmtp/module.modulemap + + # Generate version file. Commit date, not wall clock — keeps the + # output reproducible across days. + echo "Version: ${version}" > $out/libxmtp-version.txt + echo "Date: ${xmtp.gitCommitDate}" >> $out/libxmtp-version.txt + ''; + }; +in +{ + 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..2812ab82b0 --- /dev/null +++ b/nix/package/ios-xcframework/default.nix @@ -0,0 +1,25 @@ +# iOS xcframework assembly — static, dynamic, release, and dev outputs. +# +# 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, +}: +let + helpers = callPackage ./helpers.nix { }; + # 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 { }; + 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..5c30c2fdde --- /dev/null +++ b/nix/package/ios-xcframework/helpers.nix @@ -0,0 +1,213 @@ +{ 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 + ]; + }; + + # 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 + ); + }; + + # 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 = + 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 = + { + 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: 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"; + } + # 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, 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, + 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 ${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 \ + > ${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-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..4f20edb2db --- /dev/null +++ b/nix/package/ios-xcframework/mk-dynamic.nix @@ -0,0 +1,127 @@ +{ + lib, + stdenv, + cctools, + rcodesign, + helpers, +}: +{ + abis, + 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) simAbis macAbis; + headerDir = "${swiftBindings}/swift/include/libxmtp"; + + 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 ( + 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 = [ + cctools + rcodesign + ]; + 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.concatMapStrings ( + s: + helpers.mkFrameworkBundle { + 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 — 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.src} $XCF/${s.id}/ + '') slices} + cp ${infoPlist} $XCF/Info.plist + + echo "Validating dynamic xcframework..." + ${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 + 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 "$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 "$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 (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-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..51cda26a71 --- /dev/null +++ b/nix/package/ios-xcframework/mk-static.nix @@ -0,0 +1,83 @@ +{ + lib, + stdenv, + cctools, + helpers, +}: +{ + abis, + dylibs, + swiftBindings, + version, +}: +let + inherit (helpers.classifyTargets abis) simAbis macAbis; + headerDir = "${swiftBindings}/swift/include/libxmtp"; + + slices = helpers.mkSlices abis ( + group: + if group == "device" then "${dylibs.iphone64}/libxmtpv3.a" else "$TMPDIR/lipo_${group}/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 = [ cctools ]; + 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; + }} + + # 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.src} $XCF/${s.id}/libxmtpv3.a + cp -r ${headerDir}/. $XCF/${s.id}/Headers/ + '') slices} + cp ${infoPlist} $XCF/Info.plist + + echo "Validating static xcframework..." + ${lib.concatMapStrings (s: helpers.checkPlatformSnippet s "$XCF/${s.id}/libxmtpv3.a") slices} + FOUND=0 + 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" || \ + { echo "FAIL: Bad modulemap in $slice"; exit 1; } + echo " static OK: $(basename $slice)" + done + [ "$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/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/package/mls_validation_service.nix b/nix/package/mls_validation_service.nix index 9f0ef8f0a4..7e86892761 100644 --- a/nix/package/mls_validation_service.nix +++ b/nix/package/mls_validation_service.nix @@ -2,6 +2,9 @@ xmtp, lib, stdenv, + openssl, + sqlcipher, + zstd, }: let inherit (lib.fileset) unions; @@ -14,6 +17,13 @@ let specialArgs = lib.optionalAttrs stdenv.hostPlatform.isMusl { RUSTFLAGS = "-C target-feature=+crt-static"; + # 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 + ]; }; commonArgs = base.commonArgs // specialArgs; 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/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/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..b5926eab51 --- /dev/null +++ b/sdks/ios/dev/.ensure-xcode @@ -0,0 +1,98 @@ +#!/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 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; if it +# couldn't, warn and defer to the binary cache. +# +# 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_ATTR="xcode_${XCODE_VERSION//./_}" + +# 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 [[ -n "${CI:-}" ]]; then + # 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)}" + +if [[ ! -d "$XCODE_APP" ]]; then + 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 + [[ $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 + XCODE_APP=$(find_xcode_bundle || true) + if [[ ! -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 "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 7cad2cb902..c0d0d20327 100755 --- a/sdks/ios/dev/bindings +++ b/sdks/ios/dev/bindings @@ -1,6 +1,17 @@ #!/bin/bash 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" # --- Preflight sanity check on the nix binary cache --- @@ -16,11 +27,13 @@ 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}#${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" @@ -28,7 +41,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}#${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/"