Nix-darwin: override/overlay xcode + how to get a correct/working dev/build environment?

I wanted to overlay nixpkgs.darwin.xcode in a devshell flake to include newer versions that are not yet available.

I started here which gave me some basic understanding, but simply adding a modified xcode.nix to an overlay like so

final: prev: {
  darwin = prev.darwin // { inherit (prev.darwin.callPackage ./xcode.nix {}) xcode_15_2; };
  helloxx = prev.hello;
}

doesn’t work (though the overlay works (tested with hellox))
It says: error: attribute 'xcode_15_2' missing.

Did I do something wrong in the override of the darwin attribute in the overlay?

I tried to take a shortcut and directly do the derivation inline in buildInputs with the requireFile expression etc. from xcode.nix for my specific xcode version, but couldn’t get it to work, probably due to making mistakes in the chaining of expressions.

As a workaround I can get xcode working in a devShell flake without overlay by just doing the appleSDK thing in the below flake.

{
  description = "flutter shell";

  inputs = {
    # nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, flake-utils, nixpkgs }:
    flake-utils.lib.eachDefaultSystem (system: {
      devShell =
        let
          pkgs = import nixpkgs {
            inherit system;
            config.allowUnfree = true;
            overlays =
              [ (import ./.nix/overlay) ]; # somehow doesn't work as expected?
          };

          inherit (pkgs) lib;

          appleSDK =
            if pkgs.stdenv.isDarwin then
              (pkgs.darwin.callPackage ./.nix/overlay/xcode.nix { }).xcode_15_2
            else
              { };

          myllvm = pkgs.llvmPackages_15;

          libs = with pkgs;
            if stdenv.isLinux then [
              atk
              at-spi2-core.dev
              dbus.dev
              gtk3
              pango
              cairo
              harfbuzz
              gdk-pixbuf
              glib # these are transitive but explicit here for the LD_LIBRARY_PATH
              fontconfig
              libdatrie
              libselinux
              libsepol
              pcre
              libthai
              libxkbcommon
              pcre2
              util-linux.dev
              xorg.libX11.dev
              xorg.libXdmcp
              xorg.libXtst
              libappindicator.dev
              libepoxy
              libdeflate
              gnome.zenity
            ] else if stdenv.isDarwin then
              [
                # libs needed for darwin
              ]
            else
              builtins.throw "Unsupported system (not Linux or Darwin)";
        in
        (pkgs.mkShell.override {
          stdenv =
            if pkgs.stdenv.isDarwin then
              pkgs.stdenv
            else
              myllvm.stdenv;
        }) {
          nativeBuildInputs = with pkgs;
            [ pkg-config ninja cmake dart flutter319 go envsubst ]
            ++ lib.optionals stdenv.isLinux [
              myllvm.bintools # https://matklad.github.io/2022/03/14/rpath-or-why-lld-doesnt-work-on-nixos.html
            ];

          buildInputs = libs ++ lib.optionals pkgs.stdenv.isLinux
            (with myllvm; [ libcxxClang libunwind ])
            ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs;
            [
              # this whole stuff is prepared as follows:
              #    https://github.com/NixOS/nixpkgs/blob/032324fd20e3be4124ffefd00da5bd66b0550e8c/pkgs/os-specific/darwin/xcode/default.nix#L23-L32
              appleSDK
            ]);

          shellHook =
            if pkgs.stdenv.isDarwin then ''
              # PATH=${appleSDK}/Contents/Developer/usr/bin:${appleSDK}/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin:$PATH
              PATH=${appleSDK}/Contents/Developer/usr/bin:$PATH
              SDKROOT=${appleSDK}/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
            '' else
              "";
        };
    });
}

That even makes the flutter part of my project work.

BUT: depending on which PATH I set in the shellHook a go library I link into the flutter app won’t compile.

If I don’t set clang to the env’s Xcode (from Toolchain), I get

clang-16: error: cannot use 'cpp-output' output with multiple -arch options

and if I set the path I get

# runtime/cgo
_cgo_export.c:3:10: fatal error: 'stdlib.h' file not found

Anyway it seems that setting the paths for the stdenv by setting PATH in a shellHook is very hacky, is there a “natural” way to force the actually installed apple SDK?
(If I don’t set any paths I get the ancient SDK version 11). I guess that would more naturally solve library resolution?

@ppenguin did you come up with a better solution for this?

