@@ -0,0 +1,167 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE PartialTypeSignatures #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+
5+module Stackage2nix where
6+
7+import AllCabalHashes
8+import Data.Monoid
9+import Options.Applicative
10+import Stackage.BuildPlan
11+import Stackage.Types (SystemInfo(..))
12+import Paths_stack2nix as Main
13+import Distribution.Text (display)
14+import Control.Monad
15+import Text.PrettyPrint.HughesPJClass hiding ((<>))
16+import Distribution.Compiler (CompilerInfo(..), AbiTag(NoAbiTag), CompilerId(..), CompilerFlavor(GHC))
17+import Distribution.System (Platform(..))
18+import Distribution.Package (PackageName, PackageIdentifier(..), Dependency(..))
19+import Distribution.Nixpkgs.Haskell.PackageSourceSpec
20+import Distribution.Nixpkgs.Haskell.FromStack
21+import Distribution.Nixpkgs.Haskell.FromStack.Package
22+import Distribution.Nixpkgs.Fetch
23+import Distribution.Nixpkgs.PackageMap (readNixpkgPackageMap, resolve, PackageMap)
24+import Distribution.Version (Version, withinRange)
25+import Language.Haskell.Extension (Language(Haskell98, Haskell2010))
26+import Language.Nix
27+import Control.Lens
28+import Data.Maybe
29+import System.IO (withFile, IOMode(..), hPutStrLn)
30+
31+import qualified Data.Aeson as Aeson
32+import qualified Data.Yaml as Yaml
33+import qualified Data.Map as Map
34+import qualified Data.Set as Set
35+import qualified Data.ByteString as ByteString
36+import qualified Data.Text as Text
37+import qualified Data.HashMap.Strict as HashMap
38+
39+
40+data Options = Options
41+ { optBuildPlanFile :: FilePath
42+ , optAllCabalHashes :: FilePath
43+ , optDoCheck :: Bool
44+ , optDoHaddock :: Bool
45+ , optNixpkgsRepository :: FilePath
46+ , optNixpkgsMap :: Maybe FilePath
47+ , optOutPackages :: FilePath
48+ , optOutConfig :: FilePath
49+ }
50+
51+options :: Parser Options
52+options = Options
53+ <$> strArgument (metavar "PLAN" <> help "stackage build plan (YAML)")
54+ <*> strArgument (metavar "CABALFILES" <> help "path to checkout of all-cabal-hashes")
55+ <*> switch (long "do-check" <> help "enable tests for Stackage packages")
56+ <*> switch (long "do-haddock" <> help "enable haddock for Stackage packages")
57+ <*> strOption (long "nixpkgs" <> help "path to Nixpkgs repository" <> value "<nixpkgs>" <> showDefaultWith id <> metavar "PATH")
58+ <*> optional (strOption (long "package-map" <> help "path to a serialized nixpkgs package map" <> metavar "PATH"))
59+ <*> strOption (long "out-packages" <> help "name of the output file for the package set" <> value "packages.nix" <> metavar "PATH")
60+ <*> strOption (long "out-config" <> help "name of the output file for the package set configuration" <> value "configuration-packages.nix" <> metavar "PATH")
61+
62+pinfo :: ParserInfo Options
63+pinfo = info
64+ ( helper
65+ <*> infoOption ("stackage2nix " ++ display Main.version) (long "version" <> help "Show version number")
66+ <*> options
67+ )
68+ ( fullDesc
69+ <> header "stackage2nix converts stackage snapshots into a nix package set"
70+ )
71+
72+loadBuildPlan :: FilePath -> IO BuildPlan
73+loadBuildPlan = Yaml.decodeFileEither >=> \case
74+ Left err -> fail $ "Failed to parse stackage build plan: " ++ show err
75+ Right bp -> return bp
76+
77+loadPackage :: FilePath -> Maybe SHA1Hash -> PackageIdentifier -> IO Package
78+loadPackage allCabalHashesPath mSha1Hash pkgId = do
79+ (pkgDesc, _) <- case mSha1Hash of
80+ Just sha1 -> readPackageByHash allCabalHashesPath sha1
81+ Nothing -> readPackageByName allCabalHashesPath pkgId
82+ meta <- readPackageMeta allCabalHashesPath pkgId
83+ let
84+ tarballSHA256 = fromMaybe
85+ (error (display pkgId ++ ": meta data has no SHA256 hash for the tarball"))
86+ (view (mHashes . at "SHA256") meta)
87+ source = DerivationSource "url" ("mirror://hackage/" ++ display pkgId ++ ".tar.gz") "" tarballSHA256
88+ return $ Package source pkgDesc
89+
90+ghcCompilerInfo :: Version -> CompilerInfo
91+ghcCompilerInfo v = CompilerInfo
92+ { compilerInfoId = CompilerId GHC v
93+ , compilerInfoAbiTag = NoAbiTag
94+ , compilerInfoCompat = Just []
95+ , compilerInfoLanguages = Just [Haskell98, Haskell2010]
96+ , compilerInfoExtensions = Nothing
97+ }
98+
99+buildPlanContainsDependency :: Map.Map PackageName Version -> Dependency -> Bool
100+buildPlanContainsDependency packageVersions (Dependency depName versionRange) =
101+ maybe False (`withinRange` versionRange) $ Map.lookup depName packageVersions
102+
103+newtype SerializablePackageMap = SerializablePackageMap { getPackageMap :: PackageMap }
104+
105+instance Aeson.ToJSON SerializablePackageMap where
106+ toJSON (SerializablePackageMap m) = Aeson.object . map makeEntry $ Map.toList m
107+ where
108+ makeEntry (i, s) = (Text.pack (view ident i), Aeson.toJSON . map makePath $ Set.toList s)
109+ makePath = view (path . mapping ident)
110+
111+instance Aeson.FromJSON SerializablePackageMap where
112+ parseJSON = Aeson.withObject "package map object" $
113+ fmap (SerializablePackageMap . Map.fromList) . traverse makeEntry . HashMap.toList
114+ where
115+ makeEntry (i, s) = Aeson.parseJSON s <&> \s' -> (ident # Text.unpack i, Set.fromList (map makePath s'))
116+ makePath = review (path . mapping ident)
117+
118+run :: Options -> IO (BuildPlan, PackageSetConfig)
119+run Options{..} = do
120+ nixpkgs <- fromMaybe (readNixpkgPackageMap optNixpkgsRepository Nothing) $ do
121+ mapFile <- optNixpkgsMap
122+ return $ do
123+ contents <- ByteString.readFile mapFile
124+ case Aeson.eitherDecodeStrict contents of
125+ Left err -> fail $ "Failed to decode package map " ++ show mapFile ++ ": " ++ err
126+ Right v -> return $ getPackageMap v
127+
128+ buildPlan <- loadBuildPlan optBuildPlanFile
129+
130+ let
131+ systemInfo = bpSystemInfo buildPlan
132+ packageVersions = fmap ppVersion (bpPackages buildPlan) `Map.union` siCorePackages systemInfo
133+ conf = PackageSetConfig
134+ { packageLoader = loadPackage optAllCabalHashes
135+ , targetPlatform = Platform (siArch systemInfo) (siOS systemInfo)
136+ , targetCompiler = ghcCompilerInfo (siGhcVersion systemInfo)
137+ , nixpkgsResolver = resolve (Map.map (Set.map (over path ("pkgs":))) nixpkgs)
138+ , haskellResolver = buildPlanContainsDependency packageVersions
139+ , enableCheck = optDoCheck
140+ , enableHaddock = optDoHaddock
141+ }
142+ return (buildPlan, conf)
143+
144+writeOutPackages :: FilePath -> FilePath -> [Node] -> IO ()
145+writeOutPackages optBuildPlanFile optOutPackages nodes =
146+ withFile optOutPackages WriteMode $ \h -> do
147+ hPutStrLn h ("# Generated by stackage2nix from " ++ optBuildPlanFile)
148+ hPutStrLn h $ render $ pPrintOutPackages (view nodeDerivation <$> nodes)
149+
150+writeOutConfig :: FilePath -> FilePath -> SystemInfo -> [Node] -> IO ()
151+writeOutConfig optBuildPlanFile optOutConfig systemInfo nodes =
152+ withFile optOutConfig WriteMode $ \h -> do
153+ hPutStrLn h ("# Generated by stackage2nix from " ++ optBuildPlanFile)
154+ hPutStrLn h $ render $ pPrintOutConfig systemInfo nodes
155+
156+main :: IO ()
157+main = do
158+ opts@Options{..} <- execParser pinfo
159+ (buildPlan, conf) <- run opts
160+ let systemInfo = bpSystemInfo buildPlan
161+ nodes <- traverse (uncurry $ buildNodeM conf) $ Map.toList (bpPackages buildPlan)
162+ withFile optOutPackages WriteMode $ \h -> do
163+ hPutStrLn h ("# Generated by stackage2nix from " ++ optBuildPlanFile)
164+ hPutStrLn h $ render $ pPrintOutPackages (view nodeDerivation <$> nodes)
165+ withFile optOutConfig WriteMode $ \h -> do
166+ hPutStrLn h ("# Generated by stackage2nix from " ++ optBuildPlanFile)
167+ hPutStrLn h $ render $ pPrintOutConfig systemInfo nodes