From 73602a7c6f4dc6f4f4bea8368a8564403b7b5604 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 6 Jul 2024 21:31:24 +0200 Subject: [PATCH 1/5] nix-shell: Look for shell.nix when directory is specified --- src/libexpr/eval.cc | 4 ++-- src/libexpr/eval.hh | 4 +++- src/nix-build/nix-build.cc | 34 ++++++++++++++++++++++----- tests/functional/nix-shell.sh | 44 +++++++++++++++++++++++++++++++++++ tests/functional/shell.nix | 11 +++++++++ 5 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 48ed66883..2a0862123 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2650,7 +2650,7 @@ void EvalState::printStatistics() } -SourcePath resolveExprPath(SourcePath path) +SourcePath resolveExprPath(SourcePath path, bool addDefaultNix) { unsigned int followCount = 0, maxFollow = 1024; @@ -2666,7 +2666,7 @@ SourcePath resolveExprPath(SourcePath path) } /* If `path' refers to a directory, append `/default.nix'. */ - if (path.resolveSymlinks().lstat().type == SourceAccessor::tDirectory) + if (addDefaultNix && path.resolveSymlinks().lstat().type == SourceAccessor::tDirectory) return path / "default.nix"; return path; diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index b84bc9907..e45358055 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -850,8 +850,10 @@ std::string showType(const Value & v); /** * If `path` refers to a directory, then append "/default.nix". + * + * @param addDefaultNix Whether to append "/default.nix" after resolving symlinks. */ -SourcePath resolveExprPath(SourcePath path); +SourcePath resolveExprPath(SourcePath path, bool addDefaultNix = true); /** * Whether a URI is allowed, assuming restrictEval is enabled diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index e873f712b..30cc86456 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -90,6 +90,26 @@ static std::vector shellwords(const std::string & s) return res; } +/** + * Like `resolveExprPath`, but prefers `shell.nix` instead of `default.nix`, + * and if `path` was a directory, it checks eagerly whether `shell.nix` or + * `default.nix` exist, throwing an error if they don't. + */ +static SourcePath resolveShellExprPath(SourcePath path) +{ + auto resolvedOrDir = resolveExprPath(path, false); + if (resolvedOrDir.resolveSymlinks().lstat().type == SourceAccessor::tDirectory) { + if ((resolvedOrDir / "shell.nix").pathExists()) { + return resolvedOrDir / "shell.nix"; + } + if ((resolvedOrDir / "default.nix").pathExists()) { + return resolvedOrDir / "default.nix"; + } + throw Error("neither '%s' nor '%s' found in '%s'", "shell.nix", "default.nix", resolvedOrDir); + } + return resolvedOrDir; +} + static void main_nix_build(int argc, char * * argv) { auto dryRun = false; @@ -281,11 +301,12 @@ static void main_nix_build(int argc, char * * argv) joined << "]; } \"\""; fromArgs = true; remainingArgs = {joined.str()}; - } else if (!fromArgs) { - if (remainingArgs.empty() && isNixShell && pathExists("shell.nix")) - remainingArgs = {"shell.nix"}; - if (remainingArgs.empty()) - remainingArgs = {"default.nix"}; + } else if (!fromArgs && remainingArgs.empty()) { + remainingArgs = {"."}; + + // Instead of letting it throw later, we throw here to give a more relevant error message + if (isNixShell && !std::filesystem::exists("shell.nix") && !std::filesystem::exists("default.nix")) + throw Error("no argument specified and no '%s' or '%s' file found in the working directory", "shell.nix", "default.nix"); } if (isNixShell) @@ -317,7 +338,8 @@ static void main_nix_build(int argc, char * * argv) auto sourcePath = lookupFileArg(*state, baseDir); - auto resolvedPath = resolveExprPath(sourcePath); + auto resolvedPath = + isNixShell ? resolveShellExprPath(sourcePath) : resolveExprPath(sourcePath); exprs.push_back(state->parseExprFromFile(resolvedPath)); } diff --git a/tests/functional/nix-shell.sh b/tests/functional/nix-shell.sh index 2c94705de..f54e3621c 100755 --- a/tests/functional/nix-shell.sh +++ b/tests/functional/nix-shell.sh @@ -91,6 +91,50 @@ sed -e "s|@ENV_PROG@|$(type -P env)|" shell.shebang.nix > $TEST_ROOT/shell.sheba chmod a+rx $TEST_ROOT/shell.shebang.nix $TEST_ROOT/shell.shebang.nix +mkdir $TEST_ROOT/lookup-test $TEST_ROOT/empty + +echo "import $shellDotNix" > $TEST_ROOT/lookup-test/shell.nix +cp config.nix $TEST_ROOT/lookup-test/ +echo 'abort "do not load default.nix!"' > $TEST_ROOT/lookup-test/default.nix + +nix-shell $TEST_ROOT/lookup-test -A shellDrv --run 'echo "it works"' | grepQuiet "it works" +# https://github.com/NixOS/nix/issues/4529 +nix-shell -I "testRoot=$TEST_ROOT" '' -A shellDrv --run 'echo "it works"' | grepQuiet "it works" + +( + cd $TEST_ROOT/empty; + expectStderr 1 nix-shell | \ + grepQuiet "error.*no argument specified and no .*shell\.nix.* or .*default\.nix.* file found in the working directory" +) + +expectStderr 1 nix-shell -I "testRoot=$TEST_ROOT" '' | + grepQuiet "error.*neither .*shell\.nix.* nor .*default\.nix.* found in .*/empty" + +cat >$TEST_ROOT/lookup-test/shebangscript < $TEST_ROOT/marco/shell.nix +cat >$TEST_ROOT/marco/polo/default.nix < Date: Sat, 6 Jul 2024 22:58:15 +0200 Subject: [PATCH 2/5] Add legacy setting: nix-shell-always-looks-for-shell-nix --- src/libcmd/common-eval-args.cc | 7 +++++++ src/libcmd/common-eval-args.hh | 6 ++++++ src/libcmd/compatibility-settings.hh | 19 +++++++++++++++++++ src/libcmd/meson.build | 1 + src/nix-build/nix-build.cc | 23 ++++++++++++++++++----- tests/functional/nix-shell.sh | 9 +++++++++ 6 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/libcmd/compatibility-settings.hh diff --git a/src/libcmd/common-eval-args.cc b/src/libcmd/common-eval-args.cc index 01546f9a0..62745b681 100644 --- a/src/libcmd/common-eval-args.cc +++ b/src/libcmd/common-eval-args.cc @@ -11,6 +11,8 @@ #include "command.hh" #include "tarball.hh" #include "fetch-to-store.hh" +#include "compatibility-settings.hh" +#include "eval-settings.hh" namespace nix { @@ -33,6 +35,11 @@ EvalSettings evalSettings { static GlobalConfig::Register rEvalSettings(&evalSettings); +CompatibilitySettings compatibilitySettings {}; + +static GlobalConfig::Register rCompatibilitySettings(&compatibilitySettings); + + MixEvalArgs::MixEvalArgs() { addFlag({ diff --git a/src/libcmd/common-eval-args.hh b/src/libcmd/common-eval-args.hh index 189abf0ed..8d303ee7c 100644 --- a/src/libcmd/common-eval-args.hh +++ b/src/libcmd/common-eval-args.hh @@ -13,6 +13,7 @@ namespace nix { class Store; class EvalState; struct EvalSettings; +struct CompatibilitySettings; class Bindings; struct SourcePath; @@ -21,6 +22,11 @@ struct SourcePath; */ extern EvalSettings evalSettings; +/** + * Settings that control behaviors that have changed since Nix 2.3. + */ +extern CompatibilitySettings compatibilitySettings; + struct MixEvalArgs : virtual Args, virtual MixRepair { static constexpr auto category = "Common evaluation options"; diff --git a/src/libcmd/compatibility-settings.hh b/src/libcmd/compatibility-settings.hh new file mode 100644 index 000000000..5dc0eaf2b --- /dev/null +++ b/src/libcmd/compatibility-settings.hh @@ -0,0 +1,19 @@ +#pragma once +#include "config.hh" + +namespace nix { +struct CompatibilitySettings : public Config +{ + + CompatibilitySettings() = default; + + Setting nixShellAlwaysLooksForShellNix{this, true, "nix-shell-always-looks-for-shell-nix", R"( + Before Nix 2.24, [`nix-shell`](@docroot@/command-ref/nix-shell.md) would only look at `shell.nix` if it was in the working directory - when no file was specified. + + Since Nix 2.24, `nix-shell` always looks for a `shell.nix`, whether that's in the working directory, or in a directory that was passed as an argument. + + You may set this to `false` to revert to the Nix 2.3 behavior. + )"}; +}; + +}; diff --git a/src/libcmd/meson.build b/src/libcmd/meson.build index d9a90508a..2c8a9fa33 100644 --- a/src/libcmd/meson.build +++ b/src/libcmd/meson.build @@ -97,6 +97,7 @@ headers = [config_h] + files( 'command-installable-value.hh', 'command.hh', 'common-eval-args.hh', + 'compatibility-settings.hh', 'editor-for.hh', 'installable-attr-path.hh', 'installable-derived-path.hh', diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index 30cc86456..d37b16bdc 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -26,6 +26,7 @@ #include "legacy.hh" #include "users.hh" #include "network-proxy.hh" +#include "compatibility-settings.hh" using namespace nix; using namespace std::string_literals; @@ -100,7 +101,13 @@ static SourcePath resolveShellExprPath(SourcePath path) auto resolvedOrDir = resolveExprPath(path, false); if (resolvedOrDir.resolveSymlinks().lstat().type == SourceAccessor::tDirectory) { if ((resolvedOrDir / "shell.nix").pathExists()) { - return resolvedOrDir / "shell.nix"; + if (compatibilitySettings.nixShellAlwaysLooksForShellNix) { + return resolvedOrDir / "shell.nix"; + } else { + warn("Skipping '%1%', because the setting '%2%' is disabled. This is a deprecated behavior. Consider enabling '%2%'.", + resolvedOrDir / "shell.nix", + "nix-shell-always-looks-for-shell-nix"); + } } if ((resolvedOrDir / "default.nix").pathExists()) { return resolvedOrDir / "default.nix"; @@ -302,11 +309,17 @@ static void main_nix_build(int argc, char * * argv) fromArgs = true; remainingArgs = {joined.str()}; } else if (!fromArgs && remainingArgs.empty()) { - remainingArgs = {"."}; + if (isNixShell && !compatibilitySettings.nixShellAlwaysLooksForShellNix && std::filesystem::exists("shell.nix")) { + // If we're in 2.3 compatibility mode, we need to look for shell.nix + // now, because it won't be done later. + remainingArgs = {"shell.nix"}; + } else { + remainingArgs = {"."}; - // Instead of letting it throw later, we throw here to give a more relevant error message - if (isNixShell && !std::filesystem::exists("shell.nix") && !std::filesystem::exists("default.nix")) - throw Error("no argument specified and no '%s' or '%s' file found in the working directory", "shell.nix", "default.nix"); + // Instead of letting it throw later, we throw here to give a more relevant error message + if (isNixShell && !std::filesystem::exists("shell.nix") && !std::filesystem::exists("default.nix")) + throw Error("no argument specified and no '%s' or '%s' file found in the working directory", "shell.nix", "default.nix"); + } } if (isNixShell) diff --git a/tests/functional/nix-shell.sh b/tests/functional/nix-shell.sh index f54e3621c..65ff279f8 100755 --- a/tests/functional/nix-shell.sh +++ b/tests/functional/nix-shell.sh @@ -21,6 +21,10 @@ output=$(nix-shell --pure "$shellDotNix" -A shellDrv --run \ [ "$output" = " - foo - bar - true" ] +output=$(nix-shell --pure "$shellDotNix" -A shellDrv --option nix-shell-always-looks-for-shell-nix false --run \ + 'echo "$IMPURE_VAR - $VAR_FROM_STDENV_SETUP - $VAR_FROM_NIX - $TEST_inNixShell"') +[ "$output" = " - foo - bar - true" ] + # Test --keep output=$(nix-shell --pure --keep SELECTED_IMPURE_VAR "$shellDotNix" -A shellDrv --run \ 'echo "$IMPURE_VAR - $VAR_FROM_STDENV_SETUP - $VAR_FROM_NIX - $SELECTED_IMPURE_VAR"') @@ -101,6 +105,11 @@ nix-shell $TEST_ROOT/lookup-test -A shellDrv --run 'echo "it works"' | grepQuiet # https://github.com/NixOS/nix/issues/4529 nix-shell -I "testRoot=$TEST_ROOT" '' -A shellDrv --run 'echo "it works"' | grepQuiet "it works" +expectStderr 1 nix-shell $TEST_ROOT/lookup-test -A shellDrv --run 'echo "it works"' --option nix-shell-always-looks-for-shell-nix false \ + | grepQuiet -F "do not load default.nix!" # we did, because we chose to enable legacy behavior +expectStderr 1 nix-shell $TEST_ROOT/lookup-test -A shellDrv --run 'echo "it works"' --option nix-shell-always-looks-for-shell-nix false \ + | grepQuiet "Skipping .*lookup-test/shell\.nix.*, because the setting .*nix-shell-always-looks-for-shell-nix.* is disabled. This is a deprecated behavior\. Consider enabling .*nix-shell-always-looks-for-shell-nix.*" + ( cd $TEST_ROOT/empty; expectStderr 1 nix-shell | \ From c4a20a41019ef3cd806059102cbe1d45fcbdd2b8 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 6 Jul 2024 23:02:32 +0200 Subject: [PATCH 3/5] rl-next: Add note about shell.nix lookups --- .../rl-next/nix-shell-looks-for-shell-nix.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 doc/manual/rl-next/nix-shell-looks-for-shell-nix.md diff --git a/doc/manual/rl-next/nix-shell-looks-for-shell-nix.md b/doc/manual/rl-next/nix-shell-looks-for-shell-nix.md new file mode 100644 index 000000000..99be4148b --- /dev/null +++ b/doc/manual/rl-next/nix-shell-looks-for-shell-nix.md @@ -0,0 +1,28 @@ +--- +synopsis: "`nix-shell ` looks for `shell.nix`" +significance: significant +issues: +- 496 +- 2279 +- 4529 +- 5431 +- 11053 +prs: +- 11057 +--- + +`nix-shell $x` now looks for `$x/shell.nix` when `$x` resolves to a directory. + +Although this might be seen as a breaking change, its primarily interactive usage makes it a minor issue. +This adjustment addresses a commonly reported problem. + +This also applies to `nix-shell` shebang scripts. Consider the following example: + +```shell +#!/usr/bin/env nix-shell +#!nix-shell -i bash +``` + +This will now load `shell.nix` from the script's directory, if it exists; `default.nix` otherwise. + +The old behavior can be opted into by setting the option [`nix-shell-always-looks-for-shell-nix`](@docroot@/command-ref/conf-file.md#conf-nix-shell-always-looks-for-shell-nix) to `false`. From 0f8a655023be204499c6360e072b36f58f6f194c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 7 Jul 2024 13:02:21 +0200 Subject: [PATCH 4/5] tests/functional/shell.nix: Implement runHook for dummy stdenv --- tests/functional/shell.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/functional/shell.nix b/tests/functional/shell.nix index 75e3845ea..1fb00c5a3 100644 --- a/tests/functional/shell.nix +++ b/tests/functional/shell.nix @@ -26,6 +26,9 @@ let pkgs = rec { fun() { echo blabla } + runHook() { + eval "''${!1}" + } ''; stdenv = mkDerivation { From e1106b45a31228c6f5fe8be0bd5fbde08e7c3255 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 7 Jul 2024 13:03:19 +0200 Subject: [PATCH 5/5] tests/functional/nix-shell.sh: Fix Polo test for VM test It is unclear to me why this worked when not in a VM test, but the explanation would be in the part of nix-shell we're getting rid of with the devShell attribute. --- tests/functional/shell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/shell.nix b/tests/functional/shell.nix index 1fb00c5a3..750cdf0bc 100644 --- a/tests/functional/shell.nix +++ b/tests/functional/shell.nix @@ -56,6 +56,7 @@ let pkgs = rec { # See nix-shell.sh polo = mkDerivation { name = "polo"; + inherit stdenv; shellHook = '' echo Polo '';