git.iko.soy/typeable/cabal2nix
on v1.70 help sign in
typeable/cabal2nix/src/hackage4nix.hs
258 lines 11 kB history raw
1module Main ( main ) where
3import Cabal2Nix.Generate
4import Cabal2Nix.Normalize
5import Control.Exception ( bracket )
6import Control.Monad.RWS
7import Data.List
8import qualified Data.Set as Set
9import Data.Version
10import qualified Distribution.Hackage.DB as DB
11import Distribution.NixOS.Derivation.Cabal
12import Distribution.Package
13import Distribution.PackageDescription ( GenericPackageDescription() )
14import Distribution.Text
15import System.Console.GetOpt ( OptDescr(..), ArgDescr(..), ArgOrder(..), usageInfo, getOpt )
16import System.Directory
17import System.Environment
18import System.Exit
19import System.FilePath
20import System.IO
21import Text.Regex.Posix
23data Pkg = Pkg Derivation FilePath Bool
24 deriving (Show, Eq, Ord)
26type PkgSet = Set.Set Pkg
28data Configuration = Configuration
29 { _msgDebug :: String -> IO ()
30 , _msgInfo :: String -> IO ()
31 , _hackageDb :: DB.Hackage
32 , _force :: Bool
33 }
35defaultConfiguration :: Configuration
36defaultConfiguration = Configuration
37 { _msgDebug = hPutStrLn stderr
38 , _msgInfo = hPutStrLn stderr
39 , _hackageDb = DB.empty
40 , _force = False
41 }
43type Hackage4Nix a = RWST Configuration () PkgSet IO a
45io :: (MonadIO m) => IO a -> m a
46io = liftIO
48readDirectory :: FilePath -> IO [FilePath]
49readDirectory dirpath = do
50 entries <- getDirectoryContents dirpath
51 return [ x | x <- entries, x /= ".", x /= ".." ]
53msgDebug, msgInfo :: String -> Hackage4Nix ()
54msgDebug msg = ask >>= \s -> io (_msgDebug s msg)
55msgInfo msg = ask >>= \s -> io (_msgInfo s msg)
57getCabalPackage :: String -> Version -> Hackage4Nix GenericPackageDescription
58getCabalPackage name vers = do
59 db <- asks _hackageDb
60 case DB.lookup name db of
61 Just db' -> case DB.lookup vers db' of
62 Just pkg -> return pkg
63 Nothing -> fail ("hackage doesn't know about " ++ name ++ " version " ++ display vers)
64 Nothing -> fail ("hackage doesn't know about " ++ show name)
66discoverNixFiles :: (FilePath -> Hackage4Nix ()) -> FilePath -> Hackage4Nix ()
67discoverNixFiles yield dirOrFile = do
68 isFile <- io (doesFileExist dirOrFile)
69 case (isFile, takeExtension dirOrFile) of
70 (True,".nix") -> yield dirOrFile
71 (True,_) -> return ()
72 (False,_) -> io (readDirectory dirOrFile) >>= mapM_ (discoverNixFiles yield . (dirOrFile </>))
74regenerateDerivation :: Derivation -> String -> Bool
75regenerateDerivation _ buf = not (buf =~ "(pre|post)Configure|(pre|post)Install|patchPhase|patches|broken")
77parseNixFile :: FilePath -> String -> Hackage4Nix (Maybe Pkg)
78parseNixFile path buf
79 | not (buf =~ "cabal.mkDerivation")
80 = msgDebug ("ignore non-cabal package " ++ path) >> return Nothing
81 | any (`isSuffixOf`path) badPackagePaths
82 = msgDebug ("ignore known bad package " ++ path) >> return Nothing
83 | Just deriv <- parseDerivation buf
84 = do forceReGen <- asks _force
85 let reGen = regenerateDerivation deriv buf
86 return (Just (Pkg deriv path (forceReGen || reGen)))
87 | otherwise = msgInfo ("failed to parse file " ++ path) >> return Nothing
89selectLatestVersions :: PkgSet -> PkgSet
90selectLatestVersions = Set.fromList . nubBy f2 . sortBy f1 . Set.toList
91 where
92 f1 (Pkg deriv1 _ _) (Pkg deriv2 _ _)
93 | pname deriv1 == pname deriv2 = compare (version deriv2) (version deriv1)
94 | otherwise = compare (pname deriv1) (pname deriv2)
95 f2 (Pkg deriv1 _ _) (Pkg deriv2 _ _) = pname deriv1 == pname deriv2
97discoverUpdates :: String -> Version -> Hackage4Nix [Version]
98discoverUpdates name vers = do
99 db <- asks _hackageDb
100 case DB.lookup name db of
101 Just pkgs -> return (filter (>vers) (DB.keys pkgs))
102 Nothing -> fail ("discoverUpdates cannot find package " ++ show name ++ " on Hackage")
104updateNixPkgs :: [FilePath] -> Hackage4Nix ()
105updateNixPkgs paths = do
106 -- Traverse the given directorcy structure and discover all available
107 -- Haskell packages.
108 forM_ paths $ \fileOrDir -> do
109 msgDebug $ "scanning " ++ show fileOrDir
110 flip discoverNixFiles fileOrDir $ \file -> do
111 nix' <- io (readFile file) >>= parseNixFile file
112 flip (maybe (return ())) nix' $ \nix ->
113 modify (Set.insert nix)
115 -- Re-generate all Haskell packages in-place (unless
116 -- 'regenerateDerivation' decided that this particular package
117 -- shouldn't be touched.
118 latestVersions <- gets selectLatestVersions
119 let latestVersionMap :: [(String,Version)]
120 latestVersionMap = [ (pname deriv, version deriv) | Pkg deriv _ _ <- Set.toList latestVersions ]
121 let isLatest :: Derivation -> Bool
122 isLatest deriv = maybe False (version deriv ==) (lookup (pname deriv) latestVersionMap)
123 get >>= \pkgset -> forM_ (Set.toList pkgset) $ \nix -> do
124 let Pkg deriv path regenerate = nix
125 maints = maintainers (metaSection deriv)
126 plats = platforms (metaSection deriv)
127 hplats = if isLatest deriv then hydraPlatforms (metaSection deriv) else ["self.stdenv.lib.platforms.none"]
129 when regenerate $ do
130 msgDebug ("re-generate " ++ path)
131 pkg <- getCabalPackage (pname deriv) (version deriv)
132 let deriv' = (cabal2nix pkg) { src = src deriv
133 , runHaddock = runHaddock deriv
134 , doCheck = doCheck deriv
135 , jailbreak = jailbreak deriv
136 , hyperlinkSource = hyperlinkSource deriv
137 , enableSplitObjs = enableSplitObjs deriv
138 }
139 meta = metaSection deriv'
140 plats' = if null plats then platforms meta else plats
141 deriv'' = deriv' { metaSection = meta
142 { maintainers = maints
143 , platforms = plats'
144 , hydraPlatforms = hplats
145 }
146 }
147 io $ writeFile path (show (disp (normalize deriv'')))
149 -- Discover available updates and print the appropriate cabal2nix
150 -- command line to the console for the user to execute at his/her
151 -- discretion.
152 updates' <- forM (Set.elems latestVersions) $ \pkg -> do
153 let Pkg deriv _ _ = pkg
154 updates <- discoverUpdates (pname deriv) (version deriv)
155 return (pkg,updates)
156 let updates = [ u | u@(_,_:_) <- updates' ]
157 unless (null updates) $ do
158 msgInfo "The following updates are available:"
159 forM_ updates $ \(pkg,versions) -> do
160 let Pkg deriv path regenerate = pkg
161 msgInfo ""
162 msgInfo $ display (packageId deriv) ++ ":"
163 forM_ versions $ \newVersion -> do
164 let deriv' = deriv { version = newVersion }
165 msgInfo $ " " ++ genCabal2NixCmdline (Pkg deriv' path regenerate)
166 return ()
168genCabal2NixCmdline :: Pkg -> String
169genCabal2NixCmdline (Pkg deriv path _) = unwords $ ["cabal2nix"] ++ opts ++ ['>':path']
170 where
171 meta = metaSection deriv
172 opts = [cabal] ++ maints' ++ plats' ++ hplats'
173 ++ (if jailbreak deriv then ["--jailbreak"] else [])
174 ++ (if runHaddock deriv then [] else ["--no-haddock"])
175 ++ (if doCheck deriv then [] else ["--no-check"])
176 cabal = "cabal://" ++ display (packageId deriv)
177 maints' = [ "--maintainer=" ++ normalizeMaintainer m | m <- maintainers meta ]
178 plats'
179 | ["self.ghc.meta.platforms"] == platforms meta = []
180 | otherwise = [ "--platform=" ++ p | p <- platforms meta ]
181 hplats'
182 | ["self.ghc.meta.platforms"] == platforms meta = []
183 | otherwise = [ "--hydra-platform=" ++ p | p <- hydraPlatforms meta ]
184 path'
185 | path =~ "/[0-9\\.]+\\.nix$" = replaceFileName path (display (version deriv) <.> "nix")
186 | otherwise = path
188normalizeMaintainer :: String -> String
189normalizeMaintainer x
190 | "self.stdenv.lib.maintainers." `isPrefixOf` x = drop 28 x
191 | otherwise = x
193data CliOption = PrintHelp | Verbose | ForceRegen
194 deriving (Eq)
196main :: IO ()
197main = bracket (return ()) (\() -> hFlush stdout >> hFlush stderr) $ \() -> do
198 let options :: [OptDescr CliOption]
199 options =
200 [ Option "h" ["help"] (NoArg PrintHelp) "show this help text"
201 , Option "v" ["verbose"] (NoArg Verbose) "enable noisy debug output"
202 , Option "" ["force"] (NoArg ForceRegen) "re-generate all files in place"
203 ]
205 usage :: String
206 usage = usageInfo "Usage: hackage4nix [options] [dir-or-file ...]" options ++ unlines
207 [ ""
208 , "The purpose of 'hackage4nix' is to keep all Haskell packages in our"
209 , "repository packages up-to-date. It scans a checked-out copy of"
210 , "Nixpkgs for expressions that use 'cabal.mkDerivation', and"
211 , "re-generates them in-place with cabal2nix."
212 ]
214 cmdlineError :: String -> IO a
215 cmdlineError "" = hPutStrLn stderr usage >> exitFailure
216 cmdlineError errMsg = hPutStrLn stderr errMsg >> cmdlineError ""
218 args' <- getArgs
219 (opts,args) <- case getOpt Permute options args' of
220 (o,n,[] ) -> return (o,n)
221 (_,_,errs) -> cmdlineError (concatMap (\e -> '*':'*':'*':' ':e) errs)
223 when (PrintHelp `elem` opts) (cmdlineError "")
225 hackage <- DB.readHackage
226 let cfg = defaultConfiguration
227 { _msgDebug = if Verbose `elem` opts then _msgDebug defaultConfiguration else const (return ())
228 , _hackageDb = hackage
229 , _force = ForceRegen `elem` opts
230 }
231 ((),_,()) <- runRWST (updateNixPkgs args) cfg Set.empty
232 return ()
235-- Packages that cabal2nix cannot generate build expressions for.
237badPackagePaths :: [FilePath]
238badPackagePaths = [ -- These expression are not found on Hackage:
239 "haskell-platform/2009.2.0.2.nix", "haskell-platform/2010.1.0.0.nix"
240 , "haskell-platform/2010.2.0.0.nix", "haskell-platform/2011.2.0.0.nix"
241 , "haskell-platform/2011.2.0.1.nix", "haskell-platform/2011.4.0.0.nix"
242 , "haskell-platform/2012.2.0.0.nix", "haskell-platform/2012.4.0.0.nix"
243 , "haskell-platform/2013.2.0.0.nix", "compilers/flapjax/default.nix"
244 , "pkgs/games/uqm/3dovideo.nix"
245 -- Our primitive parser cannot handle these files.
246 , "top-level/all-packages.nix", "top-level/haskell-packages.nix"
247 -- This build is way too complicated to maintain it automatically.
248 , "pkgs/development/compilers/pakcs/default.nix"
249 , "pkgs/development/libraries/haskell/hoogle/local.nix"
250 -- Not registered on Hackage.
251 , "pkgs/development/compilers/agda/stdlib.nix"
252 , "pkgs/development/compilers/cryptol/1.8.x.nix"
253 , "pkgs/development/compilers/cryptol/2.0.x.nix"
254 , "pkgs/development/compilers/jhc/default.nix"
255 , "pkgs/development/tools/haskell/cabal-delete/default.nix"
256 , "pkgs/tools/networking/sproxy-web/default.nix"
257 , "pkgs/tools/networking/sproxy/default.nix"
258 ]