If I recall correctly I gave up and just manually installed flutter for the gitlab-runner user on the mac, to compile my flutter code for mac in gitlab-ci. I haven’t touched it for some time now, but it appears to be working reliably enough.

TBH I’m not really satisfied with the state of stdenv on darwin, the fact that SDK version 11 is still the (hardcoded) default with no documented way to override has become a PITA, so I have all but given up on doing serious stuff with nix on darwin. (It’s still ok for occasional devshells and a partial nix-darwin and HM user config, but even for the latter I still have to use some packages from brew (via the nix-darwin module, which makes it better)

24.11 changed the way SDKs work on Darwin. The SDK used in the stdenv can be overridden by adding the appropriate apple-sdk package to buildInputs. This is documented extensively in the manual in the Darwin section.

The announcement from last fall when these changes landed goes into detail about what changed as well as what the changes enable. (I think the Flutter situation probably still sucks, but someone motivated to do so should be able to fix it.)

Thank you! I ended up going in a similar route: Best Practices for Expo / React Native development with devenv - #6 by tharakadesilva

I ended up using stdenvNoCc for stdenv package and instead of using xcrun and the Xcode SDK from Nix, I ended up using the ones from the XCode installation by setting this:

export PATH=$(echo $PATH | sd "${pkgs.xcbuild.xcrun}/bin" "")
unset DEVELOPER_DIR

Thanks a lot! Apologies for basing my “rant” on stale information/experience. I’ll look into it when I use my mac again (rarely recently)…

2 Likes

Darwin had been stuck in a bad state for a long time, so I don’t fault people for assuming it hadn’t gotten better. There have been some growing pains (particularly with dev shells that mix Nix-built and native dependencies), but it’s generally in a better state now and should be able to keep up with updates in the future.

2 Likes

Would you mind explaining your setup more?
I had originally this issue
but since a lot has improved!

I have a flake based direnv setup for a Flutter project.

I am using mkShellNoCC, I am assuming you do still as well?
When I write

export PATH=$(echo $PATH | sd "${pkgs.xcbuild.xcrun}/bin" "")
unset DEVELOPER_DIR

to the shellHook = the Flutter command isn’t found anymore.

My flake.nix works - but I would like to setup Xcode more idiomatic, if there is a recommended way. Currently I am using this workaround in shellHook = :

                      # installs or checks for the right xcode version
                      echo "installing xcode ${xcode_version}"
                      xcodes install ${xcode_version} --experimental-unxip # --directory "$PWD/.xcode"
                      xcodes select ${xcode_version}
This is my complete flake.nix file { description = "Flutter toolchain. Installs all tools needed for flutter, with versions pinned for this project. Rust's own tooling handles the rust toolchain."; # nix flutter doesn't work: https://github.com/NixOS/nixpkgs/issues/243448 # thus using a local installation inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; android-nixpkgs = { url = "github:tadfisher/android-nixpkgs"; inputs = { flake-utils.follows = "flake-utils"; nixpkgs.follows = "nixpkgs"; devshell.follows = "devshell"; }; }; devshell = { url = "github:numtide/devshell"; inputs = { nixpkgs.follows = "nixpkgs"; }; }; # share rust configuration with nix ... not really needed! # rust-overlay = { # url = "github:oxalica/rust-overlay"; # inputs = { # nixpkgs.follows = "nixpkgs"; # flake-utils.follows = "flake-utils"; # }; # }; };

outputs = { nixpkgs, flake-utils, android-nixpkgs, rust-overlay, … }:

outputs = { nixpkgs, flake-utils, android-nixpkgs, … }:
flake-utils.lib.eachDefaultSystem (system:
let
# overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
# inherit system overlays;
inherit system;
config = {
allowUnfree = true;
android_sdk = {
accept_license = true;
};
};
};
# rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
androidCustomPackage = android-nixpkgs.sdk.${system} (
sdkPkgs: with sdkPkgs; [
cmdline-tools-latest
build-tools-30-0-3
# build-tools-33-0-2
build-tools-34-0-0
ndk-23-1-7779620
# ndk-26-2-11394342
platform-tools
emulator
#patcher-v4
# platforms-android-28
platforms-android-33
platforms-android-34
system-images-android-34-aosp-atd-arm64-v8a #basic image, 40% faster
system-images-android-34-google-apis-arm64-v8a #google branded
system-images-android-34-google-apis-playstore-arm64-v8a #google branded with playstore installed
]
);
pinnedJDK = pkgs.jdk17;
xcode_version = “15.4.0”;
frb_version = “latest”;
flutter_rust_bridge_codegen = import ./nix/flutter_rust_bridge_codegen.nix {
inherit pkgs frb_version;
};
appleInputs =
if builtins.elem system [ “aarch64-darwin” “x86_64-darwin” ] then [
pkgs.cocoapods
pkgs.xcodes
] else ;
in
{
devShells. default = pkgs.mkShellNoCC
{
name = “flutter-rust-dev-shell”;
buildInputs = with pkgs; [
just
# rustToolchain
flutter
pinnedJDK
androidCustomPackage
flutter_rust_bridge_codegen
] ++ appleInputs;
JAVA_HOME = pinnedJDK;
# ANDROID_SDK_ROOT = “${androidCustomPackage}/share/android-sdk”;

        # Use this to create an android emulator
        # however, this is not needed, as VSCode's Flutter Plugin can create emulators as well
        # AVD_package = "system-images;android-34;aosp_atd;arm64-v8a";
        # local_toolchain_path = "$PWD/.toolchain";
        # local_SDK_path = "${local_toolchain_path}/android";
        # local_AVD_path = "${local_SDK_path}/AVD";
        # avdmanager create avd --name android-34-pixel_8 --package '${AVD_package}' --device "pixel_8"
        shellHook = ''
                  # export PATH=$(echo $PATH | sd "${pkgs.xcbuild.xcrun}/bin" "")
                  # unset DEVELOPER_DIR
          	      #  uncomment to enable flutter-rust-bridge-codegen logging
          	      #  export RUST_BACKTRACE=1
          	      #  export RUST_LOG="debug" 

                  # installs or checks for the right xcode version
                  echo "installing xcode ${xcode_version}"
                  xcodes install ${xcode_version} --experimental-unxip # --directory "$PWD/.xcode"
                  xcodes select ${xcode_version}
                  echo
                  #  GRADLE_USER_HOME=$HOME/gradle-user-home
                  #  GRADLE_HOME=$HOME/gradle-home
        '';

        # GRADLE_USER_HOME = " /home/admin0101/.gradle ";
        # GRADLE_OPTS = " - Dorg.gradle.project.android.aapt2FromMavenOverride=${androidCustomPackage}/share/android-sdk/build-tools/34.0.0/aapt2";
      };
  }
);

}

