git.iko.soy/typeable/cabal2nix
on master help sign in
Port cabal2nix to hackage-db 2.x.

This means that we can now utilize the new index01-style tarball from Hackage, which contains additional useful information for us, e.g. the SHA256 hashes of the respective source tarballs for each package. Fixes https://github.com/NixOS/cabal2nix/issues/298. Fixes https://github.com/NixOS/cabal2nix/issues/305.

commiteef3b98ebd69d2f80b334e0d196a6cd2c9867b58 authorPeter Simons <simons@cryp.to> dateFri 22 Sep 2017, 14:02 (9 years ago) parent8a6ffec
4 files changed+90−65
cabal2nix.cabal+8−4 package.yaml+2−1 src/Distribution/Nixpkgs/Haskell/Hackage.hs+38−27 src/Distribution/Nixpkgs/Haskell/PackageSourceSpec.hs+42−33
cabal2nix.cabal+8−4
@@ -99,7 +99,7 @@ library
9999 , directory
100100 , distribution-nixpkgs >= 1.1
101101 , filepath
102− , hackage-db
102+ , hackage-db > 2
103103 , hopenssl >= 2
104104 , language-nix
105105 , lens
@@ -108,6 +108,7 @@ library
108108 , process
109109 , split
110110 , text
111+ , time
111112 , transformers
112113 , yaml
113114 exposed-modules:
@@ -147,7 +148,7 @@ executable cabal2nix
147148 , directory
148149 , distribution-nixpkgs >= 1.1
149150 , filepath
150− , hackage-db
151+ , hackage-db > 2
151152 , hopenssl >= 2
152153 , language-nix
153154 , lens
@@ -156,6 +157,7 @@ executable cabal2nix
156157 , process
157158 , split
158159 , text
160+ , time
159161 , transformers
160162 , yaml
161163 , cabal2nix
@@ -179,7 +181,7 @@ executable hackage2nix
179181 , directory
180182 , distribution-nixpkgs >= 1.1
181183 , filepath
182− , hackage-db
184+ , hackage-db > 2
183185 , hopenssl >= 2
184186 , language-nix
185187 , lens
@@ -188,6 +190,7 @@ executable hackage2nix
188190 , process
189191 , split
190192 , text
193+ , time
191194 , transformers
192195 , yaml
193196 , cabal2nix
@@ -218,7 +221,7 @@ test-suite doctests
218221 , directory
219222 , distribution-nixpkgs >= 1.1
220223 , filepath
221− , hackage-db
224+ , hackage-db > 2
222225 , hopenssl >= 2
223226 , language-nix
224227 , lens
@@ -227,6 +230,7 @@ test-suite doctests
227230 , process
228231 , split
229232 , text
233+ , time
230234 , transformers
231235 , yaml
232236 , doctest
package.yaml+2−1
@@ -90,7 +90,7 @@ dependencies:
9090 - directory
9191 - distribution-nixpkgs >= 1.1
9292 - filepath
93− - hackage-db
93+ - hackage-db > 2
9494 - hopenssl >= 2
9595 - language-nix
9696 - lens
@@ -99,6 +99,7 @@ dependencies:
9999 - process
100100 - split
101101 - text
102+ - time
102103 - transformers
103104 - yaml
104105
src/Distribution/Nixpkgs/Haskell/Hackage.hs+38−27
@@ -1,33 +1,44 @@
1−module Distribution.Nixpkgs.Haskell.Hackage ( readHashedHackage, readHashedHackage', module Distribution.Hackage.DB ) where
1+module Distribution.Nixpkgs.Haskell.Hackage
2+ ( HackageDB, PackageData, VersionData(..)
3+ , hackageTarball, readTarball
4+ ) where
25
3−import Data.ByteString.Lazy ( ByteString )
4−import Distribution.Hackage.DB
5−import qualified Distribution.Hackage.DB.Parsed as Unparsed ( parsePackage )
6−import qualified Distribution.Hackage.DB.Unparsed as Unparsed
6+import Data.Map as Map
7+import qualified Distribution.Hackage.DB.Parsed as P
8+import Distribution.Hackage.DB.Path
9+import qualified Distribution.Hackage.DB.Unparsed as U
710import Distribution.Nixpkgs.Hashes
8−import OpenSSL.Digest ( digest, digestByName )
11+import Distribution.Package
12+import Distribution.PackageDescription
13+import Distribution.Version
14+import OpenSSL.Digest
15+import Data.Time
916
10−-- | A variant of 'readHackage' that adds the SHA256 digest of the
11−-- original Cabal file to the parsed 'GenericPackageDescription'. That
12−-- hash is required to build packages with an "edited" cabal file,
13−-- because Nix needs to download the edited file and patch it into the
14−-- original tarball.
17+type HackageDB = Map PackageName PackageData
1518
16−readHashedHackage :: IO Hackage
17−readHashedHackage = hackagePath >>= readHashedHackage'
19+type PackageData = Map Version VersionData
1820
19−readHashedHackage' :: FilePath -> IO Hackage
20−readHashedHackage' = fmap parseUnparsedHackage . Unparsed.readHackage'
21− where
22− parseUnparsedHackage :: Unparsed.Hackage -> Hackage
23− parseUnparsedHackage = mapWithKey (mapWithKey . parsePackage)
21+data VersionData = VersionData
22+ { cabalFile :: !GenericPackageDescription
23+ , cabalFileSha256 :: !String
24+ , tarballSha256 :: !(Maybe String)
25+ }
26+ deriving (Show)
27+
28+readTarball :: Maybe UTCTime -> FilePath -> IO HackageDB
29+readTarball ts p = do
30+ dbu <- U.readTarball ts p
31+ let dbp = P.parseDB dbu
32+ return (mapWithKey (parsePackageData dbu) dbp)
2433
25− parsePackage :: String -> Version -> ByteString -> GenericPackageDescription
26− parsePackage name version buf =
27− let pkg = Unparsed.parsePackage name version buf
28− hash = printSHA256 (digest (digestByName "sha256") buf)
29− in
30− pkg { packageDescription = (packageDescription pkg) {
31− customFieldsPD = ("X-Cabal-File-Hash", hash) : customFieldsPD (packageDescription pkg)
32− }
33− }
34+parsePackageData :: U.HackageDB -> PackageName -> P.PackageData -> PackageData
35+parsePackageData dbu pn pd = mapWithKey (parseVersionData (dbu ! pn)) pd
36+
37+parseVersionData :: U.PackageData -> Version -> P.VersionData -> VersionData
38+parseVersionData pdu v vd = VersionData
39+ { cabalFile = P.cabalFile vd
40+ , cabalFileSha256 = printSHA256 (digest (digestByName "sha256") file)
41+ , tarballSha256 = Map.lookup "sha256" (P.tarballHashes vd)
42+ }
43+ where
44+ file = U.cabalFile (U.versions pdu ! v)
src/Distribution/Nixpkgs/Haskell/PackageSourceSpec.hs+42−33
@@ -6,19 +6,19 @@ import qualified Control.Exception as Exception
66import Control.Monad
77import Control.Monad.IO.Class
88import Control.Monad.Trans.Maybe
9−import qualified Data.ByteString.Lazy.Char8 as LBS8
109import Data.List ( isSuffixOf, isPrefixOf )
10+import qualified Data.Map as DB
1111import Data.Maybe
12−import Distribution.Hackage.DB.Parsed
1312import Distribution.Nixpkgs.Fetch
1413import Distribution.Nixpkgs.Hashes
1514import qualified Distribution.Nixpkgs.Haskell.Hackage as DB
1615import qualified Distribution.Package as Cabal
17−import qualified Distribution.Version as Cabal
1816import Distribution.PackageDescription
1917import qualified Distribution.PackageDescription as Cabal
18+import Distribution.PackageDescription.Parse as Cabal
2019import Distribution.Text ( simpleParse, display )
21−import OpenSSL.Digest ( digest, digestByName )
20+import Distribution.Version
21+import OpenSSL.Digest ( digestString, digestByName )
2222import System.Directory ( doesDirectoryExist, doesFileExist, createDirectoryIfMissing, getHomeDirectory, getDirectoryContents )
2323import System.Exit ( exitFailure )
2424import System.FilePath ( (</>), (<.>) )
@@ -30,42 +30,48 @@ data Package = Package
3030 }
3131 deriving (Show)
3232
33−getPackage :: Maybe String -> Source -> IO Package
33+getPackage :: Maybe FilePath -> Source -> IO Package
3434getPackage optHackageDB source = do
3535 (derivSource, pkgDesc) <- fetchOrFromDB optHackageDB source
3636 flip Package pkgDesc <$> maybe (sourceFromHackage (sourceHash source) (showPackageIdentifier pkgDesc) $ sourceCabalDir source) return derivSource
3737
38−fetchOrFromDB :: Maybe String -> Source -> IO (Maybe DerivationSource, Cabal.GenericPackageDescription)
38+fetchOrFromDB :: Maybe FilePath -> Source -> IO (Maybe DerivationSource, Cabal.GenericPackageDescription)
3939fetchOrFromDB optHackageDB src
40− | "cabal://" `isPrefixOf` sourceUrl src = fmap ((,) Nothing) . fromDB optHackageDB . drop (length "cabal://") $ sourceUrl src
40+ | "cabal://" `isPrefixOf` sourceUrl src = fromDB optHackageDB . drop (length "cabal://") $ sourceUrl src
4141 | otherwise = do
4242 r <- fetch (\dir -> cabalFromPath (dir </> sourceCabalDir src)) src
4343 case r of
44− Nothing ->
45− hPutStrLn stderr "*** failed to fetch source. Does the URL exist?" >> exitFailure
44+ Nothing -> fail "Failed to fetch source. Does the URL exist?"
4645 Just (derivSource, (externalSource, pkgDesc)) -> do
4746 return (derivSource <$ guard externalSource, pkgDesc)
4847
49−fromDB :: Maybe String -> String -> IO Cabal.GenericPackageDescription
48+fromDB :: Maybe FilePath -> String -> IO (Maybe DerivationSource, Cabal.GenericPackageDescription)
5049fromDB optHackageDB pkg = do
51− pkgDesc <- (lookupVersion <=< DB.lookup name) <$> maybe DB.readHashedHackage DB.readHashedHackage' optHackageDB
52− case pkgDesc of
53− Just r -> return r
54− Nothing -> hPutStrLn stderr "*** no such package in the cabal database (did you run cabal update?). " >> exitFailure
50+ dbPath <- maybe DB.hackageTarball return optHackageDB
51+ db <- DB.readTarball Nothing dbPath
52+ vd <- maybe unknownPackageError return (DB.lookup name db >>= lookupVersion)
53+ let ds = case DB.tarballSha256 vd of
54+ Nothing -> Nothing
55+ Just hash -> Just (DerivationSource "url" url "" hash)
56+ return (ds, setCabalFileHash (DB.cabalFileSha256 vd) (DB.cabalFile vd))
5557 where
5658 pkgId :: Cabal.PackageIdentifier
5759 pkgId = fromMaybe (error ("invalid Haskell package id " ++ show pkg)) (simpleParse pkg)
58− name = Cabal.unPackageName (Cabal.packageName pkgId)
60+ name = Cabal.packageName pkgId
61+
62+ unknownPackageError = fail "No such package in the cabal database. Did you run cabal update?"
5963
60− version :: [Int]
61− version = Cabal.versionNumbers $ Cabal.packageVersion pkgId
64+ url = "mirror://hackage/" ++ display pkgId ++ ".tar.gz"
65+
66+ version :: Version
67+ version = Cabal.packageVersion pkgId
6268
63− lookupVersion :: DB.Map DB.Version Cabal.GenericPackageDescription -> Maybe Cabal.GenericPackageDescription
69+ lookupVersion :: DB.Map Version DB.VersionData -> Maybe DB.VersionData
6470 -- No version is specified, pick latest one
65− lookupVersion m | [] <- version = fmap snd . listToMaybe $ DB.toDescList m
66− lookupVersion m = DB.lookup (DB.makeVersion version) m
71+ lookupVersion m | version == nullVersion = fmap snd (listToMaybe (DB.toDescList m))
72+ lookupVersion m = DB.lookup version m
6773
68−readFileMay :: String -> IO (Maybe String)
74+readFileMay :: FilePath -> IO (Maybe String)
6975readFileMay file = do
7076 e <- doesFileExist file
7177 if e
@@ -79,7 +85,7 @@ hashCachePath pid = do
7985 createDirectoryIfMissing True cacheDir
8086 return $ cacheDir </> pid <.> "sha256"
8187
82−sourceFromHackage :: Hash -> String -> String -> IO DerivationSource
88+sourceFromHackage :: Hash -> String -> FilePath -> IO DerivationSource
8389sourceFromHackage optHash pkgId cabalDir = do
8490 cacheFile <- hashCachePath pkgId
8591 cachedHash <-
@@ -135,7 +141,7 @@ cabalFromDirectory dir = do
135141 cabals <- liftIO $ getDirectoryContents dir >>= filterM doesFileExist . map (dir </>) . filter (".cabal" `isSuffixOf`)
136142 case cabals of
137143 [cabalFile] -> cabalFromFile True cabalFile
138− _ -> liftIO $ hPutStrLn stderr ("*** found zero or more than one cabal file (" ++ show cabals ++ "). Exiting.") >> exitFailure
144+ _ -> liftIO $ fail ("Found zero or more than one cabal files: " ++ show cabals ++ ". Exiting.")
139145
140146handleIO :: (Exception.IOException -> IO a) -> IO a -> IO a
141147handleIO = Exception.handle
@@ -147,13 +153,16 @@ cabalFromFile failHard file =
147153 -- wrap the whole block in `catchIO`, because of lazy IO. The `case` will force
148154 -- the reading of the file, so we will always catch the expression here.
149155 MaybeT $ handleIO (\err -> Nothing <$ hPutStrLn stderr ("*** parsing cabal file: " ++ show err)) $ do
150− buf <- LBS8.readFile file
151− let hash = printSHA256 (digest (digestByName "sha256") buf)
152− case parsePackage' buf of
153− Left msg -> if failHard
154− then fail ("*** cannot parse " ++ show file ++ ": " ++ msg)
155− else return Nothing
156− Right pkg -> do return $ Just $ pkg { packageDescription = (packageDescription pkg) {
157− customFieldsPD = ("X-Cabal-File-Hash", hash) : customFieldsPD (packageDescription pkg)
158− }
159− }
156+ buf <- readFile file
157+ let hash = printSHA256 (digestString (digestByName "sha256") buf)
158+ case parseGenericPackageDescription buf of
159+ ParseFailed perr -> if failHard
160+ then fail ("cannot parse " ++ show file ++ ": " ++ show perr)
161+ else return Nothing
162+ ParseOk _ pkg -> return $ Just $ setCabalFileHash hash pkg
163+
164+setCabalFileHash :: String -> GenericPackageDescription -> GenericPackageDescription
165+setCabalFileHash sha256 gpd = gpd { packageDescription = (packageDescription gpd) {
166+ customFieldsPD = ("X-Cabal-File-Hash", sha256) : customFieldsPD (packageDescription gpd)
167+ }
168+ }