From 17eb2e8400b22635fbad74ae3cdfbd0a51d00233 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Wed, 7 May 2025 11:24:50 -0700 Subject: [PATCH] Expose flake directory to nix fmt as PRJ_ROOT env var This was discussed in https://github.com/NixOS/nix/issues/8034. I personally like `PRJ_ROOT`, which hopefully avoids some ambiguity around with subflakes. I only implemented this for `nix fmt` because it doesn't let you point at a flake not on your filesystem. macOS compilation fixes --- src/nix/formatter-run.md | 4 +++ src/nix/formatter.cc | 21 +++++++++++++-- src/nix/run.cc | 38 ++++++++++++++++++++++++---- src/nix/run.hh | 3 ++- tests/functional/formatter.sh | 33 +++++++++++++++++++++--- tests/functional/formatter.simple.sh | 2 +- 6 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/nix/formatter-run.md b/src/nix/formatter-run.md index db8583c95..201cae92e 100644 --- a/src/nix/formatter-run.md +++ b/src/nix/formatter-run.md @@ -8,6 +8,10 @@ Flags can be forwarded to the formatter by using `--` followed by the flags. Any arguments will be forwarded to the formatter. Typically these are the files to format. +The environment variable `PRJ_ROOT` (according to [prj-spec](https://github.com/numtide/prj-spec)) +will be set to the absolute path to the directory containing the closest parent `flake.nix` +relative to the current directory. + # Example diff --git a/src/nix/formatter.cc b/src/nix/formatter.cc index 8b171b244..627fb362c 100644 --- a/src/nix/formatter.cc +++ b/src/nix/formatter.cc @@ -1,8 +1,10 @@ #include "nix/cmd/command.hh" +#include "nix/cmd/installable-flake.hh" #include "nix/cmd/installable-value.hh" #include "nix/expr/eval.hh" #include "nix/store/local-fs-store.hh" #include "nix/cmd/installable-derived-path.hh" +#include "nix/util/environment-variables.hh" #include "run.hh" using namespace nix; @@ -72,10 +74,14 @@ struct CmdFormatterRun : MixFormatter, MixJSON auto evalState = getEvalState(); auto evalStore = getEvalStore(); - auto installable_ = parseInstallable(store, "."); + auto installable_ = parseInstallable(store, ".").cast(); auto & installable = InstallableValue::require(*installable_); auto app = installable.toApp(*evalState).resolve(evalStore, store); + auto maybeFlakeDir = installable_->flakeRef.input.getSourcePath(); + assert(maybeFlakeDir.has_value()); + auto flakeDir = maybeFlakeDir.value(); + Strings programArgs{app.program}; // Propagate arguments from the CLI @@ -83,11 +89,22 @@ struct CmdFormatterRun : MixFormatter, MixJSON programArgs.push_back(i); } + // Add the path to the flake as an environment variable. This enables formatters to format the entire flake even + // if run from a subdirectory. + StringMap env = getEnv(); + env["PRJ_ROOT"] = flakeDir; + // Release our references to eval caches to ensure they are persisted to disk, because // we are about to exec out of this process without running C++ destructors. evalState->evalCaches.clear(); - execProgramInStore(store, UseLookupPath::DontUse, app.program, programArgs); + execProgramInStore( + store, + UseLookupPath::DontUse, + app.program, + programArgs, + std::nullopt, // Use default system + env); }; }; diff --git a/src/nix/run.cc b/src/nix/run.cc index 0473c99b7..3dae8ebc9 100644 --- a/src/nix/run.cc +++ b/src/nix/run.cc @@ -10,6 +10,7 @@ #include "nix/util/finally.hh" #include "nix/util/source-accessor.hh" #include "nix/expr/eval.hh" +#include "nix/util/util.hh" #include #ifdef __linux__ @@ -19,6 +20,8 @@ #include +extern char ** environ __attribute__((weak)); + namespace nix::fs { using namespace std::filesystem; } using namespace nix; @@ -27,14 +30,37 @@ std::string chrootHelperName = "__run_in_chroot"; namespace nix { +/* Convert `env` to a list of strings suitable for `execve`'s `envp` argument. */ +Strings toEnvp(StringMap env) +{ + Strings envStrs; + for (auto & i : env) { + envStrs.push_back(i.first + "=" + i.second); + } + + return envStrs; +} + void execProgramInStore(ref store, UseLookupPath useLookupPath, const std::string & program, const Strings & args, - std::optional system) + std::optional system, + std::optional env) { logger->stop(); + char **envp; + Strings envStrs; + std::vector envCharPtrs; + if (env.has_value()) { + envStrs = toEnvp(env.value()); + envCharPtrs = stringsToCharPtrs(envStrs); + envp = envCharPtrs.data(); + } else { + envp = environ; + } + restoreProcessContext(); /* If this is a diverted store (i.e. its "logical" location @@ -54,7 +80,7 @@ void execProgramInStore(ref store, Strings helperArgs = { chrootHelperName, store->storeDir, store2->getRealStoreDir(), std::string(system.value_or("")), program }; for (auto & arg : args) helperArgs.push_back(arg); - execv(getSelfExe().value_or("nix").c_str(), stringsToCharPtrs(helperArgs).data()); + execve(getSelfExe().value_or("nix").c_str(), stringsToCharPtrs(helperArgs).data(), envp); throw SysError("could not execute chroot helper"); } @@ -64,10 +90,12 @@ void execProgramInStore(ref store, linux::setPersonality(*system); #endif - if (useLookupPath == UseLookupPath::Use) + if (useLookupPath == UseLookupPath::Use) { + // We have to set `environ` by hand because there is no `execvpe` on macOS. + environ = envp; execvp(program.c_str(), stringsToCharPtrs(args).data()); - else - execv(program.c_str(), stringsToCharPtrs(args).data()); + } else + execve(program.c_str(), stringsToCharPtrs(args).data(), envp); throw SysError("unable to execute '%s'", program); } diff --git a/src/nix/run.hh b/src/nix/run.hh index 9d95b8e7c..5367c515c 100644 --- a/src/nix/run.hh +++ b/src/nix/run.hh @@ -14,6 +14,7 @@ void execProgramInStore(ref store, UseLookupPath useLookupPath, const std::string & program, const Strings & args, - std::optional system = std::nullopt); + std::optional system = std::nullopt, + std::optional env = std::nullopt); } diff --git a/tests/functional/formatter.sh b/tests/functional/formatter.sh index ea6f9e1ce..6631dd6b8 100755 --- a/tests/functional/formatter.sh +++ b/tests/functional/formatter.sh @@ -34,13 +34,38 @@ cat << EOF > flake.nix } EOF +mkdir subflake +cp ./simple.nix ./simple.builder.sh ./formatter.simple.sh "${config_nix}" "$TEST_HOME/subflake" + +cat << EOF > subflake/flake.nix +{ + outputs = _: { + formatter.$system = + with import ./config.nix; + mkDerivation { + name = "formatter"; + buildCommand = '' + mkdir -p \$out/bin + echo "#! ${shell}" > \$out/bin/formatter + cat \${./formatter.simple.sh} >> \$out/bin/formatter + chmod +x \$out/bin/formatter + ''; + }; + }; +} +EOF + # No arguments check -[[ "$(nix fmt)" = "Formatting(0):" ]] -[[ "$(nix formatter run)" = "Formatting(0):" ]] +[[ "$(nix fmt)" = "PRJ_ROOT=$TEST_HOME Formatting(0):" ]] +[[ "$(nix formatter run)" = "PRJ_ROOT=$TEST_HOME Formatting(0):" ]] # Argument forwarding check -nix fmt ./file ./folder | grep 'Formatting(2): ./file ./folder' -nix formatter run ./file ./folder | grep 'Formatting(2): ./file ./folder' +nix fmt ./file ./folder | grep "PRJ_ROOT=$TEST_HOME Formatting(2): ./file ./folder" +nix formatter run ./file ./folder | grep "PRJ_ROOT=$TEST_HOME Formatting(2): ./file ./folder" + +# test subflake +cd subflake +nix fmt ./file | grep "PRJ_ROOT=$TEST_HOME/subflake Formatting(1): ./file" # Build checks ## Defaults to a ./result. diff --git a/tests/functional/formatter.simple.sh b/tests/functional/formatter.simple.sh index e53f6c9be..355ff00ef 100755 --- a/tests/functional/formatter.simple.sh +++ b/tests/functional/formatter.simple.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -echo "Formatting(${#}):" "${@}" +echo "PRJ_ROOT=$PRJ_ROOT Formatting(${#}):" "${@}"