diff --git a/ChangeLog.md b/ChangeLog.md index c68420664b..0202c802e8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,9 @@ Major changes: +* Support for building inside a Nix-shell providing system dependencies + [#1285](https://github.com/commercialhaskell/stack/pull/1285) + Other enhancements: * Print latest applicable version of packages on conflicts diff --git a/doc/GUIDE.md b/doc/GUIDE.md index 691ec96f2f..65f442f811 100644 --- a/doc/GUIDE.md +++ b/doc/GUIDE.md @@ -1688,6 +1688,36 @@ image: and then run `stack image container` and then `docker images` to list the images. +### Nix + +stack provides an integration with [Nix](http://nixos.org/nix), +providing you with the same two benefits as the first Docker +integration discussed above: + +* more reproducible builds, since fixed versions of any system + libraries and commands required to build the project are + automatically built using Nix and managed locally per-project. These + system packages never conflict with any existing versions of these + libraries on your system. That they are managed locally to the + project means that you don't need to alter your system in any way to + build any odd project pulled from the Internet. +* implicit sharing of system packages between projects, so you don't + have more copies on-disk than you need to. + +Both Docker and Nix are methods to *isolate* builds and thereby make +them more reproducible. They just differ in the means of achieving +this isolation. Nix provides slightly weaker isolation guarantees than +Docker, but is more lightweight and more portable (Linux and OS +X mainly, but also Windows). For more on Nix, its command-line +interface and its package description language, read the +[Nix manual](http://nixos.org/nix/manual). But keep in mind that the +point of stack's support is to obviate the need to write any Nix code +in the common case or even to learn how to use the Nix tools (they're +called under the hood). + +For more information, see +[the Nix-integration documentation](nix_integration.md). + ## Power user commands The following commands are a little more powerful, and won't be needed by all diff --git a/doc/nix_integration.md b/doc/nix_integration.md new file mode 100644 index 0000000000..e6de031e48 --- /dev/null +++ b/doc/nix_integration.md @@ -0,0 +1,162 @@ +# Using Nix with Stack + +`stack` can build automatically inside a nix-shell (the equivalent of +a "container" in Docker parlance), provided Nix is already installed +on your system. To do so, please visit the +[Nix download page](http://nixos.org/nix/download.html). + +There are two ways to create a nix-shell: + +- providing a list of packages (by "attribute name") from + [Nixpkgs](http://nixos.org/nixos/packages.html), or +- providing a custom `shell.nix` file containing a Nix expression that + determines a *derivation*, i.e. a specification of what resources + are available inside the shell. + +The second requires writing code in Nix's custom language. So use this +option only if you already know Nix and have special requirements, +such as using custom Nix packages that override the standard ones or +using system libraries with special requirements. + +### Additions to your `stack.yaml` + +Add a section to your `stack.yaml` as follows: + + nix: + enable: true + packages: [glpk, pcre] + +This will instruct `stack` to build inside a nix-shell that will have +the `glpk` and `pcre` libraries installed and available. Further, the +nix-shell will implicitly also include a version of GHC matching the +configured resolver. Enabling Nix support means packages will always +be built using a GHC available inside the shell, rather than your +globally installed one if any. + +Note also that this also means that you cannot set your `resolver:` to +something that has not yet been mirrored in the Nixpkgs package +repository. In order to check this, the quickest way is to install and +launch a `nix-repl`: + +``` +$ nix-channel --update +$ nix-env -i nix-repl +$ nix-repl +``` + +Then, inside the `nix-repl`, do: + +``` +nix-repl> :l +nix-repl> haskell.packages.lts-3_13.ghc +``` + +Replace the resolver version with whatever version you are using. If it outputs +a path of a derivation in the Nix store, like + +`«derivation /nix/store/00xx8y0p3r0dqyq2frq277yr1ldqzzg0-ghc-7.10.2.drv»` + +then it means that this resolver has been mirrored and exists in your local copy of the nixpkgs. Whereas an error like + +`error: attribute ‘lts-3_99’ missing, at (string):1:1` + +means you should use a different resolver. You can also use +autocompletion with TAB to know which attributes `haskell.packages` +contains. + +In Nixpkgs master branch, you can find the mirrored resolvers in the +Haskell modules +[here on Github](https://github.com/NixOS/nixpkgs/tree/master/pkgs/development/haskell-modules). + +*Note:* currently, stack only discovers dynamic and static libraries +in the `lib/` folder of any nix package, and likewise header files in +the `include/` folder. If you're dealing with a package that doesn't +follow this standard layout, you'll have to deal with that using +a custom shell file (see below). + +### Use stack as normal + +With Nix enabled, `stack build` and `stack exec` will automatically +launch themselves in a nix-shell. Note that for now `stack ghci` will +not work, due to a bug in GHCi when working with external shared +libraries. + +If `enable:` is set to `false`, you can still build in a nix-shell by +passing the `--nix` flag to stack, for instance `stack --nix build`. +Nix just won't be used by default. + +## Command-line options + +The configuration present in your `stack.yaml` can be overriden on the +command-line. See `stack --nix-help` for a list of all Nix options. + + +## Configuration + +`stack.yaml` contains a `nix:` section with Nix settings. +Without this section, Nix will not be used. + +Here is a commented configuration file, showing the default values: + + nix: + + # `true` by default when the nix section is present. Set + # it to `false` to disable using Nix. + enable: true + + # Empty by default. The list of packages you want to be + # available in the nix-shell at build time (with `stack + # build`) and run time (with `stack exec`). + packages: [] + + # Unset by default. You cannot set this option if `packages:` + # is already present and not empty, this will result in an + # exception + shell-file: shell.nix + + # A list of strings, empty by default. Additional options that + # will be passed verbatim to the `nix-shell` command. + nix-shell-options: [] + +## Using a custom shell.nix file + +Nix is also a programming language, and as specified +[here](#using-nix-with-stack) if you know it you can provide to the +shell a fully customized derivation as an environment to use. Here is +the equivalent of the configuration used in +[this section](#enable-in-stackyaml), but with an explicit `shell.nix` +file: + +``` +with (import {}); +stdenv.mkDerivation { + name = "myEnv"; + buildInputs = [glpk pcre haskell.packages.lts-3_13.ghc]; + STACK_IN_NIX_EXTRA_ARGS="--extra-lib-dirs=${glpk}/lib --extra-include-dirs=${glpk}/include --extra-lib-dirs=${pcre}/lib --extra-include-dirs=${pcre}/include"; +} +``` + +Note that in this case, you _have_ to include (a version of) GHC in +your `buildInputs`! This potentially allows you to use a GHC which is +not the one of your `resolver:`. Also, you need to tell Stack where to +find the new libraries and headers. This is especially necessary on OS +X. The special variable `STACK_IN_NIX_EXTRA_ARGS` will be looked for +by the nix-shell when running the inner `stack` process. +`--extra-lib-dirs` and `--extra-include-dirs` are regular `stack +build` options. You can repeat these options for each dependency. + +Defining manually a `shell.nix` file gives you the possibility to +override some Nix derivations ("packages"), for instance to change +some build options of the libraries you use. + +And now for the `stack.yaml` file: + +``` +nix: + enable: true + shell-file: shell.nix +``` + +The `stack build` command will behave exactly the same as above. Note +that specifying both `packages:` and a `shell-file:` results in an +error. (Comment one out before adding the other.) diff --git a/src/Stack/Config.hs b/src/Stack/Config.hs index baf91e6aba..ae9eadf183 100644 --- a/src/Stack/Config.hs +++ b/src/Stack/Config.hs @@ -67,6 +67,7 @@ import qualified Paths_stack as Meta import Safe (headMay) import Stack.BuildPlan import Stack.Config.Docker +import Stack.Config.Nix import Stack.Constants import qualified Stack.Image as Image import Stack.Init @@ -150,6 +151,7 @@ configFromConfigMonoid configStackRoot configUserConfigPath mresolver mproject c configDocker <- dockerOptsFromMonoid (fmap fst mproject) configStackRoot mresolver configMonoidDockerOpts + configNix <- nixOptsFromMonoid (fmap fst mproject) configStackRoot configMonoidNixOpts rawEnv <- liftIO getEnvironment origEnv <- mkEnvOverride configPlatform diff --git a/src/Stack/Config/Nix.hs b/src/Stack/Config/Nix.hs new file mode 100644 index 0000000000..246719ccc4 --- /dev/null +++ b/src/Stack/Config/Nix.hs @@ -0,0 +1,45 @@ +{-# LANGUAGE RecordWildCards, DeriveDataTypeable #-} + +-- | Nix configuration +module Stack.Config.Nix + (nixOptsFromMonoid + ,StackNixException(..) + ) where + +import Data.Text (pack) +import Data.Maybe +import Data.Typeable +import Path +import Stack.Types +import Control.Exception.Lifted +import Control.Monad.Catch (throwM,MonadCatch) + + +-- | Interprets NixOptsMonoid options. +nixOptsFromMonoid :: (Monad m, MonadCatch m) => Maybe Project -> Path Abs Dir -> NixOptsMonoid -> m NixOpts +nixOptsFromMonoid mproject _stackRoot NixOptsMonoid{..} = do + let nixEnable = fromMaybe nixMonoidDefaultEnable nixMonoidEnable + nixPackages = case mproject of + Nothing -> nixMonoidPackages + Just p -> nixMonoidPackages ++ [case projectResolver p of + ResolverSnapshot (LTS x y) -> + pack ("haskell.packages.lts-" ++ show x ++ "_" ++ show y ++ ".ghc") + _ -> pack "ghc"] + nixInitFile = nixMonoidInitFile + nixShellOptions = nixMonoidShellOptions + if not (null nixMonoidPackages) && isJust nixInitFile then + throwM NixCannotUseShellFileAndPackagesException + else return () + return NixOpts{..} + +-- Exceptions thown specifically by Stack.Nix +data StackNixException + = NixCannotUseShellFileAndPackagesException + -- ^ Nix can't be given packages and a shell file at the same time + deriving (Typeable) + +instance Exception StackNixException + +instance Show StackNixException where + show NixCannotUseShellFileAndPackagesException = + "You cannot have packages and a shell-file filled at the same time in your nix-shell configuration." diff --git a/src/Stack/Docker.hs b/src/Stack/Docker.hs index 34210b11f7..1f18dcca06 100644 --- a/src/Stack/Docker.hs +++ b/src/Stack/Docker.hs @@ -16,6 +16,7 @@ module Stack.Docker ,reexecWithOptionalContainer ,reset ,reExecArgName + ,StackDockerException(..) ) where import Control.Applicative diff --git a/src/Stack/Nix.hs b/src/Stack/Nix.hs new file mode 100644 index 0000000000..e8a26bd714 --- /dev/null +++ b/src/Stack/Nix.hs @@ -0,0 +1,129 @@ +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings, TemplateHaskell #-} + +-- | Run commands in a nix-shell +module Stack.Nix + (reexecWithOptionalShell + ,nixCmdName + ) where + +import Control.Applicative +import Control.Monad +import Control.Monad.Catch (try,MonadCatch) +import Control.Monad.IO.Class (MonadIO,liftIO) +import Control.Monad.Logger (MonadLogger,logDebug) +import Control.Monad.Reader (MonadReader,asks) +import Control.Monad.Trans.Control (MonadBaseControl) +import Data.Char (toUpper) +import Data.List (intercalate) +import Data.Maybe +import Data.Monoid +import Data.Streaming.Process (ProcessExitedUnsuccessfully(..)) +import qualified Data.Text as T +import Data.Version (showVersion) +import Network.HTTP.Client.Conduit (HasHttpManager) +import qualified Paths_stack as Meta +import Prelude -- Fix redundant import warnings +import Stack.Constants (stackProgName) +import Stack.Docker (reExecArgName) +import Stack.Exec (exec) +import System.Process.Read (getEnvOverride) +import Stack.Types +import Stack.Types.Internal +import System.Environment (lookupEnv,getArgs,getExecutablePath) +import System.Exit (exitSuccess, exitWith) + + +-- | If Nix is enabled, re-runs the currently running OS command in a Nix container. +-- Otherwise, runs the inner action. +reexecWithOptionalShell + :: M env m + => IO () + -> m () +reexecWithOptionalShell inner = + do config <- asks getConfig + inShell <- getInShell + isReExec <- asks getReExec + if nixEnable (configNix config) && not inShell && not isReExec + then runShellAndExit getCmdArgs + else liftIO (inner >> exitSuccess) + where + getCmdArgs = do + args <- + fmap + (("--" ++ reExecArgName ++ "=" ++ showVersion Meta.version) :) + (liftIO getArgs) + exePath <- liftIO getExecutablePath + return (exePath, args) + +runShellAndExit :: M env m + => m (String, [String]) + -> m () +runShellAndExit getCmdArgs = do + config <- asks getConfig + envOverride <- getEnvOverride (configPlatform config) + (cmnd,args) <- getCmdArgs + let mshellFile = nixInitFile (configNix config) + pkgsInConfig = nixPackages (configNix config) + nixopts = case mshellFile of + Just filePath -> [filePath] + Nothing -> ["-E", T.unpack $ T.intercalate " " $ concat + [["with (import {});" + ,"runCommand \"myEnv\" {" + ,"buildInputs=lib.optional stdenv.isLinux glibcLocales ++ ["],pkgsInConfig,["];" + ,T.pack inShellEnvVar,"=1 ;" + ,"STACK_IN_NIX_EXTRA_ARGS=''"] + , (map (\p -> T.concat + ["--extra-lib-dirs=${",p,"}/lib" + ," --extra-include-dirs=${",p,"}/include "]) + pkgsInConfig), ["'' ;" + ,"} \"\""]]] + -- glibcLocales is necessary on Linux to avoid warnings about GHC being incapable to set the locale. + fullArgs = concat [ -- ["--pure"], + map T.unpack (nixShellOptions (configNix config)) + ,nixopts + ,["--command", intercalate " " (map escape (cmnd:args)) + ++ " $STACK_IN_NIX_EXTRA_ARGS"] + ] + $logDebug $ + "Using a nix-shell environment " <> (case mshellFile of + Just filePath -> "from file: " <> (T.pack filePath) + Nothing -> "with nix packages: " <> (T.intercalate ", " pkgsInConfig)) + e <- try (exec envOverride "nix-shell" fullArgs) + case e of + Left (ProcessExitedUnsuccessfully _ ec) -> liftIO (exitWith ec) + Right () -> liftIO exitSuccess + +-- | Shell-escape quotes inside the string and enclose it in quotes. +escape :: String -> String +escape str = "'" ++ foldr (\c -> if c == '\'' then + ("'\"'\"'"++) + else (c:)) "" str + ++ "'" + +-- | 'True' if we are currently running inside a Nix. +getInShell :: (MonadIO m) => m Bool +getInShell = liftIO (isJust <$> lookupEnv inShellEnvVar) + +-- | Environment variable used to indicate stack is running in container. +-- although we already have STACK_IN_NIX_EXTRA_ARGS that is set in the same conditions, +-- it can happen that STACK_IN_NIX_EXTRA_ARGS is set to empty. +inShellEnvVar :: String +inShellEnvVar = concat [map toUpper stackProgName,"_IN_NIXSHELL"] + +-- | Command-line argument for "nix" +nixCmdName :: String +nixCmdName = "nix" + +type M env m = + (MonadIO m + ,MonadReader env m + ,MonadLogger m + ,MonadBaseControl IO m + ,MonadCatch m + ,HasConfig env + ,HasTerminal env + ,HasReExec env + ,HasHttpManager env + ) diff --git a/src/Stack/Options.hs b/src/Stack/Options.hs index 804654228d..14601dd4f6 100644 --- a/src/Stack/Options.hs +++ b/src/Stack/Options.hs @@ -15,6 +15,7 @@ module Stack.Options ,globalOptsParser ,initOptsParser ,newOptsParser + ,nixOptsParser ,logLevelOptsParser ,ghciOptsParser ,solverOptsParser @@ -52,6 +53,7 @@ import Stack.Dot import Stack.Ghci (GhciOpts(..)) import Stack.Init import Stack.New +import Stack.Nix import Stack.Types import Stack.Types.TemplateName @@ -232,9 +234,10 @@ cleanOptsParser = CleanOpts <$> packages -- | Command-line arguments parser for configuration. configOptsParser :: Bool -> Parser ConfigMonoid configOptsParser hide0 = - (\workDir opts systemGHC installGHC arch os ghcVariant jobs includes libs skipGHCCheck skipMsys localBin modifyCodePage -> mempty + (\workDir dockerOpts nixOpts systemGHC installGHC arch os ghcVariant jobs includes libs skipGHCCheck skipMsys localBin modifyCodePage -> mempty { configMonoidWorkDir = workDir - , configMonoidDockerOpts = opts + , configMonoidDockerOpts = dockerOpts + , configMonoidNixOpts = nixOpts , configMonoidSystemGHC = systemGHC , configMonoidInstallGHC = installGHC , configMonoidSkipGHCCheck = skipGHCCheck @@ -255,6 +258,7 @@ configOptsParser hide0 = <> hide )) <*> dockerOptsParser True + <*> nixOptsParser True <*> maybeBoolFlags "system-ghc" "using the system installed GHC (on the PATH) if available and a matching version" @@ -315,6 +319,24 @@ configOptsParser hide0 = hide where hide = hideMods hide0 +nixOptsParser :: Bool -> Parser NixOptsMonoid +nixOptsParser hide0 = + NixOptsMonoid + <$> pure False + <*> maybeBoolFlags nixCmdName + "using a Nix-shell" + hide + <*> pure [] + <*> pure Nothing + <*> ((map T.pack . fromMaybe []) + <$> optional (argsOption (long "nix-shell-options" <> + metavar "OPTION" <> + help "Additional options passed to nix-shell" <> + hide))) + where + hide = hideMods hide0 + + -- | Options parser configuration for Docker. dockerOptsParser :: Bool -> Parser DockerOptsMonoid dockerOptsParser hide0 = diff --git a/src/Stack/Types.hs b/src/Stack/Types.hs index b83e08441c..26c1278984 100644 --- a/src/Stack/Types.hs +++ b/src/Stack/Types.hs @@ -13,6 +13,7 @@ import Stack.Types.PackageName as X import Stack.Types.Version as X import Stack.Types.Config as X import Stack.Types.Docker as X +import Stack.Types.Nix as X import Stack.Types.Image as X import Stack.Types.Build as X import Stack.Types.Package as X diff --git a/src/Stack/Types/Config.hs b/src/Stack/Types/Config.hs index 46f8332478..3d6bd56eed 100644 --- a/src/Stack/Types/Config.hs +++ b/src/Stack/Types/Config.hs @@ -161,6 +161,7 @@ import qualified Paths_stack as Meta import Stack.Types.BuildPlan (SnapName, renderSnapName, parseSnapName) import Stack.Types.Compiler import Stack.Types.Docker +import Stack.Types.Nix import Stack.Types.FlagName import Stack.Types.Image import Stack.Types.PackageIdentifier @@ -184,6 +185,8 @@ data Config = -- ^ Path to user configuration file (usually ~/.stack/config.yaml) ,configDocker :: !DockerOpts -- ^ Docker configuration + ,configNix :: !NixOpts + -- ^ Execution environment (e.g nix-shell) configuration ,configEnvOverride :: !(EnvSettings -> IO EnvOverride) -- ^ Environment variables to be passed to external tools ,configLocalProgramsBase :: !(Path Abs Dir) @@ -728,6 +731,8 @@ data ConfigMonoid = -- ^ See: 'configWorkDir'. , configMonoidDockerOpts :: !DockerOptsMonoid -- ^ Docker options. + , configMonoidNixOpts :: !NixOptsMonoid + -- ^ Options for the execution environment (nix-shell or container) , configMonoidConnectionCount :: !(Maybe Int) -- ^ See: 'configConnectionCount' , configMonoidHideTHLoading :: !(Maybe Bool) @@ -795,6 +800,7 @@ instance Monoid ConfigMonoid where mempty = ConfigMonoid { configMonoidWorkDir = Nothing , configMonoidDockerOpts = mempty + , configMonoidNixOpts = mempty , configMonoidConnectionCount = Nothing , configMonoidHideTHLoading = Nothing , configMonoidLatestSnapshotUrl = Nothing @@ -829,6 +835,7 @@ instance Monoid ConfigMonoid where mappend l r = ConfigMonoid { configMonoidWorkDir = configMonoidWorkDir l <|> configMonoidWorkDir r , configMonoidDockerOpts = configMonoidDockerOpts l <> configMonoidDockerOpts r + , configMonoidNixOpts = configMonoidNixOpts l <> configMonoidNixOpts r , configMonoidConnectionCount = configMonoidConnectionCount l <|> configMonoidConnectionCount r , configMonoidHideTHLoading = configMonoidHideTHLoading l <|> configMonoidHideTHLoading r , configMonoidLatestSnapshotUrl = configMonoidLatestSnapshotUrl l <|> configMonoidLatestSnapshotUrl r @@ -872,6 +879,7 @@ parseConfigMonoidJSON :: Object -> WarningParser ConfigMonoid parseConfigMonoidJSON obj = do configMonoidWorkDir <- obj ..:? configMonoidWorkDirName configMonoidDockerOpts <- jsonSubWarnings (obj ..:? configMonoidDockerOptsName ..!= mempty) + configMonoidNixOpts <- jsonSubWarnings (obj ..:? configMonoidNixOptsName ..!= mempty) configMonoidConnectionCount <- obj ..:? configMonoidConnectionCountName configMonoidHideTHLoading <- obj ..:? configMonoidHideTHLoadingName configMonoidLatestSnapshotUrl <- obj ..:? configMonoidLatestSnapshotUrlName @@ -954,6 +962,9 @@ configMonoidWorkDirName = "work-dir" configMonoidDockerOptsName :: Text configMonoidDockerOptsName = "docker" +configMonoidNixOptsName :: Text +configMonoidNixOptsName = "nix" + configMonoidConnectionCountName :: Text configMonoidConnectionCountName = "connection-count" diff --git a/src/Stack/Types/Nix.hs b/src/Stack/Types/Nix.hs new file mode 100644 index 0000000000..e7c40b3528 --- /dev/null +++ b/src/Stack/Types/Nix.hs @@ -0,0 +1,83 @@ +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +-- | Nix types. + +module Stack.Types.Nix where + +import Control.Applicative +import Data.Aeson.Extended +import Data.Monoid +import Data.Text (Text) + +-- | Nix configuration. +data NixOpts = NixOpts + {nixEnable :: !Bool + ,nixPackages :: ![Text] + -- ^ The system packages to be installed in the environment before it runs + ,nixInitFile :: !(Maybe String) + -- ^ The path of a file containing preconfiguration of the environment (e.g shell.nix) + ,nixShellOptions :: ![Text] + -- ^ Options to be given to the nix-shell command line + } + deriving (Show) + +-- | An uninterpreted representation of nix options. +-- Configurations may be "cascaded" using mappend (left-biased). +data NixOptsMonoid = NixOptsMonoid + {nixMonoidDefaultEnable :: !Bool + -- ^ Should nix-shell be defaulted to enabled (does @nix:@ section exist in the config)? + ,nixMonoidEnable :: !(Maybe Bool) + -- ^ Is using nix-shell enabled? + ,nixMonoidPackages :: ![Text] + -- ^ System packages to use (given to nix-shell) + ,nixMonoidInitFile :: !(Maybe String) + -- ^ The path of a file containing preconfiguration of the environment (e.g shell.nix) + ,nixMonoidShellOptions :: ![Text] + -- ^ Options to be given to the nix-shell command line + } + deriving (Show) + +-- | Decode uninterpreted nix options from JSON/YAML. +instance FromJSON (NixOptsMonoid, [JSONWarning]) where + parseJSON = withObjectWarnings "DockerOptsMonoid" + (\o -> do nixMonoidDefaultEnable <- pure True + nixMonoidEnable <- o ..:? nixEnableArgName + nixMonoidPackages <- o ..:? nixPackagesArgName ..!= [] + nixMonoidInitFile <- o ..:? nixInitFileArgName + nixMonoidShellOptions <- o ..:? nixShellOptsArgName ..!= [] + return NixOptsMonoid{..}) + +-- | Left-biased combine nix options +instance Monoid NixOptsMonoid where + mempty = NixOptsMonoid + {nixMonoidDefaultEnable = False + ,nixMonoidEnable = Nothing + ,nixMonoidPackages = [] + ,nixMonoidInitFile = Nothing + ,nixMonoidShellOptions = [] + } + mappend l r = NixOptsMonoid + {nixMonoidDefaultEnable = nixMonoidDefaultEnable l || nixMonoidDefaultEnable r + ,nixMonoidEnable = nixMonoidEnable l <|> nixMonoidEnable r + ,nixMonoidPackages = nixMonoidPackages l <> nixMonoidPackages r + ,nixMonoidInitFile = nixMonoidInitFile l <|> nixMonoidInitFile r + ,nixMonoidShellOptions = nixMonoidShellOptions l <> nixMonoidShellOptions r + } + +-- | Nix enable argument name. +nixEnableArgName :: Text +nixEnableArgName = "enable" + +-- | Nix packages (build inputs) argument name. +nixPackagesArgName :: Text +nixPackagesArgName = "packages" + +-- | shell.nix file path argument name. +nixInitFileArgName :: Text +nixInitFileArgName = "shell-file" + +-- | Extra options for the nix-shell command argument name. +nixShellOptsArgName :: Text +nixShellOptsArgName = "nix-shell-options" diff --git a/src/main/Main.hs b/src/main/Main.hs index 0092959c19..b08ac80f98 100644 --- a/src/main/Main.hs +++ b/src/main/Main.hs @@ -63,6 +63,7 @@ import Stack.Coverage import qualified Stack.Docker as Docker import Stack.Dot import Stack.Exec +import qualified Stack.Nix as Nix import Stack.Fetch import Stack.FileWatch import Stack.GhcPkg (getGlobalDB, mkGhcPackagePath) @@ -120,6 +121,10 @@ main = withInterpreterArgs stackProgName $ \args isInterpreter -> do dockerHelpOptName (dockerOptsParser False) ("Only showing --" ++ Docker.dockerCmdName ++ "* options.") + execExtraHelp args + nixHelpOptName + (nixOptsParser False) + ("Only showing --" ++ Nix.nixCmdName ++ "* options.") #ifdef USE_GIT_INFO let commitCount = $gitCommitCount versionString' = concat $ concat @@ -136,6 +141,7 @@ main = withInterpreterArgs stackProgName $ \args isInterpreter -> do let globalOpts hide = extraHelpOption hide progName (Docker.dockerCmdName ++ "*") dockerHelpOptName <*> + extraHelpOption hide progName (Nix.nixCmdName ++ "*") nixHelpOptName <*> globalOptsParser hide addCommand' cmd title footerStr constr = addCommand cmd title footerStr constr (globalOpts True) @@ -454,6 +460,7 @@ main = withInterpreterArgs stackProgName $ \args isInterpreter -> do where ignoreCheckSwitch = switch (long "ignore-check" <> help "Do not check package for common mistakes") dockerHelpOptName = Docker.dockerCmdName ++ "-help" + nixHelpOptName = Nix.nixCmdName ++ "-help" cmdFooter = "Run 'stack --help' for global options that apply to all subcommands." -- | Print out useful path information in a human-readable format (and @@ -635,7 +642,9 @@ setupCmd SetupCmdOpts{..} go@GlobalOpts{..} = do Docker.reexecWithOptionalContainer (lcProjectRoot lc) Nothing - (runStackLoggingTGlobal manager go $ do + (runStackTGlobal manager (lcConfig lc) go $ + Nix.reexecWithOptionalShell $ + runStackLoggingTGlobal manager go $ do (wantedCompiler, compilerCheck, mstack) <- case scoCompilerVersion of Just v -> return (v, MatchMinor, Nothing) @@ -796,10 +805,16 @@ withBuildConfigExt go@GlobalOpts{..} mbefore inner mafter = do (inner' lk) runStackTGlobal manager (lcConfig lc) go $ - Docker.reexecWithOptionalContainer (lcProjectRoot lc) mbefore (inner'' lk0) mafter - (Just $ liftIO $ - do lk' <- readIORef curLk - munlockFile lk') + Docker.reexecWithOptionalContainer + (lcProjectRoot lc) + mbefore + (runStackTGlobal manager (lcConfig lc) go $ + Nix.reexecWithOptionalShell (inner'' lk0) + ) + mafter + (Just $ liftIO $ + do lk' <- readIORef curLk + munlockFile lk') cleanCmd :: CleanOpts -> GlobalOpts -> IO () cleanCmd opts go = withBuildConfigAndLock go (const (clean opts)) @@ -936,7 +951,9 @@ execCmd ExecOpts {..} go@GlobalOpts{..} = (runStackTGlobal manager (lcConfig lc) go $ do config <- asks getConfig menv <- liftIO $ configEnvOverride config plainEnvSettings - exec menv cmd args) + Nix.reexecWithOptionalShell + (runStackTGlobal manager (lcConfig lc) go $ + exec menv cmd args)) Nothing Nothing -- Unlocked already above. ExecOptsEmbellished {..} -> diff --git a/src/test/Stack/NixSpec.hs b/src/test/Stack/NixSpec.hs new file mode 100644 index 0000000000..354d6aaae2 --- /dev/null +++ b/src/test/Stack/NixSpec.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell, OverloadedStrings #-} +module Stack.NixSpec where + +import Test.Hspec + +import Control.Monad.Logger +import Control.Exception +import Data.Monoid +import Network.HTTP.Conduit (Manager) +import System.Environment +import Path +import System.Directory +import System.IO.Temp (withSystemTempDirectory) + +import Stack.Config +import Stack.Types.Config +import Stack.Types.StackT +import Stack.Types.Nix + +import Prelude -- to remove the warning about Data.Monoid being redundant on GHC 7.10 + + +sampleConfig :: String +sampleConfig = + "resolver: lts-2.10\n" ++ + "packages: ['.']\n" ++ + "nix:\n" ++ + " enable: True\n" ++ + " packages: [glpk]" + +stackDotYaml :: Path Rel File +stackDotYaml = $(mkRelFile "stack.yaml") + +data T = T + { manager :: Manager + } + +setup :: IO T +setup = do + manager <- newTLSManager + unsetEnv "STACK_YAML" + return T{..} + +teardown :: T -> IO () +teardown _ = return () + +spec :: Spec +spec = beforeAll setup $ afterAll teardown $ do + let loadConfig' m = runStackLoggingT m LevelDebug False False (loadConfig mempty Nothing Nothing) + inTempDir action = do + currentDirectory <- getCurrentDirectory + withSystemTempDirectory "Stack_ConfigSpec" $ \tempDir -> do + let enterDir = setCurrentDirectory tempDir + exitDir = setCurrentDirectory currentDirectory + bracket_ enterDir exitDir action + describe "nix" $ do + it "sees that the nix shell is enabled" $ \T{..} -> inTempDir $ do + writeFile (toFilePath stackDotYaml) sampleConfig + lc <- loadConfig' manager + (nixEnable $ configNix $ lcConfig lc) `shouldBe` True + it "sees that the only package asked for is glpk and adds GHC from nixpkgs mirror of LTS resolver" $ \T{..} -> inTempDir $ do + writeFile (toFilePath stackDotYaml) sampleConfig + lc <- loadConfig' manager + (nixPackages $ configNix $ lcConfig lc) `shouldBe` ["glpk", "haskell.packages.lts-2_10.ghc"] + diff --git a/stack.cabal b/stack.cabal index c186d235dc..7db3182c25 100644 --- a/stack.cabal +++ b/stack.cabal @@ -51,6 +51,7 @@ library Stack.Clean Stack.Config Stack.Config.Docker + Stack.Config.Nix Stack.ConfigCmd Stack.Constants Stack.Coverage @@ -63,6 +64,7 @@ library Stack.GhcPkg Stack.Init Stack.New + Stack.Nix Stack.Options Stack.Package Stack.PackageDump @@ -83,6 +85,7 @@ library Stack.Types.FlagName Stack.Types.GhcPkgId Stack.Types.Image + Stack.Types.Nix Stack.Types.PackageIdentifier Stack.Types.PackageIndex Stack.Types.PackageName @@ -262,6 +265,7 @@ test-suite stack-test , Stack.DotSpec , Stack.PackageDumpSpec , Stack.ArgsSpec + , Stack.NixSpec , Network.HTTP.Download.VerifiedSpec ghc-options: -Wall -threaded build-depends: base >=4.7 && <5