module Distribution.Nixpkgs.Haskell.PackageSourceSpec
( HpackUse(..), Package(..), getPackage, getPackage', loadHackageDB, sourceFromHackage
) where
import qualified Control.Exception as Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe
import Data.Bifunctor
import qualified Data.ByteString.Char8 as BS
import Data.List ( isSuffixOf, isPrefixOf )
import qualified Data.Map as DB
import Data.Maybe
import Data.Time
import Distribution.Nixpkgs.Fetch
import Distribution.Nixpkgs.Hashes
import qualified Distribution.Nixpkgs.Haskell.Hackage as DB
import qualified Distribution.Package as Cabal
import Distribution.PackageDescription
import qualified Distribution.PackageDescription as Cabal
import Distribution.PackageDescription.Parsec as Cabal
import Distribution.Parsec.Common (showPError)
import Distribution.Text ( simpleParse, display )
import Distribution.Version
import qualified Hpack.Config as Hpack
import qualified Hpack.Render as Hpack
import OpenSSL.Digest ( digest, digestByName )
import System.Directory ( doesDirectoryExist, doesFileExist, createDirectoryIfMissing, getHomeDirectory, getDirectoryContents )
import System.Exit ( exitFailure )
import System.FilePath ( (</>), (<.>) )
import System.IO
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
data HpackUse
= ForceHpack
| PackageYamlHpack
| NoHpack
data Package = Package
{ pkgSource :: DerivationSource
, pkgRanHpack :: Bool
, pkgCabal :: Cabal.GenericPackageDescription
}
deriving (Show)
getPackage :: HpackUse
-> Bool
-> Maybe FilePath
-> Maybe UTCTime
-> Source
-> IO Package
getPackage optHpack optSubmodules optHackageDB optHackageSnapshot =
getPackage' optHpack optSubmodules (loadHackageDB optHackageDB optHackageSnapshot)
getPackage' :: HpackUse
-> Bool
-> IO DB.HackageDB
-> Source
-> IO Package
getPackage' optHpack optSubmodules hackageDB source = do
(derivSource, ranHpack, pkgDesc) <- fetchOrFromDB optHpack optSubmodules hackageDB source
(\s -> Package s ranHpack pkgDesc) <$> maybe (sourceFromHackage (sourceHash source) (showPackageIdentifier pkgDesc) $ sourceCabalDir source) return derivSource
fetchOrFromDB :: HpackUse
-> Bool
-> IO DB.HackageDB
-> Source
-> IO (Maybe DerivationSource, Bool, Cabal.GenericPackageDescription)
fetchOrFromDB optHpack optSubmodules hackageDB src
| "cabal://" `isPrefixOf` sourceUrl src = do
(msrc, pkgDesc) <- fromDB hackageDB . drop (length "cabal://") $ sourceUrl src
return (msrc, False, pkgDesc)
| otherwise = do
r <- fetch optSubmodules (\dir -> cabalFromPath optHpack (dir </> sourceCabalDir src)) src
case r of
Nothing -> fail $ "Failed to fetch source. Does this source exist? " ++ show src
Just (derivSource, (externalSource, ranHpack, pkgDesc)) ->
return (derivSource <$ guard externalSource, ranHpack, pkgDesc)
loadHackageDB :: Maybe FilePath
-> Maybe UTCTime
-> IO DB.HackageDB
loadHackageDB optHackageDB optHackageSnapshot = do
dbPath <- maybe DB.hackageTarball return optHackageDB
DB.readTarball optHackageSnapshot dbPath
fromDB :: IO DB.HackageDB
-> String
-> IO (Maybe DerivationSource, Cabal.GenericPackageDescription)
fromDB hackageDBIO pkg = do
hackageDB <- hackageDBIO
vd <- maybe unknownPackageError return (DB.lookup name hackageDB >>= lookupVersion)
let ds = case DB.tarballSha256 vd of
Nothing -> Nothing
Just hash -> Just (urlDerivationSource url hash)
return (ds, setCabalFileHash (DB.cabalFileSha256 vd) (DB.cabalFile vd))
where
pkgId :: Cabal.PackageIdentifier
pkgId = fromMaybe (error ("invalid Haskell package id " ++ show pkg)) (simpleParse pkg)
name = Cabal.packageName pkgId
unknownPackageError = fail $ "No such package " ++ display pkgId ++ " in the cabal database. Did you run cabal update?"
url = "mirror://hackage/" ++ display pkgId ++ ".tar.gz"
version :: Version
version = Cabal.packageVersion pkgId
lookupVersion :: DB.Map Version DB.VersionData -> Maybe DB.VersionData
lookupVersion m | version == nullVersion = fmap snd (listToMaybe (DB.toDescList m))
lookupVersion m = DB.lookup version m
readFileMay :: FilePath -> IO (Maybe String)
readFileMay file = do
e <- doesFileExist file
if e
then Just <$> readFile file
else return Nothing
hashCachePath :: String -> IO String
hashCachePath pid = do
home <- getHomeDirectory
let cacheDir = home </> ".cache/cabal2nix"
createDirectoryIfMissing True cacheDir
return $ cacheDir </> pid <.> "sha256"
sourceFromHackage :: Hash -> String -> FilePath -> IO DerivationSource
sourceFromHackage optHash pkgId cabalDir = do
cacheFile <- hashCachePath pkgId
cachedHash <-
case optHash of
Certain h -> return . Certain $ h
Guess h -> return . Guess $ h
_ -> fmap (maybe UnknownHash Certain) . readFileMay $ cacheFile
let url = "mirror://hackage/" ++ pkgId ++ ".tar.gz"
case cachedHash of
Guess hash -> return $ urlDerivationSource url hash
Certain hash ->
seq (length hash) $
urlDerivationSource url hash <$ writeFile cacheFile hash
UnknownHash -> do
maybeHash <- runMaybeT (derivHash . fst <$> fetchWith (False, "url", []) (Source url "" UnknownHash cabalDir))
case maybeHash of
Just hash ->
seq (length hash) $
urlDerivationSource url hash <$ writeFile cacheFile hash
Nothing -> do
hPutStr stderr $ unlines
[ "*** cannot compute hash. (Not a hackage project?)"
, " If your project is not on hackage, please supply the path to the root directory of"
, " the project, not to the cabal file."
, ""
, " If your project is on hackage but you still want to specify the hash manually, you"
, " can use the --sha256 option."
]
exitFailure
showPackageIdentifier :: Cabal.GenericPackageDescription -> String
showPackageIdentifier pkgDesc = name ++ "-" ++ display version where
pkgId = Cabal.package . Cabal.packageDescription $ pkgDesc
name = Cabal.unPackageName (Cabal.packageName pkgId)
version = Cabal.packageVersion pkgId
cabalFromPath :: HpackUse
-> FilePath -> MaybeT IO (Bool, Bool, Cabal.GenericPackageDescription)
cabalFromPath optHpack path = do
d <- liftIO $ doesDirectoryExist path
if d
then do
(ranHpack, pkg) <- cabalFromDirectory optHpack path
return (d, ranHpack, pkg)
else (,,) d False <$> cabalFromFile False path
cabalFromDirectory :: HpackUse
-> FilePath -> MaybeT IO (Bool, Cabal.GenericPackageDescription)
cabalFromDirectory ForceHpack dir = hpackDirectory dir
cabalFromDirectory NoHpack dir = onlyCabalFromDirectory dir "*** No .cabal file was found. Exiting."
cabalFromDirectory PackageYamlHpack dir = do
useHpack <- liftIO $ doesFileExist (dir </> "package.yaml")
if useHpack
then do
liftIO $ hPutStrLn stderr "*** found package.yaml. Using hpack..."
hpackDirectory dir
else onlyCabalFromDirectory dir "*** Found neither a .cabal file nor package.yaml. Exiting."
onlyCabalFromDirectory :: FilePath -> String -> MaybeT IO (Bool, Cabal.GenericPackageDescription)
onlyCabalFromDirectory dir errMsg = do
cabals <- liftIO $ getDirectoryContents dir >>= filterM doesFileExist . map (dir </>) . filter (".cabal" `isSuffixOf`)
case cabals of
[] -> fail errMsg
[cabalFile] -> (,) False <$> cabalFromFile True cabalFile
_ -> liftIO $ fail ("*** found more than one cabal file (" ++ show cabals ++ "). Exiting.")
handleIO :: (Exception.IOException -> IO a) -> IO a -> IO a
handleIO = Exception.handle
encodeUtf8 :: String -> BS.ByteString
encodeUtf8 = T.encodeUtf8 . T.pack
hpackDirectory :: FilePath -> MaybeT IO (Bool, Cabal.GenericPackageDescription)
hpackDirectory dir = do
mPackage <- liftIO $ Hpack.readPackageConfig Hpack.defaultDecodeOptions {
Hpack.decodeOptionsProgramName = Hpack.ProgramName "cabal2nix"
, Hpack.decodeOptionsTarget = dir </> Hpack.packageConfig
}
case mPackage of
Left err -> liftIO $ hPutStrLn stderr ("*** hpack error: " ++ show err ++ ". Exiting.") >> exitFailure
Right r -> do
let hpackOutput =
let body = Hpack.renderPackage [] (Hpack.decodeResultPackage r)
cabalVersion = Hpack.decodeResultCabalVersion r
in encodeUtf8 $ cabalVersion ++ body
hash = printSHA256 $ digest (digestByName "sha256") hpackOutput
case runParseGenericPackageDescription "<hpack output>" hpackOutput of
Left msg -> liftIO $ do
hPutStrLn stderr "*** hpack output:"
BS.hPutStrLn stderr hpackOutput
hPutStrLn stderr "*** cannot parse hpack output:"
hPutStrLn stderr msg
fail "*** Exiting."
Right pkg -> MaybeT $ return $ Just $ (,) True $ setCabalFileHash hash pkg
cabalFromFile :: Bool -> FilePath -> MaybeT IO Cabal.GenericPackageDescription
cabalFromFile failHard file =
MaybeT $ handleIO (\err -> Nothing <$ hPutStrLn stderr ("*** parsing cabal file: " ++ show err)) $ do
buf <- BS.readFile file
let hash = printSHA256 (digest (digestByName "sha256") buf)
case runParseGenericPackageDescription file buf of
Left msg | failHard -> liftIO $ do
hPutStrLn stderr $ "*** cannot parse " ++ show file ++ ":"
hPutStrLn stderr msg
fail "*** Exiting."
Left _ -> return Nothing
Right pkg -> return $ Just $ setCabalFileHash hash pkg
runParseGenericPackageDescription
:: FilePath
-> BS.ByteString
-> Either String Cabal.GenericPackageDescription
runParseGenericPackageDescription fpath
= first (unlines . fmap (showPError fpath) . snd)
. snd . runParseResult
. parseGenericPackageDescription
setCabalFileHash :: String -> GenericPackageDescription -> GenericPackageDescription
setCabalFileHash sha256 gpd = gpd { packageDescription = (packageDescription gpd) {
customFieldsPD = ("X-Cabal-File-Hash", sha256) : customFieldsPD (packageDescription gpd)
}
}