@@ -1,20 +1,18 @@
11module Cabal2Nix.Package where
22
3−import Control.Exception ( handle, SomeException(..))
43import Control.Monad
5−import Data.Functor ( (<$>) )
6−import Data.List ( isSuffixOf )
7−import Data.Maybe ( fromMaybe, fromJust )
4+import Control.Monad.IO.Class
5+import Control.Monad.Trans.Maybe
6+import Data.Functor ( (<$>), (<$) )
7+import Data.List ( isSuffixOf, isPrefixOf )
8+import Data.Maybe ( listToMaybe )
89import Distribution.NixOS.Derivation.Cabal
10+import Distribution.NixOS.Fetch
911import Distribution.Text ( simpleParse )
10−import Network.Browser ( browse, request, setCheckForProxy, setDebugLog, setOutHandler )
11−import Network.HTTP ( getRequest, rspBody )
1212import System.Directory ( doesDirectoryExist, doesFileExist, createDirectoryIfMissing, getHomeDirectory, getDirectoryContents )
1313import System.Exit ( exitFailure )
14−import System.FilePath ( dropFileName, (</>), (<.>) )
15−import System.IO ( hPutStrLn, stderr)
16−import System.Process ( readProcess )
17−import Text.URI
14+import System.FilePath ( (</>), (<.>) )
15+import System.IO ( hPutStrLn, stderr, hPutStr )
1816
1917import qualified Distribution.Hackage.DB as DB
2018import qualified Distribution.Package as Cabal
@@ -26,102 +24,91 @@ data Package = Package
2624 , cabal :: Cabal.GenericPackageDescription
2725 }
2826
29−readPackage :: Maybe String -> URI -> IO Package
30−readPackage optHash uri = case uriScheme uri of
31− Nothing -> local
32− Just "file" -> local
33− Just "cabal" -> cabalPackage optHash $ fromMaybe "" (uriRegName uri) ++ uriPath uri
34− Just scheme -> hPutStrLn stderr ("*** unsupported URI scheme: " ++ scheme) >> exitFailure
35− where local = localPackage optHash $ fromMaybe "" (uriRegName uri) ++ uriPath uri
36−
37−localPackage :: Maybe String -> FilePath -> IO Package
38−localPackage optHash path = do
39− isDir <- doesDirectoryExist path
40− cabalFile <- if isDir
41− then do
42− cabals <- filter (".cabal" `isSuffixOf`) <$> getDirectoryContents path
43− case cabals of
44− [] -> hPutStrLn stderr "*** No cabal file found in given directory" >> exitFailure
45− (f:_) -> return $ path </> f
46− else return path
47− pkgDesc <- readFile cabalFile >>= readPackageDescription
48− if isDir
49− then return $ Package (Directory path) pkgDesc
50− else do
51− hash <- getHash optHash pkgDesc
52− return $ Package (Hackage hash) pkgDesc
53−
54−cabalPackage :: Maybe String -> String -> IO Package
55−cabalPackage optHash packageName = do
56− pkgDesc <- case versionBranch $ Cabal.pkgVersion $ pid packageName of
57− [] -> do
58− packageDescription <- DB.lookup packageName `fmap` DB.readHackage
59− case packageDescription of
60− Just d -> return $ snd $ last $ DB.toList d
61− Nothing -> error "No such package"
62− _ -> fetchUrl (hackagePath (pid packageName) Cabal) >>= readPackageDescription
63− hash <- getHash optHash pkgDesc
64− return $ Package (Hackage hash) pkgDesc
65−
66− where pid p = fromJust $ simpleParse p
67−
68−fetchUrl :: String -> IO String
69−fetchUrl url = do
70− (_,rsp) <- browse $ do
71− setCheckForProxy True
72− setDebugLog Nothing
73− setOutHandler (\_ -> return ())
74− request (getRequest url)
75− return (rspBody rsp)
76−
77−readPackageDescription :: String -> IO Cabal.GenericPackageDescription
78−readPackageDescription source = case Cabal.parsePackageDescription source of
79− Cabal.ParseOk _ a -> return a
80− Cabal.ParseFailed err -> do
81− hPutStrLn stderr ("*** cannot parse cabal file: " ++ show err)
82− exitFailure
83−
84−getHash :: Maybe String -> Cabal.GenericPackageDescription -> IO String
85−getHash optHash pkgDesc = maybe (hashPackage $ Cabal.package $ Cabal.packageDescription pkgDesc) return optHash
86−
87−data Ext = TarGz | Cabal deriving Eq
88−
89−showExt :: Ext -> String
90−showExt TarGz = ".tar.gz"
91−showExt Cabal = ".cabal"
92−
93−hackagePath :: Cabal.PackageIdentifier -> Ext -> String
94−hackagePath (Cabal.PackageIdentifier (Cabal.PackageName name) version') ext =
95− "http://hackage.haskell.org/packages/archive/" ++
96− name ++ "/" ++ version ++ "/" ++ name ++
97− (if ext == TarGz then '-' : version else "") ++
98− showExt ext
99− where
100− version = showVersion version'
101−
102−hashPackage :: Cabal.PackageIdentifier -> IO String
103−hashPackage pkg = do
104− cachePath <- hashCachePath pkg
105− exists <- doesFileExist cachePath
106− hash' <- if exists
107− then readFile cachePath
108− else getHackageHash
109− let hash = reverse (dropWhile (=='\n') (reverse hash'))
110− unless exists $ do
111− createDirectoryIfMissing True (dropFileName cachePath)
112− writeFile cachePath hash
113− return hash
114− where getHackageHash = do
115− let command = "exec nix-prefetch-url 2>/dev/tty " ++ hackagePath pkg TarGz
116− handle handlePrefetchError (readProcess "bash" ["-c", command] "")
117− handlePrefetchError (SomeException _) =
118− error $ "\nError: Cannot compute hash. (Not a hackage project?)\n" ++
119− "Specify hash explicitly via --sha256 and add appropriate \"src\" attribute " ++
120− "to resulting nix expression."
121−
122−hashCachePath :: Cabal.PackageIdentifier -> IO FilePath
123−hashCachePath (Cabal.PackageIdentifier (Cabal.PackageName name) version') = do
124− home <- getHomeDirectory
125− return $ home ++ "/.cache/cabal2nix" </> name ++ "-" ++ version <.> "sha256"
126− where
127− version = showVersion version'
27+getPackage :: Source -> IO Package
28+getPackage source = do
29+ (derivSource, pkgDesc) <- fetchOrFromDB source
30+ flip Package pkgDesc <$> maybe (sourceFromHackage (sourceHash source) $ showPackageIdentifier pkgDesc) return derivSource
31+
32+
33+fetchOrFromDB :: Source -> IO (Maybe DerivationSource, Cabal.GenericPackageDescription)
34+fetchOrFromDB src
35+ | "cabal://" `isPrefixOf` sourceUrl src = fmap ((,) Nothing) . fromDB . drop (length "cabal://") $ sourceUrl src
36+ | otherwise = do
37+ r <- fetch cabalFromPath src
38+ case r of
39+ Nothing -> hPutStrLn stderr "*** failed to fetch source. Does the URL exist?" >> exitFailure
40+ Just (derivSource, (externalSource, pkgDesc)) ->
41+ return (derivSource <$ guard externalSource, pkgDesc)
42+
43+fromDB :: String -> IO Cabal.GenericPackageDescription
44+fromDB pkg = do
45+ pkgDesc <- (lookupVersion <=< DB.lookup name) <$> DB.readHackage
46+ case pkgDesc of
47+ Just r -> return r
48+ Nothing -> hPutStrLn stderr "*** no such package in the cabal database (did you run cabal update?). " >> exitFailure
49+ where
50+ Just pkgId = simpleParse pkg
51+ Cabal.PackageName name = Cabal.pkgName pkgId
52+ version = Cabal.pkgVersion pkgId
53+
54+ lookupVersion :: DB.Map DB.Version Cabal.GenericPackageDescription -> Maybe Cabal.GenericPackageDescription
55+ lookupVersion
56+ | null (versionBranch version) = fmap snd . listToMaybe . reverse . DB.toAscList
57+ | otherwise = DB.lookup version
58+
59+
60+readFileMay :: String -> IO (Maybe String)
61+readFileMay file = do
62+ e <- doesFileExist file
63+ if e
64+ then Just <$> readFile file
65+ else return Nothing
66+
67+hashCachePath :: String -> IO String
68+hashCachePath pid = do
69+ home <- getHomeDirectory
70+ let cacheDir = home </> ".cache/cabal2nix"
71+ createDirectoryIfMissing True cacheDir
72+ return $ cacheDir </> pid <.> "sha256"
73+
74+sourceFromHackage :: Maybe String -> String -> IO DerivationSource
75+sourceFromHackage optHash pkgId = do
76+ cacheFile <- hashCachePath pkgId
77+ hash <- maybe (readFileMay cacheFile) (return . Just) optHash
78+ res <- runMaybeT $ fst <$> fetchWith (False, "url") (Source ("mirror://hackage/" ++ pkgId ++ ".tar.gz") "" hash)
79+ case res of
80+ Just r -> writeFile cacheFile (derivHash r) >> return r
81+ Nothing -> do
82+ hPutStr stderr $ unlines
83+ [ "*** cannot compute hash. (Not a hackage project?)"
84+ , " Specify hash explicitly via --sha256 and add appropriate \"src\" attribute"
85+ , " to resulting nix expression."
86+ ]
87+ exitFailure
88+
89+showPackageIdentifier :: Cabal.GenericPackageDescription -> String
90+showPackageIdentifier pkgDesc = name ++ "-" ++ showVersion version where
91+ pkgId = Cabal.package . Cabal.packageDescription $ pkgDesc
92+ Cabal.PackageName name = Cabal.packageName pkgId
93+ version = Cabal.packageVersion pkgId
94+
95+cabalFromPath :: FilePath -> MaybeT IO (Bool, Cabal.GenericPackageDescription)
96+cabalFromPath path = do
97+ d <- liftIO $ doesDirectoryExist path
98+ (,) d <$> if d
99+ then cabalFromDirectory path
100+ else cabalFromFile path
101+
102+cabalFromDirectory :: FilePath -> MaybeT IO Cabal.GenericPackageDescription
103+cabalFromDirectory dir = do
104+ cabals <- liftIO $ getDirectoryContents dir >>= filterM doesFileExist . map (dir </>) . filter (".cabal" `isSuffixOf`)
105+ case cabals of
106+ [cabalFile] -> cabalFromFile cabalFile
107+ _ -> liftIO $ hPutStrLn stderr "*** found zero or more than one cabal file. Exiting." >> exitFailure
108+
109+cabalFromFile :: FilePath -> MaybeT IO Cabal.GenericPackageDescription
110+cabalFromFile file = do
111+ content <- liftIO $ readFile file
112+ case Cabal.parsePackageDescription content of
113+ Cabal.ParseFailed _ -> mzero
114+ Cabal.ParseOk _ a -> return a