libnix-0.3.0.0: Foreign/Nix/Shellout/Prefetch.hs
{-|
Description : Wrapper for the @nix-prefetch@ CLI utilities
Copyright : Profpatsch, 2018
License : GPL-3
Stability : experimental
Portability : nix-prefetch-scripts 2018 (no version number)
Calls to the @nix-prefetch-X@ utilities, to parse their output
into nice reusable data types.
-}
{-# LANGUAGE RecordWildCards, GeneralizedNewtypeDeriving, ApplicativeDo #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Foreign.Nix.Shellout.Prefetch
( -- * nix-prefetch-url
url, UrlOptions(..), defaultUrlOptions
-- * nix-prefetch-git
, git, GitOptions(..), defaultGitOptions, GitOutput(..)
-- * Types
, PrefetchError(..)
, Url(..), Sha256(..)
-- * Reexports
, runNixAction, NixAction(..), NixActionError(..)
) where
import Control.Error hiding (bool, err)
import qualified Data.Text as T
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as AesonT
import Foreign.Nix.Shellout.Types
import qualified Foreign.Nix.Shellout.Helpers as Helpers
import Data.Text (Text)
import Data.String (IsString)
import GHC.IO.Exception (ExitCode(ExitFailure, ExitSuccess))
import qualified Data.Text as Text
import Data.Bool (bool)
import Data.Bifunctor (first)
import qualified Data.Text.Lazy as Text.Lazy
import qualified Data.Text.Lazy.Encoding as Text.Lazy.Encoding
import qualified Data.List as List
data PrefetchError
= PrefetchOutputMalformed Text
-- ^ the tool’s output could not be parsed as expected
| ExpectedHashError
-- ^ an expected hash was given and not valid
| UnknownPrefetchError
-- ^ catch-all error
deriving (Eq, Show)
-- | A descriptive type for URLs.
newtype Url = Url { unUrl :: Text } deriving (Show, Eq, IsString)
-- | A @sha-256@ hash.
newtype Sha256 = Sha256 { unSha256 :: Text } deriving (Show, Eq, IsString)
data UrlOptions = UrlOptions
{ urlUrl :: Url
-- ^ the URL
, urlUnpack :: Bool
-- ^ whether to unpack before hashing (useful for prefetching @fetchTarball@)
, urlName :: Maybe Text
-- ^ name of the store path
, urlExpectedHash :: Maybe Sha256
-- ^ the hash we are expecting
}
-- | Takes the URL, doesn’t unpack and uses the default name.
defaultUrlOptions :: Url -> UrlOptions
defaultUrlOptions u = UrlOptions
{ urlUrl = u
, urlUnpack = False
, urlName = Nothing
, urlExpectedHash = Nothing }
-- | Runs @nix-prefetch-url@.
url :: UrlOptions -> NixAction PrefetchError (Sha256, StorePath Realized)
url UrlOptions{..} = Helpers.readProcess handler exec args
where
exec = "nix-prefetch-url"
args = if urlUnpack then ["--unpack"] else []
<> maybe [] (\n -> ["--name", n]) urlName
<> [ "--type", "sha256"
, "--print-path"
, unUrl urlUrl ]
<> maybe [] (pure.unSha256) urlExpectedHash
handler (out, err) = \case
ExitSuccess -> withExceptT PrefetchOutputMalformed $ do
let ls = T.lines $ T.stripEnd out
path <- tryLast (exec <> " didn’t output a store path") ls
sha <- let errS = (exec <> " didn’t output a hash")
in tryInit errS ls >>= tryLast errS
pure (Sha256 sha, StorePath $ Text.unpack path)
ExitFailure _ -> throwE $
if "error: hash mismatch" `T.isPrefixOf` err
then ExpectedHashError
else UnknownPrefetchError
data GitOptions = GitOptions
{ gitUrl :: Url
-- ^ the URL
, gitRev :: Maybe Text
-- ^ a git revision (hash, branch name, tag, ref, …)
, gitExpectedHash :: Maybe Sha256
-- ^ the hash we are expecting
, gitDeepClone :: Bool
-- ^ whether to do a deep instead of a shallow (@--depth=1@) git clone
, gitLeaveDotGit :: Bool
-- ^ whether to keep @.git@ directories
, gitFetchSubmodules :: Bool
-- ^ whether to fetch submodules
}
-- | Takes the url, mirrors the default `fetchgit` options in nixpkgs:
-- no deep clone, no @.git@, fetches submodules by default.
-- By default, the latest default @rev@ is used.
defaultGitOptions :: Url -> GitOptions
defaultGitOptions u = GitOptions
{ gitUrl = u
, gitRev = Nothing
, gitExpectedHash = Nothing
, gitDeepClone = False
, gitLeaveDotGit = False
, gitFetchSubmodules = True }
data GitOutput = GitOutput
{ gitOutputRev :: Text
-- ^ The actual revision that is used (useful if no 'gitRev' was given)
, gitOutputSha256 :: Sha256
-- ^ the hash
, gitOuputPath :: StorePath Realized
-- ^ the store path of the result
} deriving (Show, Eq)
-- | Runs @nix-prefetch-git@.
git :: GitOptions -> NixAction PrefetchError GitOutput
git GitOptions{..} = Helpers.readProcess handler exec args
where
exec = "nix-prefetch-git"
args = bool ["--no-deepClone"] ["--deepClone"] gitDeepClone
<> bool [] ["--leave-dotGit"] gitLeaveDotGit
<> bool [] ["--fetch-submodules"] gitFetchSubmodules
<> [ "--hash", "sha256" -- --hash is the type, not the thing
-- we need @url [rev [hash]]@,
-- otherwise we can’t expect a hash
, unUrl gitUrl
, fromMaybe "" gitRev ]
-- hash comes last
<> maybe [] (\(Sha256 h) -> [h]) gitExpectedHash
handler (out, err) = \case
ExitSuccess -> withExceptT PrefetchOutputMalformed $ do
let error' msg = exec <> " " <> msg
jsonError :: [Char] -> Text
jsonError = \msg -> error' (T.intercalate "\n"
[ "parsing json output failed:"
, Text.pack msg
, "The output was:"
, out ])
(gitOutputRev, gitOutputSha256)
<- ExceptT . pure . first jsonError $ do
val <- Aeson.eitherDecode' (Text.Lazy.Encoding.encodeUtf8 $ Text.Lazy.fromStrict out)
flip AesonT.parseEither val
$ Aeson.withObject "GitPrefetchOutput" $ \obj -> do
(,) <$> obj Aeson..: "rev"
<*> fmap Sha256 (obj Aeson..: "sha256")
-- The path isn’t output in the json, but on stderr. :(
-- So this is a bit more hacky than necessary.
gitOuputPath <- case
List.find ("path is /nix/store" `T.isPrefixOf`) (T.lines err)
>>= T.stripPrefix "path is " of
Nothing -> throwE
$ error "could not find nix store output path on stderr"
Just path -> pure $ StorePath $ Text.unpack path
pure GitOutput{..}
ExitFailure _ -> throwE $
if ("hash mismatch for URL" `T.isInfixOf` err)
then ExpectedHashError
else UnknownPrefetchError