stack2nix 0.1.3.0 → 0.2
raw patch · 17 files changed
+815/−441 lines, 17 filesdep +cabal2nixdep +distribution-nixpkgsdep +hackage-dbdep −Globdep −bytestringdep −data-fixdep ~Cabaldep ~asyncdep ~basenew-uploader
Dependencies added: cabal2nix, distribution-nixpkgs, hackage-db, hspec, language-nix, lens, path, pretty, regex-pcre, stack, time
Dependencies removed: Glob, bytestring, data-fix, hnix, monad-parallel, yaml
Dependency ranges changed: Cabal, async, base, optparse-applicative, process, temporary
Files
- ChangeLog.md +50/−0
- README.md +21/−18
- src/Stack2nix.hs +48/−337
- src/Stack2nix/External.hs +0/−7
- src/Stack2nix/External/Cabal2nix.hs +35/−29
- src/Stack2nix/External/Stack.hs +190/−0
- src/Stack2nix/External/Util.hs +11/−15
- src/Stack2nix/External/VCS.hs +0/−5
- src/Stack2nix/External/VCS/Git.hs +10/−6
- src/Stack2nix/Hackage.hs +42/−0
- src/Stack2nix/PP.hs +40/−0
- src/Stack2nix/Render.hs +159/−0
- src/Stack2nix/Types.hs +20/−0
- src/Stack2nix/Util.hs +79/−0
- stack2nix.cabal +46/−20
- stack2nix/Main.hs +47/−4
- tests/test.hs +17/−0
+ ChangeLog.md view
@@ -0,0 +1,50 @@+# Changelog++## v0.2 (2018-07-24)++Major changes:++- Use full stackage snapshot instead of relying on the build plan #83+- Get rid of hnix and rely on Derivation type from cabal2nix+- Use nix to provision executables if missing #83+- Use GHC version that belongs to the LTS #84+- ghc-options in stack.yaml are now passed to generated Nix exprs #96+- Support --bench #97++Other enhancements:++- Support --platform to set targeting system generation #79+- Use cabal2nix and stack as haskell libraries instead of relying on executable parsing #75+- Add --verbose flag #78+- Be able to pin down hackage snapshot #75+- Optimize cabal2nix calls by reusing HackageDB #75+- Rewrite tests in hspec to reduce dependencies #83+- Make stack.yaml filename configurable #90+- Add option to disable indentation #89+- When cloning git, also checkout submodules #108++Bug fixes:++- Be able to override GHC core packages #51+- Cleanup concurrency #33+- Add --haddock #38+- Add --test #35+- Support Stack subdirs #10+- Correct version parsing #67+- Silence git stdout output not to leak into Nix #91++## v0.1.3.0 (2017-07-27)++Bug fixes:++- Apply only Nix overrides without version fixes #26++## v0.1.2.0 (2017-06-22)++Bug fixes:++- Minor stack2nix.cabal improvements++## v0.1.1.0 (2017-06-22)++Initial public release.
README.md view
@@ -1,29 +1,32 @@ # stack2nix -[](https://travis-ci.org/input-output-hk/stack2nix)+[](https://travis-ci.org/input-output-hk/stack2nix) ## About -`stack2nix` automates conversion from [Stack](https://docs.haskellstack.org/en/stable/README/) configuration file to [Nix](http://nixos.org/nix/) expressions.+`stack2nix` automates conversion from [Stack](https://docs.haskellstack.org/en/stable/README/) configuration file to [Nix](http://nixos.org/nix/) expressions. The purpose is to map stack.yaml one-to-one into Nix expressions. `stack2nix` high-level workflow: -- invoke `stack list-dependencies` to determine complete fixed version list of packages based on resolver+- Generate stackage snapshot to determine complete fixed version list of packages based on resolver - apply any additional configuration (local packages, extra dependencies, etc) from `stack.yaml` - generate complete list of dependencies to Nix expressions, replacing upstream `hackage-packages.nix` ## Installation -There are two options. The first is to install stack2nix and its dependencies on your machine directly, and the second is to use the supported virtual machine configuration.+There are three options. The first - using Nix is recommended. If there are difficulties please file an issue. -If there are difficulties please file an issue. Generally the virtual machine approach should be more reliable.+## Nix (recommended) -### Native Environment+1. Install [Nix](https://nixos.org/nix/).+2. Clone this repo.+3. Run `nix-build` to build. -1. Install [nix](https://nixos.org/nix/).+### Stack++1. Install [Nix](https://nixos.org/nix/). 2. Clone this repo.-3. Run `stack install` to install.-4. Ensure `cabal2nix` v2.2.1 or higher and `stack2nix` are in your `$PATH`.+3. Run `stack install --nix` to install. ### Virtual Machine @@ -34,24 +37,24 @@ ## Usage -Nix expressions generated by stack2nix depend on a somewhat recent upstream change to nixpkgs. If `nix-env` or `nix-build` fails with an error mentioning `initialPackages`, try setting the `NIX_PATH` environment variable: `NIX_PATH=nixpkgs=https://github.com/NixOS/nixpkgs/archive/21a8239452adae3a4717772f4e490575586b2755.tar.gz`.+Nix expressions generated by stack2nix require NixOS 17.09 or later. -### Remote Packages+### Local Packages -Stack2nix can generate a nix expressions for Haskell packages hosted in git repositories.+Sometimes it's convenient to build local Haskell packages. Assuming the current directory is a locally maintained fork of Pandoc: ```- $ stack2nix --revision 242e2a064f6a32b22e1599bbfe72e64d7b6203b8 https://github.com/jgm/pandoc.git > demo.nix- $ nix-build -A pandoc demo.nix+ $ stack2nix . > default.nix+ $ nix-build -A pandoc ``` -### Local Packages+### Remote Packages -Sometimes it's convenient to build local Haskell packages. Assuming the current directory is a locally maintained fork of Pandoc:+Stack2nix can generate a nix expressions for Haskell packages hosted in git repositories. ```- $ stack2nix . > default.nix- $ nix-build -A pandoc+ $ stack2nix --revision 242e2a064f6a32b22e1599bbfe72e64d7b6203b8 https://github.com/jgm/pandoc.git > demo.nix+ $ nix-build -A pandoc demo.nix ``` ## Testing
src/Stack2nix.hs view
@@ -1,363 +1,74 @@+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Stack2nix ( Args(..)- , Package(..)- , RemotePkgConf(..)- , StackConfig(..)- , parseStackYaml , stack2nix , version ) where -import Control.Concurrent.Async-import Control.Concurrent.MSem-import Control.Exception (SomeException, catch,- onException)-import Control.Monad (unless)-import qualified Data.ByteString as BS-import Data.Fix (Fix (..))-import Data.List (foldl', sort, union, (\\))-import qualified Data.Map.Strict as Map-import Data.Maybe (fromMaybe, listToMaybe)-import Data.Monoid ((<>))-import Data.Text (Text, pack, unpack)-import qualified Data.Traversable as T-import Data.Version (Version (..), parseVersion,- showVersion)-import Data.Yaml (FromJSON (..), (.!=), (.:),- (.:?))-import qualified Data.Yaml as Y-import Distribution.Text (display)-import Nix.Expr (Binding (..), NExpr, NExprF (..),- NKeyName (..), ParamSet (..),- Params (..))-import Nix.Parser (Result (..), parseNixFile,- parseNixString)-import Nix.Pretty (prettyNix)-import Paths_stack2nix (version)-import Stack2nix.External (cabal2nix)-import Stack2nix.External.Util (runCmd, runCmdFrom)-import Stack2nix.External.VCS.Git (Command (..), ExternalCmd (..),- InternalCmd (..), git)-import System.Directory (doesFileExist)-import System.Environment (getEnv)-import System.FilePath (dropExtension, isAbsolute,- normalise, takeDirectory,- takeFileName, (</>))-import System.FilePath.Glob (glob)-import System.IO (hPutStrLn, stderr, stdout)-import System.IO.Temp (withSystemTempDirectory)-import Text.ParserCombinators.ReadP (readP_to_S)--data Args = Args- { argRev :: Maybe String- , argOutFile :: Maybe FilePath- , argUri :: String- }- deriving (Show)--data StackConfig = StackConfig- { resolver :: Text- , packages :: [Package]- , extraDeps :: [Text]- }- deriving (Show, Eq)--data Package = LocalPkg FilePath- | RemotePkg RemotePkgConf- deriving (Show, Eq)--data RemotePkgConf = RemotePkgConf- { gitUrl :: Text- , commit :: Text- , extraDep :: Bool- }- deriving (Show, Eq)--instance FromJSON StackConfig where- parseJSON (Y.Object v) =- StackConfig <$>- v .: "resolver" <*>- v .: "packages" <*>- v .: "extra-deps"- parseJSON _ = fail "Expected Object for StackConfig value"--instance FromJSON Package where- parseJSON (Y.String v) = return $ LocalPkg $ unpack v- parseJSON obj@(Y.Object _) = RemotePkg <$> parseJSON obj- parseJSON _ = fail "Expected String or Object for Package value"--instance FromJSON RemotePkgConf where- parseJSON (Y.Object v) = do- loc <- v .: "location"- gitUrl <- loc .: "git"- commit <- loc .: "commit"- extra <- v .:? "extra-dep" .!= False- return $ RemotePkgConf gitUrl commit extra- parseJSON _ = fail "Expected Object for RemotePkgConf value"--parseStackYaml :: BS.ByteString -> Maybe StackConfig-parseStackYaml = Y.decode--checkRuntimeDeps :: IO ()-checkRuntimeDeps = do- checkVer "cabal2nix" "2.2.1"- checkVer "git" "2"- checkVer "cabal" "1"- where- checkVer prog minVer = do- hPutStrLn stderr $ unwords ["Ensuring", prog, "version is >=", minVer, "..."]- result <- runCmd prog ["--version"] `onException` (error $ "Failed to run " ++ prog ++ ". Not found in PATH.")- case result of- Right out ->- let- -- heuristic for parsing version from stdout- firstLine = head . lines- lastWord = head . reverse . words- ver = parseVer . lastWord . firstLine $ out- in- unless (ver >= parseVer minVer) $ error $ unwords ["ERROR:", prog, "version must be", minVer, "or higher. Current version:", maybe "[parse failure]" showVersion ver]- Left err -> error err-- parseVer :: String -> Maybe Version- parseVer =- fmap fst . listToMaybe . reverse . readP_to_S parseVersion+import Control.Monad (unless, void)+import Data.Maybe (isJust)+import Data.Monoid ((<>))+import Paths_stack2nix (version)+import Stack2nix.External.Stack+import Stack2nix.External.Util (runCmdFrom, failHard)+import Stack2nix.External.VCS.Git (Command (..), ExternalCmd (..),+ InternalCmd (..), git)+import Stack2nix.Types (Args (..))+import Stack2nix.Util+import System.Directory (doesFileExist,+ getCurrentDirectory, withCurrentDirectory)+import System.Environment (getEnv, setEnv)+import System.FilePath ((</>))+import System.IO.Temp (withSystemTempDirectory) stack2nix :: Args -> IO () stack2nix args@Args{..} = do- checkRuntimeDeps+ ensureExecutableExists "cabal" "cabal-install"+ ensureExecutableExists "git" "git"+ ensureExecutableExists "nix-prefetch-git" "nix-prefetch-scripts"+ assertMinVer "git" "2"+ assertMinVer "cabal" "2"+ setEnv "GIT_QUIET" "y" updateCabalPackageIndex- isLocalRepo <- doesFileExist $ argUri </> "stack.yaml"+ -- cwd <- getCurrentDirectory+ -- let projRoot = if isAbsolute argUri then argUri else cwd </> argUri+ let projRoot = argUri+ isLocalRepo <- doesFileExist $ projRoot </> argStackYaml+ logDebug args $ "stack2nix (isLocalRepo): " ++ show isLocalRepo+ logDebug args $ "stack2nix (projRoot): " ++ show projRoot+ logDebug args $ "stack2nix (argUri): " ++ show argUri if isLocalRepo- then handleStackConfig Nothing argUri+ then handleStackConfig Nothing projRoot else withSystemTempDirectory "s2n-" $ \tmpDir -> tryGit tmpDir >> handleStackConfig (Just argUri) tmpDir where updateCabalPackageIndex :: IO ()- updateCabalPackageIndex =- getEnv "HOME" >>= \home -> runCmdFrom home "cabal" ["update"] >> return ()+ updateCabalPackageIndex = do+ home <- getEnv "HOME"+ out <- runCmdFrom home "cabal" ["update"]+ void $ failHard out tryGit :: FilePath -> IO () tryGit tmpDir = do- git $ OutsideRepo $ Clone argUri tmpDir+ void $ git $ OutsideRepo $ Clone argUri tmpDir case argRev of- Just r -> git $ InsideRepo tmpDir $ Checkout r+ Just r -> void $ git $ InsideRepo tmpDir (Checkout r) Nothing -> return mempty handleStackConfig :: Maybe String -> FilePath -> IO () handleStackConfig remoteUri localDir = do- let fname = localDir </> "stack.yaml"- alreadyExists <- doesFileExist fname- unless alreadyExists $ runCmdFrom localDir "stack" ["init", "--system-ghc"]- >> return ()- contents <- BS.readFile fname- case parseStackYaml contents of- Just config -> toNix args remoteUri localDir config- Nothing -> error $ "Failed to parse " <> (localDir </> "stack.yaml")---- Credit: https://stackoverflow.com/a/18898822/204305-mapPool :: T.Traversable t => Int -> (a -> IO b) -> t a -> IO (t b)-mapPool max' f xs = do- sem <- new max'- mapConcurrently (with sem . f) xs--c2nPoolSize :: Int-c2nPoolSize = 4--toNix :: Args -> Maybe String -> FilePath -> StackConfig -> IO ()-toNix Args{..} remoteUri baseDir StackConfig{..} =- withSystemTempDirectory "s2n" $ \outDir -> do- _ <- mapPool c2nPoolSize (genNixFile outDir) packages- overrides <- mapPool c2nPoolSize overrideFor =<< updateDeps outDir- _ <- mapPool c2nPoolSize (genNixFile outDir) packages- _ <- mapPool c2nPoolSize patchNixFile =<< glob (outDir </> "*.nix")- writeFile (outDir </> "initialPackages.nix") $ initialPackages $ sort overrides- pullInNixFiles $ outDir </> "initialPackages.nix"- nf <- parseNixFile $ outDir </> "initialPackages.nix"- case nf of- Success expr ->- case argOutFile of- Just fname -> writeFile fname $ defaultNix expr- Nothing -> hPutStrLn stdout $ defaultNix expr- _ -> error "failed to parse intermediary initialPackages.nix file"- where- updateDeps :: FilePath -> IO [FilePath]- updateDeps outDir = do- hPutStrLn stderr $ "Updating deps from " ++ baseDir- result <- runCmdFrom baseDir "stack" ["list-dependencies", "--system-ghc", "--separator", "-"]- case result of- Right pkgs -> do- let pkgs' = ["hscolour", "jailbreak-cabal", "cabal-doctest", "happy", "stringbuilder"] ++ lines pkgs- hPutStrLn stderr "Haskell dependencies:"- mapM_ (hPutStrLn stderr) pkgs'- _ <- mapPool c2nPoolSize (\d -> catch (handleExtraDep outDir d) ignoreError) $ pack <$> pkgs'- return ()- Left err -> error $ unlines ["FAILED: stack list-dependencies", err]- glob (outDir </> "*.nix")-- -- TODO: Remove this.- ignoreError :: SomeException -> IO ()- ignoreError _ = return ()-- genNixFile :: FilePath -> Package -> IO ()- genNixFile outDir (LocalPkg relPath) =- cabal2nix (fromMaybe baseDir remoteUri) (pack <$> argRev) (Just relPath) (Just outDir)- genNixFile outDir (RemotePkg RemotePkgConf{..}) =- cabal2nix (unpack gitUrl) (Just commit) Nothing (Just outDir)-- patchNixFile :: FilePath -> IO ()- patchNixFile fname = do- contents <- readFile fname- case parseNixString contents of- Success expr ->- case takeFileName fname of- "hspec.nix" -> do- writeFile fname $ show $ prettyNix $ (addParam "stringbuilder" . stripNonEssentialDeps) expr- _ -> writeFile fname $ show $ prettyNix $ stripNonEssentialDeps expr- _ -> error "failed to parse intermediary nix package file"-- addParam :: String -> NExpr -> NExpr- addParam param expr =- let contents = show $ prettyNix expr- (l:ls) = lines contents- (openBrace, params) = splitAt 1 (words l)- l' = unwords $ openBrace ++ [param ++ ", "] ++ params- in- case parseNixString $ unlines (l':ls) of- Success expr' -> expr'- _ -> expr-- stripNonEssentialDeps :: NExpr -> NExpr- stripNonEssentialDeps expr =- let sectsToDrop = ["testHaskellDepends", "testToolDepends", "benchmarkHaskellDepends", "benchmarkToolDepends"]- sectsToKeep = ["executableHaskellDepends", "executableToolDepends", "libraryHaskellDepends", "librarySystemDepends", "libraryToolDepends", "setupHaskellDepends"]- collectDeps sects = foldr union [] $ fmap (dependenciesFromSection expr) sects- depsToStrip = collectDeps sectsToDrop \\ collectDeps sectsToKeep- expr' = foldl dropDependencySection expr sectsToDrop- in- dropParams expr' depsToStrip-- handleExtraDep :: FilePath -> Text -> IO ()- handleExtraDep outDir dep =- cabal2nix ("cabal://" <> unpack dep) Nothing Nothing (Just outDir)-- overrideFor :: FilePath -> IO String- overrideFor nixFile = do- deps <- externalDeps- return $ " " <> (dropExtension . takeFileName) nixFile <> " = callPackage ./" <> takeFileName nixFile <> " { " <> deps <> " };"- where- externalDeps :: IO String- externalDeps = do- deps <- librarySystemDeps nixFile- return . unwords $ fmap (\d -> unpack d <> " = pkgs." <> unpack d <> ";") deps-- pullInNixFiles :: FilePath -> IO ()- pullInNixFiles nixFile = do- nf <- parseNixFile nixFile- case nf of- Success expr ->- case expr of- Fix (NAbs paramSet (Fix (NAbs fnParam (Fix (NSet attrs))))) -> do- attrs' <- mapM patchAttr attrs- let expr' = Fix (NAbs paramSet (Fix (NAbs fnParam (Fix (NSet attrs')))))- writeFile nixFile $ (++ "\n") $ show $ prettyNix expr'- _ ->- error $ "unhandled nix expression format\n" ++ show expr- _ -> error "failed to parse nix file!"- where- patchAttr :: Binding (Fix NExprF) -> IO (Binding (Fix NExprF))- patchAttr attr =- case attr of- NamedVar k (Fix (NApp (Fix (NApp (Fix (NSym "callPackage")) pkg)) deps)) -> do- pkg' <- patchPkgRef pkg- return $ NamedVar k (Fix (NApp (Fix (NApp (Fix (NSym "callPackage")) pkg')) deps))- _ -> error $ "unhandled NamedVar"-- patchPkgRef (Fix (NLiteralPath path)) = do- let p = if isAbsolute path then path else normalise $ takeDirectory nixFile </> path- nf <- parseNixFile p- case nf of- Success expr -> return expr- _ -> error $ "failed to parse referenced nix file '" ++ path ++ "'"- patchPkgRef x = return x-- dependenciesFromSection :: NExpr -> String -> [Text]- dependenciesFromSection expr name = do- case expr of- Fix (NAbs _ (Fix (NApp _ (Fix (NSet namedVars))))) ->- case lookupNamedVar namedVars name of- Just (Fix (NList deps)) ->- foldl' (\acc x ->- case x of- Fix (NSym n) -> n : acc- _ -> acc) [] deps- _ -> []- _ -> []-- librarySystemDeps :: FilePath -> IO [Text]- librarySystemDeps nixFile = do- nf <- parseNixFile nixFile- case nf of- Success expr -> return $ dependenciesFromSection expr "librarySystemDepends"- _ -> return []-- dropDependencySection :: NExpr -> String -> NExpr- dropDependencySection expr name =- case expr of- Fix (NAbs a (Fix (NApp b (Fix (NSet namedVars))))) ->- Fix (NAbs a (Fix (NApp b (Fix (NSet $ dropNamedVar namedVars name)))))- _ -> expr-- lookupNamedVar :: [Binding a] -> String -> Maybe a- lookupNamedVar [] _ = Nothing- lookupNamedVar (x:xs) name =- case x of- NamedVar [StaticKey k] val ->- if unpack k == name- then Just val- else lookupNamedVar xs name- _ -> lookupNamedVar xs name-- dropNamedVar :: [Binding a] -> String -> [Binding a]- dropNamedVar xs name = filter differentName xs- where- differentName (NamedVar [StaticKey k] _) = unpack k /= name- differentName _ = True-- dropParams :: NExpr -> [Text] -> NExpr- dropParams (Fix (NAbs (ParamSet (FixedParamSet paramMap) x)- (Fix (NApp mkDeriv (Fix (NSet args)))))) names =- Fix (NAbs (ParamSet (FixedParamSet $ foldr Map.delete paramMap names) x)- (Fix (NApp mkDeriv (Fix (NSet args)))))- dropParams x _ = x-- initialPackages overrides = unlines $- [ "{ pkgs, stdenv, callPackage }:"- , ""- , "self: {"- ] ++ overrides ++- [ "}"- ]-- defaultNix pkgsNixExpr = unlines- [ "# Generated using stack2nix " ++ display version ++ "."- , "#"- , "# Only works with sufficiently recent nixpkgs, e.g. \"NIX_PATH=nixpkgs=https://github.com/NixOS/nixpkgs/archive/21a8239452adae3a4717772f4e490575586b2755.tar.gz\"."- , ""- , "{ pkgs ? (import <nixpkgs> {})"- , ", compiler ? pkgs.haskell.packages.ghc802"- , ", ghc ? pkgs.haskell.compiler.ghc802"- , "}:"- , ""- , "with (import <nixpkgs/pkgs/development/haskell-modules/lib.nix> { inherit pkgs; });"- , ""- , "let"- , " stackPackages = " ++ show (prettyNix pkgsNixExpr) ++ ";"- , "in"- , "compiler.override {"- , " initialPackages = stackPackages;"- , " configurationCommon = { ... }: self: super: {};"- , "}"- ]+ cwd <- getCurrentDirectory+ logDebug args $ "handleStackConfig (cwd): " ++ cwd+ logDebug args $ "handleStackConfig (localDir): " ++ localDir+ logDebug args $ "handleStackConfig (remoteUri): " ++ show remoteUri+ let stackFile = localDir </> argStackYaml+ alreadyExists <- doesFileExist stackFile+ unless alreadyExists $ error $ stackFile <> " does not exist. Use 'stack init' to create it."+ logDebug args $ "handleStackConfig (alreadyExists): " ++ show alreadyExists+ let go = if isJust remoteUri+ then withCurrentDirectory localDir+ else id+ go $ runPlan localDir remoteUri args
− src/Stack2nix/External.hs
@@ -1,7 +0,0 @@-module Stack2nix.External- ( module Stack2nix.External.Cabal2nix- , module Stack2nix.External.VCS- ) where--import Stack2nix.External.Cabal2nix-import Stack2nix.External.VCS
src/Stack2nix/External/Cabal2nix.hs view
@@ -1,43 +1,49 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+ module Stack2nix.External.Cabal2nix ( cabal2nix ) where -import Data.List (stripPrefix, takeWhile)-import Data.Maybe (fromMaybe)-import Data.Monoid ((<>))-import Data.Text (Text, unpack)-import Stack2nix.External.Util (runCmd)-import System.FilePath ((</>))+import Cabal2nix (cabal2nixWithDB, parseArgs, optNixpkgsIdentifier, Options)+import Control.Lens+import Data.Maybe (fromMaybe)+import Data.Text (Text, unpack)+import qualified Distribution.Nixpkgs.Haskell.Hackage as DB+import Distribution.Nixpkgs.Haskell.Derivation (Derivation)+import Distribution.System (Platform(..), Arch(..), OS(..))+import Language.Nix+import System.IO (hPutStrLn, stderr)+import Stack2nix.Types (Args (..)) --- Requires cabal2nix >= 2.2 in PATH-cabal2nix :: FilePath -> Maybe Text -> Maybe FilePath -> Maybe FilePath -> IO ()-cabal2nix uri commit subpath odir = do- result <- runCmd exe (args $ fromMaybe "." subpath)- case result of- Right stdout ->- let basename = pname stdout <> ".nix"- fname = maybe basename (</> basename) odir- in- writeFile fname stdout- Left stderr -> error stderr- where- exe = "cabal2nix"+import Text.PrettyPrint.HughesPJClass (Doc) +cabal2nix :: Args -> FilePath -> Maybe Text -> Maybe FilePath -> DB.HackageDB -> IO (Either Doc Derivation)+cabal2nix Args{..} uri commit subpath hackageDB = do+ let runCmdArgs = args $ fromMaybe "." subpath+ hPutStrLn stderr $ unwords ("+ cabal2nix":runCmdArgs)+ options <- parseArgs runCmdArgs+ cabal2nixWithDB hackageDB $ cabalOptions options++ where args :: FilePath -> [String] args dir = concat [ maybe [] (\c -> ["--revision", unpack c]) commit- -- , maybe [] (\d -> ["--subpath", d]) subpath , ["--subpath", dir]- , ["--no-check", "--no-haddock"] -- TODO: only use on repos that need it.+ , ["--system", fromCabalPlatform argPlatform] , [uri] ] - pname :: String -> String- pname = pname' . lines+-- Override default nixpkgs resolver to do pkgs.attr instead of attr+cabalOptions :: Options -> Options+cabalOptions options =+ options {+ optNixpkgsIdentifier = \i -> Just (binding # (i, path # ["pkgs", i]))+ } - pname' :: [String] -> String- pname' [] = error "nix expression generated by cabal2nix is missing the 'pname' attr"- pname' (x:xs) =- case stripPrefix " pname = \"" x of- Just x' -> takeWhile (/= '"') x'- Nothing -> pname' xs+-- | Copied (and modified) from src/Distribution/Nixpkgs/Meta.hs+fromCabalPlatform :: Platform -> String+fromCabalPlatform (Platform I386 Linux) = "i686-linux"+fromCabalPlatform (Platform X86_64 Linux) = "x86_64-linux"+fromCabalPlatform (Platform X86_64 OSX) = "x86_64-darwin"+fromCabalPlatform p = error ("fromCabalPlatform: invalid Nix platform" ++ show p)
+ src/Stack2nix/External/Stack.hs view
@@ -0,0 +1,190 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Stack2nix.External.Stack+ ( PackageRef(..), runPlan+ ) where++import Control.Lens ((%~))+import Data.List (concat)+import qualified Data.Map.Strict as M+import Data.Maybe (fromJust)+import qualified Data.Set as Set (fromList,+ union)+import Data.Text (pack, unpack)+import Distribution.Nixpkgs.Haskell.Derivation (Derivation,+ configureFlags)+import qualified Distribution.Nixpkgs.Haskell.Hackage as DB+import Options.Applicative+import Path (parseAbsFile)+import Stack.Build.Source (getGhcOptions, loadSourceMapFull)+import Stack.Build.Target (NeedTargets (..))+import Stack.Config+import Stack.Options.BuildParser+import Stack.Options.GlobalParser+import Stack.Options.Utils (GlobalOptsContext (..))+import Stack.Prelude hiding+ (logDebug)+import Stack.Runners (loadCompilerVersion,+ withBuildConfig)+import Stack.Types.BuildPlan (PackageLocation (..),+ Repo (..))+import Stack.Types.Compiler (CVType (..),+ CompilerVersion,+ getGhcVersion)+import Stack.Types.Config+import Stack.Types.Config.Build (BuildCommand (..))+import Stack.Types.Nix+import Stack.Types.Package (PackageSource (..),+ lpLocation,+ lpPackage,+ packageName,+ packageVersion)+import Stack.Types.PackageIdentifier (PackageIdentifier (..),+ PackageIdentifierRevision (..),+ packageIdentifierString)+import Stack.Types.PackageName (PackageName)+import Stack.Types.Runner+import Stack2nix.External.Cabal2nix (cabal2nix)+import Stack2nix.Hackage (loadHackageDB)+import Stack2nix.Render (render)+import Stack2nix.Types (Args (..))+import Stack2nix.Util (ensureExecutable,+ logDebug,+ mapPool)+import System.Directory (canonicalizePath,+ createDirectoryIfMissing,+ getCurrentDirectory,+ makeRelativeToCurrentDirectory)+import System.FilePath (makeRelative,+ (</>))+import Text.PrettyPrint.HughesPJClass (Doc)++data PackageRef+ = HackagePackage PackageIdentifierRevision+ | NonHackagePackage PackageIdentifier (PackageLocation FilePath)+ deriving (Eq, Show)++genNixFile :: Args -> FilePath -> Maybe String -> Maybe String -> DB.HackageDB -> PackageRef -> IO (Either Doc Derivation)+genNixFile args baseDir uri argRev hackageDB pkgRef = do+ cwd <- getCurrentDirectory+ case pkgRef of+ NonHackagePackage _ident PLArchive {} -> error "genNixFile: No support for archive package locations"+ HackagePackage (PackageIdentifierRevision pkg _) ->+ cabal2nix args ("cabal://" <> packageIdentifierString pkg) Nothing Nothing hackageDB+ NonHackagePackage _ident (PLRepo repo) ->+ cabal2nix args (unpack $ repoUrl repo) (Just $ repoCommit repo) (Just (repoSubdirs repo)) hackageDB+ NonHackagePackage _ident (PLFilePath path) -> do+ relPath <- makeRelativeToCurrentDirectory path+ projRoot <- canonicalizePath $ cwd </> baseDir+ let defDir = baseDir </> makeRelative projRoot path+ cabal2nix args (fromMaybe defDir uri) (pack <$> argRev) (const relPath <$> uri) hackageDB++-- TODO: remove once we use flags, options+sourceMapToPackages :: Map PackageName PackageSource -> [PackageRef]+sourceMapToPackages = map sourceToPackage . M.elems+ where+ sourceToPackage :: PackageSource -> PackageRef+ sourceToPackage (PSIndex _ _flags _options pir) = HackagePackage pir+ sourceToPackage (PSFiles lp _) =+ let pkg = lpPackage lp+ ident = PackageIdentifier (packageName pkg) (packageVersion pkg)+ in NonHackagePackage ident (lpLocation lp)+++planAndGenerate+ :: HasEnvConfig env+ => BuildOptsCLI+ -> FilePath+ -> Maybe String+ -> Args+ -> String+ -> RIO env ()+planAndGenerate boptsCli baseDir remoteUri args@Args {..} ghcnixversion = do+ (_targets, _mbp, _locals, _extraToBuild, sourceMap) <- loadSourceMapFull+ NeedTargets+ boptsCli+ let pkgs = sourceMapToPackages sourceMap+ liftIO $ logDebug args $ "plan:\n" ++ show pkgs++ hackageDB <- liftIO $ loadHackageDB Nothing argHackageSnapshot+ buildConf <- envConfigBuildConfig <$> view envConfigL+ drvs <- liftIO $ mapPool+ argThreads+ (\p ->+ fmap (addGhcOptions buildConf p)+ <$> genNixFile args baseDir remoteUri argRev hackageDB p+ )+ pkgs+ let locals = map (\l -> show (packageName (lpPackage l))) _locals+ liftIO $ render drvs args locals ghcnixversion++-- | Add ghc-options declared in stack.yaml to the nix derivation for a package+-- by adding to the configureFlags attribute of the derivation+addGhcOptions :: BuildConfig -> PackageRef -> Derivation -> Derivation+addGhcOptions buildConf pkgRef drv =+ drv & configureFlags %~ (Set.union stackGhcOptions)+ where+ stackGhcOptions :: Set String+ stackGhcOptions =+ Set.fromList . map (unpack . ("--ghc-option=" <>)) $ getGhcOptions+ buildConf+ buildOpts+ pkgName+ False+ False+ pkgName :: PackageName+ pkgName = case pkgRef of+ HackagePackage (PackageIdentifierRevision (PackageIdentifier n _) _) -> n+ NonHackagePackage (PackageIdentifier n _) _ -> n++runPlan :: FilePath+ -> Maybe String+ -> Args+ -> IO ()+runPlan baseDir remoteUri args@Args{..} = do+ let stackRoot = "/tmp/s2n"+ createDirectoryIfMissing True stackRoot+ let globals = globalOpts baseDir stackRoot args+ let stackFile = baseDir </> argStackYaml++ ghcVersion <- getGhcVersionIO globals stackFile+ let ghcnixversion = filter (/= '.') $ show (getGhcVersion ghcVersion)+ ensureExecutable ("haskell.compiler.ghc" ++ ghcnixversion)+ withBuildConfig globals $ planAndGenerate buildOpts baseDir remoteUri args ghcnixversion++getGhcVersionIO :: GlobalOpts -> FilePath -> IO (CompilerVersion 'CVWanted)+getGhcVersionIO go stackFile = do+ cp <- canonicalizePath stackFile+ fp <- parseAbsFile cp+ lc <- withRunner LevelError True False ColorAuto Nothing False $ \runner ->+ -- https://www.fpcomplete.com/blog/2017/07/the-rio-monad+ runRIO runner $ loadConfig mempty Nothing (SYLOverride fp)+ loadCompilerVersion go lc++globalOpts :: FilePath -> FilePath -> Args -> GlobalOpts+globalOpts currentDir stackRoot Args{..} =+ go { globalReExecVersion = Just "1.5.1" -- TODO: obtain from stack lib if exposed+ , globalConfigMonoid =+ (globalConfigMonoid go)+ { configMonoidNixOpts = mempty+ { nixMonoidEnable = First (Just True)+ }+ }+ , globalStackYaml = SYLOverride (currentDir </> argStackYaml)+ , globalLogLevel = if argVerbose then LevelDebug else LevelInfo+ }+ where+ pinfo = info (globalOptsParser currentDir OuterGlobalOpts (Just LevelError)) briefDesc+ args = concat [ ["--stack-root", stackRoot]+ , ["--jobs", show argThreads]+ , ["--test" | argTest]+ , ["--bench" | argBench]+ , ["--haddock" | argHaddock]+ , ["--no-install-ghc"]+ ]+ go = globalOptsFromMonoid False . fromJust . getParseResult $ execParserPure defaultPrefs pinfo args++buildOpts :: BuildOptsCLI+buildOpts = fromJust . getParseResult $ execParserPure defaultPrefs (info (buildOptsParser Build) briefDesc) ["--dry-run"]
src/Stack2nix/External/Util.hs view
@@ -1,27 +1,23 @@ module Stack2nix.External.Util where -import Data.List (intercalate) import Data.Monoid ((<>)) import System.Directory (getCurrentDirectory) import System.Exit (ExitCode (..))-import System.Process (CreateProcess (..))-import System.Process (proc, readCreateProcessWithExitCode)+import System.Process (CreateProcess (..), proc,+ readCreateProcessWithExitCode) -runCmdFrom :: FilePath -> String -> [String] -> IO (Either String String)-runCmdFrom dir prog args = do- (exitCode, stdout, stderr) <- readCreateProcessWithExitCode (fromDir dir (proc prog args)) ""- case exitCode of- ExitSuccess -> return $ Right stdout- _ -> return $ Left $ "Command failed: " ++ cmd ++ "\n" ++ stderr+runCmdFrom :: FilePath -> String -> [String] -> IO (ExitCode, String, String)+runCmdFrom dir prog args = readCreateProcessWithExitCode (fromDir dir (proc prog args)) "" where fromDir :: FilePath -> CreateProcess -> CreateProcess fromDir d procDesc = procDesc { cwd = Just d } - cmd = "cd \"" <> dir <> "\" && " <> intercalate " " (prog:args)--runCmd :: String -> [String] -> IO (Either String String)+runCmd :: String -> [String] -> IO (ExitCode, String, String) runCmd prog args = getCurrentDirectory >>= (\d -> runCmdFrom d prog args) -failHard :: Show a => Either a b -> IO ()-failHard (Left stderr) = error $ show stderr-failHard (Right _) = mempty+failHard :: (ExitCode, String, String) -> IO (ExitCode, String, String)+failHard r@(ExitSuccess, _, _) = pure r+failHard (ExitFailure code, _, err) =+ error $ unlines [ "Failed with exit code " <> show code <> "..."+ , show err+ ]
− src/Stack2nix/External/VCS.hs
@@ -1,5 +0,0 @@-module Stack2nix.External.VCS- ( module Stack2nix.External.VCS.Git- ) where--import Stack2nix.External.VCS.Git
src/Stack2nix/External/VCS/Git.hs view
@@ -4,6 +4,7 @@ ) where import Stack2nix.External.Util (failHard, runCmd, runCmdFrom)+import System.Exit (ExitCode (..)) data Command = OutsideRepo ExternalCmd | InsideRepo FilePath InternalCmd@@ -16,14 +17,17 @@ exe = "git" -- Requires git binary in PATH-git :: Command -> IO ()+git :: Command -> IO (ExitCode, String, String) git (OutsideRepo cmd) = runExternal cmd git (InsideRepo dir cmd) = runInternal dir cmd -runExternal :: ExternalCmd -> IO ()-runExternal (Clone uri dir) = do- runCmd exe ["clone", uri, dir] >>= failHard+runExternal :: ExternalCmd -> IO (ExitCode, String, String)+runExternal (Clone uri dir) =+ runCmd exe ["clone", "--recurse-submodules", uri, dir] >>= failHard -runInternal :: FilePath -> InternalCmd -> IO ()+runInternal :: FilePath -> InternalCmd -> IO (ExitCode, String, String) runInternal repoDir (Checkout ref) = do- runCmdFrom repoDir exe ["checkout", ref] >>= failHard+ checkoutCmd <- runCmdFrom repoDir exe ["checkout", ref]+ _ <- failHard checkoutCmd+ submoduleCmd <- runCmdFrom repoDir exe ["submodule", "update", "--init", "--recursive"]+ failHard submoduleCmd
+ src/Stack2nix/Hackage.hs view
@@ -0,0 +1,42 @@+-- | This module is needed to replace P.parseDB which would otherwise filter out+-- deprecated packages, which may end up being in an LTS.+module Stack2nix.Hackage+ ( loadHackageDB+ ) where++import Control.Exception (SomeException, mapException)+import qualified Data.Map as Map+import Data.Time.Clock (UTCTime)+import Distribution.Hackage.DB.Path (hackageTarball)+import qualified Distribution.Hackage.DB.Unparsed as U+import qualified Distribution.Hackage.DB.Parsed as P+import Distribution.Package (PackageName)+import Distribution.Hackage.DB.Errors (HackageDBPackageName(..))+import qualified Distribution.Nixpkgs.Haskell.Hackage as H++++loadHackageDB :: Maybe FilePath+ -- ^ The path to the Hackage database.+ -> Maybe UTCTime+ -- ^ If we have hackage-snapshot time.+ -> IO H.HackageDB+loadHackageDB optHackageDB optHackageSnapshot = do+ dbPath <- maybe hackageTarball return optHackageDB+ readTarball optHackageSnapshot dbPath+++readTarball :: Maybe UTCTime -> FilePath -> IO H.HackageDB+readTarball ts p = do+ dbu <- U.readTarball ts p+ let dbp = parseDB dbu+ return (Map.mapWithKey (H.parsePackageData dbu) dbp)+++parseDB :: U.HackageDB -> P.HackageDB+parseDB = Map.mapWithKey parsePackageData++parsePackageData :: PackageName -> U.PackageData -> P.PackageData+parsePackageData pn (U.PackageData _ vs') =+ mapException (\e -> HackageDBPackageName pn (e :: SomeException)) $+ Map.mapWithKey (P.parseVersionData pn) $ vs'
+ src/Stack2nix/PP.hs view
@@ -0,0 +1,40 @@+module Stack2nix.PP+ (ppIndented, ppSingletons) where++import Text.PrettyPrint (Doc, Mode(..), TextDetails(..), fullRender, render)++-- | Formats the derivation doc with indentation and lines wrapped at 100 chars.+ppIndented :: Doc -> String+ppIndented = render++-- | Formats the derivation doc without indentation and with each list+-- element and function argument on its own line.+-- We go to this effort to "ugly-print" so that the resulting file is+-- less susceptible to merge conflicts when checked into git.+ppSingletons :: Doc -> String+ppSingletons doc = fixSpace . snd $ fullRender LeftMode 80 1.5 printer (0, "") doc+ where+ printer :: TextDetails -> (Int, String) -> (Int, String)+ printer (Chr c) (n, s) = ppChar n s c+ printer (Str s1) s2 = fmap (s1 ++) s2+ printer (PStr s1) s2 = fmap (s1 ++) s2++ ppChar :: Int -> String -> Char -> (Int, String)+ ppChar n s c = (n', s')+ where+ s' = case c of+ ',' -> "\n," ++ s+ ' ' -> (if n /= 0 then '\n' else ' '):s+ '{' -> "{\n " ++ s+ '}' -> "\n}" ++ s+ _ -> c:s+ n' = case c of+ '[' -> n - 1 -- PrettyPrint works backwards+ ']' -> n + 1+ _ -> n++ -- remove single trailing spaces+ fixSpace :: String -> String+ fixSpace (' ':'\n':s) = '\n':fixSpace s+ fixSpace (c:s) = c:fixSpace s+ fixSpace [] = []
+ src/Stack2nix/Render.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Stack2nix.Render+ (render) where++import Control.Lens+import Control.Monad (when)+import Data.Either (lefts, rights)+import Data.List (filter, isPrefixOf,+ sort)+import Data.Monoid ((<>))+import Data.Set (Set)+import qualified Data.Set as Set+import Distribution.Nixpkgs.Haskell.BuildInfo (haskell, pkgconfig,+ system, tool)+import Distribution.Nixpkgs.Haskell.Derivation (Derivation,+ benchmarkDepends,+ dependencies, doCheck,+ pkgid, runHaddock,+ testDepends)+import Distribution.Text (display)+import Distribution.Types.PackageId (PackageIdentifier (..),+ pkgName)+import Distribution.Types.PackageName (unPackageName)+import Language.Nix (path)+import Language.Nix.Binding (Binding, reference)+import Language.Nix.PrettyPrinting (disp)+import Paths_stack2nix (version)+import Stack2nix.Types (Args (..))+import Stack2nix.PP (ppIndented, ppSingletons)+import System.IO (hPutStrLn, stderr)+import qualified Text.PrettyPrint as PP+import Text.PrettyPrint.HughesPJClass (Doc, fcat, nest,+ pPrint, punctuate,+ semi, space, text)++-- TODO: this only covers GHC 8.0.2+basePackages :: Set String+basePackages = Set.fromList+ [ "array"+ , "base"+ , "binary"+ , "bytestring"+ , "Cabal"+ , "containers"+ , "deepseq"+ , "directory"+ , "filepath"+ , "ghc-boot"+ , "ghc-boot-th"+ , "ghc-prim"+ , "ghci"+ , "haskeline"+ , "hoopl"+ , "hpc"+ , "integer-gmp"+ , "pretty"+ , "process"+ , "rts"+ , "template-haskell"+ , "terminfo"+ , "time"+ , "transformers"+ , "unix"+ , "xhtml"+ ]++render :: [Either Doc Derivation] -> Args -> [String] -> String -> IO ()+render results args locals ghcnixversion = do+ let docs = lefts results+ when (length docs > 0) $ do+ hPutStrLn stderr $ show docs+ error "Error(s) happened during cabal2nix generation ^^"+ let drvs = rights results++ -- See what base packages are missing in the derivations list and null them+ let missing = sort $ Set.toList $ Set.difference basePackages $ Set.fromList (map drvToName drvs)+ let renderedMissing = map (\b -> nest 6 (text (b <> " = null;"))) missing+ let pp = if argIndent args then ppIndented else ppSingletons++ let out = defaultNix pp ghcnixversion $ renderedMissing ++ map (renderOne args locals) drvs++ case argOutFile args of+ Just fname -> writeFile fname out+ Nothing -> putStrLn out++renderOne :: Args -> [String] -> Derivation -> Doc+renderOne args locals drv' = nest 6 $ PP.hang+ (PP.doubleQuotes (text pid) <> " = callPackage")+ 2+ ("(" <> pPrint drv <> ") {" <> text (show pkgs) <> "};")+ where+ pid = drvToName drv+ deps = view dependencies drv+ nixPkgs :: [Binding]+ nixPkgs = Set.toList $ Set.union (view pkgconfig deps) (view system deps)+ -- filter out libX stuff to prevent breakage in generated set+ nonXpkgs = filter+ (\e -> not+ ( "libX"+ `Data.List.isPrefixOf` (display (((view (reference . path) e) !! 1)))+ )+ )+ nixPkgs+ pkgs = fcat $ punctuate space [ disp b <> semi | b <- nonXpkgs ]+ drv =+ filterDepends args isLocal drv'+ & doCheck+ .~ (argTest args && isLocal)+ & runHaddock+ .~ (argHaddock args && isLocal)+ isLocal = elem pid locals++filterDepends :: Args -> Bool -> Derivation -> Derivation+filterDepends args isLocal drv = drv & foldr+ (.)+ id+ (do+ (depend, predicate) <-+ [(Lens testDepends, argTest args), (Lens benchmarkDepends, argBench args)]+ binding <- [Lens haskell, Lens pkgconfig, Lens system, Lens tool]+ pure+ $ runLens depend+ . runLens binding+ .~ (if predicate && isLocal+ then view (runLens depend . runLens binding) drv+ else Set.empty+ )+ )++drvToName :: Derivation -> String+drvToName drv = unPackageName $ pkgName $ view pkgid drv++defaultNix :: (Doc -> String) -> String -> [Doc] -> String+defaultNix pp ghcnixversion drvs = unlines $+ [ "# Generated using stack2nix " <> display version <> "."+ , "#"+ , "# Only works with sufficiently recent nixpkgs, e.g. \"NIX_PATH=nixpkgs=https://github.com/NixOS/nixpkgs/archive/21a8239452adae3a4717772f4e490575586b2755.tar.gz\"."+ , ""+ , "{ pkgs ? (import <nixpkgs> {})"+ , ", compiler ? pkgs.haskell.packages.ghc" ++ ghcnixversion+ , "}:"+ , ""+ , "with pkgs.haskell.lib;"+ , ""+ , "let"+ , " stackPackages = { pkgs, stdenv, callPackage }:"+ , " self: {"+ ] ++ (map pp drvs) +++ [ " };"+ , "in compiler.override {"+ , " initialPackages = stackPackages;"+ , " configurationCommon = { ... }: self: super: {};"+ , " compilerConfig = self: super: {};"+ , "}"+ ]
+ src/Stack2nix/Types.hs view
@@ -0,0 +1,20 @@+module Stack2nix.Types where++import Data.Time (UTCTime)+import Distribution.System (Platform)++data Args = Args+ { argRev :: Maybe String+ , argOutFile :: Maybe FilePath+ , argStackYaml :: FilePath+ , argThreads :: Int+ , argTest :: Bool+ , argBench :: Bool+ , argHaddock :: Bool+ , argHackageSnapshot :: Maybe UTCTime+ , argPlatform :: Platform+ , argUri :: String+ , argIndent :: Bool+ , argVerbose :: Bool+ }+ deriving (Show)
+ src/Stack2nix/Util.hs view
@@ -0,0 +1,79 @@+module Stack2nix.Util+ ( assertMinVer+ , extractVersion+ , mapPool+ , logDebug+ , ensureExecutableExists+ , ensureExecutable+ ) where++import Control.Concurrent.Async+import Control.Concurrent.MSem+import Control.Exception (onException)+import Control.Monad (unless)+import Data.Maybe (listToMaybe)+import qualified Data.Traversable as T+import Data.Version (Version (..), parseVersion,+ showVersion)+import Data.Text (pack, strip, unpack)+import GHC.Exts (sortWith)+import Stack2nix.External.Util (runCmd)+import Stack2nix.Types (Args, argVerbose)+import System.Directory (findExecutable)+import System.Environment (getEnv, setEnv)+import System.Exit (ExitCode (..))+import System.IO (hPutStrLn, stderr)+import System.Process (readProcessWithExitCode)+import Text.ParserCombinators.ReadP (readP_to_S)+import Text.Regex.PCRE (AllTextMatches (..),+ getAllTextMatches, (=~))++-- Credit: https://stackoverflow.com/a/18898822/204305+mapPool :: T.Traversable t => Int -> (a -> IO b) -> t a -> IO (t b)+mapPool max' f xs = do+ sem <- new max'+ mapConcurrently (with sem . f) xs++-- heuristic for parsing version from stdout+extractVersion :: String -> Maybe Version+extractVersion str = ver+ where+ firstLine = head . lines $ str+ candidateVers = getAllTextMatches (firstLine =~ "[\\d\\.]+" :: AllTextMatches [] String)+ bestMatch = head . reverse . sortWith length $ candidateVers+ ver = fmap fst . listToMaybe . reverse . readP_to_S parseVersion $ bestMatch++assertMinVer :: String -> String -> IO ()+assertMinVer prog minVer = do+ hPutStrLn stderr $ unwords ["Ensuring", prog, "version is >=", minVer, "..."]+ result <- runCmd prog ["--version"] `onException` error ("Failed to run " ++ prog ++ ". Not found in PATH.")+ case result of+ (ExitSuccess, out, _) ->+ let ver = extractVersion out in+ unless (ver >= extractVersion minVer) $ error $ unwords ["ERROR:", prog, "version must be", minVer, "or higher. Current version:", maybe "[parse failure]" showVersion ver]+ (ExitFailure _, _, err) -> error err++logDebug :: Args -> String -> IO ()+logDebug args msg+ | argVerbose args = hPutStrLn stderr msg+ | otherwise = return ()+++-- check if executable is present, if not provision it with nix+ensureExecutableExists :: String -> String -> IO ()+ensureExecutableExists executable nixAttr = do+ exec <- findExecutable executable+ case exec of+ Just _ -> return ()+ Nothing -> ensureExecutable nixAttr++-- given nixAttr, build it and add $out/bin to $PATH+ensureExecutable :: String -> IO ()+ensureExecutable nixAttr = do+ (exitcode2, stdout, err2) <- readProcessWithExitCode "nix-build" ["-A", nixAttr, "<nixpkgs>", "--no-build-output", "--no-out-link"] mempty+ case exitcode2 of+ ExitSuccess -> do+ hPutStrLn stderr $ err2+ path <- getEnv "PATH"+ setEnv "PATH" (unpack (strip (pack stdout)) ++ "/bin" ++ ":" ++ path)+ ExitFailure _ -> error $ nixAttr ++ " failed to build via nix"
stack2nix.cabal view
@@ -1,5 +1,5 @@ name: stack2nix-version: 0.1.3.0+version: 0.2 synopsis: Convert stack.yaml files into Nix build instructions. description: Convert stack.yaml files into Nix build instructions. license: MIT@@ -8,7 +8,9 @@ maintainer: jacob.mitchell@iohk.io category: Distribution, Nix build-type: Simple-extra-source-files: README.md+extra-source-files:+ README.md+ ChangeLog.md cabal-version: >= 1.10 source-repository head@@ -17,38 +19,62 @@ library hs-source-dirs: src- build-depends: base >=4.9 && <4.10- , Cabal >= 1.24.2 && < 1.25- , async >= 2.1.1.1 && < 2.2- , bytestring >= 0.10.8.1 && < 0.11+ build-depends: base >=4.9 && <4.12+ , Cabal >= 2.0.0.2 && < 2.3+ , async >= 2.1.1.1 && < 2.3+ , cabal2nix >= 2.10 && < 2.11 , containers >= 0.5.7.1 && < 0.6- , data-fix == 0.0.4 , directory >= 1.3 && < 1.4+ , distribution-nixpkgs >= 1.1 && < 1.2 , filepath >= 1.4.1.1 && < 1.5- , Glob >= 0.7.14 && < 0.8- , hnix >= 0.3.4 && < 0.4- , monad-parallel >= 0.7.2.2 && < 0.8- , process >= 1.4.3 && < 1.5+ , hackage-db+ , optparse-applicative >= 0.13.2 && < 0.15+ , pretty+ , path+ , language-nix+ , lens+ , process >= 1.4.3 && < 1.7+ , regex-pcre >= 0.94.4 && < 0.95 , SafeSemaphore >= 0.10.1 && < 0.11- , temporary >= 1.2.0.4 && < 1.3+ , stack >= 1.5.1+ , temporary >= 1.2.0.4 && < 1.4 , text >= 1.2.2.1 && < 1.3- , yaml >= 0.8.22.1 && < 0.9+ , time exposed-modules: Stack2nix- other-modules: Stack2nix.External- , Stack2nix.External.Cabal2nix- , Stack2nix.External.VCS+ , Stack2nix.PP+ , Stack2nix.Render+ , Stack2nix.Types+ , Stack2nix.Util+ other-modules: Stack2nix.External.Cabal2nix+ , Stack2nix.External.Stack , Stack2nix.External.VCS.Git , Stack2nix.External.Util+ , Stack2nix.Hackage , Paths_stack2nix ghc-options: -Wall default-language: Haskell2010 executable stack2nix main-is: Main.hs- build-depends: base >=4.9 && <4.10- , Cabal >= 1.24.2 && < 1.25+ build-depends: base+ , Cabal , stack2nix- , optparse-applicative >= 0.13.2 && < 0.14+ , optparse-applicative+ , time hs-source-dirs: stack2nix- ghc-options: -Wall+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010++test-suite test+ default-language:+ Haskell2010+ type:+ exitcode-stdio-1.0+ hs-source-dirs:+ tests+ main-is:+ test.hs+ build-depends:+ base >= 4 && < 5+ , hspec+ , stack2nix
stack2nix/Main.hs view
@@ -1,18 +1,61 @@ module Main ( main ) where -import Data.Semigroup ((<>))-import Distribution.Text (display)+import Data.Semigroup ((<>))+import Data.Time (UTCTime, defaultTimeLocale,+ parseTimeM)+import qualified Distribution.Compat.ReadP as P+import Distribution.System (Arch (..), OS (..), Platform (..),+ buildPlatform)+import Distribution.Text (display) import Options.Applicative import Stack2nix+import System.IO (BufferMode (..), hSetBuffering,+ stderr, stdout) args :: Parser Args args = Args <$> optional (strOption $ long "revision" <> help "revision to use when fetching from VCS")- <*> optional (strOption $ short 'o' <> help "output file for generated nix expression")+ <*> optional (strOption $ short 'o' <> help "output file for generated nix expression" <> metavar "PATH")+ <*> strOption (long "stack-yaml" <> help "Override project stack.yaml file" <> showDefault <> value "stack.yaml")+ <*> option auto (short 'j' <> help "number of threads for subprocesses" <> showDefault <> value 4 <> metavar "INT")+ <*> switch (long "test" <> help "enable tests")+ <*> switch (long "bench" <> help "enable benchmarks")+ <*> switch (long "haddock" <> help "enable documentation generation")+ <*> optional (option utcTimeReader (long "hackage-snapshot" <> help "hackage snapshot time, ISO format"))+ <*> option (readP platformReader) (long "platform" <> help "target platform to use when invoking stack or cabal2nix" <> value buildPlatform <> showDefaultWith display) <*> strArgument (metavar "URI")+ <*> flag True False (long "no-indent" <> help "disable indentation and place one item per line")+ <*> switch (long "verbose" <> help "verbose output")+ where+ -- | A parser for the date. Hackage updates happen maybe once or twice a month.+ -- Example: parseTime defaultTimeLocale "%FT%T%QZ" "2017-11-20T12:18:35Z" :: Maybe UTCTime+ utcTimeReader :: ReadM UTCTime+ utcTimeReader = eitherReader $ \arg ->+ case parseTimeM True defaultTimeLocale "%FT%T%QZ" arg of+ Nothing -> Left $ "Cannot parse date, ISO format used ('2017-11-20T12:18:35Z'): " ++ arg+ Just utcTime -> Right utcTime + -- | A String parser for Distribution.System.Platform+ -- | Copied from cabal2nix/src/Cabal2nix.hs+ platformReader :: P.ReadP r Platform+ platformReader = do+ arch <- P.choice [P.string "i686" >> return I386, P.string "x86_64" >> return X86_64]+ _ <- P.char '-'+ os <- P.choice [P.string "linux" >> return Linux, P.string "darwin" >> return OSX]+ return (Platform arch os)++ readP :: P.ReadP a a -> ReadM a+ readP p = eitherReader $ \s ->+ case [ r' | (r',"") <- P.readP_to_S p s ] of+ (r:_) -> Right r+ _ -> Left ("invalid value " ++ show s)++ main :: IO ()-main = stack2nix =<< execParser opts+main = do+ hSetBuffering stdout LineBuffering+ hSetBuffering stderr LineBuffering+ stack2nix =<< execParser opts where opts = info (helper
+ tests/test.hs view
@@ -0,0 +1,17 @@++import Data.Version (makeVersion)+import Stack2nix.Util+import Test.Hspec+++main :: IO ()+main = hspec $ do+ describe "Stack2nix.Util" $ do+ it "cabal-install version extraction" $ do+ extractVersion "cabal-install version 2.0.0.0" `shouldBe` Just (makeVersion [2, 0, 0, 0])+ it "git version extraction" $ do -- issue #67+ extractVersion "git version 2.11.0 (Apple Git-81)" `shouldBe` Just (makeVersion [2, 11, 0])+ it "cabal2nix version extraction" $ do+ extractVersion "cabal2nix 2.7" `shouldBe` Just (makeVersion [2, 7])+ it "ghc version extraction" $ do+ extractVersion "The Glorious Glasgow Haskell Compilation System, version 8.0.2" `shouldBe` Just (makeVersion [8, 0, 2])