Note that I am using nix only on my Mac - thus I don’t have any if Darwin (which I should …). I am using it system wide as well, with flake and home-manager. But that should not really matter here …

Yes, I am.

I tried your flake, but I commented the following parts out:

  • I don’t have access to the ./nix/flutter_rust_bridge_codegen.nix file.
        # flutter_rust_bridge_codegen = import ./nix/flutter_rust_bridge_codegen.nix {
        #   inherit pkgs frb_version;
        # };
  • The objective I was going for is to use the system xcode instead of this:
        #  xcodes install ${xcode_version} --experimental-unxip # --directory "$PWD/.xcode"
        #  xcodes select ${xcode_version}

With this, I am able to see flutter (I did initially forget to uncomment the first two lines of the shell hook, but I went back and tested again):

❯ which flutter
/nix/store/rcx87jqmrn0yz8xy483xw3j2f872p1vq-flutter-wrapped-3.27.1-sdk-links/bin/flutter

❯ flutter --version
Flutter 3.27.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision nixpkgs000 () • 1970-01-01 00:00:00
Engine • revision cb4b5fff73
Tools • Dart 3.6.0 • DevTools 2.40.2

I also ensured that I don’t have access to flutter outside of the devshell. I am not sure what’s going on.

If you are curious, here’s the full flake.nix that I used:

