diff --git a/ChangeLog.md b/ChangeLog.md index 4d22828373..7afe0af7f6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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: diff --git a/doc/maintainers/stack_errors.md b/doc/maintainers/stack_errors.md index ac3086639c..5210b05d24 100644 --- a/doc/maintainers/stack_errors.md +++ b/doc/maintainers/stack_errors.md @@ -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`. @@ -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` diff --git a/doc/topics/nix_integration.md b/doc/topics/nix_integration.md index 7720033893..019d7d82ba 100644 --- a/doc/topics/nix_integration.md +++ b/doc/topics/nix_integration.md @@ -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. @@ -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: [] diff --git a/src/Stack/Config/Nix.hs b/src/Stack/Config/Nix.hs index 90a64d35f7..4830f4fb20 100644 --- a/src/Stack/Config/Nix.hs +++ b/src/Stack/Config/Nix.hs @@ -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 @@ -108,6 +109,7 @@ nixOptsFromMonoid nixMonoid os = do , pureShell , packages , initFile + , instantiateOptions , shellOptions , addGCRoots } diff --git a/src/Stack/Nix.hs b/src/Stack/Nix.hs index 666280052e..42e15170a2 100644 --- a/src/Stack/Nix.hs +++ b/src/Stack/Nix.hs @@ -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 @@ -41,6 +46,10 @@ 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 @@ -48,6 +57,47 @@ instance Pretty NixPrettyException where "[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 @@ -128,11 +178,11 @@ 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 @@ -140,12 +190,19 @@ runShellAndExit = do 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 $ @@ -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 diff --git a/src/Stack/Options/NixParser.hs b/src/Stack/Options/NixParser.hs index 8dc990cf0a..28620e34fa 100644 --- a/src/Stack/Options/NixParser.hs +++ b/src/Stack/Options/NixParser.hs @@ -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" diff --git a/src/Stack/Types/Nix.hs b/src/Stack/Types/Nix.hs index 4fcc61a968..8be0b687ea 100644 --- a/src/Stack/Types/Nix.hs +++ b/src/Stack/Types/Nix.hs @@ -20,6 +20,7 @@ module Stack.Types.Nix , nixPackagesArgName , nixPathArgName , nixPureShellArgName + , nixInstantiateOptsArgName , nixShellOptsArgName ) where @@ -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 @@ -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]) @@ -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 @@ -84,6 +90,7 @@ instance FromJSON (WithJSONWarnings NixOptsMonoid) where , pureShell , packages , initFile + , instantiateOptions , shellOptions , path , addGCRoots @@ -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"