# Configurations that should only be overrided by
# overrideAttrs
{
  pname,
  version,
  src,
  projectName, # "apptainer" or "singularity"
  vendorHash ? null,
  deleteVendor ? false,
  proxyVendor ? false,
  extraConfigureFlags ? [ ],
  extraDescription ? "",
  extraMeta ? { },
}:

let
  # Workaround for vendor-related attributes not overridable (#86349)
  # should be removed when the issue is resolved
  _defaultGoVendorArgs = {
    inherit vendorHash deleteVendor proxyVendor;
  };
in
{
  lib,
  buildGoModule,
  runCommandLocal,
  substituteAll,
  # Native build inputs
  addDriverRunpath,
  makeWrapper,
  pkg-config,
  util-linux,
  which,
  # Build inputs
  bash,
  callPackage,
  conmon,
  coreutils,
  cryptsetup,
  e2fsprogs,
  fakeroot,
  fuse2fs ? e2fsprogs.fuse2fs,
  go,
  gpgme,
  libseccomp,
  libuuid,
  mount,
  # This is for nvidia-container-cli
  nvidia-docker,
  openssl,
  squashfsTools,
  squashfuse,
  # Test dependencies
  singularity-tools,
  cowsay,
  hello,
  # Overridable configurations
  enableNvidiaContainerCli ? true,
  # --nvccli currently requires extra privileges:
  # https://github.com/apptainer/apptainer/issues/1893#issuecomment-1881240800
  forceNvcCli ? false,
  # Compile with seccomp support
  # SingularityCE 3.10.0 and above requires explicit --without-seccomp when libseccomp is not available.
  enableSeccomp ? true,
  # Whether the configure script treat SUID support as default
  # When equal to enableSuid, it supress the --with-suid / --without-suid build flag
  # It can be set to `null` to always pass either --with-suid or --without-suided
  # Type: null or boolean
  defaultToSuid ? true,
  # Whether to compile with SUID support
  enableSuid ? false,
  starterSuidPath ? null,
  # Extra system-wide /**/bin paths to prefix,
  # useful to specify directories containing binaries with SUID bit set.
  # The paths take higher precedence over the FHS system PATH specified
  # inside the upstream source code.
  # Include "/run/wrappers/bin" by default for the convenience of NixOS users.
  systemBinPaths ? [ "/run/wrappers/bin" ],
  # Path to SUID-ed newuidmap executable
  # Deprecated in favour of systemBinPaths
  # TODO(@ShamrockLee): Remove after Nixpkgs 24.05 branch-off
  newuidmapPath ? null,
  # Path to SUID-ed newgidmap executable
  # Deprecated in favour of systemBinPaths
  # TODO(@ShamrockLee): Remove after Nixpkgs 24.05 branch-off
  newgidmapPath ? null,
  # External LOCALSTATEDIR
  externalLocalStateDir ? null,
  # Remove the symlinks to `singularity*` when projectName != "singularity"
  removeCompat ? false,
  # The defaultPath values to substitute in each source files.
  #
  # `defaultPath` are PATH variables hard-coded inside Apptainer/Singularity
  # binaries to search for third-party utilities, as a hardening for
  # `$out/bin/starter-suid`.
  #
  # The upstream provided values are suitable for FHS-conformant environment.
  # We substitute them and insert Nixpkgs-specific values.
  #
  # Example:
  # {
  #   "path/to/source/file1" = [ "<originalDefaultPath11>" "<originalDefaultPath12>" ... ];
  # }
  sourceFilesWithDefaultPaths ? { },
  # Workaround #86349
  # should be removed when the issue is resolved
  vendorHash ? _defaultGoVendorArgs.vendorHash,
  deleteVendor ? _defaultGoVendorArgs.deleteVendor,
  proxyVendor ? _defaultGoVendorArgs.proxyVendor,
}@args:

let
  # Backward compatibility for privileged-un-utils.
  # TODO(@ShamrockLee): Remove after Nixpkgs 24.05 branch-off.
  privileged-un-utils =
    if ((newuidmapPath == null) && (newgidmapPath == null)) then
      null
    else
      lib.warn
        "${pname}: arguments newuidmapPath and newgidmapPath is deprecated in favour of systemBinPaths."
        (
          runCommandLocal "privileged-un-utils" { } ''
            mkdir -p "$out/bin"
            ln -s ${lib.escapeShellArg newuidmapPath} "$out/bin/newuidmap"
            ln -s ${lib.escapeShellArg newgidmapPath} "$out/bin/newgidmap"
          ''
        );

  # Backward compatibility for privileged-un-utils.
  # TODO(@ShamrockLee): Remove after Nixpkgs 24.05 branch-off.
  systemBinPaths =
    lib.optional (privileged-un-utils != null) (lib.makeBinPath [ privileged-un-utils ])
    ++ args.systemBinPaths or [ "/run/wrappers/bin" ];

  concatMapStringAttrsSep =
    sep: f: attrs:
    lib.concatMapStringsSep sep (name: f name attrs.${name}) (lib.attrNames attrs);

  addShellDoubleQuotes = s: lib.escapeShellArg ''"'' + s + lib.escapeShellArg ''"'';
