Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Major changes:

Behavior changes:

* With Nix integration, Stack uses `nix-instantiate` before `nix-shell`,
enabling Stack to catch and report when the Nix expression cannot be
evaluated by Nix. The `nix-instantiate-options` option is added, accordingly.

Other enhancements:

Bug fixes:
Expand Down
7 changes: 6 additions & 1 deletion doc/maintainers/stack_errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
In connection with considering Stack's support of the
[Haskell Error Index](https://errors.haskell.org/) initiative, this page seeks
to take stock of the errors that Stack itself can raise, by reference to the
`master` branch of the Stack repository. Last updated: 2026-04-24.
`master` branch of the Stack repository. Last updated: 2026-07-07.

* `Stack.main`: catches exceptions from action `commandLineHandler`.

Expand Down Expand Up @@ -214,6 +214,11 @@ to take stock of the errors that Stack itself can raise, by reference to the

~~~haskell
[S-7384] = CannotDetermineProjectRoot
[S-1264] | NixInstantiateCommandFailure [String] ByteString
[S-3220] | NixInstantiateCommandNoDerivationPath
[S-5924] | NixInstantiateCommandNoSingleDerivationPath [String] [S8.ByteString]
[S-1537] | NixInstantiateCommandInvalidDerivationPath [String] String

~~~

- `Stack.PackageDump.PackageDumpException`
Expand Down
14 changes: 11 additions & 3 deletions doc/topics/nix_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ C libraries that you would normally install manually.

Nix's `nix-shell` starts an interactive shell based on a Nix expression. Stack
can automatically create a Nix build environment in the background using
`nix-shell`. There are two alternative options to create such a build
environment:
`nix-instantiate` to produce a store derivation and `nix-shell` with that store
derivation. This two-step process (since Stack UNRELEASED) allows Stack to
report separately any errors in producing the store derivation. There are two
alternative options to create such a build environment:

1. provide a list of [Nix packages][nix-search-packages]. To these, Stack will
add Nix packages for the GHC compiler, `git` (the distributed version control
system), `gcc` (the GNU compiler collection), `gmp` (the GNU multiple
precision arithmetic library) and `cacert` (a bundle of X.509 certificates of
precision arithmetic library) and `cacert` (a bundle of X.509 certificates of
public Certificate Authorities); and
2. provide a `shell.nix` file that gives you more control over the libraries and
tools available inside the shell.
Expand Down Expand Up @@ -430,6 +432,12 @@ nix:
# is already present and not empty.
shell-file: shell.nix

# A list of strings, empty by default. Additional options that will be passed
# verbatim to the `nix-instantiate` command.
#
# Since Stack UNRELEASED
nix-instantiate-options: []

# A list of strings, empty by default. Additional options that will be passed
# verbatim to the `nix-shell` command.
nix-shell-options: []
Expand Down
10 changes: 6 additions & 4 deletions src/Stack/Config/Nix.hs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ nixOptsFromMonoid nixMonoid os = do
pureShell = fromFirst defaultPure nixMonoid.pureShell
packages = fromFirst [] nixMonoid.packages
initFile = getFirst nixMonoid.initFile
shellOptions =
fromFirst [] nixMonoid.shellOptions
++ prefixAll (T.pack "-I") (fromFirst [] nixMonoid.path)
addGCRoots = fromFirstFalse nixMonoid.addGCRoots
includePaths = prefixAll (T.pack "-I") (fromFirst [] nixMonoid.path)
instantiateOptions =
fromFirst [] nixMonoid.instantiateOptions ++ includePaths
shellOptions = fromFirst [] nixMonoid.shellOptions ++ includePaths
addGCRoots = fromFirstFalse nixMonoid.addGCRoots

-- Enable Nix-mode by default on NixOS, unless Docker-mode was specified
osIsNixOS <- isNixOS
Expand All @@ -108,6 +109,7 @@ nixOptsFromMonoid nixMonoid os = do
, pureShell
, packages
, initFile
, instantiateOptions
, shellOptions
, addGCRoots
}
Expand Down
114 changes: 104 additions & 10 deletions src/Stack/Nix.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ module Stack.Nix
) where

