# Define component derivations and special treatments.
{
  lib,
  stdenv,
  stdenvNoCC,
  gnutar,
  autoPatchelfHook,
  bintools,
  zlib,
  gccForLibs,
  apple-sdk ? null,
  pkgsHostHost,
  # The path to nixpkgs root.
  path,
  toRustTarget,
  removeNulls,
}:
{
  # Release version of the whole set.
  version,
  # The host platform of this set.
  platform,
  # Set of pname -> src
  srcs,
  # { clippy.to = "clippy-preview"; }
  renames,
}:
let
  inherit (lib)
    elem
    mapAttrs
    optional
    optionalString
    ;
  inherit (stdenv) hostPlatform targetPlatform;

  mkComponent =
    pname: src:
    let
      # These components link to `librustc_driver*.so` or `libLLVM*.so`.
      linksToRustc = elem pname [
        "clippy-preview"
        "miri-preview"
        "rls-preview"
        "rust-analyzer-preview"
        "rustc-codegen-cranelift-preview"
        "rustc-dev"
        "rustfmt-preview"
      ];
    in
    stdenvNoCC.mkDerivation rec {
      inherit pname version src;
      name = "${pname}-${version}-${platform}";

      passthru.platform = platform;

      # No point copying src to a build server, then copying back the
      # entire unpacked contents after just a little twiddling.
      preferLocalBuild = true;

      nativeBuildInputs = [
        gnutar
      ]
      ++
        # Darwin doesn't use ELF, and they usually just work due to relative RPATH.
        optional (!dontFixup && !hostPlatform.isDarwin) autoPatchelfHook
      ++
        # For `install_name_tool`.
        optional (hostPlatform.isDarwin && linksToRustc) bintools;

      buildInputs =
        optional (elem pname [
          "rustc"
          "cargo"
          "llvm-tools-preview"
          "rust"
        ]) zlib
        ++ optional linksToRustc self.rustc;

      # Most of binaries links to `libgcc.so` on Linux, which lives in `gccForLibs.libgcc`
      # since https://github.com/NixOS/nixpkgs/pull/209870
      # See https://github.com/oxalica/rust-overlay/issues/121
      #
      # Nightly `rustc` since 2022-02-17 links to `libstdc++.so.6` on Linux,
      # which lives in `gccForLibs.lib`.
      # https://github.com/oxalica/rust-overlay/issues/73
      # See https://github.com/oxalica/rust-overlay/issues/73
      #
      # FIXME: `libstdc++.so` is not necessary now. Figure out the time point
      # of it so we can use `gccForLibs.libgcc` instead.
      #
      # N.B. `gcc` is a compiler which is sensitive to `targetPlatform`.
      # We use `depsHostHost` instead of `buildInputs` to force it ignore the target,
      # since binaries produced by `rustc` don't actually relies on this gccForLibs.
      depsHostHost = optional (!dontFixup && !hostPlatform.isDarwin) gccForLibs.lib;

      dontConfigure = true;
      dontBuild = true;

      installPhase = ''
        runHook preInstall
        installerVersion=$(< ./rust-installer-version)
        if [[ "$installerVersion" != 3 ]]; then
          echo "Unknown installer version: $installerVersion"
        fi
        mkdir -p "$out"
        while read -r comp; do
          echo "Installing component $comp"
          # We don't want to parse the file and invoking cp in bash due to slow forking.
          cut -d: -f2 <"$comp/manifest.in" | tar -cf - -C "$comp" --files-from - | tar -xC "$out"
        done <./components
        runHook postInstall
      '';

      postInstall = ''
        # Function moves well-known files from etc/
        handleEtc() {
          if [[ -d "$1" ]]; then
            mkdir -p "$(dirname "$2")"
            mv -T "$1" "$2"
          fi
        }
        if [[ -e "$out/etc" ]]; then
          handleEtc "$out/etc/bash_completion.d" "$out/share/bash-completion/completions"
          # Remove if empty. Skip if produced `/etc/target-spec-json-schema.json`.
          rmdir --ignore-fail-on-non-empty $out/etc
        fi
      '';

      # Only contain tons of html files. Don't waste time scanning files.
      dontFixup = elem pname [
        "rust-docs"
        "rustc-docs"
      ];

      # Darwin binaries usually just work... except for these linking to rustc from another drv.
      postFixup =
        optionalString (hostPlatform.isDarwin && linksToRustc) ''
          for f in $out/bin/*; do
            install_name_tool -add_rpath "${self.rustc}/lib" "$f" || true
          done
        ''
        # Wrap the shipped `rust-lld` (lld), which is used by default on some targets.
        # Unfortunately there is no hook to conveniently wrap CC tools inside
        # derivation and `wrapBintools` is designed for wrapping a standalone
        # bintools derivation. We hereby copy minimal of their implementation.
        # The `wrap()` is from:
        # https://github.com/NixOS/nixpkgs/blob/bfb7a882678e518398ce9a31a881538679f6f092/pkgs/build-support/bintools-wrapper/default.nix#L178
        + optionalString (pname == "rustc") ''
          wrap() {
            local dst="$1"
            local wrapper="$2"
            export prog="$3"
            export use_response_file_by_default=0
            substituteAll "$wrapper" "$dst"
            chmod +x "$dst"
          }

          dsts=( "$out"/lib/rustlib/*/bin/gcc-ld/ld.lld )
          if [[ ''${#dsts} -ne 0 ]]; then
            mkdir -p $out/nix-support
            substituteAll ${
              path + "/pkgs/build-support/wrapper-common/utils.bash"
            } $out/nix-support/utils.bash
            substituteAll ${
              path + "/pkgs/build-support/bintools-wrapper/add-flags.sh"
            } $out/nix-support/add-flags.sh
            substituteAll ${
              path + "/pkgs/build-support/bintools-wrapper/add-hardening.sh"
            } $out/nix-support/add-hardening.sh
            ${
              let
                # This script exists on all platforms, but only in recent nixpkgs.
                p = path + "/pkgs/build-support/wrapper-common/darwin-sdk-setup.bash";
              in
              optionalString (builtins.pathExists p) ''
                substituteAll ${p} $out/nix-support/darwin-sdk-setup.bash
              ''
              + optionalString targetPlatform.isDarwin ''
                substituteAll \
                  ${path + "/pkgs/build-support/bintools-wrapper/add-darwin-ldflags-before.sh"} \
                  $out/nix-support/add-local-ldflags-before.sh
              ''
            }

            for dst in "''${dsts[@]}"; do
              # The ld.lld is path/name sensitive because itself is a wrapper. Keep its original name.
              unwrapped="$(dirname "$dst")-unwrapped/ld.lld"
              mkdir -p "$(dirname "$unwrapped")"
              mv "$dst" "$unwrapped"
              wrap "$dst" ${path + "/pkgs/build-support/bintools-wrapper/ld-wrapper.sh"} "$unwrapped"
            done
          fi
        ''
        + optionalString (stdenv.isLinux && pname == "cargo") ''
          patchelf --add-needed ${pkgsHostHost.libsecret}/lib/libsecret-1.so.0 $out/bin/cargo
        '';

      env = lib.optionalAttrs (pname == "rustc") (
        {
          inherit (stdenv.cc.bintools)
            expandResponseParams
            shell
            suffixSalt
            wrapperName
            coreutils_bin
            ;
          hardening_unsupported_flags = "";

          # These envvars are used by darwin specific scripts.
          # See: https://github.com/NixOS/nixpkgs/blob/0a14706530dcb90acecb81ce0da219d88baaae75/pkgs/build-support/bintools-wrapper/default.nix
          fallback_sdk = optionalString (apple-sdk != null && targetPlatform.isDarwin) (
            apple-sdk.__spliced.buildTarget or apple-sdk
          );
        }
        // lib.mapAttrs (_: lib.optionalString targetPlatform.isDarwin) {
          inherit (targetPlatform)
            darwinPlatform
            darwinSdkVersion
            darwinMinVersion
            darwinMinVersionVariable
            ;
        }
      );

      dontStrip = true;

      meta =
        lib.optionalAttrs
          (elem pname [
            "rustc"
            "rustfmt-preview"
            "rust-analyzer-preview"
            "cargo"
          ])
          ({
            mainProgram = lib.removeSuffix "-preview" pname;
          });
    };

  self = mapAttrs mkComponent srcs;

in
removeNulls (self // mapAttrs (alias: { to }: self.${to} or null) renames)
