packages feed

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 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 -[![Build Status](https://travis-ci.org/input-output-hk/stack2nix.svg)](https://travis-ci.org/input-output-hk/stack2nix)+[![Build Status](https://travis-ci.org/input-output-hk/stack2nix.svg?branch=master)](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])