{ go, cacert, git, lib, stdenv }:

{ name ? "${args'.pname}-${args'.version}"
  # The source used to build the derivation.
, src
  # Native build inputs used for the derivation.
, nativeBuildInputs ? [ ]
, passthru ? { }
, patches ? [ ]

  # A function to override the `goModules` derivation.
, overrideModAttrs ? (finalAttrs: previousAttrs: { })

  # Directory to the `go.mod` and `go.sum` relative to the `src`.
, modRoot ? "./"

  # The SRI hash of the vendored dependencies.
  # If `vendorHash` is `null`, no dependencies are fetched and
  # the build relies on the vendor folder within the source.
, vendorHash ? throw (
    if args'?vendorSha256 then
      "buildGoModule: Expect vendorHash instead of vendorSha256"
    else
      "buildGoModule: vendorHash is missing"
  )

  # Whether to delete the vendor folder supplied with the source.
, deleteVendor ? false

  # Whether to fetch (go mod download) and proxy the vendor directory.
  # This is useful if your code depends on c code and go mod tidy does not
  # include the needed sources to build or if any dependency has case-insensitive
  # conflicts which will produce platform dependant `vendorHash` checksums.
, proxyVendor ? false

  # We want parallel builds by default.
, enableParallelBuilding ? true

  # Do not enable this without good reason
  # IE: programs coupled with the compiler.
, allowGoReference ? false

  # Go env. variable to enable CGO.
, CGO_ENABLED ? go.CGO_ENABLED

  # Meta data for the final derivation.
, meta ? { }

  # Not needed with `buildGoModule`.
, goPackagePath ? ""

  # Go linker flags.
, ldflags ? [ ]
  # Go build flags.
, GOFLAGS ? [ ]

  # Needed for buildFlags{,Array} warning
, buildFlags ? "" # deprecated
, buildFlagsArray ? "" # deprecated

, ...
}@args':

assert goPackagePath != "" -> throw "`goPackagePath` is not needed with `buildGoModule`";

let
  args = removeAttrs args' [ "overrideModAttrs" "vendorSha256" ];

  GO111MODULE = "on";
  GOTOOLCHAIN = "local";