{
  description = "Flutter toolchain. Installs all tools needed for flutter, with versions pinned for this project. Rust's own tooling handles the rust toolchain.";
  # nix flutter doesn't work: https://github.com/NixOS/nixpkgs/issues/243448
  # thus using a local installation
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    android-nixpkgs = {
      url = "github:tadfisher/android-nixpkgs";
      inputs = {
        flake-utils.follows = "flake-utils";
        nixpkgs.follows = "nixpkgs";
        devshell.follows = "devshell";
      };
    };
    devshell = {
      url = "github:numtide/devshell";
      inputs = {nixpkgs.follows = "nixpkgs";};
    };
    # share rust configuration with nix ... not really needed!
    # rust-overlay = {
    # url = "github:oxalica/rust-overlay";
    # inputs = {
    # nixpkgs.follows = "nixpkgs";
    # flake-utils.follows = "flake-utils";
    # };
    # };
  };
  # outputs = { nixpkgs, flake-utils, android-nixpkgs, rust-overlay, ... }:
  outputs = {
    nixpkgs,
    flake-utils,
    android-nixpkgs,
    ...
  }:
    flake-utils.lib.eachDefaultSystem (
      system: let
        # overlays = [ (import rust-overlay) ];
        pkgs = import nixpkgs {
          # inherit system overlays;
          inherit system;
          config = {
            allowUnfree = true;
            android_sdk = {
              accept_license = true;
            };
          };
        };
        # rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
        androidCustomPackage = android-nixpkgs.sdk.${system} (
          sdkPkgs:
            with sdkPkgs; [
              cmdline-tools-latest
              build-tools-30-0-3
              # build-tools-33-0-2
              build-tools-34-0-0
              ndk-23-1-7779620
              # ndk-26-2-11394342
              platform-tools
              emulator
              #patcher-v4
              # platforms-android-28
              platforms-android-33
              platforms-android-34
              system-images-android-34-aosp-atd-arm64-v8a #basic image, 40% faster
              system-images-android-34-google-apis-arm64-v8a #google branded
              system-images-android-34-google-apis-playstore-arm64-v8a #google branded with playstore installed
            ]
        );
        pinnedJDK = pkgs.jdk17;
        xcode_version = "15.4.0";
        frb_version = "latest";
        # flutter_rust_bridge_codegen = import ./nix/flutter_rust_bridge_codegen.nix {
        #   inherit pkgs frb_version;
        # };
        appleInputs =
          if builtins.elem system ["aarch64-darwin" "x86_64-darwin"]
          then [
            pkgs.cocoapods
            pkgs.xcodes
          ]
          else [];
      in {
        devShells. default =
          pkgs.mkShellNoCC
          {
            name = "flutter-rust-dev-shell";
            buildInputs = with pkgs;
              [
                just
                # rustToolchain
                flutter
                pinnedJDK
                androidCustomPackage
                # flutter_rust_bridge_codegen
              ]
              ++ appleInputs;
            JAVA_HOME = pinnedJDK;
            # ANDROID_SDK_ROOT = "${androidCustomPackage}/share/android-sdk";
            # Use this to create an android emulator
            # however, this is not needed, as VSCode's Flutter Plugin can create emulators as well
            # AVD_package = "system-images;android-34;aosp_atd;arm64-v8a";
            # local_toolchain_path = "$PWD/.toolchain";
            # local_SDK_path = "${local_toolchain_path}/android";
            # local_AVD_path = "${local_SDK_path}/AVD";
            # avdmanager create avd --name android-34-pixel_8 --package '${AVD_package}' --device "pixel_8"
            shellHook = ''
               export PATH=$(echo $PATH | sd "${pkgs.xcbuild.xcrun}/bin" "")
               unset DEVELOPER_DIR
               # uncomment to enable flutter-rust-bridge-codegen logging
               # export RUST_BACKTRACE=1
               # export RUST_LOG="debug"

               # installs or checks for the right xcode version
               # echo "installing xcode ${xcode_version}"
               # xcodes install ${xcode_version} --experimental-unxip # --directory "$PWD/.xcode"
               # xcodes select ${xcode_version}
               # echo
               # GRADLE_USER_HOME=$HOME/gradle-user-home
               # GRADLE_HOME=$HOME/gradle-home
            '';

            # GRADLE_USER_HOME = " /home/admin0101/.gradle ";
            # GRADLE_OPTS = " - Dorg.gradle.project.android.aapt2FromMavenOverride=${androidCustomPackage}/share/android-sdk/build-tools/34.0.0/aapt2";
          };
      }
    );
}

Hi @tharakadesilva many thanks - highly appreciated that you took my flake and posted the fixed version!
And sorry for the late answer - I had/still have troubles with the rust compilation that broke … but I left the out for now to to realize why your changes (still) did not work for me:

I did not have ‘sd’ installed. To be pure, that would need to be added to the ‘buildInputs’. Without that the shellHook just quietly overwrites PATH with nothing!

Sorry for the left reference to the flutter_rust_bridge_codegen file. You (or anyone reading this) can see my latest flake.nix and nix/flutter_rust_bridge_codegen.nix in my example project.

I will play around with this way to integrate Xcode … or better to let the system wide Xcode “leak in”. I am not a big fan of this setup. I understand that Xcode cannot be installed by nix directly, but I think going back to PATH might have too much side effects and be too system dependent. I guess that my rust-building problems are because some lib formerly being in DEVELOPER_DIR and not in PATH is missing.
Nevermind this specific problem though.