import qualified Data.Text as T
import qualified Data.ByteString.Lazy.Char8 as S8
import Path ( parseAbsFile )
import Path.IO ( resolveFile )
import RIO.Process ( exec, processContextL )
import RIO.Process
( HasProcessContext (..), exec, proc, processContextL
, readProcess
)
import Stack.Config ( getInContainer, withBuildConfig )
import Stack.Config.Nix ( nixCompiler, nixCompilerVersion )
import Stack.Constants
Expand All @@ -41,13 +46,58 @@ import qualified System.FilePath as F
data NixPrettyException
= CannotDetermineProjectRoot
-- ^ Can't determine the project root (location of the shell file if any).
| NixInstantiateCommandFailure ![String] !S8.ByteString
| NixInstantiateCommandNoDerivationPath ![String]
| NixInstantiateCommandNoSingleDerivationPath ![String] ![S8.ByteString]
| NixInstantiateCommandInvalidDerivationPath ![String] !String
deriving Show

instance Pretty NixPrettyException where
pretty CannotDetermineProjectRoot =
"[S-7384]"
<> line
<> flow "Cannot determine project root directory."
pretty (NixInstantiateCommandFailure args e) =
"[S-1264]"
<> line
<> whileUsingNixInstantiate args
<> flow "Stack encountered the following error:"
<> blankLine
<> string (T.unpack . textDisplay . displayBytesUtf8 $ S8.toStrict e)
pretty (NixInstantiateCommandNoDerivationPath args) =
"[S-3220]"
<> line
<> whileUsingNixInstantiate args
<> flow "the command succeeded but did not output a path to a store \
\derivation."
pretty (NixInstantiateCommandNoSingleDerivationPath args drvPaths) =
"[S-5924]"
<> line
<> whileUsingNixInstantiate args
<> flow "the command succeeded but expected a single path to a store \
\derivation and output was:"
<> line
<> bulletedList (map (style File . string . S8.unpack) drvPaths)
<> line
pretty (NixInstantiateCommandInvalidDerivationPath args drvPath) =
"[S-1537]"
<> line
<> whileUsingNixInstantiate args
<> flow "the command succeeded but expected a valid path to a store \
\derivation and output was:"
<> line
<> style Error (string drvPath)

whileUsingNixInstantiate :: [String] -> StyleDoc
whileUsingNixInstantiate args =
fillSep
[ flow "While using"
, style Shell "nix-instantiate" <> ","
, flow "with arguments:"
]
<> line
<> bulletedList (map (style Shell . string) args)
<> blankLine

instance Exception NixPrettyException

Expand Down Expand Up @@ -128,24 +178,31 @@ runShellAndExit = do
, "} \"\""
]
]