in
(stdenv.mkDerivation (finalAttrs:
  args
  // {

  inherit modRoot vendorHash deleteVendor proxyVendor;
  goModules = if (finalAttrs.vendorHash == null) then "" else
  (stdenv.mkDerivation {
    name = "${finalAttrs.name or "${finalAttrs.pname}-${finalAttrs.version}"}-go-modules";

    nativeBuildInputs = (finalAttrs.nativeBuildInputs or [ ]) ++ [ go git cacert ];

    inherit (finalAttrs) src modRoot;
    inherit (go) GOOS GOARCH;
    inherit GO111MODULE GOTOOLCHAIN;

    # The following inheritance behavior is not trivial to expect, and some may
    # argue it's not ideal. Changing it may break vendor hashes in Nixpkgs and
    # out in the wild. In anycase, it's documented in:
    # doc/languages-frameworks/go.section.md.
    prePatch = finalAttrs.prePatch or "";
    patches = finalAttrs.patches or [ ];
    patchFlags = finalAttrs.patchFlags or [ ];
    postPatch = finalAttrs.postPatch or "";
    preBuild = finalAttrs.preBuild or "";
    postBuild = finalAttrs.modPostBuild or "";
    sourceRoot = finalAttrs.sourceRoot or "";
    setSourceRoot = finalAttrs.setSourceRoot or "";
    env = finalAttrs.env or { };

    impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
      "GIT_PROXY_COMMAND"
      "SOCKS_SERVER"
      "GOPROXY"
    ];

    configurePhase = args.modConfigurePhase or ''
      runHook preConfigure
      export GOCACHE=$TMPDIR/go-cache
      export GOPATH="$TMPDIR/go"
      cd "$modRoot"
      runHook postConfigure
    '';

    buildPhase = args.modBuildPhase or (''
      runHook preBuild
    '' + lib.optionalString finalAttrs.deleteVendor ''
      if [ ! -d vendor ]; then
        echo "vendor folder does not exist, 'deleteVendor' is not needed"
        exit 10
      else
        rm -rf vendor
      fi
    '' + ''
      if [ -d vendor ]; then
        echo "vendor folder exists, please set 'vendorHash = null;' in your expression"
        exit 10
      fi

      export GIT_SSL_CAINFO=$NIX_SSL_CERT_FILE
      ${if finalAttrs.proxyVendor then ''
        mkdir -p "''${GOPATH}/pkg/mod/cache/download"
        go mod download
      '' else ''
        if (( "''${NIX_DEBUG:-0}" >= 1 )); then
          goModVendorFlags+=(-v)
        fi
        go mod vendor "''${goModVendorFlags[@]}"
      ''}

      mkdir -p vendor

      runHook postBuild
    '');

    installPhase = args.modInstallPhase or ''
      runHook preInstall

      ${if finalAttrs.proxyVendor then ''
        rm -rf "''${GOPATH}/pkg/mod/cache/download/sumdb"
        cp -r --reflink=auto "''${GOPATH}/pkg/mod/cache/download" $out
      '' else ''
        cp -r --reflink=auto vendor $out
      ''}

      if ! [ "$(ls -A $out)" ]; then
        echo "vendor folder is empty, please set 'vendorHash = null;' in your expression"
        exit 10
      fi

      runHook postInstall
    '';

    dontFixup = true;

    outputHashMode = "recursive";
    outputHash = finalAttrs.vendorHash;
    # Handle empty `vendorHash`; avoid error:
    # empty hash requires explicit hash algorithm.
    outputHashAlgo = if finalAttrs.vendorHash == "" then "sha256" else null;
    # in case an overlay clears passthru by accident, don't fail evaluation
  }).overrideAttrs (finalAttrs.passthru.overrideModAttrs or overrideModAttrs);

    nativeBuildInputs = [ go ] ++ nativeBuildInputs;

    inherit (go) GOOS GOARCH;

    GOFLAGS = GOFLAGS
      ++ lib.warnIf (lib.any (lib.hasPrefix "-mod=") GOFLAGS) "use `proxyVendor` to control Go module/vendor behavior instead of setting `-mod=` in GOFLAGS"
        (lib.optional (!finalAttrs.proxyVendor) "-mod=vendor")
      ++ lib.warnIf (builtins.elem "-trimpath" GOFLAGS) "`-trimpath` is added by default to GOFLAGS by buildGoModule when allowGoReference isn't set to true"
        (lib.optional (!allowGoReference) "-trimpath");
    inherit CGO_ENABLED enableParallelBuilding GO111MODULE GOTOOLCHAIN;

    # If not set to an explicit value, set the buildid empty for reproducibility.
    ldflags = ldflags ++ lib.optional (!lib.any (lib.hasPrefix "-buildid=") ldflags) "-buildid=";

    configurePhase = args.configurePhase or (''
      runHook preConfigure

      export GOCACHE=$TMPDIR/go-cache
      export GOPATH="$TMPDIR/go"
      export GOPROXY=off
      export GOSUMDB=off
      cd "$modRoot"
    '' + lib.optionalString (finalAttrs.vendorHash != null) ''
      ${if finalAttrs.proxyVendor then ''
        export GOPROXY="file://$goModules"
      '' else ''
        rm -rf vendor
        cp -r --reflink=auto "$goModules" vendor
      ''}
    '' + ''

      # currently pie is only enabled by default in pkgsMusl
      # this will respect the `hardening{Disable,Enable}` flags if set
      if [[ $NIX_HARDENING_ENABLE =~ "pie" ]]; then
        export GOFLAGS="-buildmode=pie $GOFLAGS"
      fi

      runHook postConfigure
    '');

    buildPhase = args.buildPhase or (
      lib.warnIf (buildFlags != "" || buildFlagsArray != "")
        "`buildFlags`/`buildFlagsArray` are deprecated and will be removed in the 24.11 release. Use the `ldflags` and/or `tags` attributes instead of `buildFlags`/`buildFlagsArray`"
      lib.warnIf (builtins.elem "-buildid=" ldflags)
        "`-buildid=` is set by default as ldflag by buildGoModule"
    ''
      runHook preBuild

      exclude='\(/_\|examples\|Godeps\|testdata'
      if [[ -n "$excludedPackages" ]]; then
        IFS=' ' read -r -a excludedArr <<<$excludedPackages
        printf -v excludedAlternates '%s\\|' "''${excludedArr[@]}"
        excludedAlternates=''${excludedAlternates%\\|} # drop final \| added by printf
        exclude+='\|'"$excludedAlternates"
      fi
      exclude+='\)'

      buildGoDir() {
        local cmd="$1" dir="$2"

        declare -ga buildFlagsArray
        declare -a flags
        flags+=($buildFlags "''${buildFlagsArray[@]}")
        flags+=(''${tags:+-tags=''${tags// /,}})
        flags+=(''${ldflags:+-ldflags="$ldflags"})
        flags+=("-p" "$NIX_BUILD_CORES")

        if [ "$cmd" = "test" ]; then
          flags+=(-vet=off)
          flags+=($checkFlags)
        fi

        local OUT
        if ! OUT="$(go $cmd "''${flags[@]}" $dir 2>&1)"; then
          if ! echo "$OUT" | grep -qE '(no( buildable| non-test)?|build constraints exclude all) Go (source )?files'; then
            echo "$OUT" >&2
            return 1
          fi
        fi
        if [ -n "$OUT" ]; then
          echo "$OUT" >&2
        fi
        return 0
      }

      getGoDirs() {
        local type;
        type="$1"
        if [ -n "$subPackages" ]; then
          echo "$subPackages" | sed "s,\(^\| \),\1./,g"
        else
          find . -type f -name \*$type.go -exec dirname {} \; | grep -v "/vendor/" | sort --unique | grep -v "$exclude"
        fi
      }

      if (( "''${NIX_DEBUG:-0}" >= 1 )); then
        buildFlagsArray+=(-x)
      fi

      if [ -z "$enableParallelBuilding" ]; then
          export NIX_BUILD_CORES=1
      fi
      for pkg in $(getGoDirs ""); do
        echo "Building subPackage $pkg"
        buildGoDir install "$pkg"
      done
    '' + lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) ''
      # normalize cross-compiled builds w.r.t. native builds
      (
        dir=$GOPATH/bin/${go.GOOS}_${go.GOARCH}
        if [[ -n "$(shopt -s nullglob; echo $dir/*)" ]]; then
          mv $dir/* $dir/..
        fi
        if [[ -d $dir ]]; then
          rmdir $dir
        fi
      )
    '' + ''
      runHook postBuild
    '');

    doCheck = args.doCheck or true;
    checkPhase = args.checkPhase or ''
      runHook preCheck
      # We do not set trimpath for tests, in case they reference test assets
      export GOFLAGS=''${GOFLAGS//-trimpath/}

      for pkg in $(getGoDirs test); do
        buildGoDir test "$pkg"
      done

      runHook postCheck
    '';

    installPhase = args.installPhase or ''
      runHook preInstall

      mkdir -p $out
      dir="$GOPATH/bin"
      [ -e "$dir" ] && cp -r $dir $out

      runHook postInstall
    '';

    strictDeps = true;

    disallowedReferences = lib.optional (!allowGoReference) go;

    passthru = {
      inherit go;
      # Canonicallize `overrideModAttrs` as an attribute overlay.
      # `passthru.overrideModAttrs` will be overridden
      # when users want to override `goModules`.
      overrideModAttrs = lib.toExtension overrideModAttrs;
    } // passthru;

    meta = {
      # Add default meta information.
      platforms = go.meta.platforms or lib.platforms.all;
    } // meta;
  }
))