in
(buildGoModule {
  inherit pname version src;

  patches = lib.optionals (projectName == "apptainer") [
    (substituteAll {
      src = ./apptainer/0001-ldCache-patch-for-driverLink.patch;
      inherit (addDriverRunpath) driverLink;
    })
  ];

  # Override vendorHash with the output got from
  # nix-prefetch -E "{ sha256 }: ((import ./. { }).apptainer.override { vendorHash = sha256; }).goModules"
  # or with `null` when using vendored source tarball.
  inherit vendorHash deleteVendor proxyVendor;

  # go is used to compile extensions when building container images
  allowGoReference = true;

  strictDeps = true;

  passthru = {
    inherit
      enableSeccomp
      enableSuid
      externalLocalStateDir
      projectName
      removeCompat
      starterSuidPath
      ;
  };

  nativeBuildInputs = [
    makeWrapper
    pkg-config
    util-linux
    which
  ];

  # Search inside the project sources
  # and see the `control` file of the Debian package from upstream repos
  # for build-time dependencies and run-time utilities
  # apptainer/apptainer: https://github.com/apptainer/apptainer/blob/main/dist/debian/control
  # sylabs/singularity: https://github.com/sylabs/singularity/blob/main/debian/control

  buildInputs =
    [
      bash # To patch /bin/sh shebangs.
      conmon
      cryptsetup
      gpgme
      libuuid
      openssl
      squashfsTools # Required at build time by SingularityCE
    ]
    # Optional dependencies.
    # Formatting: Optional dependencies are likely to increase.
    # Don't squash them into the same line.
    ++ lib.optional enableNvidiaContainerCli nvidia-docker
    ++ lib.optional enableSeccomp libseccomp;

  configureScript = "./mconfig";

  configureFlags =
    [
      "--localstatedir=${
        if externalLocalStateDir != null then externalLocalStateDir else "${placeholder "out"}/var/lib"
      }"
      "--runstatedir=/var/run"
    ]
    ++ lib.optional (!enableSeccomp) "--without-seccomp"
    ++ lib.optional (enableSuid != defaultToSuid) (
      if enableSuid then "--with-suid" else "--without-suid"
    )
    ++ extraConfigureFlags;

  # causes redefinition of _FORTIFY_SOURCE
  hardeningDisable = [ "fortify3" ];

  # Packages to provide fallback bin paths
  # to the Apptainer/Singularity container runtime default PATHs.
  # Override with `<pkg>.overrideAttrs`.
  defaultPathInputs = [
    bash
    coreutils
    cryptsetup # cryptsetup
    fakeroot
    fuse2fs # Mount ext3 filesystems
    go
    mount # mount
    squashfsTools # mksquashfs unsquashfs # Make / unpack squashfs image
    squashfuse # squashfuse_ll squashfuse # Mount (without unpacking) a squashfs image without privileges
  ] ++ lib.optional enableNvidiaContainerCli nvidia-docker;

  postPatch = ''
    if [[ ! -e .git || ! -e VERSION ]]; then
      echo "${version}" > VERSION
    fi

    # Patch shebangs for script run during build
    patchShebangs --build "$configureScript" makeit e2e scripts mlocal/scripts

    # Patching the hard-coded defaultPath by prefixing the packages in defaultPathInputs
    ${concatMapStringAttrsSep "\n" (fileName: originalDefaultPaths: ''
      substituteInPlace ${lib.escapeShellArg fileName} \
        ${
          lib.concatMapStringsSep " \\\n  " (
            originalDefaultPath:
            lib.concatStringsSep " " [
              "--replace-fail"
              (addShellDoubleQuotes (lib.escapeShellArg originalDefaultPath))
              (addShellDoubleQuotes ''$systemDefaultPath''${systemDefaultPath:+:}${lib.escapeShellArg originalDefaultPath}''${inputsDefaultPath:+:}$inputsDefaultPath'')
            ]
          ) originalDefaultPaths
        }
    '') sourceFilesWithDefaultPaths}

    substituteInPlace internal/pkg/util/gpu/nvidia.go \
      --replace \
        'return fmt.Errorf("/usr/bin not writable in the container")' \
        ""
  '';

  postConfigure = ''
    # Code borrowed from pkgs/stdenv/generic/setup.sh configurePhase()

    # set to empty if unset
    : ''${configureFlags=}

    # shellcheck disable=SC2086
    $configureScript -V ${version} "''${prefixKey:---prefix=}$prefix" $configureFlags "''${configureFlagsArray[@]}"

    # End of the code from pkgs/stdenv/generic/setup.sh configurPhase()
  '';

  buildPhase = ''
    runHook preBuild
    make -C builddir -j"$NIX_BUILD_CORES"
    runHook postBuild
  '';

  installPhase = ''
    runHook preInstall
    make -C builddir install LOCALSTATEDIR="$out/var/lib"
    runHook postInstall
  '';

  postFixup = ''
    substituteInPlace "$out/bin/run-singularity" \
      --replace "/usr/bin/env ${projectName}" "$out/bin/${projectName}"
    # Respect PATH from the environment/the user.
    # Fallback to bin paths provided by Nixpkgs packages.
    wrapProgram "$out/bin/${projectName}" \
      --suffix PATH : "$systemDefaultPath" \
      --suffix PATH : "$inputsDefaultPath"
    # Make changes in the config file
    ${lib.optionalString forceNvcCli ''
      substituteInPlace "$out/etc/${projectName}/${projectName}.conf" \
        --replace "use nvidia-container-cli = no" "use nvidia-container-cli = yes"
    ''}
    ${lib.optionalString (enableNvidiaContainerCli && projectName == "singularity") ''
      substituteInPlace "$out/etc/${projectName}/${projectName}.conf" \
        --replace "# nvidia-container-cli path =" "nvidia-container-cli path = ${nvidia-docker}/bin/nvidia-container-cli"
    ''}
    ${lib.optionalString (removeCompat && (projectName != "singularity")) ''
      unlink "$out/bin/singularity"
      for file in "$out"/share/man/man?/singularity*.gz; do
        if [[ -L "$file" ]]; then
          unlink "$file"
        fi
      done
      for file in "$out"/share/*-completion/completions/singularity; do
        if [[ -e "$file" ]]
        rm "$file"
      done
    ''}
    ${lib.optionalString enableSuid (
      lib.warnIf (starterSuidPath == null)
        "${projectName}: Null starterSuidPath when enableSuid produces non-SUID-ed starter-suid and run-time permission denial."
        ''
          chmod +x $out/libexec/${projectName}/bin/starter-suid
        ''
    )}
    ${lib.optionalString (enableSuid && (starterSuidPath != null)) ''
      mv "$out"/libexec/${projectName}/bin/starter-suid{,.orig}
      ln -s ${lib.escapeShellArg starterSuidPath} "$out/libexec/${projectName}/bin/starter-suid"
    ''}
  '';

  meta =
    with lib;
    {
      description = "Application containers for linux" + extraDescription;
      longDescription = ''
        Singularity (the upstream) renamed themselves to Apptainer
        to distinguish themselves from a fork made by Sylabs Inc.. See

        https://sylabs.io/2021/05/singularity-community-edition
        https://apptainer.org/news/community-announcement-20211130
      '';
      license = licenses.bsd3;
      platforms = platforms.linux;
      maintainers = with maintainers; [
        jbedo
        ShamrockLee
      ];
      mainProgram = projectName;
    }
    // extraMeta;
}).overrideAttrs
  (
    finalAttrs: prevAttrs: {
      systemDefaultPath = lib.concatStringsSep ":" systemBinPaths;
      inputsDefaultPath = lib.makeBinPath finalAttrs.defaultPathInputs;
      passthru = prevAttrs.passthru or { } // {
        inherit sourceFilesWithDefaultPaths;
        tests = {
          image-hello-cowsay = singularity-tools.buildImage {
            name = "hello-cowsay";
            contents = [
              hello
              cowsay
            ];
            singularity = finalAttrs.finalPackage;
          };
        };
        gpuChecks = lib.optionalAttrs (projectName == "apptainer") {
          # Should be in tests, but Ofborg would skip image-hello-cowsay because
          # saxpy is unfree.
          image-saxpy = callPackage (
            { singularity-tools, cudaPackages }:
            singularity-tools.buildImage {
              name = "saxpy";
              contents = [ cudaPackages.saxpy ];
              memSize = 2048;
              diskSize = 2048;
              singularity = finalAttrs.finalPackage;
            }
          ) { };
          saxpy = callPackage (
            { runCommand, writeShellScriptBin }:
            let
              unwrapped = writeShellScriptBin "apptainer-cuda-saxpy" ''
                ${lib.getExe finalAttrs.finalPackage} exec --nv $@ ${finalAttrs.passthru.gpuChecks.image-saxpy} saxpy
              '';
            in
            runCommand "run-apptainer-cuda-saxpy"
              {
                requiredSystemFeatures = [ "cuda" ];
                nativeBuildInputs = [ unwrapped ];
                passthru = {
                  inherit unwrapped;
                };
              }
              ''
                apptainer-cuda-saxpy
              ''
          ) { };
        };
      };
    }
  )
