feat: Add temp-dir setting

This adds a new temp-dir setting for controlling the temporary directory
without having to change the TMPDIR env var. This can be used to e.g.
use a path on a case-sensitive store on macOS for temporary files
without changing the TMPDIR var used by interactive shells or commands
invoked with `nix run`.

This also stops unsetting `TMPDIR` on darwin when the env var value
starts with `/var/folders/`, preferring instead to just do the check
when reading `TMPDIR`. This way the inherited `TMPDIR` env var is
preserved for child processes (such as interactive shells).

As a side effect this changes the behavior of `nix-build -o ''` to act
like `nix-build --no-out-link` instead of failing with an error caused
by trying to create a symlink at the cwd.

Fixes: #253
Fixes: #112
Change-Id: I9ee826323f2deca62854715a77ca7a373a948a29
This commit is contained in:
Lily Ballard 2024-10-21 00:01:44 -07:00
parent 72cce7be3f
commit 3c8096e5cb
24 changed files with 182 additions and 47 deletions

View file

@ -0,0 +1,13 @@
---
synopsis: Change `nix-build -o ""` to behave like `--no-out-link`
cls: [2103]
category: Fixes
credits: lilyball
---
[`nix-build`](@docroot@/command-ref/nix-build.md)now treats <code>[--out-link](@docroot@/command-ref/nix-build.md#opt-out-link) ''</code>
the same as [`--no-out-link`](@docroot@/command-ref/nix-build.md#opt-no-out-link). This matches
[`nix build`](@docroot@/command-ref/new-cli/nix3-build.md) behavior. Previously when building the default output it
would have resulted in throwing an error saying the current working directory already exists, and when building any
other output it would have resulted in a symlink starting with a hyphen such as `-doc`, which is a footgun for
terminal commands.

View file

@ -0,0 +1,15 @@
---
synopsis: Add a `temp-dir` setting to set the temporary directory location
issues: [7731, 8995, fj#112, fj#253]
cls: [2103]
category: Improvements
credits: lilyball
---
[`temp-dir`](@docroot@/command-ref/conf-file.md#conf-temp-dir) can now be set in the Nix
configuration to change the temporary directory. This can be used to relocate all temporary files
to another filesystem without affecting the `TMPDIR` env var inherited by interactive
`nix-shell`/`nix shell` shells or `nix run` commands.
Also on macOS, the `TMPDIR` env var is no longer unset for interactive shells when pointing
to a per-session `/var/folders/` directory.

View file

@ -25,6 +25,7 @@
#include "legacy.hh"
#include "shlex.hh"
#include "nix-build.hh"
#include "temporary-dir.hh"
extern char * * environ __attribute__((weak)); // Man what even is this
@ -55,8 +56,6 @@ static void main_nix_build(int argc, char * * argv)
std::string script;
std::vector<std::string> savedArgs;
AutoDelete tmpDir(createTempDir("", myName));
std::string outLink = "./result";
// List of environment variables kept for --pure
@ -102,7 +101,6 @@ static void main_nix_build(int argc, char * * argv)
MyArgs myArgs(myName, [&](Strings::iterator & arg, const Strings::iterator & end) {
if (*arg == "--help") {
deletePath(tmpDir);
showManPage(myName);
}
@ -113,7 +111,7 @@ static void main_nix_build(int argc, char * * argv)
; // obsolete
else if (*arg == "--no-out-link" || *arg == "--no-link")
outLink = (Path) tmpDir + "/result";
outLink = "";
else if (*arg == "--attr" || *arg == "-A")
attrPaths.push_back(getArg(*arg, arg, end));
@ -198,6 +196,10 @@ static void main_nix_build(int argc, char * * argv)
if (packages && fromArgs)
throw UsageError("'-p' and '-E' are mutually exclusive");
AutoDelete tmpDir(createTempDir("", myName));
if (outLink.empty())
outLink = (Path) tmpDir + "/result";
auto store = openStore();
auto evalStore = myArgs.evalStoreUrl ? openStore(*myArgs.evalStoreUrl) : store;
@ -416,8 +418,6 @@ static void main_nix_build(int argc, char * * argv)
// Set the environment.
auto env = getEnv();
auto tmp = defaultTempDir();
if (pure) {
decltype(env) newEnv;
for (auto & i : env)
@ -428,7 +428,8 @@ static void main_nix_build(int argc, char * * argv)
env["__ETC_PROFILE_SOURCED"] = "1";
}
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmp;
// Don't use defaultTempDir() here! We want to preserve the user's TMPDIR for the shell
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = getEnvNonEmpty("TMPDIR").value_or("/tmp");
env["NIX_STORE"] = store->storeDir;
env["NIX_BUILD_CORES"] = std::to_string(settings.buildCores);

View file

@ -6,6 +6,7 @@
#include "legacy.hh"
#include "fetchers.hh"
#include "eval-settings.hh" // for defexpr
#include "temporary-dir.hh"
#include "users.hh"
#include "nix-channel.hh"

View file

@ -6,6 +6,7 @@
#include "logging.hh"
#include "names.hh"
#include "store-api.hh"
#include "temporary-dir.hh"
#include "url-name.hh"
namespace nix

View file

@ -5,6 +5,7 @@
#include "processes.hh"
#include "tarfile.hh"
#include "store-api.hh"
#include "temporary-dir.hh"
#include "url-parts.hh"
#include "pathlocks.hh"
#include "users.hh"

View file

@ -2,6 +2,7 @@
#include "cache.hh"
#include "processes.hh"
#include "store-api.hh"
#include "temporary-dir.hh"
#include "url-parts.hh"
#include "users.hh"

View file

@ -5,6 +5,7 @@
#include "store-api.hh"
#include "archive.hh"
#include "tarfile.hh"
#include "temporary-dir.hh"
#include "types.hh"
#include "split.hh"

View file

@ -8,6 +8,7 @@
#include "remote-fs-accessor.hh"
#include "nar-info-disk-cache.hh" // IWYU pragma: keep
#include "nar-accessor.hh"
#include "temporary-dir.hh"
#include "thread-pool.hh"
#include "signals.hh"
#include "strings.hh"

View file

@ -3,6 +3,7 @@
#include "indirect-root-store.hh"
#include "machines.hh"
#include "store-api.hh"
#include "temporary-dir.hh"
#include "worker.hh"
#include "builtins.hh"
#include "builtins/buildenv.hh"

View file

@ -468,11 +468,6 @@ void initLibStore() {
[1] https://github.com/apple-oss-distributions/objc4/blob/01edf1705fbc3ff78a423cd21e03dfc21eb4d780/runtime/objc-initialize.mm#L614-L636
*/
curl_global_init(CURL_GLOBAL_ALL);
/* On macOS, don't use the per-session TMPDIR (as set e.g. by
sshd). This breaks build users because they don't have access
to the TMPDIR, in particular in nix-store --serve. */
if (defaultTempDir().starts_with("/var/folders/"))
unsetenv("TMPDIR");
#endif
registerStoreImplementations();

View file

@ -615,11 +615,11 @@ public:
)"};
#endif
Setting<std::optional<Path>> buildDir{this, std::nullopt, "build-dir",
PathsSetting<std::optional<Path>> buildDir{this, std::nullopt, "build-dir",
R"(
The directory on the host, in which derivations' temporary build directories are created.
If not set, Nix will use the system temporary directory indicated by the `TMPDIR` environment variable.
If not set, Nix will use the [`temp-dir`](#conf-temp-dir) setting if set, otherwise the system temporary directory indicated by the `TMPDIR` environment variable.
Note that builds are often performed by the Nix daemon, so its `TMPDIR` is used, and not that of the Nix command line interface.
This is also the location where [`--keep-failed`](@docroot@/command-ref/opt-common.md#opt-keep-failed) leaves its files.
@ -627,6 +627,18 @@ public:
If Nix runs without sandbox, or if the platform does not support sandboxing with bind mounts (e.g. macOS), then the [`builder`](@docroot@/language/derivations.md#attr-builder)'s environment will contain this directory, instead of the virtual location [`sandbox-build-dir`](#conf-sandbox-build-dir).
)"};
PathsSetting<std::optional<Path>> tempDir{this, std::nullopt, "temp-dir",
R"(
The directory on the host used as the default temporary directory.
If not set, Nix will use the system temporary directory indicated by the `TMPDIR` environment variable.
This will be used for anything that would otherwise fall back to `TMPDIR`, and the inherited `TMPDIR` value will be preserved for child processes to use.
If [`build-dir`](#conf-build-dir) is set, that takes precedence over this where it applies.
If set, the value must be a path that exists and is accessible to all users.
)"};
Setting<PathSet> allowedImpureHostPrefixes{this, {}, "allowed-impure-host-deps",
"Which prefixes to allow derivations to ask for access to (primarily for Darwin)."};

View file

@ -2,6 +2,7 @@
#include "globals.hh"
#include "archive.hh"
#include "pathlocks.hh"
#include "temporary-dir.hh"
#include "worker-protocol.hh"
#include "derivations.hh"
#include "nar-info.hh"

View file

@ -77,6 +77,7 @@ libstore_sources = files(
'ssh-store.cc',
'ssh.cc',
'store-api.cc',
'temporary-dir.cc',
'uds-remote-store.cc',
'worker-protocol.cc',
'build/child.cc',
@ -160,6 +161,7 @@ libstore_headers = files(
'ssh-store.hh',
'store-api.hh',
'store-cast.hh',
'temporary-dir.hh',
'uds-remote-store.hh',
'worker-protocol-impl.hh',
'worker-protocol.hh',

View file

@ -4,6 +4,7 @@
#include "finally.hh"
#include "logging.hh"
#include "strings.hh"
#include "temporary-dir.hh"
namespace nix {

View file

@ -0,0 +1,42 @@
#include "temporary-dir.hh"
#include "file-system.hh"
#include "globals.hh"
namespace nix {
Path createTempDir(const Path & tmpRoot, const Path & prefix,
bool includePid, bool useGlobalCounter, mode_t mode)
{
return createTempSubdir(tmpRoot.empty() ? defaultTempDir() : tmpRoot, prefix, includePid, useGlobalCounter, mode);
}
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
{
Path tmpl(defaultTempDir() + "/" + prefix + ".XXXXXX");
// FIXME: use O_TMPFILE.
AutoCloseFD fd(mkstemp(tmpl.data()));
if (!fd)
throw SysError("creating temporary file '%s'", tmpl);
closeOnExec(fd.get());
return {std::move(fd), tmpl};
}
Path defaultTempDir()
{
return settings.tempDir.get().or_else([] {
return getEnvNonEmpty("TMPDIR").and_then([](auto val) -> std::optional<Path> {
#if __APPLE__
/* On macOS, don't use the per-session TMPDIR (as set e.g. by
sshd). This breaks build users because they don't have access
to the TMPDIR, in particular in nix-store --serve. */
if (val.starts_with("/var/folders/")) {
return std::nullopt;
}
#endif
return val;
});
}).value_or("/tmp");
}
}

View file

@ -0,0 +1,24 @@
#pragma once
///@file
#include "file-system.hh"
namespace nix {
/**
* Create a temporary directory.
*/
Path createTempDir(const Path & tmpRoot = "", const Path & prefix = "nix",
bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755);
/**
* Create a temporary file, returning a file handle and its path.
*/
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix = "nix");
/**
* Return settings.tempDir, `TMPDIR`, or the default temporary directory if unset or empty.
*/
Path defaultTempDir();
}

View file

@ -562,21 +562,17 @@ void AutoDelete::reset(const Path & p, bool recursive) {
//////////////////////////////////////////////////////////////////////
std::string defaultTempDir() {
return getEnvNonEmpty("TMPDIR").value_or("/tmp");
}
static Path tempName(Path tmpRoot, const Path & prefix, bool includePid,
static Path tempName(PathView parent, const Path & prefix, bool includePid,
std::atomic<unsigned int> & counter)
{
tmpRoot = canonPath(tmpRoot.empty() ? defaultTempDir() : tmpRoot, true);
auto tmpRoot = canonPath(parent, true);
if (includePid)
return fmt("%1%/%2%-%3%-%4%", tmpRoot, prefix, getpid(), counter++);
else
return fmt("%1%/%2%-%3%", tmpRoot, prefix, counter++);
}
Path createTempDir(const Path & tmpRoot, const Path & prefix,
Path createTempSubdir(const Path & parent, const Path & prefix,
bool includePid, bool useGlobalCounter, mode_t mode)
{
static std::atomic<unsigned int> globalCounter = 0;
@ -585,7 +581,7 @@ Path createTempDir(const Path & tmpRoot, const Path & prefix,
while (1) {
checkInterrupt();
Path tmpDir = tempName(tmpRoot, prefix, includePid, counter);
Path tmpDir = tempName(parent, prefix, includePid, counter);
if (mkdir(tmpDir.c_str(), mode) == 0) {
#if __FreeBSD__
/* Explicitly set the group of the directory. This is to
@ -606,18 +602,6 @@ Path createTempDir(const Path & tmpRoot, const Path & prefix,
}
}
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix)
{
Path tmpl(defaultTempDir() + "/" + prefix + ".XXXXXX");
// FIXME: use O_TMPFILE.
AutoCloseFD fd(mkstemp(tmpl.data()));
if (!fd)
throw SysError("creating temporary file '%s'", tmpl);
closeOnExec(fd.get());
return {std::move(fd), tmpl};
}
Path makeTempPath(const Path & root, const Path & suffix)
{
// start the counter at a random value to minimize issues with preexisting temp paths
@ -721,7 +705,7 @@ void moveFile(const Path & oldName, const Path & newName)
auto newPath = fs::path(newName);
// For the move to be as atomic as possible, copy to a temporary
// directory
fs::path temp = createTempDir(newPath.parent_path(), "rename-tmp");
fs::path temp = createTempSubdir(newPath.parent_path(), "rename-tmp");
Finally removeTemp = [&]() { fs::remove(temp); };
auto tempCopyTarget = temp / "copy-target";
if (e.code().value() == EXDEV) {

View file

@ -288,21 +288,11 @@ struct DIRDeleter
typedef std::unique_ptr<DIR, DIRDeleter> AutoCloseDir;
/**
* Create a temporary directory.
* Create a temporary directory in a given parent directory.
*/
Path createTempDir(const Path & tmpRoot = "", const Path & prefix = "nix",
Path createTempSubdir(const Path & parent, const Path & prefix = "nix",
bool includePid = true, bool useGlobalCounter = true, mode_t mode = 0755);
/**
* Create a temporary file, returning a file handle and its path.
*/
std::pair<AutoCloseFD, Path> createTempFile(const Path & prefix = "nix");
/**
* Return `TMPDIR`, or the default temporary directory if unset or empty.
*/
Path defaultTempDir();
/**
* Return temporary path constructed by appending a suffix to a root path.
*

View file

@ -7,6 +7,7 @@
#include "outputs-spec.hh"
#include "derivations.hh"
#include "run.hh"
#include "temporary-dir.hh"
#include <iterator>
#include <memory>

View file

@ -8,6 +8,7 @@
#include "attr-path.hh"
#include "eval-inline.hh" // IWYU pragma: keep
#include "legacy.hh"
#include "temporary-dir.hh"
#include "terminal.hh"
#include "prefetch-command.hh"

View file

@ -13,6 +13,7 @@
#include "current-process.hh"
#if __linux__
#include "temporary-dir.hh"
#include <sys/mount.h>
#endif

View file

@ -49,6 +49,50 @@ test_custom_build_dir() {
}
test_custom_build_dir
test_custom_temp_dir() {
# like test_custom_build_dir(), but uses the temp-dir setting instead
# build-dir inherits from temp-dir when build-dir is unset
local customTempDir="$TEST_ROOT/custom-temp-dir"
mkdir "$customTempDir"
nix-build check.nix -A failed --argstr checkBuildId $checkBuildId \
--no-out-link --keep-failed --option temp-dir "$customTempDir" 2> $TEST_ROOT/log || status=$?
[ "$status" = "100" ]
[[ 1 == "$(count "$customTempDir/nix-build-"*)" ]]
local buildDir="$customTempDir/nix-build-"*
grep $checkBuildId $buildDir/checkBuildId
# also check a separate code path that doesn't involve build-dir
# nix-shell uses temp-dir for its rcfile path
rcpath=$(NIX_BUILD_SHELL=$SHELL nix-shell check.nix -A deterministic --option temp-dir "$customTempDir" --run 'echo $0' 2> $TEST_ROOT/log)
# rcpath is <temp-dir>/nix-shell-*/rc
[[ $rcpath = "$customTempDir"/* ]]
}
test_custom_temp_dir
test_shell_preserves_tmpdir() {
# ensure commands that spawn interactive shells don't overwrite TMPDIR with temp-dir
local envTempDir=$TEST_ROOT/shell-temp-dir-env
mkdir $envTempDir
local settingTempDir=$TEST_ROOT/shell-temp-dir-setting
mkdir $settingTempDir
# FIXME: switch to check.nix's deterministic once `nix develop` doesn't need `outputs`
# https://git.lix.systems/lix-project/lix/issues/556
local expr='with import ./config.nix; mkDerivation { name = "foo"; buildCommand = "echo foo > $out"; outputs = [ "out" ]; }'
local output
output=$(TMPDIR=$envTempDir NIX_BUILD_SHELL=$SHELL nix-shell -E "$expr" --option temp-dir "$settingTempDir" --command 'echo $TMPDIR' 2> $TEST_ROOT/log)
[[ $output = "$envTempDir" ]]
output=$(TMPDIR=$envTempDir nix develop --impure -E "$expr" --option temp-dir "$settingTempDir" --command bash -c 'echo $TMPDIR' 2> $TEST_ROOT/log)
[[ $output = "$envTempDir"/nix-shell.* ]]
output=$(TMPDIR=$envTempDir nix shell --impure -E "$expr" --option temp-dir "$settingTempDir" --command bash -c 'echo $TMPDIR' 2> $TEST_ROOT/log)
[[ $output = "$envTempDir" ]]
}
test_shell_preserves_tmpdir
nix-build check.nix -A deterministic --argstr checkBuildId $checkBuildId \
--no-out-link 2> $TEST_ROOT/log
checkBuildTempDirRemoved $TEST_ROOT/log

View file

@ -3,6 +3,7 @@
#include <gtest/gtest.h>
#include <rapidcheck/gtest.h>
#include "sqlite.hh"
#include "temporary-dir.hh"
#include <sqlite3.h>