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