# generic builder for Cabal packages

{ stdenv, fetchurl, lib, pkgconfig, ghc, Cabal, jailbreakCabal, glibcLocales
, gnugrep, coreutils, hscolour
, enableLibraryProfiling ? false
, enableSharedLibraries ? false
, enableSharedExecutables ? false
, enableStaticLibraries ? true
, enableCheckPhase ? stdenv.lib.versionOlder "7.4" ghc.version
, enableHyperlinkSource ? true
, extension ? (self : super : {})
}:

let
  enableFeature         = stdenv.lib.enableFeature;
  versionOlder          = stdenv.lib.versionOlder;
  optional              = stdenv.lib.optional;
  optionals             = stdenv.lib.optionals;
  optionalString        = stdenv.lib.optionalString;
  filter                = stdenv.lib.filter;

  defaultSetupHs        = builtins.toFile "Setup.hs" ''
                            import Distribution.Simple
                            main = defaultMain
                          '';
in

# Cabal shipped with GHC 6.12.4 or earlier doesn't know the "--enable-tests configure" flag.
assert enableCheckPhase -> versionOlder "7" ghc.version;

# GHC prior to 7.4.x doesn't know the "--enable-executable-dynamic" flag.
assert enableSharedExecutables -> versionOlder "7.4" ghc.version;

# Our GHC 6.10.x builds do not provide sharable versions of their core libraries.
assert enableSharedLibraries -> versionOlder "6.12" ghc.version;

# Pure shared library builds don't work before GHC 7.8.x.
assert !enableStaticLibraries -> versionOlder "7.7" ghc.version;