Anyways, thanks for showing your implementation and help so I could get that work!

I took a brief look into integrating Xcode with
pkgs.darwin.xcode
but that requires to install Xcode manually into the store, even though it is locally already present:

       > ***
       > Unfortunately, we cannot download Xcode.app automatically.
       > Please go to https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_16.2/Xcode_16.2.xip
       > to download it yourself, and add it to the Nix store by running the following commands.
       > Note: download (~ 5GB), extraction and storing of Xcode will take a while
       >
       > open -W Xcode_16.2.xip
       > rm -rf Xcode_16.2.xip
       >
       > nix-store --add-fixed --recursive sha256 Xcode.app
       > rm -rf Xcode.app
       >
       > ***
       >
       For full logs, run 'nix log /nix/store/wlh1dchi1lb6dk5jfkmadfrhr0h0xwh4-Xcode.app.drv'.

Fair enough, but I prefer to stick with my original way of using xcodes to do that all for me. Should be more convenient especially when switching version …

If you want to expose the system Xcode in your dev shell, you can use xcodeenv.composeXcodeWrapper. That doesn’t require you to copy Xcode to your store.

How can one do that? My nix knowledge is not good enough to understand … I cannot get xcodeenv into my flake.

I actually have a huge problem: I cannot get Rust and Flutter working in one flake!
My tries are in the history of my flake.nix file.
Any help is appreciated!

I thought in between that it works - but either ‘cargo test’ works (=doesn’t miss iconv) or flutter run does - but not both.
Strangely ++ lib.optionals stdenv.isDarwin [libiconv] doesn’t work, rust works only when adding clang, which is too old and leads flutter to invalid argument '-mmacos-version-min=11.3' not allowed with '-mios-simulator-version-min=12.0 :frowning:

and
both integrations
lead to a flutter build ios error
github:oxalica/rust-overlay: “Error (Xcode): Unknown option: -Xlinker”
github:nix-community/fenix: “Error (Xcode): invalid argument ‘-mmacos-version-min=11.3’ not allowed with ‘-miphoneos-version-min=12.0’”

@tharakadesilva I finally got my flake working again (thanks to @reckenrode)!

And - you where spot-on with

  unset DEVELOPER_DIR;

In fact, mkShellNoCC binds outdated sdks to this variable, like SDKROOT does.

I used xcodes to install specific Xcode versions - which works well, but is not embedded in nix. I only executed it within nix.

Nixpkgs’ Xcode can be used to copy Xcode into the nix/store … but I opted for using xcodeenv, as recommended in the documentation (and by @reckenrode), which is creating symlinks in the nix/store to the Xcode installation.

Ultimately, IMHO, not using the latest Xcode suiting the MacOS version leads to problems … and thus I opted for leaving Xcode as-is and using nix around it.

So, the final version of my flake, and my recommendation to integrate Xcode in a nix flake, is:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-24.11";

  outputs =
    { self, nixpkgs }:
    let
      inherit (nixpkgs) lib;
    in
    {
      devShells = lib.genAttrs [ "aarch64-darwin" "x86_64-darwin" ]
        (
          system:
          let
            pkgs = import nixpkgs {
              inherit system;
              config = {
                allowUnfree = true;
                android_sdk.accept_license = true;
              };
              overlays = [ ];
            };
            xcodeenv = import (nixpkgs + "/pkgs/development/mobile/xcodeenv") { inherit (pkgs) callPackage; };
            frb_version = "latest";
            flutter_rust_bridge_codegen = import ./nix/flutter_rust_bridge_codegen.nix {
              inherit pkgs frb_version;
            };
          in
          {
            default = pkgs.mkShellNoCC
              {
                strictDeps = true;
                packages = with pkgs; [
                  (xcodeenv.composeXcodeWrapper { versions = [ "16.2" ]; })
                  cocoapods
                  flutter
                  flutter_rust_bridge_codegen
                  just
                ];
                shellHook = ''
                  unset DEVELOPER_DIR
                  unset SDKROOT
                '';
              };
          }
        );
    };
}

(not the the android sdk is not defined completely, so

            pkgs = import nixpkgs {
              inherit system;
              config = {
                allowUnfree = true;
                android_sdk.accept_license = true;
              };
              overlays = [ ];
            };

can simply be

            pkgs = import nixpkgs {};