diff --git a/doc/manual/rl-next/relative-and-tilde-paths-in-config.md b/doc/manual/rl-next/relative-and-tilde-paths-in-config.md new file mode 100644 index 000000000..6645496a2 --- /dev/null +++ b/doc/manual/rl-next/relative-and-tilde-paths-in-config.md @@ -0,0 +1,30 @@ +--- +synopsis: Relative and tilde paths in configuration +issues: [fj#482] +cls: [1851, 1863, 1864] +category: Features +credits: [9999years] +--- + +[Configuration settings](@docroot@/command-ref/conf-file.md) can now refer to +files with paths relative to the file they're written in or relative to your +home directory (with `~/`). + +This makes settings like +[`repl-overlays`](@docroot@/command-ref/conf-file.md#conf-repl-overlays) and +[`secret-key-files`](@docroot@/command-ref/conf-file.md#conf-repl-overlays) +much easier to set, especially if you'd like to refer to files in an existing +dotfiles repo cloned into your home directory. + +If you put `repl-overlays = repl.nix` in your `~/.config/nix/nix.conf`, it'll +load `~/.config/nix/repl.nix`. Similarly, you can set `repl-overlays = +~/.dotfiles/repl.nix` to load a file relative to your home directory. + +Configuration files can also +[`include`](@docroot@/command-ref/conf-file.md#file-format) paths relative to +your home directory. + +Only user configuration files (like `$XDG_CONFIG_HOME/nix/nix.conf` or the +files listed in `$NIX_USER_CONF_FILES`) can use tilde paths relative to your +home directory. Configuration listed in the `$NIX_CONFIG` environment variable +may not use relative paths. diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index ab461e739..29ec60105 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -131,8 +131,9 @@ void loadConfFile() globalConfig.resetOverridden(); auto files = settings.nixUserConfFiles; + auto home = getHome(); for (auto file = files.rbegin(); file != files.rend(); file++) { - applyConfigFile(ApplyConfigOptions{.path = *file}); + applyConfigFile(ApplyConfigOptions{.path = *file, .home = home}); } auto nixConfEnv = getEnv("NIX_CONFIG"); diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 333deb388..778da1413 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -126,7 +126,7 @@ static void applyConfigInner(const std::string & contents, const ApplyConfigOpti if (!options.path) { throw UsageError("can only include configuration '%1%' from files", tokens[1]); } - auto pathToInclude = absPath(tokens[1], dirOf(*options.path)); + auto pathToInclude = absPath(tildePath(tokens[1], options.home), dirOf(*options.path)); if (pathExists(pathToInclude)) { auto includeOptions = ApplyConfigOptions { .path = pathToInclude, @@ -437,10 +437,16 @@ template class BaseSetting; static Path parsePath(const AbstractSetting & s, const std::string & str, const ApplyConfigOptions & options) { - if (str == "") + if (str == "") { throw UsageError("setting '%s' is a path and paths cannot be empty", s.name); - else - return canonPath(str); + } else { + auto tildeResolvedPath = tildePath(str, options.home); + if (options.path) { + return absPath(tildeResolvedPath, dirOf(*options.path)); + } else { + return canonPath(tildeResolvedPath); + } + } } template<> Path PathsSetting::parse(const std::string & str, const ApplyConfigOptions & options) const diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 631cf076b..234c73163 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -117,6 +117,21 @@ Path realPath(Path const & path) return ret; } +Path tildePath(Path const & path, const std::optional & home) +{ + if (path.starts_with("~/")) { + if (home) { + return *home + "/" + path.substr(2); + } else { + throw UsageError("`~` path not allowed: %1%", path); + } + } else if (path.starts_with('~')) { + throw UsageError("`~` paths must start with `~/`: %1%", path); + } else { + return path; + } +} + void chmodPath(const Path & path, mode_t mode) { if (chmod(path.c_str(), mode) == -1) diff --git a/src/libutil/file-system.hh b/src/libutil/file-system.hh index e49323e84..0a54d1a3b 100644 --- a/src/libutil/file-system.hh +++ b/src/libutil/file-system.hh @@ -62,6 +62,16 @@ Path canonPath(PathView path, bool resolveSymlinks = false); */ Path realPath(Path const & path); +/** + * Resolve a tilde path like `~/puppy.nix` into an absolute path. + * + * If `home` is given, it's substituted for `~/` at the start of the input + * `path`. Otherwise, an error is thrown. + * + * If the path starts with `~` but not `~/`, an error is thrown. + */ +Path tildePath(Path const & path, const std::optional & home = std::nullopt); + /** * Change the permissions of a path * Not called `chmod` as it shadows and could be confused with diff --git a/tests/unit/libutil/paths-setting.cc b/tests/unit/libutil/paths-setting.cc index c198b25e0..2d37ad525 100644 --- a/tests/unit/libutil/paths-setting.cc +++ b/tests/unit/libutil/paths-setting.cc @@ -48,7 +48,60 @@ TEST_F(PathsSettingTest, parse) ASSERT_THAT(config.paths.parse("/puppy/../doggy.nix", {}), Eq({"/doggy.nix"})); } -TEST_F(PathsSettingTest, append) { +TEST_F(PathsSettingTest, parseRelative) +{ + auto options = ApplyConfigOptions{.path = "/doggy/kinds/config.nix"}; + auto config = mkConfig(); + ASSERT_THAT( + config.paths.parse("puppy.nix", options), + Eq({"/doggy/kinds/puppy.nix"}) + ); + + // Splits on whitespace: + ASSERT_THAT( + config.paths.parse("puppy.nix /doggy.nix", options), Eq({"/doggy/kinds/puppy.nix", "/doggy.nix"}) + ); + + // Canonicizes paths: + ASSERT_THAT(config.paths.parse("../soft.nix", options), Eq({"/doggy/soft.nix"})); + + // Canonicizes paths: + ASSERT_THAT(config.paths.parse("./soft.nix", options), Eq({"/doggy/kinds/soft.nix"})); +} + +TEST_F(PathsSettingTest, parseHome) +{ + auto options = ApplyConfigOptions{ + .path = "/doggy/kinds/config.nix", + .home = "/home/puppy" + }; + auto config = mkConfig(); + + ASSERT_THAT( + config.paths.parse("puppy.nix", options), + Eq({"/doggy/kinds/puppy.nix"}) + ); + + ASSERT_THAT( + config.paths.parse("~/.config/nix/puppy.nix", options), + Eq({"/home/puppy/.config/nix/puppy.nix"}) + ); + + // Splits on whitespace: + ASSERT_THAT( + config.paths.parse("~/puppy.nix ~/doggy.nix", options), + Eq({"/home/puppy/puppy.nix", "/home/puppy/doggy.nix"}) + ); + + // Canonicizes paths: + ASSERT_THAT(config.paths.parse("~/../why.nix", options), Eq({"/home/why.nix"})); + + // Home paths for other users not allowed. Needs to start with `~/`. + ASSERT_THROW(config.paths.parse("~root/config.nix", options), Error); +} + +TEST_F(PathsSettingTest, append) +{ auto config = mkConfig(); ASSERT_TRUE(config.paths.isAppendable());