fullArgs = concat
[ [ "--pure" | pureShell ]
, if addGCRoots
instantiateArgs = concat
[ if addGCRoots
then [ "--indirect"
-- From Nix 2.4, the --indirect flag is a no op and
-- --add-root always generates indirect GC roots.
, "--add-root"
, toFilePath
config.workDir
F.</> "nix-gc-symlinks"
F.</> "gc-root"
]
else []
, map T.unpack config.nix.shellOptions
, nixopts
, ["--run", unwords (cmnd:"$STACK_IN_NIX_EXTRA_ARGS":args')]
-- Using --run instead of --command so we cannot end up in the
-- nix-shell if stack build is Ctrl-C'd
, map T.unpack config.nix.instantiateOptions
, [ "--show-trace" ]
]
shellArgs = concat
[ [ "--pure" | pureShell ]
, map T.unpack config.nix.shellOptions
, [ "--run"
-- Using --run instead of --command so we cannot end up in the
-- nix-shell if stack build is Ctrl-C'd
, unwords (cmnd : "$STACK_IN_NIX_EXTRA_ARGS" : args')
]
]
pathVar <- liftIO $ lookupEnv "PATH"
logDebug $ "PATH is: " <> displayShow pathVar
logDebug $
Expand All @@ -158,7 +215,44 @@ runShellAndExit = do
"with nix packages: "
<> display (T.intercalate ", " pkgs)
)
exec "nix-shell" fullArgs
runNixShellExec instantiateArgs shellArgs

runNixShellExec
:: (HasProcessContext env, HasLogFunc env)
=> [String]
-- ^ nix-instantiate args
-> [String]
-- ^ nix-shell args
-> RIO env a
runNixShellExec instantiateArgs shellArgs = do
-- The nix-instantiate command instantiates (produces) a store derivation
-- from a Nix expression and prints the path of the store derivation on
-- standard output:
(ec, out, err) <- proc "nix-instantiate" instantiateArgs readProcess
case ec of
ExitFailure _ ->
-- As of Nix 2.34, there appears to be no way to capture coloured output
prettyThrowIO (NixInstantiateCommandFailure instantiateArgs err)
ExitSuccess -> do
drvPath <- case S8.lines out of
[] -> prettyThrowIO $
NixInstantiateCommandNoDerivationPath instantiateArgs
-- nix-instantiate prints the .drv path
[drvPath'] ->
let drvPath'' = S8.unpack drvPath'
in if isJust $ parseAbsFile drvPath''
then
pure drvPath''
else
prettyThrowIO $
NixInstantiateCommandInvalidDerivationPath
instantiateArgs
drvPath''
drvPaths -> prettyThrowIO $
NixInstantiateCommandNoSingleDerivationPath
instantiateArgs
drvPaths
exec "nix-shell" (drvPath : shellArgs)

-- | Shell-escape quotes inside the string and enclose it in quotes.
escape :: String -> String
Expand Down
6 changes: 6 additions & 0 deletions src/Stack/Options/NixParser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ nixOptsParser hide0 = overrideActivation <$>
\users)."
<> hide
))
<*> optionalFirst (textArgsOption
( long "nix-instantiate-options"
<> metavar "OPTIONS"
<> help "Additional options passed to nix-instantiate."
<> hide
))
<*> optionalFirst (textArgsOption
( long "nix-shell-options"
<> metavar "OPTIONS"
Expand Down
11 changes: 11 additions & 0 deletions src/Stack/Types/Nix.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Stack.Types.Nix
, nixPackagesArgName
, nixPathArgName
, nixPureShellArgName
, nixInstantiateOptsArgName
, nixShellOptsArgName
) where

Expand All @@ -39,6 +40,8 @@ data NixOpts = NixOpts
, initFile :: !(Maybe FilePath)
-- ^ The path of a file containing preconfiguration of the environment
-- (e.g shell.nix)
, instantiateOptions :: ![Text]
-- ^ Options to be given to the nix-instantiate command line
, shellOptions :: ![Text]
-- ^ Options to be given to the nix-shell command line
, addGCRoots :: !Bool
Expand All @@ -59,6 +62,8 @@ data NixOptsMonoid = NixOptsMonoid
, initFile :: !(First FilePath)
-- ^ The path of a file containing preconfiguration of the environment (e.g
-- shell.nix)
, instantiateOptions :: !(First [Text])
-- ^ Options to be given to the nix-instantiate command line
, shellOptions :: !(First [Text])
-- ^ Options to be given to the nix-shell command line
, path :: !(First [Text])
Expand All @@ -76,6 +81,7 @@ instance FromJSON (WithJSONWarnings NixOptsMonoid) where
pureShell <- First <$> o ..:? nixPureShellArgName
packages <- First <$> o ..:? nixPackagesArgName
initFile <- First <$> o ..:? nixInitFileArgName
instantiateOptions <- First <$> o ..:? nixInstantiateOptsArgName
shellOptions <- First <$> o ..:? nixShellOptsArgName
path <- First <$> o ..:? nixPathArgName
addGCRoots <- FirstFalse <$> o ..:? nixAddGCRootsArgName
Expand All @@ -84,6 +90,7 @@ instance FromJSON (WithJSONWarnings NixOptsMonoid) where
, pureShell
, packages
, initFile
, instantiateOptions
, shellOptions
, path
, addGCRoots
Expand Down Expand Up @@ -114,6 +121,10 @@ nixPackagesArgName = "packages"
nixInitFileArgName :: Text
nixInitFileArgName = "shell-file"

-- | Extra options for the nix-instantiate command argument name.
nixInstantiateOptsArgName :: Text
nixInstantiateOptsArgName = "nix-instantiate-options"

-- | Extra options for the nix-shell command argument name.
nixShellOptsArgName :: Text
nixShellOptsArgName = "nix-shell-options"
Expand Down
Loading