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";
          };
      }
    );
}