{
  mkDerivation =
    args : # arguments for the individual package, can modify the defaults
    let # These attributes are removed in the end. This is in order not to spoil the build
        # environment overly, but also to keep hash-backwards-compatible with the old cabal.nix.
        internalAttrs = [
          "internalAttrs" "buildDepends" "buildTools" "extraLibraries" "pkgconfigDepends"
          "isLibrary" "isExecutable" "testDepends"
        ];

        # Stuff happening after the user preferences have been processed. We remove
        # internal attributes and strip null elements from the dependency lists, all
        # in the interest of keeping hashes stable.
        postprocess =
          x : (removeAttrs x internalAttrs) // {
                buildInputs           = filter (y : ! (y == null)) x.buildInputs;
                propagatedBuildInputs = filter (y : ! (y == null)) x.propagatedBuildInputs;
                propagatedUserEnvPkgs = filter (y : ! (y == null)) x.propagatedUserEnvPkgs;
                doCheck               = enableCheckPhase && x.doCheck;
                hyperlinkSource       = enableHyperlinkSource && x.hyperlinkSource;
                # Disable Darwin builds: <https://github.com/NixOS/nixpkgs/issues/2689>.
                meta                  = let meta = x.meta or {};
                                            hydraPlatforms = meta.hydraPlatforms or meta.platforms or [];
                                            noElem         = p: ps: !stdenv.lib.elem p ps;
                                            noDarwin       = p: noElem p stdenv.lib.platforms.darwin;
                                        in
                                        meta // { hydraPlatforms = filter noDarwin hydraPlatforms; };
              };

        defaults =
          self : { # self is the final version of the attribute set

            # pname should be defined by the client to be the package basename
            # version should be defined by the client to be the package version

            # fname is the internal full name of the package
            fname = "${self.pname}-${self.version}";

            # name is the external full name of the package; usually we prefix
            # all packages with haskell- to avoid name clashes for libraries;
            # if that is not desired (for applications), name can be set to
            # fname.
            name = if self.isLibrary then
                     if enableLibraryProfiling && self.enableSharedLibraries then
                       "haskell-${self.pname}-ghc${ghc.ghc.version}-${self.version}-profiling-shared"
                     else if enableLibraryProfiling && !self.enableSharedLibraries then
                       "haskell-${self.pname}-ghc${ghc.ghc.version}-${self.version}-profiling"
                     else if !enableLibraryProfiling && self.enableSharedLibraries then
                       "haskell-${self.pname}-ghc${ghc.ghc.version}-${self.version}-shared"
                     else
                       "haskell-${self.pname}-ghc${ghc.ghc.version}-${self.version}"
                   else
                     "${self.pname}-${self.version}";

            # the default download location for Cabal packages is Hackage,
            # you still have to specify the checksum
            src = fetchurl {
              url = "mirror://hackage/${self.pname}/${self.fname}.tar.gz";
              inherit (self) sha256;
            };

            # default buildInputs are just ghc, if more buildInputs are required
            # buildInputs can be extended by the client by using extraBuildInputs,
            # but often propagatedBuildInputs is preferable anyway
            buildInputs = [ghc Cabal] ++ self.extraBuildInputs;
            extraBuildInputs = self.buildTools ++
                               (optionals self.doCheck self.testDepends) ++
                               (optional self.hyperlinkSource hscolour) ++
                               (if self.pkgconfigDepends == [] then [] else [pkgconfig]) ++
                               (if self.isLibrary then [] else self.buildDepends ++ self.extraLibraries ++ self.pkgconfigDepends);

            # we make sure that propagatedBuildInputs is defined, so that we don't
            # have to check for its existence
            propagatedBuildInputs = if self.isLibrary then self.buildDepends ++ self.extraLibraries ++ self.pkgconfigDepends else [];

            # By default, also propagate all dependencies to the user environment. This is required, otherwise packages would be broken, because
            # GHC also needs all dependencies to be available.
            propagatedUserEnvPkgs = if self.isLibrary then self.buildDepends else [];

            # library directories that have to be added to the Cabal files
            extraLibDirs = [];

            # build-depends Cabal field
            buildDepends = [];

            # target(s) passed to the cabal build phase as an argument
            buildTarget = "";

            # build-depends Cabal fields stated in test-suite stanzas
            testDepends = [];

            # target(s) passed to the cabal test phase as an argument
            testTarget = "";

            # build-tools Cabal field
            buildTools = [];

            # extra-libraries Cabal field
            extraLibraries = [];

            # pkgconfig-depends Cabal field
            pkgconfigDepends = [];

            isLibrary = ! self.isExecutable;
            isExecutable = false;

            # ignore version restrictions on the build inputs that the cabal file might specify
            jailbreak = false;

            # pass the '--enable-split-objs' flag to cabal in the configure stage
            enableSplitObjs = !stdenv.isDarwin;         # http://hackage.haskell.org/trac/ghc/ticket/4013

            # pass the '--enable-tests' flag to cabal in the configure stage
            # and run any regression test suites the package might have
            doCheck = enableCheckPhase;

            # pass the '--hyperlink-source' flag to ./Setup haddock
            hyperlinkSource = enableHyperlinkSource;

            # abort the build if the configure phase detects that the package
            # depends on multiple versions of the same build input
            strictConfigurePhase = true;

            # pass the '--enable-library-vanilla' flag to cabal in the
            # configure stage to enable building shared libraries
            inherit enableStaticLibraries;

            # pass the '--enable-shared' flag to cabal in the configure
            # stage to enable building shared libraries
            inherit enableSharedLibraries;

            # pass the '--enable-executable-dynamic' flag to cabal in
            # the configure stage to enable linking shared libraries
            inherit enableSharedExecutables;

            extraConfigureFlags = [
              (enableFeature self.enableSplitObjs "split-objs")
              (enableFeature enableLibraryProfiling "library-profiling")
              (enableFeature self.enableSharedLibraries "shared")
              (optional (versionOlder "7" ghc.version) (enableFeature self.enableStaticLibraries "library-vanilla"))
              (optional (versionOlder "7.4" ghc.version) (enableFeature self.enableSharedExecutables "executable-dynamic"))
              (optional (versionOlder "7" ghc.version) (enableFeature self.doCheck "tests"))
            ];

            # GHC needs the locale configured during the Haddock phase.
            LANG = "en_US.UTF-8";
            LOCALE_ARCHIVE = optionalString stdenv.isLinux "${glibcLocales}/lib/locale/locale-archive";

            # compiles Setup and configures
            configurePhase = ''
              eval "$preConfigure"

              ${optionalString self.jailbreak "${jailbreakCabal}/bin/jailbreak-cabal ${self.pname}.cabal"}

              for i in Setup.hs Setup.lhs ${defaultSetupHs}; do
                test -f $i && break
              done
              ghc --make -o Setup -odir $TMPDIR -hidir $TMPDIR $i

              for p in $extraBuildInputs $propagatedNativeBuildInputs; do
                if [ -d "$p/lib/ghc-${ghc.ghc.version}/package.conf.d" ]; then
                  # Haskell packages don't need any extra configuration.
                  continue;
                fi
                if [ -d "$p/include" ]; then
                  extraConfigureFlags+=" --extra-include-dirs=$p/include"
                fi
                for d in lib{,64}; do
                  if [ -d "$p/$d" ]; then
                    extraConfigureFlags+=" --extra-lib-dirs=$p/$d"
                  fi
                done
              done

              ${optionalString (self.enableSharedExecutables && self.stdenv.isLinux) ''
                configureFlags+=" --ghc-option=-optl=-Wl,-rpath=$out/lib/${ghc.ghc.name}/${self.pname}-${self.version}"
              ''}
              ${optionalString (self.enableSharedExecutables && self.stdenv.isDarwin) ''
                configureFlags+=" --ghc-option=-optl=-Wl,-headerpad_max_install_names"
              ''}
              ${optionalString (versionOlder "7.8" ghc.version && !self.isLibrary) ''
                configureFlags+=" --ghc-option=-j$NIX_BUILD_CORES"
              ''}

              echo "configure flags: $extraConfigureFlags $configureFlags"
              ./Setup configure --verbose --prefix="$out" --libdir='$prefix/lib/$compiler' \
                --libsubdir='$pkgid' $extraConfigureFlags $configureFlags 2>&1 \
              ${optionalString self.strictConfigurePhase ''
                | ${coreutils}/bin/tee "$NIX_BUILD_TOP/cabal-configure.log"
                if ${gnugrep}/bin/egrep -q '^Warning:.*depends on multiple versions' "$NIX_BUILD_TOP/cabal-configure.log"; then
                  echo >&2 "*** abort because of serious configure-time warning from Cabal"
                  exit 1
                fi
              ''}

              eval "$postConfigure"
            '';

            # builds via Cabal
            buildPhase = ''
              eval "$preBuild"

              ./Setup build ${self.buildTarget}

              export GHC_PACKAGE_PATH=$(${ghc.GHCPackages})
              test -n "$noHaddock" || ./Setup haddock --html --hoogle \
                  ${optionalString self.hyperlinkSource "--hyperlink-source"}

              eval "$postBuild"
            '';

            checkPhase = optional self.doCheck ''
              eval "$preCheck"

              ./Setup test ${self.testTarget}

              eval "$postCheck"
            '';

            # installs via Cabal; creates a registration file for nix-support
            # so that the package can be used in other Haskell-builds; also
            # adds all propagated build inputs to the user environment packages
            installPhase = ''
              eval "$preInstall"

              ./Setup copy

              mkdir -p $out/bin # necessary to get it added to PATH

              local confDir=$out/lib/ghc-${ghc.ghc.version}/package.conf.d
              local installedPkgConf=$confDir/${self.fname}.installedconf
              local pkgConf=$confDir/${self.fname}.conf
              mkdir -p $confDir
              ./Setup register --gen-pkg-config=$pkgConf
              if test -f $pkgConf; then
                echo '[]' > $installedPkgConf
                GHC_PACKAGE_PATH=$installedPkgConf ghc-pkg --global register $pkgConf --force
              fi

              if test -f $out/nix-support/propagated-native-build-inputs; then
                ln -s $out/nix-support/propagated-native-build-inputs $out/nix-support/propagated-user-env-packages
              fi

              ${optionalString (self.enableSharedExecutables && self.isExecutable && self.stdenv.isDarwin) ''
                for exe in "$out/bin/"* ; do
                  install_name_tool -add_rpath \
                    $out/lib/${ghc.ghc.name}/${self.pname}-${self.version} $exe
                done
              ''}

              eval "$postInstall"
            '';

            # We inherit stdenv and ghc so that they can be used
            # in Cabal derivations.
            inherit stdenv ghc;
          };
    in
    stdenv.mkDerivation (postprocess (let super = defaults self // args self;
                                          self  = super // extension self super;
                                      in self));
}