# Copyright (c) 2003-2024 Eelco Dolstra and the Nixpkgs/NixOS contributors
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{
  hugo,
  go,
  cacert,
  git,
  lib,
  stdenv,
}:

# A modified Go builder for generating a website with Hugo. Since it relies on
# Hugo modules (which is basically wrapper around Go modules), this can be used
# for Hugo projects that heavily uses them.
#
# Take note, this doesn't work for Hugo projects with remote resources
# right in the content since Hugo allows network access when generating
# the website.
{
  name ? "${args'.pname}-${args'.version}",

  nativeBuildInputs ? [ ],
  passthru ? { },

  # A function to override the goModules derivation
  overrideModAttrs ? (_oldAttrs: { }),

  # path to go.mod and go.sum directory
  modRoot ? "./",

  # vendorHash is the SRI hash of the vendored dependencies
  #
  # if vendorHash is null, then we won't fetch any dependencies and
  # rely on the vendor folder within the source.
  vendorHash ? throw (
    if args' ? vendorSha256 then
      "buildHugoSite: Expect vendorHash instead of vendorSha256"
    else
      "buildHugoSite: 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,

  CGO_ENABLED ? go.CGO_ENABLED,

  meta ? { },

  ldflags ? [ ],

  GOFLAGS ? [ ],

  ...
}@args':

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

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

  hugoModules =
    if (vendorHash == null) then
      ""
    else
      (stdenv.mkDerivation {
        name = "${name}-hugo-modules";

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

        inherit (args) src;
        inherit (go) GOOS GOARCH;
        inherit GO111MODULE GOTOOLCHAIN;

        # The following inheritence 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 = args.prePatch or "";
        patches = args.patches or [ ];
        patchFlags = args.patchFlags or [ ];
        postPatch = args.postPatch or "";
        preBuild = args.preBuild or "";
        postBuild = args.modPostBuild or "";
        sourceRoot = args.sourceRoot or "";
        setSourceRoot = args.setSourceRoot or "";
        env = args.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 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

              ${
                if proxyVendor then
                  ''
                    mkdir -p "''${GOPATH}/pkg/mod/cache/download"
                    hugo mod vendor
                  ''
                else
                  ''
                    if (( "''${NIX_DEBUG:-0}" >= 1 )); then
                      hugoModVendorFlags+=(-v)
                    fi
                    hugo mod vendor "''${hugoModVendorFlags[@]}"
                  ''
              }

              mkdir -p _vendor

              runHook postBuild
            ''
          );

        installPhase =
          args.modInstallPhase or ''
            runHook preInstall

            ${
              if 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 = vendorHash;
        # Handle empty vendorHash; avoid
        # error: empty hash requires explicit hash algorithm
        outputHashAlgo = if vendorHash == "" then "sha256" else null;
      }).overrideAttrs
        overrideModAttrs;

  package = stdenv.mkDerivation (
    args
    // {
      nativeBuildInputs = [
        hugo
        git
        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 (!proxyVendor) "-mod=vendor")
        ++
          lib.warnIf (builtins.elem "-trimpath" GOFLAGS)
            "`-trimpath` is added by default to GOFLAGS by buildHugoSite 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 (vendorHash != null) ''
            ${
              if proxyVendor then
                ''
                  export GOPROXY=file://${hugoModules}
                ''
              else
                ''
                  rm -rf _vendor
                  cp -r --reflink=auto ${hugoModules} _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 ''
          runHook preBuild
          hugo "''${buildFlags[@]}" --destination public
          runHook postBuild
        '';

      doCheck = args.doCheck or true;
      checkPhase =
        args.checkPhase or ''
          runHook preCheck

          runHook postCheck
        '';

      installPhase =
        args.installPhase or ''
          runHook preInstall

          mkdir -p $out
          cp -r public/* $out

          runHook postInstall
        '';

      strictDeps = true;

      disallowedReferences = lib.optional (!allowGoReference) go;

      passthru = passthru // {
        inherit
          go
          hugo
          hugoModules
          vendorHash
          ;
      };

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