Portable CMake builds w/catch2 (maddening!)

I have been struggling with this for 2 days now with no apparent resolution or even an idea what I should do. I am using the nixos 23.11, with nix-shell and home manager. I have a fairly large project that has all of the components containerized for Docker. For the C++ applications I am using CMake. For unit testing I am using Catch2. I have scripts that take care of all of the details of building, testing and packaging. It has taken me a while to get to this point, but I have only been working through getting nixos setup just to support this evironment and now that it is done, using my scripts, I finally built and packaged my apps only to find that they don’t run outside of nixos because of all the linker dependencies.
After investigating, I found one solution is to use a flake to encapsulate the build so that the end result goes through nix to rewrite the dependencies before it goes into a container. So I created a flake.nix file and ran nix build. No matter what I do, I get an error. Either Git cannot clone catch2-populate or cannot find github. How do I fix these error in the flake or what is the best way to make a portable build for a C++ application that actually works?

Here is one version that results in a clone error.

Blockquote

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    catch2_src = {  # ← Fetched by Nix (not CMake)
      url = "github:catchorg/Catch2/v3.4.0";
      flake = false;
    };
  };

  outputs = { self, nixpkgs, catch2_src }:
    let pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      packages.x86_64-linux.default = pkgs.stdenv.mkDerivation {
        # ...
        preConfigure = ''
          cp -r ${catch2_src} ./catch2
          substituteInPlace CMakeLists.txt \
            --replace "GIT_REPOSITORY https://github.com/catchorg/Catch2.git" "SOURCE_DIR ${catch2_src}"
        '';
      };
    };
}

I created a toy directory with a hello world and a unit test. I have changed the CmakeLists.txt to use addsubdirectory and have the flake.nix pull Catch2 from github and then copy it into the subdirectory.

Blockquote

cmake_minimum_required(VERSION 3.10)
project(hello-cpp)

set(CMAKE_CXX_STANDARD 17)

add_executable(hello src/main.cpp)

# Add Catch2 from vendored source (copied in flake)
add_subdirectory(external/Catch2)

add_executable(tests test/test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)

include(CTest)

add_test(NAME tests COMMAND tests)
enable_testing()

Blockquote

{
  description = "CMake project using Catch2 via add_subdirectory";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    catch2 = {
      url = "github:catchorg/Catch2/v3.4.0";
      flake = false;  # We only want the source
    };
  };

  outputs = { self, nixpkgs, catch2 }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      packages.x86_64-linux.default = pkgs.stdenv.mkDerivation {
        pname = "my-project";
        version = "1.0";
        src = ./.;

        nativeBuildInputs = [ pkgs.cmake ];
        buildInputs = [ pkgs.catch2 ];  # Still declare for linking

        # Copy Catch2 source into place before CMake runs
        preConfigure = ''
          mkdir -p external/Catch2
          cp -r ${catch2}/* external/Catch2/
          chmod -R +w external/Catch2  # Ensure writable for CMake
        '';

        # Phase 3: Build
        buildPhase = ''
          cmake --build build --parallel $NIX_BUILD_CORES
        '';


        cmakeFlags = [
          "-DCATCH_BUILD_TESTING=OFF"  # Disable Catch2's own tests
          "-DCATCH_INSTALL_DOCS=OFF"
        ];
      };

      # Development shell (optional)
      devShells.x86_64-linux.default = pkgs.mkShell {
        packages = [ pkgs.cmake ];
        shellHook = ''
          mkdir -p external/Catch2
          cp -r ${catch2}/* external/Catch2/
        '';
      };
    };
}

Different error now but still I am just flayling.

Blockquote

error: builder for '/nix/store/j6mm4nbafr4crgjixijybkzva0avw2s7-my-project-1.0.drv' failed with exit code 1;
       last 25 log lines:
       > -- Detecting C compile features
       > -- Detecting C compile features - done
       > -- Detecting CXX compiler ABI info
       > -- Detecting CXX compiler ABI info - done
       > -- Check for working CXX compiler: /nix/store/zd2viirgdm4ffgipgpslmysmlzs6fscb-gcc-wrapper-12.3.0/bin/g++ - skipped
       > -- Detecting CXX compile features
       > -- Detecting CXX compile features - done
       > -- Performing Test HAVE_FLAG__ffile_prefix_map__build_nyivzj20hqbp7zb56k7qm6vdnxdab9ck_source_external_Catch2__
       > -- Performing Test HAVE_FLAG__ffile_prefix_map__build_nyivzj20hqbp7zb56k7qm6vdnxdab9ck_source_external_Catch2__ - Success
       > -- Configuring done (1.0s)
       > -- Generating done (0.0s)
       > CMake Warning:
       >   Manually-specified variables were not used by the project:
       >
       >     CMAKE_EXPORT_NO_PACKAGE_REGISTRY
       >     CMAKE_FIND_USE_PACKAGE_REGISTRY
       >     CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY
       >     CMAKE_POLICY_DEFAULT_CMP0025
       >
       > 
       > -- Build files have been written to: /build/nyivzj20hqbp7zb56k7qm6vdnxdab9ck-source/build
       > cmake: enabled parallel building
       > cmake: enabled parallel installing
       > Running phase: buildPhase
       > Error: /build/nyivzj20hqbp7zb56k7qm6vdnxdab9ck-source/build/build is not a directory
       For full logs, run 'nix log /nix/store/j6mm4nbafr4crgjixijybkzva0avw2s7-my-project-1.0.drv'.

In case anyone is curious here is the solution:

{
  description = "CMake project with guaranteed output";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    catch2 = {
      url = "github:catchorg/Catch2/v3.4.0";
      flake = false;
    };
  };

  outputs = { self, nixpkgs, catch2 }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      packages.x86_64-linux.default = pkgs.stdenv.mkDerivation {
        pname = "my-project";
        version = "1.0";
        src = ./.;

        nativeBuildInputs = [ pkgs.cmake ];
        buildInputs = [ pkgs.catch2 ];

        # Phase 1: Prepare Catch2
        preConfigure = ''
          mkdir -p extern
          cp -r ${catch2} extern/Catch2
          chmod -R +w extern/Catch2
        '';

        # Phase 2: Configure
        configurePhase = ''
          cmake -B build -S . \
            -DCMAKE_INSTALL_PREFIX=$out \
            -DCMAKE_BUILD_TYPE=Release
        '';

        # Phase 3: Build
        buildPhase = ''
          cmake --build build --parallel $NIX_BUILD_CORES
        '';

        # Phase 4: Install with verification
        installPhase = ''
          cmake --install build
          
          # Verify installation
          if [ ! -d "$out" ]; then
            echo "ERROR: No files were installed to $out"
            exit 1
          fi
          echo "=== Installed files ==="
          find $out -type f
        '';

        # Extra protection against empty outputs
        dontInstall = false;
      };

      devShells.x86_64-linux.default = pkgs.mkShell {
        packages = [ pkgs.cmake ];
        shellHook = ''
          mkdir -p extern/Catch2
          cp -r ${catch2}/* extern/Catch2/
        '';
      };
    };
}