packages feed

doctest-parallel-0.4: src/Test/DocTest/Internal/Nix.hs

{-# LANGUAGE LambdaCase #-}

module Test.DocTest.Internal.Nix where

import Control.Monad (msum)
import Control.Monad.Extra (ifM)
import Control.Monad.Trans.Maybe
import Data.Bool (bool)
import Data.List (intercalate, isSuffixOf)
import Data.Maybe (isJust)
import Data.Version
import GHC.Base (mzero)
import System.Directory
import System.Environment (lookupEnv)
import System.FilePath ((</>), isDrive, takeDirectory)
import System.Process (readProcess)

import GHC.Data.Maybe (liftMaybeT)
import System.Info (fullCompilerVersion)

-- | E.g. @9.0.2@
compilerVersionStr :: String
compilerVersionStr = intercalate "." (map show (versionBranch fullCompilerVersion))

-- | Traverse upwards until one of the following conditions is met:
--
--   * Current working directory is either root or a home directory
--   * The predicate function returns 'Just'
--
findDirectoryUp :: (FilePath -> IO (Maybe a)) -> MaybeT IO a
findDirectoryUp f = do
  home <- liftMaybeT getHomeDirectory
  MaybeT (go home =<< getCurrentDirectory)
 where
  go home cwd
    | isDrive cwd = pure Nothing
    | cwd == home = pure Nothing
    | otherwise =
      f cwd >>= \case
        Just a -> pure (Just a)
        Nothing -> go home (takeDirectory cwd)

-- | Like 'findDirectoryUp', but takes a predicate function instead. If the predicate
-- yields 'True', the filepath is returned.
findDirectoryUpPredicate :: (FilePath -> IO Bool) -> MaybeT IO FilePath
findDirectoryUpPredicate f = findDirectoryUp (\fp -> bool Nothing (Just fp) <$> f fp)

-- | Find the root of the Cabal project relative to the current directory.
findCabalProjectRoot :: MaybeT IO FilePath
findCabalProjectRoot =
  msum
    [ findDirectoryUpPredicate containsCabalProject
    , findDirectoryUpPredicate containsCabalPackage
    ]
 where
  containsCabalPackage :: FilePath -> IO Bool
  containsCabalPackage fp = elem "cabal.project" <$> getDirectoryContents fp

  containsCabalProject :: FilePath -> IO Bool
  containsCabalProject fp = any (".cabal" `isSuffixOf`) <$> getDirectoryContents fp

-- | Find the local package database in @dist-newstyle@.
findLocalPackageDb :: MaybeT IO FilePath
findLocalPackageDb = do
  projectRoot <- findCabalProjectRoot
  let
    relDir = "dist-newstyle" </> "packagedb" </> ("ghc-" ++ compilerVersionStr)
    absDir = projectRoot </> relDir
  ifM
    (liftMaybeT (doesDirectoryExist absDir))
    (return absDir)
    mzero

-- | Are we running in a Nix shell?
inNixShell :: IO Bool
inNixShell = isJust <$> lookupEnv "IN_NIX_SHELL"

-- | Are we running in a Nix build environment?
inNixBuild :: IO Bool
inNixBuild = isJust <$> lookupEnv "NIX_BUILD_TOP"

getLocalCabalPackageDbArgs :: IO [String]
getLocalCabalPackageDbArgs = do
  runMaybeT findLocalPackageDb >>= \case
     Nothing -> pure []
     Just s -> pure ["-package-db", s]

getLocalNixPackageDbArgs :: IO [String]
getLocalNixPackageDbArgs = do
  pkgDb <- makeAbsolute ("dist" </> "package.conf.inplace")
  ifM
    (doesDirectoryExist pkgDb)
    (pure ["-package-db", pkgDb])
    (pure [])

-- | Get global package db; used in a NIX_SHELL context
getGlobalPackageDb :: IO String
getGlobalPackageDb = init <$> readProcess "ghc" ["--print-global-package-db"] ""

-- | Get flags to be used when running in a Nix context (either in a build, or a
-- shell).
getNixGhciArgs :: IO [String]
getNixGhciArgs =
  ifM inNixShell goShell (ifM inNixBuild goBuild (pure []))
 where
  goShell = do
    globalPkgDb <- getGlobalPackageDb
    localPkgDbFlag <- getLocalCabalPackageDbArgs
    let globalDbFlag = ["-package-db", globalPkgDb]
    pure (defaultArgs ++ globalDbFlag ++ localPkgDbFlag)

  goBuild = do
    localDbFlag <- getLocalNixPackageDbArgs
    pure (defaultArgs ++ localDbFlag)

  defaultArgs =
    [ "-package-env", "-"

    -- Nix doesn't always expose the GHC library (_specifically_ the GHC lib) even
    -- if a package lists it as a dependency. This simply always exposes it as a
    -- workaround.
    , "-package", "ghc"
    ]