From 408dcfc0d3898fb855c1038d24c09c82fe58c379 Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 5 Sep 2022 15:42:10 +0200 Subject: [PATCH 001/299] Improve experimental-features error wording --- src/libutil/experimental-features.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index fa79cca6b..d8b0d30fa 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -52,7 +52,7 @@ std::set parseFeatures(const std::set & rawFea } MissingExperimentalFeature::MissingExperimentalFeature(ExperimentalFeature feature) - : Error("experimental Nix feature '%1%' is disabled; use '--extra-experimental-features %1%' to override", showExperimentalFeature(feature)) + : Error("experimental Nix feature '%1%' is disabled; pass '--extra-experimental-features %1%' as an argument to enable it", showExperimentalFeature(feature)) , missingFeature(feature) {} From 481e4082bf96f5613693785e97a9a5cec7c09940 Mon Sep 17 00:00:00 2001 From: Sandro Date: Wed, 9 Nov 2022 12:03:53 +0100 Subject: [PATCH 002/299] Update src/libutil/experimental-features.cc Co-authored-by: Valentin Gagarin --- src/libutil/experimental-features.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index d8b0d30fa..052988b34 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -52,7 +52,7 @@ std::set parseFeatures(const std::set & rawFea } MissingExperimentalFeature::MissingExperimentalFeature(ExperimentalFeature feature) - : Error("experimental Nix feature '%1%' is disabled; pass '--extra-experimental-features %1%' as an argument to enable it", showExperimentalFeature(feature)) + : Error("experimental Nix feature '%1%' is disabled; add '--extra-experimental-features %1%' to enable it", showExperimentalFeature(feature)) , missingFeature(feature) {} From c74248c56e6e5558603be6c6eda43eb439772f20 Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Thu, 12 Jan 2023 15:45:19 +0100 Subject: [PATCH 003/299] install-multi-user: ignore profile_target backups that have no change If there was a prior nix installation that created this backup file and then you tried to install it again, it would stop to tell you there is this file. But if the file and its backup are identical in content, there is no harm in continuing and in a later step overwriting the existing backup file with the identical one. This is just a convenience feature. --- scripts/install-multi-user.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/install-multi-user.sh b/scripts/install-multi-user.sh index f149ea0d7..b2f2a9e45 100644 --- a/scripts/install-multi-user.sh +++ b/scripts/install-multi-user.sh @@ -445,6 +445,14 @@ EOF # a row for different files. if [ -e "$profile_target$PROFILE_BACKUP_SUFFIX" ]; then # this backup process first released in Nix 2.1 + + if diff -q "$profile_target$PROFILE_BACKUP_SUFFIX" "$profile_target" > /dev/null; then + # a backup file for the rc-file exist, but they are identical, + # so we can safely ignore it and overwrite it with the same + # content later + continue + fi + failure < Date: Mon, 23 Jan 2023 10:50:44 +0000 Subject: [PATCH 004/299] improved help command listing. --- src/libcmd/repl.cc | 53 +++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index e3afb1531..5fc503c2b 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -486,35 +486,40 @@ bool NixRepl::processLine(std::string line) std::cout << "The following commands are available:\n" << "\n" - << " Evaluate and print expression\n" - << " = Bind expression to variable\n" - << " :a Add attributes from resulting set to scope\n" - << " :b Build a derivation\n" - << " :bl Build a derivation, creating GC roots in the working directory\n" - << " :e Open package or function in $EDITOR\n" - << " :i Build derivation, then install result into current profile\n" - << " :l Load Nix expression and add it to scope\n" - << " :lf Load Nix flake and add it to scope\n" - << " :p Evaluate and print expression recursively\n" - << " :q Exit nix-repl\n" - << " :r Reload all files\n" - << " :sh Build dependencies of derivation, then start nix-shell\n" - << " :t Describe result of evaluation\n" - << " :u Build derivation, then start nix-shell\n" - << " :doc Show documentation of a builtin function\n" - << " :log Show logs for a derivation\n" - << " :te [bool] Enable, disable or toggle showing traces for errors\n" + << " Evaluate and print expression\n" + << " = Bind expression to variable\n" + << " :a, :add Add attributes from resulting set to scope\n" + << " :b Build a derivation\n" + << " :bl Build a derivation, creating GC roots in the\n" + << " working directory\n" + << " :e, :edit Open package or function in $EDITOR\n" + << " :i Build derivation, then install result into\n" + << " current profile\n" + << " :l, :load Load Nix expression and add it to scope\n" + << " :lf, :load-flake Load Nix flake and add it to scope\n" + << " :p, :print Evaluate and print expression recursively\n" + << " :q, :quit Exit nix-repl\n" + << " :r, :reload Reload all files\n" + << " :sh Build dependencies of derivation, then start\n" + << " nix-shell\n" + << " :t Describe result of evaluation\n" + << " :u Build derivation, then start nix-shell\n" + << " :doc Show documentation of a builtin function\n" + << " :log Show logs for a derivation\n" + << " :te, :trace-enable [bool] Enable, disable or toggle showing traces for\n" + << " errors\n" + << " :?, :help Brings up this help menu\n" ; if (state->debugRepl) { std::cout << "\n" << " Debug mode commands\n" - << " :env Show env stack\n" - << " :bt Show trace stack\n" - << " :st Show current trace\n" - << " :st Change to another trace in the stack\n" - << " :c Go until end of program, exception, or builtins.break\n" - << " :s Go one step\n" + << " :env Show env stack\n" + << " :bt, :backtrace Show trace stack\n" + << " :st Show current trace\n" + << " :st Change to another trace in the stack\n" + << " :c, :continue Go until end of program, exception, or builtins.break\n" + << " :s, :step Go one step\n" ; } From c4df53f1542086836d6d59cf82cff8f5cbe5d8f1 Mon Sep 17 00:00:00 2001 From: Rynn Blackmon Date: Wed, 19 Apr 2023 16:32:47 -0700 Subject: [PATCH 005/299] Mention `$DRV_PATH` in post-build-hook docs --- doc/manual/src/advanced-topics/post-build-hook.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/manual/src/advanced-topics/post-build-hook.md b/doc/manual/src/advanced-topics/post-build-hook.md index 1479cc3a4..17b67111c 100644 --- a/doc/manual/src/advanced-topics/post-build-hook.md +++ b/doc/manual/src/advanced-topics/post-build-hook.md @@ -69,6 +69,8 @@ exec nix copy --to "s3://example-nix-cache" $OUT_PATHS > store sign`. Nix guarantees the paths will not contain any spaces, > however a store path might contain glob characters. The `set -f` > disables globbing in the shell. +> If you want to upload the .drv file too, the `$DRV_PATH` variable +> is also defined for the script and works just like `$OUT_PATHS`. Then make sure the hook program is executable by the `root` user: From bf693319f63601c0e72107012328c298cd78e88b Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 14 Mar 2023 11:54:04 -0300 Subject: [PATCH 006/299] feat: add always-allow-substitutes This adds a new configuration option to Nix, `always-allow-substitutes`, whose effect is simple: it causes the `allowSubstitutes` attribute in derivations to be ignored, and for substituters to always be used. This is extremely valuable for users of Nix in CI, where usually `nix-build-uncached` is used. There, derivations which disallow substitutes cause headaches as the inputs for building already-cached derivations need to be fetched to spuriously rebuild some simple text file. This option should be a good middle-ground, since it doesn't imply rebuilding the world, such as the approach I took in https://github.com/NixOS/nixpkgs/pull/221048 --- doc/manual/src/language/advanced-attributes.md | 3 +++ src/libstore/globals.hh | 8 ++++++++ src/libstore/parsed-derivations.cc | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/language/advanced-attributes.md b/doc/manual/src/language/advanced-attributes.md index 307971434..211d195b2 100644 --- a/doc/manual/src/language/advanced-attributes.md +++ b/doc/manual/src/language/advanced-attributes.md @@ -261,6 +261,9 @@ Derivations can declare some infrequently used optional attributes. useful for very trivial derivations (such as `writeText` in Nixpkgs) that are cheaper to build than to substitute from a binary cache. + You may disable the effects of this attibute by enabling the + `always-allow-substitutes` configuration option in Nix. + > **Note** > > You need to have a builder configured which satisfies the diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 31dfe5b4e..8b0e3fc7d 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -255,6 +255,14 @@ public: For the exact format and examples, see [the manual chapter on remote builds](../advanced-topics/distributed-builds.md) )"}; + Setting alwaysAllowSubstitutes{ + this, false, "always-allow-substitutes", + R"( + If set to `true`, Nix will ignore the `allowSubstitutes` attribute in + derivations and always attempt to use available substituters. + For more information on `allowSubstitutes`, see [the manual chapter on advanced attributes](../language/advanced-attributes.md). + )"}; + Setting buildersUseSubstitutes{ this, false, "builders-use-substitutes", R"( diff --git a/src/libstore/parsed-derivations.cc b/src/libstore/parsed-derivations.cc index cc4a94fab..1d900c272 100644 --- a/src/libstore/parsed-derivations.cc +++ b/src/libstore/parsed-derivations.cc @@ -122,7 +122,7 @@ bool ParsedDerivation::willBuildLocally(Store & localStore) const bool ParsedDerivation::substitutesAllowed() const { - return getBoolAttr("allowSubstitutes", true); + return settings.alwaysAllowSubstitutes ? true : getBoolAttr("allowSubstitutes", true); } bool ParsedDerivation::useUidRange() const From f11445952fcf0f5fd3f827997591d43a41f2074a Mon Sep 17 00:00:00 2001 From: Michal Sojka Date: Sun, 11 Jun 2023 21:36:56 +0200 Subject: [PATCH 007/299] Document builtins.fetchTree Co-authored-by: Valentin Gagarin Supersedes #6740 --- src/libexpr/primops/fetchTree.cc | 43 ++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 579a45f92..3b048c446 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -198,10 +198,49 @@ static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, fetchTree(state, pos, args, v, std::nullopt, FetchTreeParams { .allowNameArgument = false }); } -// FIXME: document static RegisterPrimOp primop_fetchTree({ .name = "fetchTree", - .arity = 1, + .args = {"input"}, + .doc = R"( + Fetch a source tree or a plain file using one of the supported backends. + *input* can be an attribute set representation of [flake reference](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references) or a URL. + The input should be "locked", that is, it should contain a commit hash or content hash unless impure evaluation (`--impure`) is allowed. + + Here are some examples of how to use `fetchTree`: + + - Fetch a GitHub repository: + + ```nix + builtins.fetchTree { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + rev = "ae2e6b3958682513d28f7d633734571fb18285dd"; + } + ``` + + This evaluates to attribute set: + + ``` + { + lastModified = 1686503798; + lastModifiedDate = "20230611171638"; + narHash = "sha256-rA9RqKP9OlBrgGCPvfd5HVAXDOy8k2SmPtB/ijShNXc="; + outPath = "/nix/store/l5m6qlvfs9sdw14ja3qbzpglcjlb6j1x-source"; + rev = "ae2e6b3958682513d28f7d633734571fb18285dd"; + shortRev = "ae2e6b3"; + } + ``` + - Fetch a single file from a URL: + + ```nix + builtins.fetchTree "https://example.com/" + ``` + + > **Note** + > + > This function requires the [`flakes` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-flakes) to be enabled. + )", .fun = prim_fetchTree }); From ab78d8804e35f889965a561719d84ba6cf904ddc Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 6 Jul 2023 11:26:51 +0200 Subject: [PATCH 008/299] .gitignore: Add .cache/ --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 969194650..ec952b918 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,6 @@ nix-rust/target result .vscode/ + +# clangd and possibly more +.cache/ From 3b3822ea1ddf34f8fcb44c67cda2b778b5b41a34 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 7 Jul 2023 15:08:25 +0200 Subject: [PATCH 009/299] tests: Reformat exit code error message Now looks like: Expected exit code '123' but got '0' from command 'echo' 'hi' --- tests/common/vars-and-functions.sh.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common/vars-and-functions.sh.in b/tests/common/vars-and-functions.sh.in index a9e6c802f..33a764c3f 100644 --- a/tests/common/vars-and-functions.sh.in +++ b/tests/common/vars-and-functions.sh.in @@ -195,7 +195,7 @@ expect() { shift "$@" && res=0 || res="$?" if [[ $res -ne $expected ]]; then - echo "Expected '$expected' but got '$res' while running '${*@Q}'" >&2 + echo "Expected exit code '$expected' but got '$res' from command ${*@Q}" >&2 return 1 fi return 0 @@ -209,7 +209,7 @@ expectStderr() { shift "$@" 2>&1 && res=0 || res="$?" if [[ $res -ne $expected ]]; then - echo "Expected '$expected' but got '$res' while running '${*@Q}'" >&2 + echo "Expected exit code '$expected' but got '$res' from command ${*@Q}" >&2 return 1 fi return 0 From feb01b22ed2954232455b5c8346cc5ed09c21f81 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 17 Jul 2023 11:28:12 +0200 Subject: [PATCH 010/299] add links to store API documentation --- src/libstore/store-api.hh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 14a862eef..43aa00637 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -136,19 +136,22 @@ struct StoreConfig : public Config Setting priority{this, 0, "priority", R"( - Priority of this store when used as a substituter. A lower value means a higher priority. + Priority of this store when used as a [substituter](@docroot@/command-ref/conf-file.md#conf-substituters). + A lower value means a higher priority. )"}; Setting wantMassQuery{this, false, "want-mass-query", R"( - Whether this store (when used as a substituter) can be - queried efficiently for path validity. + Whether this store can be queried efficiently for path validity when used as a [substituter](@docroot@/command-ref/conf-file.md#conf-substituters). )"}; Setting systemFeatures{this, getDefaultSystemFeatures(), "system-features", - "Optional features that the system this store builds on implements (like \"kvm\")."}; + R"( + Optional [system features](@docroot@/command-ref/conf-file.md#conf-system-features) available on the system this store uses to build derivations. + Example: `"kvm"` + )" }; }; class Store : public std::enable_shared_from_this, public virtual StoreConfig From 493ddf617f0b2c406dc35026b6d481a69cf1d934 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 17 Jul 2023 11:28:27 +0200 Subject: [PATCH 011/299] reformat `system-features` setting documentation --- src/libstore/globals.hh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index d4b8fb1f9..879588375 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -689,19 +689,21 @@ public: getDefaultSystemFeatures(), "system-features", R"( - A set of system “features” supported by this machine, e.g. `kvm`. - Derivations can express a dependency on such features through the - derivation attribute `requiredSystemFeatures`. For example, the - attribute + A set of system “features” supported by this machine. + Derivations can express a dependency on such features through the derivation attribute `requiredSystemFeatures`. + + For example, the attribute requiredSystemFeatures = [ "kvm" ]; - ensures that the derivation can only be built on a machine with the - `kvm` feature. + ensures that the derivation can only be built on a machine with the `kvm` feature. - This setting by default includes `kvm` if `/dev/kvm` is accessible, - and the pseudo-features `nixos-test`, `benchmark` and `big-parallel` - that are used in Nixpkgs to route builds to specific machines. + This setting by default includes + - `kvm` if `/dev/kvm` is accessible + - historical pseudo-features for backwards compatibility, used in Nixpkgs to route Hydra builds to specific machines + - `nixos-test` + - `benchmark` + - `big-parallel` )", {}, false}; Setting substituters{ From 5f37ebcf83c856b08a63778e07bfc5c84ea4a5ec Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 19 Jul 2023 11:57:20 +0200 Subject: [PATCH 012/299] document all special system features and their behavior --- .../src/language/advanced-attributes.md | 12 ++++++ doc/manual/src/release-notes/rl-2.12.md | 14 +------ src/libstore/globals.hh | 39 +++++++++++++------ 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/doc/manual/src/language/advanced-attributes.md b/doc/manual/src/language/advanced-attributes.md index 307971434..2961f9dcc 100644 --- a/doc/manual/src/language/advanced-attributes.md +++ b/doc/manual/src/language/advanced-attributes.md @@ -345,3 +345,15 @@ Derivations can declare some infrequently used optional attributes. This is useful, for example, when generating self-contained filesystem images with their own embedded Nix store: hashes found inside such an image refer to the embedded store and not to the host's Nix store. + +- [`requiredSystemFeatures`]{#adv-attr-requiredSystemFeatures}\ + + If a derivation has the `requiredSystemFeatures` attribute, then Nix will only build it on a machine that has the corresponding features set in its [`system-features` configuration](@docroot@/command-ref/conf-file.md#conf-system-features). + + For example, setting + + ```nix + requiredSystemFeatures = [ "kvm" ]; + ``` + + ensures that the derivation can only be built on a machine with the `kvm` feature. diff --git a/doc/manual/src/release-notes/rl-2.12.md b/doc/manual/src/release-notes/rl-2.12.md index e2045d7bf..57d092e01 100644 --- a/doc/manual/src/release-notes/rl-2.12.md +++ b/doc/manual/src/release-notes/rl-2.12.md @@ -2,20 +2,8 @@ * On Linux, Nix can now run builds in a user namespace where they run as root (UID 0) and have 65,536 UIDs available. - - This is primarily useful for running containers such as `systemd-nspawn` - inside a Nix build. For an example, see [`tests/systemd-nspawn/nix`][nspawn]. - [nspawn]: https://github.com/NixOS/nix/blob/67bcb99700a0da1395fa063d7c6586740b304598/tests/systemd-nspawn.nix. - - A build can enable this by setting the derivation attribute: - - ``` - requiredSystemFeatures = [ "uid-range" ]; - ``` - - The `uid-range` [system feature] requires the [`auto-allocate-uids`] - setting to be enabled. + This can be used by requiring `uid-range` [system feature] in derivations. [system feature]: ../command-ref/conf-file.md#conf-system-features diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 879588375..9f852dc7b 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -337,7 +337,7 @@ public: users in `build-users-group`. UIDs are allocated starting at 872415232 (0x34000000) on Linux and 56930 on macOS. - )"}; + )", {}, true, Xp::AutoAllocateUids}; Setting startId{this, #if __linux__ @@ -690,20 +690,37 @@ public: "system-features", R"( A set of system “features” supported by this machine. - Derivations can express a dependency on such features through the derivation attribute `requiredSystemFeatures`. - For example, the attribute + This complements the [`system`](#conf-system) and [`extra-platforms`](#conf-extra-platforms) configuration options and the corresponding [`system`](@docroot@/language/derivations.md#attr-system) attribute on derivations. - requiredSystemFeatures = [ "kvm" ]; + Derivations can require system features in the derivation attribute [`requiredSystemFeatures`](@docroot@/language/advanced-attributes.md#adv-attr-requiredSystemFeatures). - ensures that the derivation can only be built on a machine with the `kvm` feature. + System features are generally user-defined, but the following have special treatment: - This setting by default includes - - `kvm` if `/dev/kvm` is accessible - - historical pseudo-features for backwards compatibility, used in Nixpkgs to route Hydra builds to specific machines - - `nixos-test` - - `benchmark` - - `big-parallel` + - `kvm` + + Set by default if `/dev/kvm` is accessible. + + - `nixos-test`, `benchmark`, `big-parallel` + + These historical pseudo-features are always enabled for backwards compatibility, used in Nixpkgs to route Hydra builds to specific machines. + + - `ca-derivations` + + Set by default if the [`ca-derivations` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-ca-derivations) is enabled. + + - `recursive-nix` + + Set by default if the [`recursive-nix` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-recursive-nix) is enabled. + + - `uid-range` + + On Linux, Nix can run builds in a user namespace where they run as root (UID 0) and have 65,536 UIDs available. + This is primarily useful for running containers such as `systemd-nspawn` inside a Nix build. For an example, see [`tests/systemd-nspawn/nix`][nspawn]. + + [nspawn]: https://github.com/NixOS/nix/blob/67bcb99700a0da1395fa063d7c6586740b304598/tests/systemd-nspawn.nix. + + Set by default on Linux if the [`auto-allocate-uids`](#conf-auto-allocate-uids) setting is enabled. )", {}, false}; Setting substituters{ From 2fa90e5824635998b445b6862bb49469e8fc75de Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 19 Jul 2023 13:59:18 +0200 Subject: [PATCH 013/299] add more details on CA derivations --- doc/manual/src/language/advanced-attributes.md | 2 ++ src/libstore/globals.hh | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/manual/src/language/advanced-attributes.md b/doc/manual/src/language/advanced-attributes.md index 2961f9dcc..5292a2ffc 100644 --- a/doc/manual/src/language/advanced-attributes.md +++ b/doc/manual/src/language/advanced-attributes.md @@ -229,6 +229,8 @@ Derivations can declare some infrequently used optional attributes. [`outputHashAlgo`](#adv-attr-outputHashAlgo) like for *fixed-output derivations* (see above). + It also implicitly requires that the machine to build the derivation must have the `ca-derivations` [system feature](@docroot@/command-ref/conf-file.md#conf-system-features). + - [`passAsFile`]{#adv-attr-passAsFile}\ A list of names of attributes that should be passed via files rather than environment variables. For example, if you have diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 9f852dc7b..c08a1a943 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -693,25 +693,27 @@ public: This complements the [`system`](#conf-system) and [`extra-platforms`](#conf-extra-platforms) configuration options and the corresponding [`system`](@docroot@/language/derivations.md#attr-system) attribute on derivations. - Derivations can require system features in the derivation attribute [`requiredSystemFeatures`](@docroot@/language/advanced-attributes.md#adv-attr-requiredSystemFeatures). + A derivation can require system features in the [`requiredSystemFeatures` attribute](@docroot@/language/advanced-attributes.md#adv-attr-requiredSystemFeatures), and the machine to build the derivation must have them. - System features are generally user-defined, but the following have special treatment: + System features are user-defined, but Nix sets the following defaults: - `kvm` - Set by default if `/dev/kvm` is accessible. + Included by default if `/dev/kvm` is accessible. - `nixos-test`, `benchmark`, `big-parallel` - These historical pseudo-features are always enabled for backwards compatibility, used in Nixpkgs to route Hydra builds to specific machines. + These historical pseudo-features are always enabled for backwards compatibility, as they are used in Nixpkgs to route Hydra builds to specific machines. - `ca-derivations` - Set by default if the [`ca-derivations` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-ca-derivations) is enabled. + Included by default if the [`ca-derivations` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-ca-derivations) is enabled. + + This system feature is implicitly required by derivations with the [`__contentAddressed` attribute](@docroot@/language/advanced-attributes.md#adv-attr-__contentAddressed). - `recursive-nix` - Set by default if the [`recursive-nix` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-recursive-nix) is enabled. + Included by default if the [`recursive-nix` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-recursive-nix) is enabled. - `uid-range` @@ -720,7 +722,7 @@ public: [nspawn]: https://github.com/NixOS/nix/blob/67bcb99700a0da1395fa063d7c6586740b304598/tests/systemd-nspawn.nix. - Set by default on Linux if the [`auto-allocate-uids`](#conf-auto-allocate-uids) setting is enabled. + Included by default on Linux if the [`auto-allocate-uids`](#conf-auto-allocate-uids) setting is enabled. )", {}, false}; Setting substituters{ From 9b908fa70a07219a110ddd63ec3593c2c2269918 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 24 Jul 2023 14:02:05 -0400 Subject: [PATCH 014/299] Factor out `nix-defexpr` path computation Avoid duplicated code, and also avoid "on the fly" path construction (which makes it harder to keep track of which paths we use). The factored out code doesn't create the Nix state dir anymore, but this is fine because other in nix-env and nix-channel does: - nix-channel: Line 158 in this commit - nix-env: Line 1407 in this commit --- src/libexpr/eval-settings.cc | 9 ++++++++- src/libexpr/eval-settings.hh | 5 +++++ src/nix-channel/nix-channel.cc | 3 ++- src/nix-env/nix-env.cc | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/libexpr/eval-settings.cc b/src/libexpr/eval-settings.cc index 422aaf8d5..93b4a5289 100644 --- a/src/libexpr/eval-settings.cc +++ b/src/libexpr/eval-settings.cc @@ -63,7 +63,7 @@ Strings EvalSettings::getDefaultNixPath() }; if (!evalSettings.restrictEval && !evalSettings.pureEval) { - add(settings.useXDGBaseDirectories ? getStateDir() + "/nix/defexpr/channels" : getHome() + "/.nix-defexpr/channels"); + add(getNixDefExpr() + "/channels"); add(rootChannelsDir() + "/nixpkgs", "nixpkgs"); add(rootChannelsDir()); } @@ -92,4 +92,11 @@ EvalSettings evalSettings; static GlobalConfig::Register rEvalSettings(&evalSettings); +Path getNixDefExpr() +{ + return settings.useXDGBaseDirectories + ? getStateDir() + "/nix/defexpr" + : getHome() + "/.nix-defexpr"; +} + } diff --git a/src/libexpr/eval-settings.hh b/src/libexpr/eval-settings.hh index 043af6cab..e6666061a 100644 --- a/src/libexpr/eval-settings.hh +++ b/src/libexpr/eval-settings.hh @@ -95,4 +95,9 @@ struct EvalSettings : Config extern EvalSettings evalSettings; +/** + * Conventionally part of the default nix path in impure mode. + */ +Path getNixDefExpr(); + } diff --git a/src/nix-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc index c1c8edd1d..95f401441 100755 --- a/src/nix-channel/nix-channel.cc +++ b/src/nix-channel/nix-channel.cc @@ -5,6 +5,7 @@ #include "store-api.hh" #include "legacy.hh" #include "fetchers.hh" +#include "eval-settings.hh" // for defexpr #include "util.hh" #include @@ -165,7 +166,7 @@ static int main_nix_channel(int argc, char ** argv) // Figure out the name of the `.nix-channels' file to use auto home = getHome(); channelsList = settings.useXDGBaseDirectories ? createNixStateDir() + "/channels" : home + "/.nix-channels"; - nixDefExpr = settings.useXDGBaseDirectories ? createNixStateDir() + "/defexpr" : home + "/.nix-defexpr"; + nixDefExpr = getNixDefExpr(); // Figure out the name of the channels profile. profile = profilesDir() + "/channels"; diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index 91b073b49..3dc3db0fc 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -15,6 +15,7 @@ #include "value-to-json.hh" #include "xml-writer.hh" #include "legacy.hh" +#include "eval-settings.hh" // for defexpr #include #include @@ -1399,7 +1400,7 @@ static int main_nix_env(int argc, char * * argv) globals.instSource.type = srcUnknown; globals.instSource.systemFilter = "*"; - Path nixExprPath = settings.useXDGBaseDirectories ? createNixStateDir() + "/defexpr" : getHome() + "/.nix-defexpr"; + Path nixExprPath = getNixDefExpr(); if (!pathExists(nixExprPath)) { try { From 31a6e10fe52fd4265501553ca479c51b510137e1 Mon Sep 17 00:00:00 2001 From: Simon Rainerson Date: Tue, 31 Jan 2023 11:36:13 +0100 Subject: [PATCH 015/299] Fix misread of source if path is already valid When receiving a stream of NARs through the ssh-ng protocol, an already existing path would cause the NAR archive to not be read in the stream, resulting in trying to parse the NAR as a ValidPathInfo. This results in the error message: error: not an absolute path: 'nix-archive-1' Fixes #6253 Usually this problem is avoided by running QueryValidPaths before AddMultipleToStore, but can arise when two parallel nix processes gets the same response from QueryValidPaths. This makes the problem more prominent when running builds in parallel. --- src/libstore/local-store.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 40a3bc194..17b4ecc73 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -1196,6 +1196,15 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, if (checkSigs && pathInfoIsUntrusted(info)) throw Error("cannot add path '%s' because it lacks a signature by a trusted key", printStorePath(info.path)); + /* In case we are not interested in reading the NAR: discard it. */ + bool narRead = false; + Finally cleanup = [&]() { + if (!narRead) { + ParseSink sink; + parseDump(sink, source); + } + }; + addTempRoot(info.path); if (repair || !isValidPath(info.path)) { @@ -1220,6 +1229,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, TeeSource wrapperSource { source, hashSink }; + narRead = true; restorePath(realPath, wrapperSource); auto hashResult = hashSink.finish(); From afac001c39aadedcbe52ce45fbde8220834cf13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?= Date: Mon, 7 Aug 2023 11:23:19 +0200 Subject: [PATCH 016/299] Test the parallel copy over ssh-ng Regression test for https://github.com/NixOS/nix/issues/6253 --- tests/local.mk | 1 + tests/nix-copy-ssh-ng.sh | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/nix-copy-ssh-ng.sh diff --git a/tests/local.mk b/tests/local.mk index 2afe91220..4edf31303 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -90,6 +90,7 @@ nix_tests = \ zstd.sh \ compression-levels.sh \ nix-copy-ssh.sh \ + nix-copy-ssh-ng.sh \ post-hook.sh \ function-trace.sh \ flakes/config.sh \ diff --git a/tests/nix-copy-ssh-ng.sh b/tests/nix-copy-ssh-ng.sh new file mode 100644 index 000000000..45e53c9c0 --- /dev/null +++ b/tests/nix-copy-ssh-ng.sh @@ -0,0 +1,18 @@ +source common.sh + +clearStore +clearCache + +remoteRoot=$TEST_ROOT/store2 +chmod -R u+w "$remoteRoot" || true +rm -rf "$remoteRoot" + +outPath=$(nix-build --no-out-link dependencies.nix) + +nix store ping --store "ssh-ng://localhost?store=$NIX_STORE_DIR&remote-store=$remoteRoot%3fstore=$NIX_STORE_DIR%26real=$remoteRoot$NIX_STORE_DIR" + +# Regression test for https://github.com/NixOS/nix/issues/6253 +nix copy --to "ssh-ng://localhost?store=$NIX_STORE_DIR&remote-store=$remoteRoot%3fstore=$NIX_STORE_DIR%26real=$remoteRoot$NIX_STORE_DIR" $outPath --no-check-sigs & +nix copy --to "ssh-ng://localhost?store=$NIX_STORE_DIR&remote-store=$remoteRoot%3fstore=$NIX_STORE_DIR%26real=$remoteRoot$NIX_STORE_DIR" $outPath --no-check-sigs + +[ -f $remoteRoot$outPath/foobar ] From 4b1bd822ac7fd35b30f37c1b7705c523cc462761 Mon Sep 17 00:00:00 2001 From: Peter Waller Date: Fri, 30 Jun 2023 16:11:12 +0100 Subject: [PATCH 017/299] Try to realise CA derivations during queryMissing This enables nix to correctly report what will be fetched in the case that everything is a cache hit. Note however that if an intermediate build of something which is not cached could still cause products to end up being substituted if the intermediate build results in a CA path which is in the cache. Fixes #8615. Signed-off-by: Peter Waller --- src/libstore/misc.cc | 30 ++++++++++++++++++++ tests/ca/build-cache.sh | 51 ++++++++++++++++++++++++++++++++++ tests/ca/build.sh | 12 +------- tests/ca/content-addressed.nix | 18 ++++++++++++ tests/ca/local.mk | 1 + 5 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 tests/ca/build-cache.sh diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index 14160dc8b..2fdf0a68d 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -200,6 +200,36 @@ void Store::queryMissing(const std::vector & targets, auto drv = make_ref(derivationFromPath(bfd.drvPath)); ParsedDerivation parsedDrv(StorePath(bfd.drvPath), *drv); + if (!knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { + experimentalFeatureSettings.require(Xp::CaDerivations); + + // If there are unknown output paths, attempt to find if the + // paths are known to substituters through a realisation. + auto outputHashes = staticOutputHashes(*this, *drv); + knownOutputPaths = true; + + for (auto [outputName, hash] : outputHashes) { + if (!bfd.outputs.contains(outputName)) + continue; + + bool found = false; + for (auto &sub : getDefaultSubstituters()) { + auto realisation = sub->queryRealisation({hash, outputName}); + if (!realisation) + continue; + found = true; + if (!isValidPath(realisation->outPath)) + invalid.insert(realisation->outPath); + break; + } + if (!found) { + // Some paths did not have a realisation, this must be built. + knownOutputPaths = false; + break; + } + } + } + if (knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { auto drvState = make_ref>(DrvState(invalid.size())); for (auto & output : invalid) diff --git a/tests/ca/build-cache.sh b/tests/ca/build-cache.sh new file mode 100644 index 000000000..6a4080fec --- /dev/null +++ b/tests/ca/build-cache.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +source common.sh + +# The substituters didn't work prior to this time. +requireDaemonNewerThan "2.18.0pre20230808" + +drv=$(nix-instantiate ./content-addressed.nix -A rootCA --arg seed 1)^out +nix derivation show "$drv" --arg seed 1 + +buildAttr () { + local derivationPath=$1 + local seedValue=$2 + shift; shift + local args=("./content-addressed.nix" "-A" "$derivationPath" --arg seed "$seedValue" "--no-out-link") + args+=("$@") + nix-build "${args[@]}" +} + +copyAttr () { + local derivationPath=$1 + local seedValue=$2 + shift; shift + local args=("-f" "./content-addressed.nix" "$derivationPath" --arg seed "$seedValue") + args+=("$@") + # Note: to copy CA derivations, we need to copy the realisations, which + # currently requires naming the installables, not just the derivation output + # path. + nix copy --to file://$cacheDir "${args[@]}" +} + +testRemoteCacheFor () { + local derivationPath=$1 + clearCache + copyAttr "$derivationPath" 1 + clearStore + # Check nothing gets built. + buildAttr "$derivationPath" 1 --option substituters file://$cacheDir --no-require-sigs |& grepQuietInverse " will be built:" +} + +testRemoteCache () { + testRemoteCacheFor rootCA + testRemoteCacheFor dependentCA + testRemoteCacheFor dependentNonCA + testRemoteCacheFor dependentFixedOutput + testRemoteCacheFor dependentForBuildCA + testRemoteCacheFor dependentForBuildNonCA +} + +clearStore +testRemoteCache \ No newline at end of file diff --git a/tests/ca/build.sh b/tests/ca/build.sh index 7754ad276..e1a8a7625 100644 --- a/tests/ca/build.sh +++ b/tests/ca/build.sh @@ -2,7 +2,7 @@ source common.sh -drv=$(nix-instantiate ./content-addressed.nix -A rootCA --arg seed 1) +drv=$(nix-instantiate ./content-addressed.nix -A rootCA --arg seed 1)^out nix derivation show "$drv" --arg seed 1 buildAttr () { @@ -14,14 +14,6 @@ buildAttr () { nix-build "${args[@]}" } -testRemoteCache () { - clearCache - local outPath=$(buildAttr dependentNonCA 1) - nix copy --to file://$cacheDir $outPath - clearStore - buildAttr dependentNonCA 1 --option substituters file://$cacheDir --no-require-sigs |& grepQuietInverse "building dependent-non-ca" -} - testDeterministicCA () { [[ $(buildAttr rootCA 1) = $(buildAttr rootCA 2) ]] } @@ -66,8 +58,6 @@ testNormalization () { test "$(stat -c %Y $outPath)" -eq 1 } -# Disabled until we have it properly working -# testRemoteCache clearStore testNormalization testDeterministicCA diff --git a/tests/ca/content-addressed.nix b/tests/ca/content-addressed.nix index 81bc4bf5c..2559c562f 100644 --- a/tests/ca/content-addressed.nix +++ b/tests/ca/content-addressed.nix @@ -61,6 +61,24 @@ rec { echo ${rootCA}/non-ca-hello > $out/dep ''; }; + dependentForBuildCA = mkCADerivation { + name = "dependent-for-build-ca"; + buildCommand = '' + echo "Depends on rootCA for building only" + mkdir -p $out + echo ${rootCA} + touch $out + ''; + }; + dependentForBuildNonCA = mkDerivation { + name = "dependent-for-build-non-ca"; + buildCommand = '' + echo "Depends on rootCA for building only" + mkdir -p $out + echo ${rootCA} + touch $out + ''; + }; dependentFixedOutput = mkDerivation { name = "dependent-fixed-output"; outputHashMode = "recursive"; diff --git a/tests/ca/local.mk b/tests/ca/local.mk index d15312708..0852e592e 100644 --- a/tests/ca/local.mk +++ b/tests/ca/local.mk @@ -1,6 +1,7 @@ ca-tests := \ $(d)/build-with-garbage-path.sh \ $(d)/build.sh \ + $(d)/build-cache.sh \ $(d)/concurrent-builds.sh \ $(d)/derivation-json.sh \ $(d)/duplicate-realisation-in-closure.sh \ From 60b7121d2c6d4322b7c2e8e7acfec7b701b2d3a1 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 15 Jan 2023 17:39:04 -0500 Subject: [PATCH 018/299] Make the Derived Path family of types inductive for dynamic derivations We want to be able to write down `foo.drv^bar.drv^baz`: `foo.drv^bar.drv` is the dynamic derivation (since it is itself a derivation output, `bar.drv` from `foo.drv`). To that end, we create `Single{Derivation,BuiltPath}` types, that are very similar except instead of having multiple outputs (in a set or map), they have a single one. This is for everything to the left of the rightmost `^`. `NixStringContextElem` has an analogous change, and now can reuse `SingleDerivedPath` at the top level. In fact, if we ever get rid of `DrvDeep`, `NixStringContextElem` could be replaced with `SingleDerivedPath` entirely! Important note: some JSON formats have changed. We already can *produce* dynamic derivations, but we can't refer to them directly. Today, we can merely express building or example at the top imperatively over time by building `foo.drv^bar.drv`, and then with a second nix invocation doing `^baz`, but this is not declarative. The ethos of Nix of being able to write down the full plan everything you want to do, and then execute than plan with a single command, and for that we need the new inductive form of these types. Co-authored-by: Robert Hensing Co-authored-by: Valentin Gagarin --- doc/manual/src/release-notes/rl-next.md | 3 + src/build-remote/build-remote.cc | 7 +- src/libcmd/built-path.cc | 76 +++++- src/libcmd/built-path.hh | 50 +++- src/libcmd/installable-attr-path.cc | 2 +- src/libcmd/installable-derived-path.cc | 15 +- src/libcmd/installable-flake.cc | 2 +- src/libcmd/installable-value.cc | 3 +- src/libcmd/installables.cc | 36 ++- src/libcmd/repl.cc | 2 +- src/libexpr/eval-cache.cc | 2 +- src/libexpr/eval.cc | 59 +++-- src/libexpr/eval.hh | 10 +- src/libexpr/primops.cc | 15 +- src/libexpr/primops/context.cc | 7 +- src/libexpr/tests/derived-path.cc | 26 +-- src/libexpr/tests/value/context.cc | 70 ++++-- src/libexpr/value/context.cc | 97 +++++--- src/libexpr/value/context.hh | 21 +- src/libstore/build/derivation-goal.cc | 10 +- src/libstore/build/entry-points.cc | 2 +- src/libstore/build/local-derivation-goal.cc | 15 +- src/libstore/build/worker.cc | 7 +- src/libstore/derived-path.cc | 247 ++++++++++++++++++-- src/libstore/derived-path.hh | 222 ++++++++++++++++-- src/libstore/downstream-placeholder.cc | 15 ++ src/libstore/downstream-placeholder.hh | 12 + src/libstore/legacy-ssh-store.cc | 3 + src/libstore/misc.cc | 85 ++++++- src/libstore/path-with-outputs.cc | 47 ++-- src/libstore/path-with-outputs.hh | 4 +- src/libstore/remote-store.cc | 21 +- src/libstore/store-api.hh | 1 + src/libstore/tests/derived-path.cc | 91 +++++++- src/libstore/tests/derived-path.hh | 14 +- src/libutil/comparator.hh | 16 ++ src/nix-build/nix-build.cc | 9 +- src/nix-env/nix-env.cc | 4 +- src/nix/app.cc | 7 +- src/nix/build.cc | 10 +- src/nix/bundle.cc | 2 +- src/nix/develop.cc | 2 +- src/nix/flake.cc | 6 +- src/nix/log.cc | 20 +- tests/build.sh | 2 +- tests/dyn-drv/build-built-drv.sh | 21 ++ tests/dyn-drv/local.mk | 3 +- tests/test-libstoreconsumer/main.cc | 2 +- 48 files changed, 1136 insertions(+), 267 deletions(-) create mode 100644 tests/dyn-drv/build-built-drv.sh diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 6516a2663..be922f95a 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -16,3 +16,6 @@ [unsafeDiscardReferences](@docroot@/contributing/experimental-features.md#xp-feature-discard-references) attribute is no longer guarded by an experimental flag and can be used freely. + +- The JSON output for derived paths with are store paths is now a string, not an object with a single `path` field. + This only affects `nix-build --json` when "building" non-derivation things like fetched sources, which is a no-op. diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc index 2fb17d06f..c1f03a8ef 100644 --- a/src/build-remote/build-remote.cc +++ b/src/build-remote/build-remote.cc @@ -322,7 +322,12 @@ connected: throw Error("build of '%s' on '%s' failed: %s", store->printStorePath(*drvPath), storeUri, result.errorMsg); } else { copyClosure(*store, *sshStore, StorePathSet {*drvPath}, NoRepair, NoCheckSigs, substitute); - auto res = sshStore->buildPathsWithResults({ DerivedPath::Built { *drvPath, OutputsSpec::All {} } }); + auto res = sshStore->buildPathsWithResults({ + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(*drvPath), + .outputs = OutputsSpec::All {}, + } + }); // One path to build should produce exactly one build result assert(res.size() == 1); optResult = std::move(res[0]); diff --git a/src/libcmd/built-path.cc b/src/libcmd/built-path.cc index db9c440e3..c6cc7fa9c 100644 --- a/src/libcmd/built-path.cc +++ b/src/libcmd/built-path.cc @@ -8,13 +8,39 @@ namespace nix { -nlohmann::json BuiltPath::Built::toJSON(ref store) const { - nlohmann::json res; - res["drvPath"] = store->printStorePath(drvPath); - for (const auto& [output, path] : outputs) { - res["outputs"][output] = store->printStorePath(path); +#define CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, COMPARATOR) \ + bool MY_TYPE ::operator COMPARATOR (const MY_TYPE & other) const \ + { \ + const MY_TYPE* me = this; \ + auto fields1 = std::make_tuple(*me->drvPath, me->FIELD); \ + me = &other; \ + auto fields2 = std::make_tuple(*me->drvPath, me->FIELD); \ + return fields1 COMPARATOR fields2; \ } - return res; +#define CMP(CHILD_TYPE, MY_TYPE, FIELD) \ + CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, ==) \ + CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, !=) \ + CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, <) + +#define FIELD_TYPE std::pair +CMP(SingleBuiltPath, SingleBuiltPathBuilt, output) +#undef FIELD_TYPE + +#define FIELD_TYPE std::map +CMP(SingleBuiltPath, BuiltPathBuilt, outputs) +#undef FIELD_TYPE + +#undef CMP +#undef CMP_ONE + +StorePath SingleBuiltPath::outPath() const +{ + return std::visit( + overloaded{ + [](const SingleBuiltPath::Opaque & p) { return p.path; }, + [](const SingleBuiltPath::Built & b) { return b.output.second; }, + }, raw() + ); } StorePathSet BuiltPath::outPaths() const @@ -32,6 +58,40 @@ StorePathSet BuiltPath::outPaths() const ); } +nlohmann::json BuiltPath::Built::toJSON(const Store & store) const +{ + nlohmann::json res; + res["drvPath"] = drvPath->toJSON(store); + for (const auto & [outputName, outputPath] : outputs) { + res["outputs"][outputName] = store.printStorePath(outputPath); + } + return res; +} + +nlohmann::json SingleBuiltPath::Built::toJSON(const Store & store) const +{ + nlohmann::json res; + res["drvPath"] = drvPath->toJSON(store); + auto & [outputName, outputPath] = output; + res["output"] = outputName; + res["outputPath"] = store.printStorePath(outputPath); + return res; +} + +nlohmann::json SingleBuiltPath::toJSON(const Store & store) const +{ + return std::visit([&](const auto & buildable) { + return buildable.toJSON(store); + }, raw()); +} + +nlohmann::json BuiltPath::toJSON(const Store & store) const +{ + return std::visit([&](const auto & buildable) { + return buildable.toJSON(store); + }, raw()); +} + RealisedPath::Set BuiltPath::toRealisedPaths(Store & store) const { RealisedPath::Set res; @@ -40,7 +100,7 @@ RealisedPath::Set BuiltPath::toRealisedPaths(Store & store) const [&](const BuiltPath::Opaque & p) { res.insert(p.path); }, [&](const BuiltPath::Built & p) { auto drvHashes = - staticOutputHashes(store, store.readDerivation(p.drvPath)); + staticOutputHashes(store, store.readDerivation(p.drvPath->outPath())); for (auto& [outputName, outputPath] : p.outputs) { if (experimentalFeatureSettings.isEnabled( Xp::CaDerivations)) { @@ -48,7 +108,7 @@ RealisedPath::Set BuiltPath::toRealisedPaths(Store & store) const if (!drvOutput) throw Error( "the derivation '%s' has unrealised output '%s' (derived-path.cc/toRealisedPaths)", - store.printStorePath(p.drvPath), outputName); + store.printStorePath(p.drvPath->outPath()), outputName); auto thisRealisation = store.queryRealisation( DrvOutput{*drvOutput, outputName}); assert(thisRealisation); // We’ve built it, so we must diff --git a/src/libcmd/built-path.hh b/src/libcmd/built-path.hh index 744e8090b..747bcc440 100644 --- a/src/libcmd/built-path.hh +++ b/src/libcmd/built-path.hh @@ -3,19 +3,60 @@ namespace nix { +struct SingleBuiltPath; + +struct SingleBuiltPathBuilt { + ref drvPath; + std::pair output; + + std::string to_string(const Store & store) const; + static SingleBuiltPathBuilt parse(const Store & store, std::string_view, std::string_view); + nlohmann::json toJSON(const Store & store) const; + + DECLARE_CMP(SingleBuiltPathBuilt); +}; + +using _SingleBuiltPathRaw = std::variant< + DerivedPathOpaque, + SingleBuiltPathBuilt +>; + +struct SingleBuiltPath : _SingleBuiltPathRaw { + using Raw = _SingleBuiltPathRaw; + using Raw::Raw; + + using Opaque = DerivedPathOpaque; + using Built = SingleBuiltPathBuilt; + + inline const Raw & raw() const { + return static_cast(*this); + } + + StorePath outPath() const; + + static SingleBuiltPath parse(const Store & store, std::string_view); + nlohmann::json toJSON(const Store & store) const; +}; + +static inline ref staticDrv(StorePath drvPath) +{ + return make_ref(SingleBuiltPath::Opaque { drvPath }); +} + /** * A built derived path with hints in the form of optional concrete output paths. * * See 'BuiltPath' for more an explanation. */ struct BuiltPathBuilt { - StorePath drvPath; + ref drvPath; std::map outputs; - nlohmann::json toJSON(ref store) const; - static BuiltPathBuilt parse(const Store & store, std::string_view); + std::string to_string(const Store & store) const; + static BuiltPathBuilt parse(const Store & store, std::string_view, std::string_view); + nlohmann::json toJSON(const Store & store) const; - GENERATE_CMP(BuiltPathBuilt, me->drvPath, me->outputs); + DECLARE_CMP(BuiltPathBuilt); }; using _BuiltPathRaw = std::variant< @@ -41,6 +82,7 @@ struct BuiltPath : _BuiltPathRaw { StorePathSet outPaths() const; RealisedPath::Set toRealisedPaths(Store & store) const; + nlohmann::json toJSON(const Store & store) const; }; typedef std::vector BuiltPaths; diff --git a/src/libcmd/installable-attr-path.cc b/src/libcmd/installable-attr-path.cc index b35ca2910..2f89eee02 100644 --- a/src/libcmd/installable-attr-path.cc +++ b/src/libcmd/installable-attr-path.cc @@ -92,7 +92,7 @@ DerivedPathsWithInfo InstallableAttrPath::toDerivedPaths() for (auto & [drvPath, outputs] : byDrvPath) res.push_back({ .path = DerivedPath::Built { - .drvPath = drvPath, + .drvPath = makeConstantStorePathRef(drvPath), .outputs = outputs, }, .info = make_ref(ExtraPathInfoValue::Value { diff --git a/src/libcmd/installable-derived-path.cc b/src/libcmd/installable-derived-path.cc index 6ecf54b7c..b45641e8a 100644 --- a/src/libcmd/installable-derived-path.cc +++ b/src/libcmd/installable-derived-path.cc @@ -18,14 +18,7 @@ DerivedPathsWithInfo InstallableDerivedPath::toDerivedPaths() std::optional InstallableDerivedPath::getStorePath() { - return std::visit(overloaded { - [&](const DerivedPath::Built & bfd) { - return bfd.drvPath; - }, - [&](const DerivedPath::Opaque & bo) { - return bo.path; - }, - }, derivedPath.raw()); + return derivedPath.getBaseStorePath(); } InstallableDerivedPath InstallableDerivedPath::parse( @@ -42,7 +35,7 @@ InstallableDerivedPath InstallableDerivedPath::parse( // Remove this prior to stabilizing the new CLI. if (storePath.isDerivation()) { auto oldDerivedPath = DerivedPath::Built { - .drvPath = storePath, + .drvPath = makeConstantStorePathRef(storePath), .outputs = OutputsSpec::All { }, }; warn( @@ -55,8 +48,10 @@ InstallableDerivedPath InstallableDerivedPath::parse( }, // If the user did use ^, we just do exactly what is written. [&](const ExtendedOutputsSpec::Explicit & outputSpec) -> DerivedPath { + auto drv = make_ref(SingleDerivedPath::parse(*store, prefix)); + drvRequireExperiment(*drv); return DerivedPath::Built { - .drvPath = store->parseStorePath(prefix), + .drvPath = std::move(drv), .outputs = outputSpec, }; }, diff --git a/src/libcmd/installable-flake.cc b/src/libcmd/installable-flake.cc index 4da9b131b..1b169c3bd 100644 --- a/src/libcmd/installable-flake.cc +++ b/src/libcmd/installable-flake.cc @@ -118,7 +118,7 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths() return {{ .path = DerivedPath::Built { - .drvPath = std::move(drvPath), + .drvPath = makeConstantStorePathRef(std::move(drvPath)), .outputs = std::visit(overloaded { [&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec { std::set outputsToInstall; diff --git a/src/libcmd/installable-value.cc b/src/libcmd/installable-value.cc index 1eff293cc..08ad35105 100644 --- a/src/libcmd/installable-value.cc +++ b/src/libcmd/installable-value.cc @@ -55,7 +55,8 @@ std::optional InstallableValue::trySinglePathToDerivedPaths else if (v.type() == nString) { return {{ - .path = state->coerceToDerivedPath(pos, v, errorCtx), + .path = DerivedPath::fromSingle( + state->coerceToSingleDerivedPath(pos, v, errorCtx)), .info = make_ref(), }}; } diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 9d593a01f..4c7d134ec 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -515,6 +515,30 @@ ref SourceExprCommand::parseInstallable( return installables.front(); } +static SingleBuiltPath getBuiltPath(ref evalStore, ref store, const SingleDerivedPath & b) +{ + return std::visit( + overloaded{ + [&](const SingleDerivedPath::Opaque & bo) -> SingleBuiltPath { + return SingleBuiltPath::Opaque { bo.path }; + }, + [&](const SingleDerivedPath::Built & bfd) -> SingleBuiltPath { + auto drvPath = getBuiltPath(evalStore, store, *bfd.drvPath); + // Resolving this instead of `bfd` will yield the same result, but avoid duplicative work. + SingleDerivedPath::Built truncatedBfd { + .drvPath = makeConstantStorePathRef(drvPath.outPath()), + .output = bfd.output, + }; + auto outputPath = resolveDerivedPath(*store, truncatedBfd, &*evalStore); + return SingleBuiltPath::Built { + .drvPath = make_ref(std::move(drvPath)), + .output = { bfd.output, outputPath }, + }; + }, + }, + b.raw()); +} + std::vector Installable::build( ref evalStore, ref store, @@ -568,7 +592,10 @@ std::vector, BuiltPathWithResult>> Installable::build [&](const DerivedPath::Built & bfd) { auto outputs = resolveDerivedPath(*store, bfd, &*evalStore); res.push_back({aux.installable, { - .path = BuiltPath::Built { bfd.drvPath, outputs }, + .path = BuiltPath::Built { + .drvPath = make_ref(getBuiltPath(evalStore, store, *bfd.drvPath)), + .outputs = outputs, + }, .info = aux.info}}); }, [&](const DerivedPath::Opaque & bo) { @@ -597,7 +624,10 @@ std::vector, BuiltPathWithResult>> Installable::build for (auto & [outputName, realisation] : buildResult.builtOutputs) outputs.emplace(outputName, realisation.outPath); res.push_back({aux.installable, { - .path = BuiltPath::Built { bfd.drvPath, outputs }, + .path = BuiltPath::Built { + .drvPath = make_ref(getBuiltPath(evalStore, store, *bfd.drvPath)), + .outputs = outputs, + }, .info = aux.info, .result = buildResult}}); }, @@ -691,7 +721,7 @@ StorePathSet Installable::toDerivations( : throw Error("argument '%s' did not evaluate to a derivation", i->what())); }, [&](const DerivedPath::Built & bfd) { - drvPaths.insert(bfd.drvPath); + drvPaths.insert(resolveDerivedPath(*store, *bfd.drvPath)); }, }, b.path.raw()); diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index d15162e76..ea7d0e2ec 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -648,7 +648,7 @@ bool NixRepl::processLine(std::string line) if (command == ":b" || command == ":bl") { state->store->buildPaths({ DerivedPath::Built { - .drvPath = drvPath, + .drvPath = makeConstantStorePathRef(drvPath), .outputs = OutputsSpec::All { }, }, }); diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index 9e734e654..6a60ba87b 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -599,7 +599,7 @@ string_t AttrCursor::getStringWithContext() return d.drvPath; }, [&](const NixStringContextElem::Built & b) -> const StorePath & { - return b.drvPath; + return b.drvPath->getBaseStorePath(); }, [&](const NixStringContextElem::Opaque & o) -> const StorePath & { return o.path; diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index a8e6baea6..3e521b732 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1042,7 +1042,7 @@ void EvalState::mkOutputString( : DownstreamPlaceholder::unknownCaOutput(drvPath, outputName, xpSettings).render(), NixStringContext { NixStringContextElem::Built { - .drvPath = drvPath, + .drvPath = makeConstantStorePathRef(drvPath), .output = outputName, } }); @@ -2299,7 +2299,7 @@ StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringCon } -std::pair EvalState::coerceToDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx) +std::pair EvalState::coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx) { NixStringContext context; auto s = forceString(v, context, pos, errorCtx); @@ -2310,21 +2310,16 @@ std::pair EvalState::coerceToDerivedPathUnchecked s, csize) .withTrace(pos, errorCtx).debugThrow(); auto derivedPath = std::visit(overloaded { - [&](NixStringContextElem::Opaque && o) -> DerivedPath { - return DerivedPath::Opaque { - .path = std::move(o.path), - }; + [&](NixStringContextElem::Opaque && o) -> SingleDerivedPath { + return std::move(o); }, - [&](NixStringContextElem::DrvDeep &&) -> DerivedPath { + [&](NixStringContextElem::DrvDeep &&) -> SingleDerivedPath { error( "string '%s' has a context which refers to a complete source and binary closure. This is not supported at this time", s).withTrace(pos, errorCtx).debugThrow(); }, - [&](NixStringContextElem::Built && b) -> DerivedPath { - return DerivedPath::Built { - .drvPath = std::move(b.drvPath), - .outputs = OutputsSpec::Names { std::move(b.output) }, - }; + [&](NixStringContextElem::Built && b) -> SingleDerivedPath { + return std::move(b); }, }, ((NixStringContextElem &&) *context.begin()).raw()); return { @@ -2334,12 +2329,12 @@ std::pair EvalState::coerceToDerivedPathUnchecked } -DerivedPath EvalState::coerceToDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx) +SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx) { - auto [derivedPath, s_] = coerceToDerivedPathUnchecked(pos, v, errorCtx); + auto [derivedPath, s_] = coerceToSingleDerivedPathUnchecked(pos, v, errorCtx); auto s = s_; std::visit(overloaded { - [&](const DerivedPath::Opaque & o) { + [&](const SingleDerivedPath::Opaque & o) { auto sExpected = store->printStorePath(o.path); if (s != sExpected) error( @@ -2347,25 +2342,27 @@ DerivedPath EvalState::coerceToDerivedPath(const PosIdx pos, Value & v, std::str s, sExpected) .withTrace(pos, errorCtx).debugThrow(); }, - [&](const DerivedPath::Built & b) { - // TODO need derived path with single output to make this - // total. Will add as part of RFC 92 work and then this is - // cleaned up. - auto output = *std::get(b.outputs).begin(); - - auto drv = store->readDerivation(b.drvPath); - auto i = drv.outputs.find(output); - if (i == drv.outputs.end()) - throw Error("derivation '%s' does not have output '%s'", store->printStorePath(b.drvPath), output); - auto optOutputPath = i->second.path(*store, drv.name, output); - // This is testing for the case of CA derivations - auto sExpected = optOutputPath - ? store->printStorePath(*optOutputPath) - : DownstreamPlaceholder::unknownCaOutput(b.drvPath, output).render(); + [&](const SingleDerivedPath::Built & b) { + auto sExpected = std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) { + auto drv = store->readDerivation(o.path); + auto i = drv.outputs.find(b.output); + if (i == drv.outputs.end()) + throw Error("derivation '%s' does not have output '%s'", b.drvPath->to_string(*store), b.output); + auto optOutputPath = i->second.path(*store, drv.name, b.output); + // This is testing for the case of CA derivations + return optOutputPath + ? store->printStorePath(*optOutputPath) + : DownstreamPlaceholder::fromSingleDerivedPathBuilt(b).render(); + }, + [&](const SingleDerivedPath::Built & o) { + return DownstreamPlaceholder::fromSingleDerivedPathBuilt(b).render(); + }, + }, b.drvPath->raw()); if (s != sExpected) error( "string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'", - s, output, store->printStorePath(b.drvPath), sExpected) + s, b.output, b.drvPath->to_string(*store), sExpected) .withTrace(pos, errorCtx).debugThrow(); } }, derivedPath.raw()); diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 29d0f05a1..0268a2a12 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -22,7 +22,7 @@ namespace nix { class Store; class EvalState; class StorePath; -struct DerivedPath; +struct SingleDerivedPath; enum RepairFlag : bool; @@ -532,12 +532,12 @@ public: StorePath coerceToStorePath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx); /** - * Part of `coerceToDerivedPath()` without any store IO which is exposed for unit testing only. + * Part of `coerceToSingleDerivedPath()` without any store IO which is exposed for unit testing only. */ - std::pair coerceToDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx); + std::pair coerceToSingleDerivedPathUnchecked(const PosIdx pos, Value & v, std::string_view errorCtx); /** - * Coerce to `DerivedPath`. + * Coerce to `SingleDerivedPath`. * * Must be a string which is either a literal store path or a * "placeholder (see `DownstreamPlaceholder`). @@ -551,7 +551,7 @@ public: * source of truth, and ultimately tells us what we want, and then * we ensure the string corresponds to it. */ - DerivedPath coerceToDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx); + SingleDerivedPath coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx); public: diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 430607214..54943b481 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -56,7 +56,7 @@ StringMap EvalState::realiseContext(const NixStringContext & context) .drvPath = b.drvPath, .outputs = OutputsSpec::Names { b.output }, }); - ensureValid(b.drvPath); + ensureValid(b.drvPath->getBaseStorePath()); }, [&](const NixStringContextElem::Opaque & o) { auto ctxS = store->printStorePath(o.path); @@ -77,7 +77,7 @@ StringMap EvalState::realiseContext(const NixStringContext & context) if (!evalSettings.enableImportFromDerivation) debugThrowLastTrace(Error( "cannot build '%1%' during evaluation because the option 'allow-import-from-derivation' is disabled", - store->printStorePath(drvs.begin()->drvPath))); + drvs.begin()->to_string(*store))); /* Build/substitute the context. */ std::vector buildReqs; @@ -95,7 +95,11 @@ StringMap EvalState::realiseContext(const NixStringContext & context) /* Get all the output paths corresponding to the placeholders we had */ if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { res.insert_or_assign( - DownstreamPlaceholder::unknownCaOutput(drv.drvPath, outputName).render(), + DownstreamPlaceholder::fromSingleDerivedPathBuilt( + SingleDerivedPath::Built { + .drvPath = drv.drvPath, + .output = outputName, + }).render(), store->printStorePath(outputPath) ); } @@ -1251,7 +1255,10 @@ drvName, Bindings * attrs, Value & v) } }, [&](const NixStringContextElem::Built & b) { - drv.inputDrvs[b.drvPath].insert(b.output); + if (auto * p = std::get_if(&*b.drvPath)) + drv.inputDrvs[p->path].insert(b.output); + else + throw UnimplementedError("Dependencies on the outputs of dynamic derivations are not yet supported"); }, [&](const NixStringContextElem::Opaque & o) { drv.inputSrcs.insert(o.path); diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc index 8b3468009..bfc731744 100644 --- a/src/libexpr/primops/context.cc +++ b/src/libexpr/primops/context.cc @@ -106,7 +106,10 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args, contextInfos[std::move(d.drvPath)].allOutputs = true; }, [&](NixStringContextElem::Built && b) { - contextInfos[std::move(b.drvPath)].outputs.emplace_back(std::move(b.output)); + // FIXME should eventually show string context as is, no + // resolving here. + auto drvPath = resolveDerivedPath(*state.store, *b.drvPath); + contextInfos[std::move(drvPath)].outputs.emplace_back(std::move(b.output)); }, [&](NixStringContextElem::Opaque && o) { contextInfos[std::move(o.path)].path = true; @@ -222,7 +225,7 @@ static void prim_appendContext(EvalState & state, const PosIdx pos, Value * * ar for (auto elem : iter->value->listItems()) { auto outputName = state.forceStringNoCtx(*elem, iter->pos, "while evaluating an output name within a string context"); context.emplace(NixStringContextElem::Built { - .drvPath = namePath, + .drvPath = makeConstantStorePathRef(namePath), .output = std::string { outputName }, }); } diff --git a/src/libexpr/tests/derived-path.cc b/src/libexpr/tests/derived-path.cc index c713fe28a..2a5ca64f6 100644 --- a/src/libexpr/tests/derived-path.cc +++ b/src/libexpr/tests/derived-path.cc @@ -21,12 +21,12 @@ TEST_F(DerivedPathExpressionTest, force_init) RC_GTEST_FIXTURE_PROP( DerivedPathExpressionTest, prop_opaque_path_round_trip, - (const DerivedPath::Opaque & o)) + (const SingleDerivedPath::Opaque & o)) { auto * v = state.allocValue(); state.mkStorePathString(o.path, *v); - auto d = state.coerceToDerivedPath(noPos, *v, ""); - RC_ASSERT(DerivedPath { o } == d); + auto d = state.coerceToSingleDerivedPath(noPos, *v, ""); + RC_ASSERT(SingleDerivedPath { o } == d); } // TODO use DerivedPath::Built for parameter once it supports a single output @@ -46,12 +46,12 @@ RC_GTEST_FIXTURE_PROP( auto * v = state.allocValue(); state.mkOutputString(*v, drvPath, outputName.name, std::nullopt, mockXpSettings); - auto [d, _] = state.coerceToDerivedPathUnchecked(noPos, *v, ""); - DerivedPath::Built b { - .drvPath = drvPath, - .outputs = OutputsSpec::Names { outputName.name }, + auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, ""); + SingleDerivedPath::Built b { + .drvPath = makeConstantStorePathRef(drvPath), + .output = outputName.name, }; - RC_ASSERT(DerivedPath { b } == d); + RC_ASSERT(SingleDerivedPath { b } == d); } RC_GTEST_FIXTURE_PROP( @@ -61,12 +61,12 @@ RC_GTEST_FIXTURE_PROP( { auto * v = state.allocValue(); state.mkOutputString(*v, drvPath, outputName.name, outPath); - auto [d, _] = state.coerceToDerivedPathUnchecked(noPos, *v, ""); - DerivedPath::Built b { - .drvPath = drvPath, - .outputs = OutputsSpec::Names { outputName.name }, + auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, ""); + SingleDerivedPath::Built b { + .drvPath = makeConstantStorePathRef(drvPath), + .output = outputName.name, }; - RC_ASSERT(DerivedPath { b } == d); + RC_ASSERT(SingleDerivedPath { b } == d); } } /* namespace nix */ diff --git a/src/libexpr/tests/value/context.cc b/src/libexpr/tests/value/context.cc index 0d9381577..c56b50b59 100644 --- a/src/libexpr/tests/value/context.cc +++ b/src/libexpr/tests/value/context.cc @@ -8,6 +8,8 @@ namespace nix { +// Test a few cases of invalid string context elements. + TEST(NixStringContextElemTest, empty_invalid) { EXPECT_THROW( NixStringContextElem::parse(""), @@ -38,6 +40,10 @@ TEST(NixStringContextElemTest, slash_invalid) { BadStorePath); } +/** + * Round trip (string <-> data structure) test for + * `NixStringContextElem::Opaque`. + */ TEST(NixStringContextElemTest, opaque) { std::string_view opaque = "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x"; auto elem = NixStringContextElem::parse(opaque); @@ -47,6 +53,10 @@ TEST(NixStringContextElemTest, opaque) { ASSERT_EQ(elem.to_string(), opaque); } +/** + * Round trip (string <-> data structure) test for + * `NixStringContextElem::DrvDeep`. + */ TEST(NixStringContextElemTest, drvDeep) { std::string_view drvDeep = "=g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; auto elem = NixStringContextElem::parse(drvDeep); @@ -56,28 +66,62 @@ TEST(NixStringContextElemTest, drvDeep) { ASSERT_EQ(elem.to_string(), drvDeep); } -TEST(NixStringContextElemTest, built) { +/** + * Round trip (string <-> data structure) test for a simpler + * `NixStringContextElem::Built`. + */ +TEST(NixStringContextElemTest, built_opaque) { std::string_view built = "!foo!g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; auto elem = NixStringContextElem::parse(built); auto * p = std::get_if(&elem); ASSERT_TRUE(p); ASSERT_EQ(p->output, "foo"); - ASSERT_EQ(p->drvPath, StorePath { built.substr(5) }); + ASSERT_EQ(*p->drvPath, ((SingleDerivedPath) SingleDerivedPath::Opaque { + .path = StorePath { built.substr(5) }, + })); ASSERT_EQ(elem.to_string(), built); } +/** + * Round trip (string <-> data structure) test for a more complex, + * inductive `NixStringContextElem::Built`. + */ +TEST(NixStringContextElemTest, built_built) { + /** + * We set these in tests rather than the regular globals so we don't have + * to worry about race conditions if the tests run concurrently. + */ + ExperimentalFeatureSettings mockXpSettings; + mockXpSettings.set("experimental-features", "dynamic-derivations ca-derivations"); + + std::string_view built = "!foo!bar!g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; + auto elem = NixStringContextElem::parse(built, mockXpSettings); + auto * p = std::get_if(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->output, "foo"); + auto * drvPath = std::get_if(&*p->drvPath); + ASSERT_TRUE(drvPath); + ASSERT_EQ(drvPath->output, "bar"); + ASSERT_EQ(*drvPath->drvPath, ((SingleDerivedPath) SingleDerivedPath::Opaque { + .path = StorePath { built.substr(9) }, + })); + ASSERT_EQ(elem.to_string(), built); +} + +/** + * Without the right experimental features enabled, we cannot parse a + * complex inductive string context element. + */ +TEST(NixStringContextElemTest, built_built_xp) { + ASSERT_THROW( + NixStringContextElem::parse("!foo!bar!g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"), MissingExperimentalFeature); +} + } namespace rc { using namespace nix; -Gen Arbitrary::arbitrary() -{ - return gen::just(NixStringContextElem::Opaque { - .path = *gen::arbitrary(), - }); -} - Gen Arbitrary::arbitrary() { return gen::just(NixStringContextElem::DrvDeep { @@ -85,14 +129,6 @@ Gen Arbitrary::arb }); } -Gen Arbitrary::arbitrary() -{ - return gen::just(NixStringContextElem::Built { - .drvPath = *gen::arbitrary(), - .output = (*gen::arbitrary()).name, - }); -} - Gen Arbitrary::arbitrary() { switch (*gen::inRange(0, std::variant_size_v)) { diff --git a/src/libexpr/value/context.cc b/src/libexpr/value/context.cc index f76fc76e4..d8116011e 100644 --- a/src/libexpr/value/context.cc +++ b/src/libexpr/value/context.cc @@ -4,29 +4,52 @@ namespace nix { -NixStringContextElem NixStringContextElem::parse(std::string_view s0) +NixStringContextElem NixStringContextElem::parse( + std::string_view s0, + const ExperimentalFeatureSettings & xpSettings) { std::string_view s = s0; + std::function parseRest; + parseRest = [&]() -> SingleDerivedPath { + // Case on whether there is a '!' + size_t index = s.find("!"); + if (index == std::string_view::npos) { + return SingleDerivedPath::Opaque { + .path = StorePath { s }, + }; + } else { + std::string output { s.substr(0, index) }; + // Advance string to parse after the '!' + s = s.substr(index + 1); + auto drv = make_ref(parseRest()); + drvRequireExperiment(*drv, xpSettings); + return SingleDerivedPath::Built { + .drvPath = std::move(drv), + .output = std::move(output), + }; + } + }; + if (s.size() == 0) { throw BadNixStringContextElem(s0, "String context element should never be an empty string"); } + switch (s.at(0)) { case '!': { - s = s.substr(1); // advance string to parse after first ! - size_t index = s.find("!"); - // This makes index + 1 safe. Index can be the length (one after index - // of last character), so given any valid character index --- a - // successful find --- we can add one. - if (index == std::string_view::npos) { + // Advance string to parse after the '!' + s = s.substr(1); + + // Find *second* '!' + if (s.find("!") == std::string_view::npos) { throw BadNixStringContextElem(s0, "String content element beginning with '!' should have a second '!'"); } - return NixStringContextElem::Built { - .drvPath = StorePath { s.substr(index + 1) }, - .output = std::string(s.substr(0, index)), - }; + + return std::visit( + [&](auto x) -> NixStringContextElem { return std::move(x); }, + parseRest()); } case '=': { return NixStringContextElem::DrvDeep { @@ -34,33 +57,51 @@ NixStringContextElem NixStringContextElem::parse(std::string_view s0) }; } default: { - return NixStringContextElem::Opaque { - .path = StorePath { s }, - }; + // Ensure no '!' + if (s.find("!") != std::string_view::npos) { + throw BadNixStringContextElem(s0, + "String content element not beginning with '!' should not have a second '!'"); + } + return std::visit( + [&](auto x) -> NixStringContextElem { return std::move(x); }, + parseRest()); } } } -std::string NixStringContextElem::to_string() const { - return std::visit(overloaded { +std::string NixStringContextElem::to_string() const +{ + std::string res; + + std::function toStringRest; + toStringRest = [&](auto & p) { + std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) { + res += o.path.to_string(); + }, + [&](const SingleDerivedPath::Built & o) { + res += o.output; + res += '!'; + toStringRest(*o.drvPath); + }, + }, p.raw()); + }; + + std::visit(overloaded { [&](const NixStringContextElem::Built & b) { - std::string res; res += '!'; - res += b.output; - res += '!'; - res += b.drvPath.to_string(); - return res; - }, - [&](const NixStringContextElem::DrvDeep & d) { - std::string res; - res += '='; - res += d.drvPath.to_string(); - return res; + toStringRest(b); }, [&](const NixStringContextElem::Opaque & o) { - return std::string { o.path.to_string() }; + toStringRest(o); + }, + [&](const NixStringContextElem::DrvDeep & d) { + res += '='; + res += d.drvPath.to_string(); }, }, raw()); + + return res; } } diff --git a/src/libexpr/value/context.hh b/src/libexpr/value/context.hh index 287ae08a9..a1b71695b 100644 --- a/src/libexpr/value/context.hh +++ b/src/libexpr/value/context.hh @@ -3,7 +3,7 @@ #include "util.hh" #include "comparator.hh" -#include "path.hh" +#include "derived-path.hh" #include @@ -31,11 +31,7 @@ public: * * Encoded as just the path: ‘’. */ -struct NixStringContextElem_Opaque { - StorePath path; - - GENERATE_CMP(NixStringContextElem_Opaque, me->path); -}; +typedef SingleDerivedPath::Opaque NixStringContextElem_Opaque; /** * Path to a derivation and its entire build closure. @@ -57,12 +53,7 @@ struct NixStringContextElem_DrvDeep { * * Encoded in the form ‘!!’. */ -struct NixStringContextElem_Built { - StorePath drvPath; - std::string output; - - GENERATE_CMP(NixStringContextElem_Built, me->drvPath, me->output); -}; +typedef SingleDerivedPath::Built NixStringContextElem_Built; using _NixStringContextElem_Raw = std::variant< NixStringContextElem_Opaque, @@ -93,8 +84,12 @@ struct NixStringContextElem : _NixStringContextElem_Raw { * - ‘’ * - ‘=’ * - ‘!!’ + * + * @param xpSettings Stop-gap to avoid globals during unit tests. */ - static NixStringContextElem parse(std::string_view s); + static NixStringContextElem parse( + std::string_view s, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); std::string to_string() const; }; diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 5e37f7ecb..8bdf2f367 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -65,7 +65,7 @@ namespace nix { DerivationGoal::DerivationGoal(const StorePath & drvPath, const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) - : Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs }) + : Goal(worker, DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath), .outputs = wantedOutputs }) , useDerivation(true) , drvPath(drvPath) , wantedOutputs(wantedOutputs) @@ -74,7 +74,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, state = &DerivationGoal::getDerivation; name = fmt( "building of '%s' from .drv file", - DerivedPath::Built { drvPath, wantedOutputs }.to_string(worker.store)); + DerivedPath::Built { makeConstantStorePathRef(drvPath), wantedOutputs }.to_string(worker.store)); trace("created"); mcExpectedBuilds = std::make_unique>(worker.expectedBuilds); @@ -84,7 +84,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) - : Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs }) + : Goal(worker, DerivedPath::Built { .drvPath = makeConstantStorePathRef(drvPath), .outputs = wantedOutputs }) , useDerivation(false) , drvPath(drvPath) , wantedOutputs(wantedOutputs) @@ -95,7 +95,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation state = &DerivationGoal::haveDerivation; name = fmt( "building of '%s' from in-memory derivation", - DerivedPath::Built { drvPath, drv.outputNames() }.to_string(worker.store)); + DerivedPath::Built { makeConstantStorePathRef(drvPath), drv.outputNames() }.to_string(worker.store)); trace("created"); mcExpectedBuilds = std::make_unique>(worker.expectedBuilds); @@ -1490,7 +1490,7 @@ void DerivationGoal::waiteeDone(GoalPtr waitee, ExitCode result) for (auto & outputName : outputs->second) { auto buildResult = dg->getBuildResult(DerivedPath::Built { - .drvPath = dg->drvPath, + .drvPath = makeConstantStorePathRef(dg->drvPath), .outputs = OutputsSpec::Names { outputName }, }); if (buildResult.success()) { diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index 4aa4d6dca..e941b4e65 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -77,7 +77,7 @@ BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivat try { worker.run(Goals{goal}); return goal->getBuildResult(DerivedPath::Built { - .drvPath = drvPath, + .drvPath = makeConstantStorePathRef(drvPath), .outputs = OutputsSpec::All {}, }); } catch (Error & e) { diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 8b7fbdcda..920097680 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1172,6 +1172,19 @@ void LocalDerivationGoal::writeStructuredAttrs() } +static StorePath pathPartOfReq(const SingleDerivedPath & req) +{ + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & bo) { + return bo.path; + }, + [&](const SingleDerivedPath::Built & bfd) { + return pathPartOfReq(*bfd.drvPath); + }, + }, req.raw()); +} + + static StorePath pathPartOfReq(const DerivedPath & req) { return std::visit(overloaded { @@ -1179,7 +1192,7 @@ static StorePath pathPartOfReq(const DerivedPath & req) return bo.path; }, [&](const DerivedPath::Built & bfd) { - return bfd.drvPath; + return pathPartOfReq(*bfd.drvPath); }, }, req.raw()); } diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index a9ca9cbbc..6779dbcf3 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -111,7 +111,10 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) { return std::visit(overloaded { [&](const DerivedPath::Built & bfd) -> GoalPtr { - return makeDerivationGoal(bfd.drvPath, bfd.outputs, buildMode); + if (auto bop = std::get_if(&*bfd.drvPath)) + return makeDerivationGoal(bop->path, bfd.outputs, buildMode); + else + throw UnimplementedError("Building dynamic derivations in one shot is not yet implemented."); }, [&](const DerivedPath::Opaque & bo) -> GoalPtr { return makePathSubstitutionGoal(bo.path, buildMode == bmRepair ? Repair : NoRepair); @@ -265,7 +268,7 @@ void Worker::run(const Goals & _topGoals) for (auto & i : _topGoals) { topGoals.insert(i); if (auto goal = dynamic_cast(i.get())) { - topPaths.push_back(DerivedPath::Built{goal->drvPath, goal->wantedOutputs}); + topPaths.push_back(DerivedPath::Built{makeConstantStorePathRef(goal->drvPath), goal->wantedOutputs}); } else if (auto goal = dynamic_cast(i.get())) { topPaths.push_back(DerivedPath::Opaque{goal->storePath}); } diff --git a/src/libstore/derived-path.cc b/src/libstore/derived-path.cc index 52d073f81..3594b7570 100644 --- a/src/libstore/derived-path.cc +++ b/src/libstore/derived-path.cc @@ -7,52 +7,133 @@ namespace nix { -nlohmann::json DerivedPath::Opaque::toJSON(ref store) const { +#define CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, COMPARATOR) \ + bool MY_TYPE ::operator COMPARATOR (const MY_TYPE & other) const \ + { \ + const MY_TYPE* me = this; \ + auto fields1 = std::make_tuple(*me->drvPath, me->FIELD); \ + me = &other; \ + auto fields2 = std::make_tuple(*me->drvPath, me->FIELD); \ + return fields1 COMPARATOR fields2; \ + } +#define CMP(CHILD_TYPE, MY_TYPE, FIELD) \ + CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, ==) \ + CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, !=) \ + CMP_ONE(CHILD_TYPE, MY_TYPE, FIELD, <) + +#define FIELD_TYPE std::string +CMP(SingleDerivedPath, SingleDerivedPathBuilt, output) +#undef FIELD_TYPE + +#define FIELD_TYPE OutputsSpec +CMP(SingleDerivedPath, DerivedPathBuilt, outputs) +#undef FIELD_TYPE + +#undef CMP +#undef CMP_ONE + +nlohmann::json DerivedPath::Opaque::toJSON(const Store & store) const +{ + return store.printStorePath(path); +} + +nlohmann::json SingleDerivedPath::Built::toJSON(Store & store) const { nlohmann::json res; - res["path"] = store->printStorePath(path); + res["drvPath"] = drvPath->toJSON(store); + // Fallback for the input-addressed derivation case: We expect to always be + // able to print the output paths, so let’s do it + // FIXME try-resolve on drvPath + const auto outputMap = store.queryPartialDerivationOutputMap(resolveDerivedPath(store, *drvPath)); + res["output"] = output; + auto outputPathIter = outputMap.find(output); + if (outputPathIter == outputMap.end()) + res["outputPath"] = nullptr; + else if (std::optional p = outputPathIter->second) + res["outputPath"] = store.printStorePath(*p); + else + res["outputPath"] = nullptr; return res; } -nlohmann::json DerivedPath::Built::toJSON(ref store) const { +nlohmann::json DerivedPath::Built::toJSON(Store & store) const { nlohmann::json res; - res["drvPath"] = store->printStorePath(drvPath); + res["drvPath"] = drvPath->toJSON(store); // Fallback for the input-addressed derivation case: We expect to always be // able to print the output paths, so let’s do it - const auto outputMap = store->queryPartialDerivationOutputMap(drvPath); + // FIXME try-resolve on drvPath + const auto outputMap = store.queryPartialDerivationOutputMap(resolveDerivedPath(store, *drvPath)); for (const auto & [output, outputPathOpt] : outputMap) { if (!outputs.contains(output)) continue; if (outputPathOpt) - res["outputs"][output] = store->printStorePath(*outputPathOpt); + res["outputs"][output] = store.printStorePath(*outputPathOpt); else res["outputs"][output] = nullptr; } return res; } +nlohmann::json SingleDerivedPath::toJSON(Store & store) const +{ + return std::visit([&](const auto & buildable) { + return buildable.toJSON(store); + }, raw()); +} + +nlohmann::json DerivedPath::toJSON(Store & store) const +{ + return std::visit([&](const auto & buildable) { + return buildable.toJSON(store); + }, raw()); +} + std::string DerivedPath::Opaque::to_string(const Store & store) const { return store.printStorePath(path); } +std::string SingleDerivedPath::Built::to_string(const Store & store) const +{ + return drvPath->to_string(store) + "^" + output; +} + +std::string SingleDerivedPath::Built::to_string_legacy(const Store & store) const +{ + return drvPath->to_string(store) + "!" + output; +} + std::string DerivedPath::Built::to_string(const Store & store) const { - return store.printStorePath(drvPath) + return drvPath->to_string(store) + '^' + outputs.to_string(); } std::string DerivedPath::Built::to_string_legacy(const Store & store) const { - return store.printStorePath(drvPath) - + '!' + return drvPath->to_string_legacy(store) + + "!" + outputs.to_string(); } +std::string SingleDerivedPath::to_string(const Store & store) const +{ + return std::visit( + [&](const auto & req) { return req.to_string(store); }, + raw()); +} + std::string DerivedPath::to_string(const Store & store) const +{ + return std::visit( + [&](const auto & req) { return req.to_string(store); }, + raw()); +} + +std::string SingleDerivedPath::to_string_legacy(const Store & store) const { return std::visit(overloaded { - [&](const DerivedPath::Built & req) { return req.to_string(store); }, - [&](const DerivedPath::Opaque & req) { return req.to_string(store); }, + [&](const SingleDerivedPath::Built & req) { return req.to_string_legacy(store); }, + [&](const SingleDerivedPath::Opaque & req) { return req.to_string(store); }, }, this->raw()); } @@ -70,30 +151,156 @@ DerivedPath::Opaque DerivedPath::Opaque::parse(const Store & store, std::string_ return {store.parseStorePath(s)}; } -DerivedPath::Built DerivedPath::Built::parse(const Store & store, std::string_view drvS, std::string_view outputsS) +void drvRequireExperiment( + const SingleDerivedPath & drv, + const ExperimentalFeatureSettings & xpSettings) { + std::visit(overloaded { + [&](const SingleDerivedPath::Opaque &) { + // plain drv path; no experimental features required. + }, + [&](const SingleDerivedPath::Built &) { + xpSettings.require(Xp::DynamicDerivations); + }, + }, drv.raw()); +} + +SingleDerivedPath::Built SingleDerivedPath::Built::parse( + const Store & store, ref drv, + std::string_view output, + const ExperimentalFeatureSettings & xpSettings) +{ + drvRequireExperiment(*drv, xpSettings); return { - .drvPath = store.parseStorePath(drvS), + .drvPath = drv, + .output = std::string { output }, + }; +} + +DerivedPath::Built DerivedPath::Built::parse( + const Store & store, ref drv, + std::string_view outputsS, + const ExperimentalFeatureSettings & xpSettings) +{ + drvRequireExperiment(*drv, xpSettings); + return { + .drvPath = drv, .outputs = OutputsSpec::parse(outputsS), }; } -static inline DerivedPath parseWith(const Store & store, std::string_view s, std::string_view separator) +static SingleDerivedPath parseWithSingle( + const Store & store, std::string_view s, std::string_view separator, + const ExperimentalFeatureSettings & xpSettings) { - size_t n = s.find(separator); + size_t n = s.rfind(separator); + return n == s.npos + ? (SingleDerivedPath) SingleDerivedPath::Opaque::parse(store, s) + : (SingleDerivedPath) SingleDerivedPath::Built::parse(store, + make_ref(parseWithSingle( + store, + s.substr(0, n), + separator, + xpSettings)), + s.substr(n + 1), + xpSettings); +} + +SingleDerivedPath SingleDerivedPath::parse( + const Store & store, + std::string_view s, + const ExperimentalFeatureSettings & xpSettings) +{ + return parseWithSingle(store, s, "^", xpSettings); +} + +SingleDerivedPath SingleDerivedPath::parseLegacy( + const Store & store, + std::string_view s, + const ExperimentalFeatureSettings & xpSettings) +{ + return parseWithSingle(store, s, "!", xpSettings); +} + +static DerivedPath parseWith( + const Store & store, std::string_view s, std::string_view separator, + const ExperimentalFeatureSettings & xpSettings) +{ + size_t n = s.rfind(separator); return n == s.npos ? (DerivedPath) DerivedPath::Opaque::parse(store, s) - : (DerivedPath) DerivedPath::Built::parse(store, s.substr(0, n), s.substr(n + 1)); + : (DerivedPath) DerivedPath::Built::parse(store, + make_ref(parseWithSingle( + store, + s.substr(0, n), + separator, + xpSettings)), + s.substr(n + 1), + xpSettings); } -DerivedPath DerivedPath::parse(const Store & store, std::string_view s) +DerivedPath DerivedPath::parse( + const Store & store, + std::string_view s, + const ExperimentalFeatureSettings & xpSettings) { - return parseWith(store, s, "^"); + return parseWith(store, s, "^", xpSettings); } -DerivedPath DerivedPath::parseLegacy(const Store & store, std::string_view s) +DerivedPath DerivedPath::parseLegacy( + const Store & store, + std::string_view s, + const ExperimentalFeatureSettings & xpSettings) { - return parseWith(store, s, "!"); + return parseWith(store, s, "!", xpSettings); +} + +DerivedPath DerivedPath::fromSingle(const SingleDerivedPath & req) +{ + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) -> DerivedPath { + return o; + }, + [&](const SingleDerivedPath::Built & b) -> DerivedPath { + return DerivedPath::Built { + .drvPath = b.drvPath, + .outputs = OutputsSpec::Names { b.output }, + }; + }, + }, req.raw()); +} + +const StorePath & SingleDerivedPath::Built::getBaseStorePath() const +{ + return drvPath->getBaseStorePath(); +} + +const StorePath & DerivedPath::Built::getBaseStorePath() const +{ + return drvPath->getBaseStorePath(); +} + +template +static inline const StorePath & getBaseStorePath_(const DP & derivedPath) +{ + return std::visit(overloaded { + [&](const typename DP::Built & bfd) -> auto & { + return bfd.drvPath->getBaseStorePath(); + }, + [&](const typename DP::Opaque & bo) -> auto & { + return bo.path; + }, + }, derivedPath.raw()); +} + +const StorePath & SingleDerivedPath::getBaseStorePath() const +{ + return getBaseStorePath_(*this); +} + +const StorePath & DerivedPath::getBaseStorePath() const +{ + return getBaseStorePath_(*this); } } diff --git a/src/libstore/derived-path.hh b/src/libstore/derived-path.hh index 7a4261ce0..ec30dac61 100644 --- a/src/libstore/derived-path.hh +++ b/src/libstore/derived-path.hh @@ -24,28 +24,37 @@ class Store; struct DerivedPathOpaque { StorePath path; - nlohmann::json toJSON(ref store) const; std::string to_string(const Store & store) const; static DerivedPathOpaque parse(const Store & store, std::string_view); + nlohmann::json toJSON(const Store & store) const; GENERATE_CMP(DerivedPathOpaque, me->path); }; +struct SingleDerivedPath; + /** - * A derived path that is built from a derivation + * A single derived path that is built from a derivation * - * Built derived paths are pair of a derivation and some output names. - * They are evaluated by building the derivation, and then replacing the - * output names with the resulting outputs. - * - * Note that does mean a derived store paths evaluates to multiple - * opaque paths, which is sort of icky as expressions are supposed to - * evaluate to single values. Perhaps this should have just a single - * output name. + * Built derived paths are pair of a derivation and an output name. They are + * evaluated by building the derivation, and then taking the resulting output + * path of the given output name. */ -struct DerivedPathBuilt { - StorePath drvPath; - OutputsSpec outputs; +struct SingleDerivedPathBuilt { + ref drvPath; + std::string output; + + /** + * Get the store path this is ultimately derived from (by realising + * and projecting outputs). + * + * Note that this is *not* a property of the store object being + * referred to, but just of this path --- how we happened to be + * referring to that store object. In other words, this means this + * function breaks "referential transparency". It should therefore + * be used only with great care. + */ + const StorePath & getBaseStorePath() const; /** * Uses `^` as the separator @@ -57,11 +66,139 @@ struct DerivedPathBuilt { std::string to_string_legacy(const Store & store) const; /** * The caller splits on the separator, so it works for both variants. + * + * @param xpSettings Stop-gap to avoid globals during unit tests. */ - static DerivedPathBuilt parse(const Store & store, std::string_view drvPath, std::string_view outputs); - nlohmann::json toJSON(ref store) const; + static SingleDerivedPathBuilt parse( + const Store & store, ref drvPath, + std::string_view outputs, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + nlohmann::json toJSON(Store & store) const; - GENERATE_CMP(DerivedPathBuilt, me->drvPath, me->outputs); + DECLARE_CMP(SingleDerivedPathBuilt); +}; + +using _SingleDerivedPathRaw = std::variant< + DerivedPathOpaque, + SingleDerivedPathBuilt +>; + +/** + * A "derived path" is a very simple sort of expression (not a Nix + * language expression! But an expression in a the general sense) that + * evaluates to (concrete) store path. It is either: + * + * - opaque, in which case it is just a concrete store path with + * possibly no known derivation + * + * - built, in which case it is a pair of a derivation path and an + * output name. + */ +struct SingleDerivedPath : _SingleDerivedPathRaw { + using Raw = _SingleDerivedPathRaw; + using Raw::Raw; + + using Opaque = DerivedPathOpaque; + using Built = SingleDerivedPathBuilt; + + inline const Raw & raw() const { + return static_cast(*this); + } + + /** + * Get the store path this is ultimately derived from (by realising + * and projecting outputs). + * + * Note that this is *not* a property of the store object being + * referred to, but just of this path --- how we happened to be + * referring to that store object. In other words, this means this + * function breaks "referential transparency". It should therefore + * be used only with great care. + */ + const StorePath & getBaseStorePath() const; + + /** + * Uses `^` as the separator + */ + std::string to_string(const Store & store) const; + /** + * Uses `!` as the separator + */ + std::string to_string_legacy(const Store & store) const; + /** + * Uses `^` as the separator + * + * @param xpSettings Stop-gap to avoid globals during unit tests. + */ + static SingleDerivedPath parse( + const Store & store, + std::string_view, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + /** + * Uses `!` as the separator + * + * @param xpSettings Stop-gap to avoid globals during unit tests. + */ + static SingleDerivedPath parseLegacy( + const Store & store, + std::string_view, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + nlohmann::json toJSON(Store & store) const; +}; + +static inline ref makeConstantStorePathRef(StorePath drvPath) +{ + return make_ref(SingleDerivedPath::Opaque { drvPath }); +} + +/** + * A set of derived paths that are built from a derivation + * + * Built derived paths are pair of a derivation and some output names. + * They are evaluated by building the derivation, and then replacing the + * output names with the resulting outputs. + * + * Note that does mean a derived store paths evaluates to multiple + * opaque paths, which is sort of icky as expressions are supposed to + * evaluate to single values. Perhaps this should have just a single + * output name. + */ +struct DerivedPathBuilt { + ref drvPath; + OutputsSpec outputs; + + /** + * Get the store path this is ultimately derived from (by realising + * and projecting outputs). + * + * Note that this is *not* a property of the store object being + * referred to, but just of this path --- how we happened to be + * referring to that store object. In other words, this means this + * function breaks "referential transparency". It should therefore + * be used only with great care. + */ + const StorePath & getBaseStorePath() const; + + /** + * Uses `^` as the separator + */ + std::string to_string(const Store & store) const; + /** + * Uses `!` as the separator + */ + std::string to_string_legacy(const Store & store) const; + /** + * The caller splits on the separator, so it works for both variants. + * + * @param xpSettings Stop-gap to avoid globals during unit tests. + */ + static DerivedPathBuilt parse( + const Store & store, ref, + std::string_view, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + nlohmann::json toJSON(Store & store) const; + + DECLARE_CMP(DerivedPathBuilt); }; using _DerivedPathRaw = std::variant< @@ -71,13 +208,13 @@ using _DerivedPathRaw = std::variant< /** * A "derived path" is a very simple sort of expression that evaluates - * to (concrete) store path. It is either: + * to one or more (concrete) store paths. It is either: * - * - opaque, in which case it is just a concrete store path with + * - opaque, in which case it is just a single concrete store path with * possibly no known derivation * - * - built, in which case it is a pair of a derivation path and an - * output name. + * - built, in which case it is a pair of a derivation path and some + * output names. */ struct DerivedPath : _DerivedPathRaw { using Raw = _DerivedPathRaw; @@ -90,6 +227,18 @@ struct DerivedPath : _DerivedPathRaw { return static_cast(*this); } + /** + * Get the store path this is ultimately derived from (by realising + * and projecting outputs). + * + * Note that this is *not* a property of the store object being + * referred to, but just of this path --- how we happened to be + * referring to that store object. In other words, this means this + * function breaks "referential transparency". It should therefore + * be used only with great care. + */ + const StorePath & getBaseStorePath() const; + /** * Uses `^` as the separator */ @@ -100,14 +249,43 @@ struct DerivedPath : _DerivedPathRaw { std::string to_string_legacy(const Store & store) const; /** * Uses `^` as the separator + * + * @param xpSettings Stop-gap to avoid globals during unit tests. */ - static DerivedPath parse(const Store & store, std::string_view); + static DerivedPath parse( + const Store & store, + std::string_view, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** * Uses `!` as the separator + * + * @param xpSettings Stop-gap to avoid globals during unit tests. */ - static DerivedPath parseLegacy(const Store & store, std::string_view); + static DerivedPath parseLegacy( + const Store & store, + std::string_view, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + + /** + * Convert a `SingleDerivedPath` to a `DerivedPath`. + */ + static DerivedPath fromSingle(const SingleDerivedPath &); + + nlohmann::json toJSON(Store & store) const; }; typedef std::vector DerivedPaths; +/** + * Used by various parser functions to require experimental features as + * needed. + * + * Somewhat unfortunate this cannot just be an implementation detail for + * this module. + * + * @param xpSettings Stop-gap to avoid globals during unit tests. + */ +void drvRequireExperiment( + const SingleDerivedPath & drv, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); } diff --git a/src/libstore/downstream-placeholder.cc b/src/libstore/downstream-placeholder.cc index d623c05e2..885b3604d 100644 --- a/src/libstore/downstream-placeholder.cc +++ b/src/libstore/downstream-placeholder.cc @@ -38,4 +38,19 @@ DownstreamPlaceholder DownstreamPlaceholder::unknownDerivation( }; } +DownstreamPlaceholder DownstreamPlaceholder::fromSingleDerivedPathBuilt( + const SingleDerivedPath::Built & b) +{ + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) { + return DownstreamPlaceholder::unknownCaOutput(o.path, b.output); + }, + [&](const SingleDerivedPath::Built & b2) { + return DownstreamPlaceholder::unknownDerivation( + DownstreamPlaceholder::fromSingleDerivedPathBuilt(b2), + b.output); + }, + }, b.drvPath->raw()); +} + } diff --git a/src/libstore/downstream-placeholder.hh b/src/libstore/downstream-placeholder.hh index 97f77e6b8..9372dcd58 100644 --- a/src/libstore/downstream-placeholder.hh +++ b/src/libstore/downstream-placeholder.hh @@ -3,6 +3,7 @@ #include "hash.hh" #include "path.hh" +#include "derived-path.hh" namespace nix { @@ -73,6 +74,17 @@ public: const DownstreamPlaceholder & drvPlaceholder, std::string_view outputName, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + + /** + * Convenience constructor that handles both cases (unknown + * content-addressed output and unknown derivation), delegating as + * needed to `unknownCaOutput` and `unknownDerivation`. + * + * Recursively builds up a placeholder from a + * `SingleDerivedPath::Built.drvPath` chain. + */ + static DownstreamPlaceholder fromSingleDerivedPathBuilt( + const SingleDerivedPath::Built & built); }; } diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index fa17d606d..78b05031a 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -358,6 +358,9 @@ public: [&](const StorePath & drvPath) { throw Error("wanted to fetch '%s' but the legacy ssh protocol doesn't support merely substituting drv files via the build paths command. It would build them instead. Try using ssh-ng://", printStorePath(drvPath)); }, + [&](std::monostate) { + throw Error("wanted build derivation that is itself a build product, but the legacy ssh protocol doesn't support that. Try using ssh-ng://"); + }, }, sOrDrvPath); } conn->to << ss; diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index 14160dc8b..1ece7a4c0 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -132,7 +132,7 @@ void Store::queryMissing(const std::vector & targets, } for (auto & i : drv.inputDrvs) - pool.enqueue(std::bind(doPath, DerivedPath::Built { i.first, i.second })); + pool.enqueue(std::bind(doPath, DerivedPath::Built { makeConstantStorePathRef(i.first), i.second })); }; auto checkOutput = [&]( @@ -176,10 +176,18 @@ void Store::queryMissing(const std::vector & targets, std::visit(overloaded { [&](const DerivedPath::Built & bfd) { - if (!isValidPath(bfd.drvPath)) { + auto drvPathP = std::get_if(&*bfd.drvPath); + if (!drvPathP) { + // TODO make work in this case. + warn("Ignoring dynamic derivation %s while querying missing paths; not yet implemented", bfd.drvPath->to_string(*this)); + return; + } + auto & drvPath = drvPathP->path; + + if (!isValidPath(drvPath)) { // FIXME: we could try to substitute the derivation. auto state(state_.lock()); - state->unknown.insert(bfd.drvPath); + state->unknown.insert(drvPath); return; } @@ -187,7 +195,7 @@ void Store::queryMissing(const std::vector & targets, /* true for regular derivations, and CA derivations for which we have a trust mapping for all wanted outputs. */ auto knownOutputPaths = true; - for (auto & [outputName, pathOpt] : queryPartialDerivationOutputMap(bfd.drvPath)) { + for (auto & [outputName, pathOpt] : queryPartialDerivationOutputMap(drvPath)) { if (!pathOpt) { knownOutputPaths = false; break; @@ -197,15 +205,15 @@ void Store::queryMissing(const std::vector & targets, } if (knownOutputPaths && invalid.empty()) return; - auto drv = make_ref(derivationFromPath(bfd.drvPath)); - ParsedDerivation parsedDrv(StorePath(bfd.drvPath), *drv); + auto drv = make_ref(derivationFromPath(drvPath)); + ParsedDerivation parsedDrv(StorePath(drvPath), *drv); if (knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { auto drvState = make_ref>(DrvState(invalid.size())); for (auto & output : invalid) - pool.enqueue(std::bind(checkOutput, bfd.drvPath, drv, output, drvState)); + pool.enqueue(std::bind(checkOutput, drvPath, drv, output, drvState)); } else - mustBuildDrv(bfd.drvPath, *drv); + mustBuildDrv(drvPath, *drv); }, [&](const DerivedPath::Opaque & bo) { @@ -310,7 +318,9 @@ std::map drvOutputReferences( OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, Store * evalStore_) { - auto outputsOpt_ = store.queryPartialDerivationOutputMap(bfd.drvPath, evalStore_); + auto drvPath = resolveDerivedPath(store, *bfd.drvPath, evalStore_); + + auto outputsOpt_ = store.queryPartialDerivationOutputMap(drvPath, evalStore_); auto outputsOpt = std::visit(overloaded { [&](const OutputsSpec::All &) { @@ -325,7 +335,7 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, if (!pOutputPathOpt) throw Error( "the derivation '%s' doesn't have an output named '%s'", - store.printStorePath(bfd.drvPath), output); + bfd.drvPath->to_string(store), output); outputsOpt.insert_or_assign(output, std::move(*pOutputPathOpt)); } return outputsOpt; @@ -335,11 +345,64 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, OutputPathMap outputs; for (auto & [outputName, outputPathOpt] : outputsOpt) { if (!outputPathOpt) - throw MissingRealisation(store.printStorePath(bfd.drvPath), outputName); + throw MissingRealisation(bfd.drvPath->to_string(store), outputName); auto & outputPath = *outputPathOpt; outputs.insert_or_assign(outputName, outputPath); } return outputs; } + +StorePath resolveDerivedPath(Store & store, const SingleDerivedPath & req, Store * evalStore_) +{ + auto & evalStore = evalStore_ ? *evalStore_ : store; + + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & bo) { + return bo.path; + }, + [&](const SingleDerivedPath::Built & bfd) { + auto drvPath = resolveDerivedPath(store, *bfd.drvPath, evalStore_); + auto outputPaths = evalStore.queryPartialDerivationOutputMap(drvPath, evalStore_); + if (outputPaths.count(bfd.output) == 0) + throw Error("derivation '%s' does not have an output named '%s'", + store.printStorePath(drvPath), bfd.output); + auto & optPath = outputPaths.at(bfd.output); + if (!optPath) + throw Error("'%s' does not yet map to a known concrete store path", + bfd.to_string(store)); + return *optPath; + }, + }, req.raw()); +} + + +OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd) +{ + auto drvPath = resolveDerivedPath(store, *bfd.drvPath); + auto outputMap = store.queryDerivationOutputMap(drvPath); + auto outputsLeft = std::visit(overloaded { + [&](const OutputsSpec::All &) { + return StringSet {}; + }, + [&](const OutputsSpec::Names & names) { + return static_cast(names); + }, + }, bfd.outputs.raw()); + for (auto iter = outputMap.begin(); iter != outputMap.end();) { + auto & outputName = iter->first; + if (bfd.outputs.contains(outputName)) { + outputsLeft.erase(outputName); + ++iter; + } else { + iter = outputMap.erase(iter); + } + } + if (!outputsLeft.empty()) + throw Error("derivation '%s' does not have an outputs %s", + store.printStorePath(drvPath), + concatStringsSep(", ", quoteStrings(std::get(bfd.outputs)))); + return outputMap; +} + } diff --git a/src/libstore/path-with-outputs.cc b/src/libstore/path-with-outputs.cc index 869b490ad..81f360f3a 100644 --- a/src/libstore/path-with-outputs.cc +++ b/src/libstore/path-with-outputs.cc @@ -16,10 +16,16 @@ std::string StorePathWithOutputs::to_string(const Store & store) const DerivedPath StorePathWithOutputs::toDerivedPath() const { if (!outputs.empty()) { - return DerivedPath::Built { path, OutputsSpec::Names { outputs } }; + return DerivedPath::Built { + .drvPath = makeConstantStorePathRef(path), + .outputs = OutputsSpec::Names { outputs }, + }; } else if (path.isDerivation()) { assert(outputs.empty()); - return DerivedPath::Built { path, OutputsSpec::All { } }; + return DerivedPath::Built { + .drvPath = makeConstantStorePathRef(path), + .outputs = OutputsSpec::All { }, + }; } else { return DerivedPath::Opaque { path }; } @@ -34,29 +40,36 @@ std::vector toDerivedPaths(const std::vector } -std::variant StorePathWithOutputs::tryFromDerivedPath(const DerivedPath & p) +StorePathWithOutputs::ParseResult StorePathWithOutputs::tryFromDerivedPath(const DerivedPath & p) { return std::visit(overloaded { - [&](const DerivedPath::Opaque & bo) -> std::variant { + [&](const DerivedPath::Opaque & bo) -> StorePathWithOutputs::ParseResult { if (bo.path.isDerivation()) { // drv path gets interpreted as "build", not "get drv file itself" return bo.path; } return StorePathWithOutputs { bo.path }; }, - [&](const DerivedPath::Built & bfd) -> std::variant { - return StorePathWithOutputs { - .path = bfd.drvPath, - // Use legacy encoding of wildcard as empty set - .outputs = std::visit(overloaded { - [&](const OutputsSpec::All &) -> StringSet { - return {}; - }, - [&](const OutputsSpec::Names & outputs) { - return static_cast(outputs); - }, - }, bfd.outputs.raw()), - }; + [&](const DerivedPath::Built & bfd) -> StorePathWithOutputs::ParseResult { + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & bo) -> StorePathWithOutputs::ParseResult { + return StorePathWithOutputs { + .path = bo.path, + // Use legacy encoding of wildcard as empty set + .outputs = std::visit(overloaded { + [&](const OutputsSpec::All &) -> StringSet { + return {}; + }, + [&](const OutputsSpec::Names & outputs) { + return static_cast(outputs); + }, + }, bfd.outputs.raw()), + }; + }, + [&](const SingleDerivedPath::Built &) -> StorePathWithOutputs::ParseResult { + return std::monostate {}; + }, + }, bfd.drvPath->raw()); }, }, p.raw()); } diff --git a/src/libstore/path-with-outputs.hh b/src/libstore/path-with-outputs.hh index d75850868..57e03252d 100644 --- a/src/libstore/path-with-outputs.hh +++ b/src/libstore/path-with-outputs.hh @@ -23,7 +23,9 @@ struct StorePathWithOutputs DerivedPath toDerivedPath() const; - static std::variant tryFromDerivedPath(const DerivedPath &); + typedef std::variant ParseResult; + + static StorePathWithOutputs::ParseResult tryFromDerivedPath(const DerivedPath &); }; std::vector toDerivedPaths(const std::vector); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 21258daec..58f72beb9 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -656,6 +656,9 @@ static void writeDerivedPaths(RemoteStore & store, RemoteStore::Connection & con GET_PROTOCOL_MAJOR(conn.daemonVersion), GET_PROTOCOL_MINOR(conn.daemonVersion)); }, + [&](std::monostate) { + throw Error("wanted to build a derivation that is itself a build product, but the legacy 'ssh://' protocol doesn't support that. Try using 'ssh-ng://'"); + }, }, sOrDrvPath); } conn.to << ss; @@ -670,9 +673,16 @@ void RemoteStore::copyDrvsFromEvalStore( /* The remote doesn't have a way to access evalStore, so copy the .drvs. */ RealisedPath::Set drvPaths2; - for (auto & i : paths) - if (auto p = std::get_if(&i)) - drvPaths2.insert(p->drvPath); + for (const auto & i : paths) { + std::visit(overloaded { + [&](const DerivedPath::Opaque & bp) { + // Do nothing, path is hopefully there already + }, + [&](const DerivedPath::Built & bp) { + drvPaths2.insert(bp.drvPath->getBaseStorePath()); + }, + }, i.raw()); + } copyClosure(*evalStore, *this, drvPaths2); } } @@ -742,7 +752,8 @@ std::vector RemoteStore::buildPathsWithResults( }; OutputPathMap outputs; - auto drv = evalStore->readDerivation(bfd.drvPath); + auto drvPath = resolveDerivedPath(*evalStore, *bfd.drvPath); + auto drv = evalStore->readDerivation(drvPath); const auto outputHashes = staticOutputHashes(*evalStore, drv); // FIXME: expensive auto built = resolveDerivedPath(*this, bfd, &*evalStore); for (auto & [output, outputPath] : built) { @@ -750,7 +761,7 @@ std::vector RemoteStore::buildPathsWithResults( if (!outputHash) throw Error( "the derivation '%s' doesn't have an output named '%s'", - printStorePath(bfd.drvPath), output); + printStorePath(drvPath), output); auto outputId = DrvOutput{ *outputHash, output }; if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { auto realisation = diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index f9029ade1..da1a3eefb 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -945,6 +945,7 @@ void removeTempRoots(); * Resolve the derived path completely, failing if any derivation output * is unknown. */ +StorePath resolveDerivedPath(Store &, const SingleDerivedPath &, Store * evalStore = nullptr); OutputPathMap resolveDerivedPath(Store &, const DerivedPath::Built &, Store * evalStore = nullptr); diff --git a/src/libstore/tests/derived-path.cc b/src/libstore/tests/derived-path.cc index 160443ec1..d6549f66f 100644 --- a/src/libstore/tests/derived-path.cc +++ b/src/libstore/tests/derived-path.cc @@ -17,14 +17,34 @@ Gen Arbitrary::arbitrary() }); } +Gen Arbitrary::arbitrary() +{ + return gen::just(SingleDerivedPath::Built { + .drvPath = make_ref(*gen::arbitrary()), + .output = (*gen::arbitrary()).name, + }); +} + Gen Arbitrary::arbitrary() { return gen::just(DerivedPath::Built { - .drvPath = *gen::arbitrary(), + .drvPath = make_ref(*gen::arbitrary()), .outputs = *gen::arbitrary(), }); } +Gen Arbitrary::arbitrary() +{ + switch (*gen::inRange(0, std::variant_size_v)) { + case 0: + return gen::just(*gen::arbitrary()); + case 1: + return gen::just(*gen::arbitrary()); + default: + assert(false); + } +} + Gen Arbitrary::arbitrary() { switch (*gen::inRange(0, std::variant_size_v)) { @@ -45,12 +65,69 @@ class DerivedPathTest : public LibStoreTest { }; -// FIXME: `RC_GTEST_FIXTURE_PROP` isn't calling `SetUpTestSuite` because it is -// no a real fixture. -// -// See https://github.com/emil-e/rapidcheck/blob/master/doc/gtest.md#rc_gtest_fixture_propfixture-name-args -TEST_F(DerivedPathTest, force_init) -{ +/** + * Round trip (string <-> data structure) test for + * `DerivedPath::Opaque`. + */ +TEST_F(DerivedPathTest, opaque) { + std::string_view opaque = "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x"; + auto elem = DerivedPath::parse(*store, opaque); + auto * p = std::get_if(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->path, store->parseStorePath(opaque)); + ASSERT_EQ(elem.to_string(*store), opaque); +} + +/** + * Round trip (string <-> data structure) test for a simpler + * `DerivedPath::Built`. + */ +TEST_F(DerivedPathTest, built_opaque) { + std::string_view built = "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv^bar,foo"; + auto elem = DerivedPath::parse(*store, built); + auto * p = std::get_if(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->outputs, ((OutputsSpec) OutputsSpec::Names { "foo", "bar" })); + ASSERT_EQ(*p->drvPath, ((SingleDerivedPath) SingleDerivedPath::Opaque { + .path = store->parseStorePath(built.substr(0, 49)), + })); + ASSERT_EQ(elem.to_string(*store), built); +} + +/** + * Round trip (string <-> data structure) test for a more complex, + * inductive `DerivedPath::Built`. + */ +TEST_F(DerivedPathTest, built_built) { + /** + * We set these in tests rather than the regular globals so we don't have + * to worry about race conditions if the tests run concurrently. + */ + ExperimentalFeatureSettings mockXpSettings; + mockXpSettings.set("experimental-features", "dynamic-derivations ca-derivations"); + + std::string_view built = "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv^foo^bar,baz"; + auto elem = DerivedPath::parse(*store, built, mockXpSettings); + auto * p = std::get_if(&elem); + ASSERT_TRUE(p); + ASSERT_EQ(p->outputs, ((OutputsSpec) OutputsSpec::Names { "bar", "baz" })); + auto * drvPath = std::get_if(&*p->drvPath); + ASSERT_TRUE(drvPath); + ASSERT_EQ(drvPath->output, "foo"); + ASSERT_EQ(*drvPath->drvPath, ((SingleDerivedPath) SingleDerivedPath::Opaque { + .path = store->parseStorePath(built.substr(0, 49)), + })); + ASSERT_EQ(elem.to_string(*store), built); +} + +/** + * Without the right experimental features enabled, we cannot parse a + * complex inductive derived path. + */ +TEST_F(DerivedPathTest, built_built_xp) { + ASSERT_THROW( + DerivedPath::parse(*store, "/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv^foo^bar,baz"), + MissingExperimentalFeature); } RC_GTEST_FIXTURE_PROP( diff --git a/src/libstore/tests/derived-path.hh b/src/libstore/tests/derived-path.hh index 506f3ccb1..98d61f228 100644 --- a/src/libstore/tests/derived-path.hh +++ b/src/libstore/tests/derived-path.hh @@ -12,8 +12,18 @@ namespace rc { using namespace nix; template<> -struct Arbitrary { - static Gen arbitrary(); +struct Arbitrary { + static Gen arbitrary(); +}; + +template<> +struct Arbitrary { + static Gen arbitrary(); +}; + +template<> +struct Arbitrary { + static Gen arbitrary(); }; template<> diff --git a/src/libutil/comparator.hh b/src/libutil/comparator.hh index 9f661c5c3..7982fdc5e 100644 --- a/src/libutil/comparator.hh +++ b/src/libutil/comparator.hh @@ -1,6 +1,22 @@ #pragma once ///@file +/** + * Declare comparison methods without defining them. + */ +#define DECLARE_ONE_CMP(COMPARATOR, MY_TYPE) \ + bool operator COMPARATOR(const MY_TYPE & other) const; +#define DECLARE_EQUAL(my_type) \ + DECLARE_ONE_CMP(==, my_type) +#define DECLARE_LEQ(my_type) \ + DECLARE_ONE_CMP(<, my_type) +#define DECLARE_NEQ(my_type) \ + DECLARE_ONE_CMP(!=, my_type) +#define DECLARE_CMP(my_type) \ + DECLARE_EQUAL(my_type) \ + DECLARE_LEQ(my_type) \ + DECLARE_NEQ(my_type) + /** * Awful hacky generation of the comparison operators by doing a lexicographic * comparison between the choosen fields. diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index 6510df8f0..66f319c3e 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -393,7 +393,7 @@ static void main_nix_build(int argc, char * * argv) auto bashDrv = drv->requireDrvPath(); pathsToBuild.push_back(DerivedPath::Built { - .drvPath = bashDrv, + .drvPath = makeConstantStorePathRef(bashDrv), .outputs = OutputsSpec::Names {"out"}, }); pathsToCopy.insert(bashDrv); @@ -417,7 +417,7 @@ static void main_nix_build(int argc, char * * argv) })) { pathsToBuild.push_back(DerivedPath::Built { - .drvPath = inputDrv, + .drvPath = makeConstantStorePathRef(inputDrv), .outputs = OutputsSpec::Names { inputOutputs }, }); pathsToCopy.insert(inputDrv); @@ -590,7 +590,10 @@ static void main_nix_build(int argc, char * * argv) if (outputName == "") throw Error("derivation '%s' lacks an 'outputName' attribute", store->printStorePath(drvPath)); - pathsToBuild.push_back(DerivedPath::Built{drvPath, OutputsSpec::Names{outputName}}); + pathsToBuild.push_back(DerivedPath::Built{ + .drvPath = makeConstantStorePathRef(drvPath), + .outputs = OutputsSpec::Names{outputName}, + }); pathsToBuildOrdered.push_back({drvPath, {outputName}}); drvsToCopy.insert(drvPath); diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index 91b073b49..8dd071aa6 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -481,7 +481,7 @@ static void printMissing(EvalState & state, DrvInfos & elems) for (auto & i : elems) if (auto drvPath = i.queryDrvPath()) targets.push_back(DerivedPath::Built{ - .drvPath = *drvPath, + .drvPath = makeConstantStorePathRef(*drvPath), .outputs = OutputsSpec::All { }, }); else @@ -759,7 +759,7 @@ static void opSet(Globals & globals, Strings opFlags, Strings opArgs) std::vector paths { drvPath ? (DerivedPath) (DerivedPath::Built { - .drvPath = *drvPath, + .drvPath = makeConstantStorePathRef(*drvPath), .outputs = OutputsSpec::All { }, }) : (DerivedPath) (DerivedPath::Opaque { diff --git a/src/nix/app.cc b/src/nix/app.cc index e0f68b4fc..16a921194 100644 --- a/src/nix/app.cc +++ b/src/nix/app.cc @@ -25,7 +25,8 @@ StringPairs resolveRewrites( if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) for (auto & [ outputName, outputPath ] : drvDep->outputs) res.emplace( - DownstreamPlaceholder::unknownCaOutput(drvDep->drvPath, outputName).render(), + DownstreamPlaceholder::unknownCaOutput( + drvDep->drvPath->outPath(), outputName).render(), store.printStorePath(outputPath) ); return res; @@ -65,7 +66,7 @@ UnresolvedApp InstallableValue::toApp(EvalState & state) [&](const NixStringContextElem::DrvDeep & d) -> DerivedPath { /* We want all outputs of the drv */ return DerivedPath::Built { - .drvPath = d.drvPath, + .drvPath = makeConstantStorePathRef(d.drvPath), .outputs = OutputsSpec::All {}, }; }, @@ -106,7 +107,7 @@ UnresolvedApp InstallableValue::toApp(EvalState & state) auto program = outPath + "/bin/" + mainProgram; return UnresolvedApp { App { .context = { DerivedPath::Built { - .drvPath = drvPath, + .drvPath = makeConstantStorePathRef(drvPath), .outputs = OutputsSpec::Names { outputName }, } }, .program = program, diff --git a/src/nix/build.cc b/src/nix/build.cc index ad1842a4e..479100186 100644 --- a/src/nix/build.cc +++ b/src/nix/build.cc @@ -9,18 +9,18 @@ using namespace nix; -nlohmann::json derivedPathsToJSON(const DerivedPaths & paths, ref store) +static nlohmann::json derivedPathsToJSON(const DerivedPaths & paths, Store & store) { auto res = nlohmann::json::array(); for (auto & t : paths) { - std::visit([&res, store](const auto & t) { + std::visit([&](const auto & t) { res.push_back(t.toJSON(store)); }, t.raw()); } return res; } -nlohmann::json builtPathsWithResultToJSON(const std::vector & buildables, ref store) +static nlohmann::json builtPathsWithResultToJSON(const std::vector & buildables, const Store & store) { auto res = nlohmann::json::array(); for (auto & b : buildables) { @@ -125,7 +125,7 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile printMissing(store, pathsToBuild, lvlError); if (json) - logger->cout("%s", derivedPathsToJSON(pathsToBuild, store).dump()); + logger->cout("%s", derivedPathsToJSON(pathsToBuild, *store).dump()); return; } @@ -136,7 +136,7 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixJSON, MixProfile installables, repair ? bmRepair : buildMode); - if (json) logger->cout("%s", builtPathsWithResultToJSON(buildables, store).dump()); + if (json) logger->cout("%s", builtPathsWithResultToJSON(buildables, *store).dump()); if (outLink != "") if (auto store2 = store.dynamic_pointer_cast()) diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc index bcc00d490..5a80f0308 100644 --- a/src/nix/bundle.cc +++ b/src/nix/bundle.cc @@ -109,7 +109,7 @@ struct CmdBundle : InstallableValueCommand store->buildPaths({ DerivedPath::Built { - .drvPath = drvPath, + .drvPath = makeConstantStorePathRef(drvPath), .outputs = OutputsSpec::All { }, }, }); diff --git a/src/nix/develop.cc b/src/nix/develop.cc index 195eeaa21..c033804e4 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -235,7 +235,7 @@ static StorePath getDerivationEnvironment(ref store, ref evalStore /* Build the derivation. */ store->buildPaths( { DerivedPath::Built { - .drvPath = shellDrvPath, + .drvPath = makeConstantStorePathRef(shellDrvPath), .outputs = OutputsSpec::All { }, }}, bmNormal, evalStore); diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 3ce1de44a..83b74c8ca 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -544,9 +544,9 @@ struct CmdFlakeCheck : FlakeCommand *attr2.value, attr2.pos); if (drvPath && attr_name == settings.thisSystem.get()) { drvPaths.push_back(DerivedPath::Built { - .drvPath = *drvPath, - .outputs = OutputsSpec::All { }, - }); + .drvPath = makeConstantStorePathRef(*drvPath), + .outputs = OutputsSpec::All { }, + }); } } } diff --git a/src/nix/log.cc b/src/nix/log.cc index aaf829764..9a9bd30f9 100644 --- a/src/nix/log.cc +++ b/src/nix/log.cc @@ -33,6 +33,17 @@ struct CmdLog : InstallableCommand auto b = installable->toDerivedPath(); + // For compat with CLI today, TODO revisit + auto oneUp = std::visit(overloaded { + [&](const DerivedPath::Opaque & bo) { + return make_ref(bo); + }, + [&](const DerivedPath::Built & bfd) { + return bfd.drvPath; + }, + }, b.path.raw()); + auto path = resolveDerivedPath(*store, *oneUp); + RunPager pager; for (auto & sub : subs) { auto * logSubP = dynamic_cast(&*sub); @@ -42,14 +53,7 @@ struct CmdLog : InstallableCommand } auto & logSub = *logSubP; - auto log = std::visit(overloaded { - [&](const DerivedPath::Opaque & bo) { - return logSub.getBuildLog(bo.path); - }, - [&](const DerivedPath::Built & bfd) { - return logSub.getBuildLog(bfd.drvPath); - }, - }, b.path.raw()); + auto log = logSub.getBuildLog(path); if (!log) continue; stopProgressBar(); printInfo("got build log for '%s' from '%s'", installable->what(), logSub.getUri()); diff --git a/tests/build.sh b/tests/build.sh index 8ae20f0df..7fbdb0f07 100644 --- a/tests/build.sh +++ b/tests/build.sh @@ -78,7 +78,7 @@ expectStderr 1 nix build --impure --expr 'with (import ./multiple-outputs.nix).e | grepQuiet "has 2 entries in its context. It should only have exactly one entry" nix build --impure --json --expr 'builtins.unsafeDiscardOutputDependency (import ./multiple-outputs.nix).e.a_a.drvPath' --no-link | jq --exit-status ' - (.[0] | .path | match(".*multiple-outputs-e.drv")) + (.[0] | match(".*multiple-outputs-e.drv")) ' # Test building from raw store path to drv not expression. diff --git a/tests/dyn-drv/build-built-drv.sh b/tests/dyn-drv/build-built-drv.sh new file mode 100644 index 000000000..647be9457 --- /dev/null +++ b/tests/dyn-drv/build-built-drv.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +source common.sh + +# In the corresponding nix file, we have two derivations: the first, named `hello`, +# is a normal recursive derivation, while the second, named dependent, has the +# new outputHashMode "text". Note that in "dependent", we don't refer to the +# build output of `hello`, but only to the path of the drv file. For this reason, +# we only need to: +# +# - instantiate `hello` +# - build `producingDrv` +# - check that the path of the output coincides with that of the original derivation + +out1=$(nix build -f ./text-hashed-output.nix hello --no-link) + +clearStore + +drvDep=$(nix-instantiate ./text-hashed-output.nix -A producingDrv) + +expectStderr 1 nix build "${drvDep}^out^out" --no-link | grepQuiet "Building dynamic derivations in one shot is not yet implemented" diff --git a/tests/dyn-drv/local.mk b/tests/dyn-drv/local.mk index f065a5627..b087ecd1c 100644 --- a/tests/dyn-drv/local.mk +++ b/tests/dyn-drv/local.mk @@ -1,6 +1,7 @@ dyn-drv-tests := \ $(d)/text-hashed-output.sh \ - $(d)/recursive-mod-json.sh + $(d)/recursive-mod-json.sh \ + $(d)/build-built-drv.sh install-tests-groups += dyn-drv diff --git a/tests/test-libstoreconsumer/main.cc b/tests/test-libstoreconsumer/main.cc index 31b6d8ef1..c61489af6 100644 --- a/tests/test-libstoreconsumer/main.cc +++ b/tests/test-libstoreconsumer/main.cc @@ -23,7 +23,7 @@ int main (int argc, char **argv) std::vector paths { DerivedPath::Built { - .drvPath = store->parseStorePath(drvPath), + .drvPath = makeConstantStorePathRef(store->parseStorePath(drvPath)), .outputs = OutputsSpec::Names{"out"} } }; From b9b51f9579c15c9789814cd4b6969b0f85e13980 Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Fri, 11 Aug 2023 11:58:33 +0200 Subject: [PATCH 019/299] Prevent overriding virtual methods that are called in a destructor Virtual methods are no longer valid once the derived destructor has run. This means the compiler is free to optimize them to be non-virtual. Found using clang-tidy --- src/libmain/progress-bar.cc | 3 ++- src/libstore/build/local-derivation-goal.hh | 4 +++- src/libstore/build/substitution-goal.hh | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index 6600ec177..45b1fdfd1 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -108,7 +108,8 @@ public: stop(); } - void stop() override + /* Called by destructor, can't be overridden */ + void stop() override final { { auto state(state_.lock()); diff --git a/src/libstore/build/local-derivation-goal.hh b/src/libstore/build/local-derivation-goal.hh index 9acd7593d..8827bfca3 100644 --- a/src/libstore/build/local-derivation-goal.hh +++ b/src/libstore/build/local-derivation-goal.hh @@ -272,8 +272,10 @@ struct LocalDerivationGoal : public DerivationGoal /** * Forcibly kill the child process, if any. + * + * Called by destructor, can't be overridden */ - void killChild() override; + void killChild() override final; /** * Kill any processes running under the build user UID or in the diff --git a/src/libstore/build/substitution-goal.hh b/src/libstore/build/substitution-goal.hh index 9fc041920..1b693baa1 100644 --- a/src/libstore/build/substitution-goal.hh +++ b/src/libstore/build/substitution-goal.hh @@ -114,7 +114,8 @@ public: void handleChildOutput(int fd, std::string_view data) override; void handleEOF(int fd) override; - void cleanup() override; + /* Called by destructor, can't be overridden */ + void cleanup() override final; JobCategory jobCategory() override { return JobCategory::Substitution; }; }; From 1ffb26311b5d74e9f607ec31bee6c17bbae6fdae Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Fri, 11 Aug 2023 12:00:11 +0200 Subject: [PATCH 020/299] MultiCommand::toJSON: Fix use-after-move --- src/libutil/args.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/args.cc b/src/libutil/args.cc index 3cf3ed9ca..00281ce6f 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -410,8 +410,8 @@ nlohmann::json MultiCommand::toJSON() auto cat = nlohmann::json::object(); cat["id"] = command->category(); cat["description"] = trim(categories[command->category()]); - j["category"] = std::move(cat); cat["experimental-feature"] = command->experimentalFeature(); + j["category"] = std::move(cat); cmds[name] = std::move(j); } From 2e5096e4f0959cb2974a17fe50ad5ad891b2384f Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Fri, 11 Aug 2023 12:00:31 +0200 Subject: [PATCH 021/299] FileTransfer::download: fix use-after-move std::move(state->data) and data.empty() were called in a loop, and could run with no other threads intervening. Accessing moved objects is undefined behavior, and could cause a crash. --- src/libstore/filetransfer.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libstore/filetransfer.cc b/src/libstore/filetransfer.cc index 38b691279..a283af5a2 100644 --- a/src/libstore/filetransfer.cc +++ b/src/libstore/filetransfer.cc @@ -863,6 +863,8 @@ void FileTransfer::download(FileTransferRequest && request, Sink & sink) } chunk = std::move(state->data); + /* Reset state->data after the move, since we check data.empty() */ + state->data = ""; state->request.notify_one(); } From e78e9a6bd1c5f26684a9da0d94ede7d960788e0e Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Fri, 11 Aug 2023 12:05:45 +0200 Subject: [PATCH 022/299] SimpleLogger::log: fix unintended fallthrough --- src/libutil/logging.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index 5a2dd99af..54702e4ea 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -67,7 +67,7 @@ public: case lvlWarn: c = '4'; break; case lvlNotice: case lvlInfo: c = '5'; break; case lvlTalkative: case lvlChatty: c = '6'; break; - case lvlDebug: case lvlVomit: c = '7'; + case lvlDebug: case lvlVomit: c = '7'; break; default: c = '7'; break; // should not happen, and missing enum case is reported by -Werror=switch-enum } prefix = std::string("<") + c + ">"; From c4dbb55ba93c9c81d798faa6a19285ebe2717a25 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 11 Aug 2023 17:22:55 +0200 Subject: [PATCH 023/299] initLibUtil: Add exception handling self-check --- src/libutil/error.cc | 5 +++++ src/libutil/error.hh | 4 ++++ src/libutil/util.cc | 17 +++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/libutil/error.cc b/src/libutil/error.cc index c9d61942a..4a1b346ef 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -14,6 +14,11 @@ void BaseError::addTrace(std::shared_ptr && e, hintformat hint, boo err.traces.push_front(Trace { .pos = std::move(e), .hint = hint, .frame = frame }); } +void throwExceptionSelfCheck(){ + // This is meant to be caught in initLibUtil() + throw SysError("C++ exception handling is broken. This would appear to be a problem with the way Nix was compiled and/or linked and/or loaded."); +} + // c++ std::exception descendants must have a 'const char* what()' function. // This stringifies the error and caches it for use by what(), or similarly by msg(). const std::string & BaseError::calcWhat() const diff --git a/src/libutil/error.hh b/src/libutil/error.hh index 6a0923081..be5c5e252 100644 --- a/src/libutil/error.hh +++ b/src/libutil/error.hh @@ -214,4 +214,8 @@ public: } }; +/** Throw an exception for the purpose of checking that exception handling works; see 'initLibUtil()'. + */ +void throwExceptionSelfCheck(); + } diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 26f9dc8a8..f24a6e165 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -48,6 +48,23 @@ extern char * * environ __attribute__((weak)); namespace nix { void initLibUtil() { + // Check that exception handling works. Exception handling has been observed + // not to work on darwin when the linker flags aren't quite right. + // In this case we don't want to expose the user to some unrelated uncaught + // exception, but rather tell them exactly that exception handling is + // broken. + // When exception handling fails, the message tends to be printed by the + // C++ runtime, followed by an abort. + // For example on macOS we might see an error such as + // libc++abi: terminating with uncaught exception of type nix::SysError: error: C++ exception handling is broken. This would appear to be a problem with the way Nix was compiled and/or linked and/or loaded. + bool caught = false; + try { + throwExceptionSelfCheck(); + } catch (nix::Error _e) { + caught = true; + } + // This is not actually the main point of this check, but let's make sure anyway: + assert(caught); } std::optional getEnv(const std::string & key) From a04720e68ce45943ce0eae3d477f9d554b0d63a4 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 14 Aug 2023 08:10:39 -0400 Subject: [PATCH 024/299] Rename `optOutputPath` to `optStaticOutputPath` This choice of variable name makes it more clear what is going on. Co-authored-by: Robert Hensing --- src/libexpr/eval.cc | 12 ++++++------ src/libexpr/eval.hh | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 3e521b732..063e34c56 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1031,12 +1031,12 @@ void EvalState::mkOutputString( Value & value, const StorePath & drvPath, const std::string outputName, - std::optional optOutputPath, + std::optional optStaticOutputPath, const ExperimentalFeatureSettings & xpSettings) { value.mkString( - optOutputPath - ? store->printStorePath(*std::move(optOutputPath)) + optStaticOutputPath + ? store->printStorePath(*std::move(optStaticOutputPath)) /* Downstream we would substitute this for an actual path once we build the floating CA derivation */ : DownstreamPlaceholder::unknownCaOutput(drvPath, outputName, xpSettings).render(), @@ -2349,10 +2349,10 @@ SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value & auto i = drv.outputs.find(b.output); if (i == drv.outputs.end()) throw Error("derivation '%s' does not have output '%s'", b.drvPath->to_string(*store), b.output); - auto optOutputPath = i->second.path(*store, drv.name, b.output); + auto optStaticOutputPath = i->second.path(*store, drv.name, b.output); // This is testing for the case of CA derivations - return optOutputPath - ? store->printStorePath(*optOutputPath) + return optStaticOutputPath + ? store->printStorePath(*optStaticOutputPath) : DownstreamPlaceholder::fromSingleDerivedPathBuilt(b).render(); }, [&](const SingleDerivedPath::Built & o) { diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 0268a2a12..662c123b9 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -685,7 +685,7 @@ public: * * @param outputName Name of the output * - * @param optOutputPath Optional output path for that string. Must + * @param optStaticOutputPath Optional output path for that string. Must * be passed if and only if output store object is input-addressed. * Will be printed to form string if passed, otherwise a placeholder * will be used (see `DownstreamPlaceholder`). @@ -696,7 +696,7 @@ public: Value & value, const StorePath & drvPath, const std::string outputName, - std::optional optOutputPath, + std::optional optStaticOutputPath, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx); From e7c39ff00b81dfdc48ebe7f41847db84ba779676 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 10 Mar 2021 04:22:56 +0000 Subject: [PATCH 025/299] Rework evaluator `SingleDerivedPath` infra `EvalState::mkSingleDerivedPathString` previously contained its own inverse (printing, rather than parsing) in order to validate what was parsed. Now that is pulled out into its own separate function: `EvalState::coerceToSingleDerivedPath`. In additional that pulled out logic is deduplicated with `EvalState::mkOutputString` via `EvalState::mkOutputStringRaw`, which is itself deduplicated (and generalized) with `DownstreamPlaceholder::mkOutputStringRaw`. All these changes make the unit tests simpler. (We would ideally write more unit tests for `mkSingleDerivedPathString` `coerceToSingleDerivedPath` directly, but we cannot yet do that because the IO in reading the store path won't work when the dummy store cannot hold anything. Someday we'll have a proper in-memory store which will work for this.) Co-authored-by: Robert Hensing --- src/libexpr/eval.cc | 101 ++++++++++++++++--------- src/libexpr/eval.hh | 53 +++++++++---- src/libexpr/primops.cc | 6 +- src/libexpr/tests/derived-path.cc | 20 ++--- src/libstore/downstream-placeholder.cc | 10 ++- src/libstore/downstream-placeholder.hh | 3 +- 6 files changed, 122 insertions(+), 71 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 063e34c56..79e2c4727 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1027,24 +1027,67 @@ void EvalState::mkStorePathString(const StorePath & p, Value & v) } +std::string EvalState::mkOutputStringRaw( + const SingleDerivedPath::Built & b, + std::optional optStaticOutputPath, + const ExperimentalFeatureSettings & xpSettings) +{ + /* In practice, this is testing for the case of CA derivations, or + dynamic derivations. */ + return optStaticOutputPath + ? store->printStorePath(*std::move(optStaticOutputPath)) + /* Downstream we would substitute this for an actual path once + we build the floating CA derivation */ + : DownstreamPlaceholder::fromSingleDerivedPathBuilt(b, xpSettings).render(); +} + + void EvalState::mkOutputString( Value & value, - const StorePath & drvPath, - const std::string outputName, + const SingleDerivedPath::Built & b, std::optional optStaticOutputPath, const ExperimentalFeatureSettings & xpSettings) { value.mkString( - optStaticOutputPath - ? store->printStorePath(*std::move(optStaticOutputPath)) - /* Downstream we would substitute this for an actual path once - we build the floating CA derivation */ - : DownstreamPlaceholder::unknownCaOutput(drvPath, outputName, xpSettings).render(), + mkOutputStringRaw(b, optStaticOutputPath, xpSettings), + NixStringContext { b }); +} + + +std::string EvalState::mkSingleDerivedPathStringRaw( + const SingleDerivedPath & p) +{ + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) { + return store->printStorePath(o.path); + }, + [&](const SingleDerivedPath::Built & b) { + auto optStaticOutputPath = std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) { + auto drv = store->readDerivation(o.path); + auto i = drv.outputs.find(b.output); + if (i == drv.outputs.end()) + throw Error("derivation '%s' does not have output '%s'", b.drvPath->to_string(*store), b.output); + return i->second.path(*store, drv.name, b.output); + }, + [&](const SingleDerivedPath::Built & o) -> std::optional { + return std::nullopt; + }, + }, b.drvPath->raw()); + return mkOutputStringRaw(b, optStaticOutputPath); + } + }, p.raw()); +} + + +void EvalState::mkSingleDerivedPathString( + const SingleDerivedPath & p, + Value & v) +{ + v.mkString( + mkSingleDerivedPathStringRaw(p), NixStringContext { - NixStringContextElem::Built { - .drvPath = makeConstantStorePathRef(drvPath), - .output = outputName, - } + std::visit([](auto && v) -> NixStringContextElem { return v; }, p), }); } @@ -2333,39 +2376,25 @@ SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value & { auto [derivedPath, s_] = coerceToSingleDerivedPathUnchecked(pos, v, errorCtx); auto s = s_; - std::visit(overloaded { - [&](const SingleDerivedPath::Opaque & o) { - auto sExpected = store->printStorePath(o.path); - if (s != sExpected) + auto sExpected = mkSingleDerivedPathStringRaw(derivedPath); + if (s != sExpected) { + /* `std::visit` is used here just to provide a more precise + error message. */ + std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & o) { error( "path string '%s' has context with the different path '%s'", s, sExpected) .withTrace(pos, errorCtx).debugThrow(); - }, - [&](const SingleDerivedPath::Built & b) { - auto sExpected = std::visit(overloaded { - [&](const SingleDerivedPath::Opaque & o) { - auto drv = store->readDerivation(o.path); - auto i = drv.outputs.find(b.output); - if (i == drv.outputs.end()) - throw Error("derivation '%s' does not have output '%s'", b.drvPath->to_string(*store), b.output); - auto optStaticOutputPath = i->second.path(*store, drv.name, b.output); - // This is testing for the case of CA derivations - return optStaticOutputPath - ? store->printStorePath(*optStaticOutputPath) - : DownstreamPlaceholder::fromSingleDerivedPathBuilt(b).render(); - }, - [&](const SingleDerivedPath::Built & o) { - return DownstreamPlaceholder::fromSingleDerivedPathBuilt(b).render(); - }, - }, b.drvPath->raw()); - if (s != sExpected) + }, + [&](const SingleDerivedPath::Built & b) { error( "string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'", s, b.output, b.drvPath->to_string(*store), sExpected) .withTrace(pos, errorCtx).debugThrow(); - } - }, derivedPath.raw()); + } + }, derivedPath.raw()); + } return derivedPath; } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 662c123b9..0c3eb6505 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -668,37 +668,46 @@ public: /** * Create a string representing a store path. * - * The string is the printed store path with a context containing a single - * `NixStringContextElem::Opaque` element of that store path. + * The string is the printed store path with a context containing a + * single `NixStringContextElem::Opaque` element of that store path. */ void mkStorePathString(const StorePath & storePath, Value & v); /** - * Create a string representing a `DerivedPath::Built`. + * Create a string representing a `SingleDerivedPath::Built`. * - * The string is the printed store path with a context containing a single - * `NixStringContextElem::Built` element of the drv path and output name. + * The string is the printed store path with a context containing a + * single `NixStringContextElem::Built` element of the drv path and + * output name. * * @param value Value we are settings * - * @param drvPath Path the drv whose output we are making a string for + * @param b the drv whose output we are making a string for, and the + * output * - * @param outputName Name of the output - * - * @param optStaticOutputPath Optional output path for that string. Must - * be passed if and only if output store object is input-addressed. - * Will be printed to form string if passed, otherwise a placeholder - * will be used (see `DownstreamPlaceholder`). + * @param optStaticOutputPath Optional output path for that string. + * Must be passed if and only if output store object is + * input-addressed or fixed output. Will be printed to form string + * if passed, otherwise a placeholder will be used (see + * `DownstreamPlaceholder`). * * @param xpSettings Stop-gap to avoid globals during unit tests. */ void mkOutputString( Value & value, - const StorePath & drvPath, - const std::string outputName, + const SingleDerivedPath::Built & b, std::optional optStaticOutputPath, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + /** + * Create a string representing a `SingleDerivedPath`. + * + * A combination of `mkStorePathString` and `mkOutputString`. + */ + void mkSingleDerivedPathString( + const SingleDerivedPath & p, + Value & v); + void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx); /** @@ -714,6 +723,22 @@ public: private: + /** + * Like `mkOutputString` but just creates a raw string, not an + * string Value, which would also have a string context. + */ + std::string mkOutputStringRaw( + const SingleDerivedPath::Built & b, + std::optional optStaticOutputPath, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); + + /** + * Like `mkSingleDerivedPathStringRaw` but just creates a raw string + * Value, which would also have a string context. + */ + std::string mkSingleDerivedPathStringRaw( + const SingleDerivedPath & p); + unsigned long nrEnvs = 0; unsigned long nrValuesInEnvs = 0; unsigned long nrValues = 0; diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 54943b481..be78bca02 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -156,8 +156,10 @@ static void mkOutputString( { state.mkOutputString( attrs.alloc(o.first), - drvPath, - o.first, + SingleDerivedPath::Built { + .drvPath = makeConstantStorePathRef(drvPath), + .output = o.first, + }, o.second.path(*state.store, Derivation::nameFromPath(drvPath), o.first)); } diff --git a/src/libexpr/tests/derived-path.cc b/src/libexpr/tests/derived-path.cc index 2a5ca64f6..d01735db8 100644 --- a/src/libexpr/tests/derived-path.cc +++ b/src/libexpr/tests/derived-path.cc @@ -34,8 +34,8 @@ RC_GTEST_FIXTURE_PROP( RC_GTEST_FIXTURE_PROP( DerivedPathExpressionTest, - prop_built_path_placeholder_round_trip, - (const StorePath & drvPath, const StorePathName & outputName)) + prop_derived_path_built_placeholder_round_trip, + (const SingleDerivedPath::Built & b)) { /** * We set these in tests rather than the regular globals so we don't have @@ -45,27 +45,19 @@ RC_GTEST_FIXTURE_PROP( mockXpSettings.set("experimental-features", "ca-derivations"); auto * v = state.allocValue(); - state.mkOutputString(*v, drvPath, outputName.name, std::nullopt, mockXpSettings); + state.mkOutputString(*v, b, std::nullopt, mockXpSettings); auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, ""); - SingleDerivedPath::Built b { - .drvPath = makeConstantStorePathRef(drvPath), - .output = outputName.name, - }; RC_ASSERT(SingleDerivedPath { b } == d); } RC_GTEST_FIXTURE_PROP( DerivedPathExpressionTest, - prop_built_path_out_path_round_trip, - (const StorePath & drvPath, const StorePathName & outputName, const StorePath & outPath)) + prop_derived_path_built_out_path_round_trip, + (const SingleDerivedPath::Built & b, const StorePath & outPath)) { auto * v = state.allocValue(); - state.mkOutputString(*v, drvPath, outputName.name, outPath); + state.mkOutputString(*v, b, outPath); auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, ""); - SingleDerivedPath::Built b { - .drvPath = makeConstantStorePathRef(drvPath), - .output = outputName.name, - }; RC_ASSERT(SingleDerivedPath { b } == d); } diff --git a/src/libstore/downstream-placeholder.cc b/src/libstore/downstream-placeholder.cc index 885b3604d..d951b7b7d 100644 --- a/src/libstore/downstream-placeholder.cc +++ b/src/libstore/downstream-placeholder.cc @@ -39,16 +39,18 @@ DownstreamPlaceholder DownstreamPlaceholder::unknownDerivation( } DownstreamPlaceholder DownstreamPlaceholder::fromSingleDerivedPathBuilt( - const SingleDerivedPath::Built & b) + const SingleDerivedPath::Built & b, + const ExperimentalFeatureSettings & xpSettings) { return std::visit(overloaded { [&](const SingleDerivedPath::Opaque & o) { - return DownstreamPlaceholder::unknownCaOutput(o.path, b.output); + return DownstreamPlaceholder::unknownCaOutput(o.path, b.output, xpSettings); }, [&](const SingleDerivedPath::Built & b2) { return DownstreamPlaceholder::unknownDerivation( - DownstreamPlaceholder::fromSingleDerivedPathBuilt(b2), - b.output); + DownstreamPlaceholder::fromSingleDerivedPathBuilt(b2, xpSettings), + b.output, + xpSettings); }, }, b.drvPath->raw()); } diff --git a/src/libstore/downstream-placeholder.hh b/src/libstore/downstream-placeholder.hh index 9372dcd58..d58a2ac14 100644 --- a/src/libstore/downstream-placeholder.hh +++ b/src/libstore/downstream-placeholder.hh @@ -84,7 +84,8 @@ public: * `SingleDerivedPath::Built.drvPath` chain. */ static DownstreamPlaceholder fromSingleDerivedPathBuilt( - const SingleDerivedPath::Built & built); + const SingleDerivedPath::Built & built, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); }; } From 44c8d83831e310d445493d7071161c622bc302aa Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 10 Mar 2021 04:22:56 +0000 Subject: [PATCH 026/299] Create `outputOf` primop. In the Nix language, given a drv path, we should be able to construct another string referencing to one of its output. We can do this today with `(import drvPath).output`, but this only works for derivations we already have. With dynamic derivations, however, that doesn't work well because the `drvPath` isn't yet built: importing it like would need to trigger IFD, when the whole point of this feature is to do "dynamic build graph" without IFD! Instead, what we want to do is create a placeholder value with the right string context to refer to the output of the as-yet unbuilt derivation. A new primop in the language, analogous to `builtins.placeholder` can be used to create one. This will achieve all the right properties. The placeholder machinery also will match out the `outPath` attribute for CA derivations works. In 60b7121d2c6d4322b7c2e8e7acfec7b701b2d3a1 we added that type of placeholder, and the derived path and string holder changes necessary to support it. Then in the previous commit we cleaned up the code (inspiration finally hit me!) to deduplicate the code and expose exactly what we need. Now, we can wire up the primop trivally! Part of RFC 92: dynamic derivations (tracking issue #6316) Co-authored-by: Robert Hensing --- doc/manual/src/release-notes/rl-next.md | 3 + src/libexpr/primops.cc | 39 ++++++++++++ src/nix/main.cc | 1 + tests/dyn-drv/dep-built-drv.sh | 9 +++ tests/dyn-drv/eval-outputOf.sh | 80 +++++++++++++++++++++++++ tests/dyn-drv/local.mk | 4 +- tests/dyn-drv/recursive-mod-json.sh | 2 + tests/dyn-drv/text-hashed-output.nix | 10 +++- 8 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 tests/dyn-drv/dep-built-drv.sh create mode 100644 tests/dyn-drv/eval-outputOf.sh diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index be922f95a..7ddb5ca00 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -19,3 +19,6 @@ - The JSON output for derived paths with are store paths is now a string, not an object with a single `path` field. This only affects `nix-build --json` when "building" non-derivation things like fetched sources, which is a no-op. + +- Introduce a new [`outputOf`](@docroot@/language/builtins.md#builtins-outputOf) builtin. + It is part of the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index be78bca02..283f99a48 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1838,6 +1838,45 @@ static RegisterPrimOp primop_readDir({ .fun = prim_readDir, }); +/* Extend single element string context with another output. */ +static void prim_outputOf(EvalState & state, const PosIdx pos, Value * * args, Value & v) +{ + SingleDerivedPath drvPath = state.coerceToSingleDerivedPath(pos, *args[0], "while evaluating the first argument to builtins.outputOf"); + + std::string_view outputName = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument to builtins.outputOf"); + + state.mkSingleDerivedPathString( + SingleDerivedPath::Built { + .drvPath = make_ref(drvPath), + .output = std::string { outputName }, + }, + v); +} + +static RegisterPrimOp primop_outputOf({ + .name = "__outputOf", + .args = {"derivation-reference", "output-name"}, + .doc = R"( + Return the output path of a derivation, literally or using a placeholder if needed. + + If the derivation has a statically-known output path (i.e. the derivation output is input-addressed, or fixed content-addresed), the output path will just be returned. + But if the derivation is content-addressed or if the derivation is itself not-statically produced (i.e. is the output of another derivation), a placeholder will be returned instead. + + *`derivation reference`* must be a string that may contain a regular store path to a derivation, or may be a placeholder reference. If the derivation is produced by a derivation, you must explicitly select `drv.outPath`. + This primop can be chained arbitrarily deeply. + For instance, + ```nix + builtins.outputOf + (builtins.outputOf myDrv "out) + "out" + ``` + will return a placeholder for the output of the output of `myDrv`. + + This primop corresponds to the `^` sigil for derivable paths, e.g. as part of installable syntax on the command line. + )", + .fun = prim_outputOf, + .experimentalFeature = Xp::DynamicDerivations, +}); /************************************************************* * Creating files diff --git a/src/nix/main.cc b/src/nix/main.cc index c5a9c8b33..d05bac68e 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -359,6 +359,7 @@ void mainWrapped(int argc, char * * argv) experimentalFeatureSettings.experimentalFeatures = { Xp::Flakes, Xp::FetchClosure, + Xp::DynamicDerivations, }; evalSettings.pureEval = false; EvalState state({}, openStore("dummy://")); diff --git a/tests/dyn-drv/dep-built-drv.sh b/tests/dyn-drv/dep-built-drv.sh new file mode 100644 index 000000000..8c4a45e3b --- /dev/null +++ b/tests/dyn-drv/dep-built-drv.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +source common.sh + +out1=$(nix-build ./text-hashed-output.nix -A hello --no-out-link) + +clearStore + +expectStderr 1 nix-build ./text-hashed-output.nix -A wrapper --no-out-link | grepQuiet "Dependencies on the outputs of dynamic derivations are not yet supported" diff --git a/tests/dyn-drv/eval-outputOf.sh b/tests/dyn-drv/eval-outputOf.sh new file mode 100644 index 000000000..99d917c06 --- /dev/null +++ b/tests/dyn-drv/eval-outputOf.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +source ./common.sh + +# Without the dynamic-derivations XP feature, we don't have the builtin. +nix --experimental-features 'nix-command' eval --impure --expr \ + 'assert ! (builtins ? outputOf); ""' + +# Test that a string is required. +# +# We currently require a string to be passed, rather than a derivation +# object that could be coerced to a string. We might liberalise this in +# the future so it does work, but there are some design questions to +# resolve first. Adding a test so we don't liberalise it by accident. +expectStderr 1 nix --experimental-features 'nix-command dynamic-derivations' eval --impure --expr \ + 'builtins.outputOf (import ../dependencies.nix) "out"' \ + | grepQuiet "value is a set while a string was expected" + +# Test that "DrvDeep" string contexts are not supported at this time +# +# Like the above, this is a restriction we could relax later. +expectStderr 1 nix --experimental-features 'nix-command dynamic-derivations' eval --impure --expr \ + 'builtins.outputOf (import ../dependencies.nix).drvPath "out"' \ + | grepQuiet "has a context which refers to a complete source and binary closure. This is not supported at this time" + +# Test using `builtins.outputOf` with static derivations +testStaticHello () { + nix eval --impure --expr \ + 'with (import ./text-hashed-output.nix); let + a = hello.outPath; + b = builtins.outputOf (builtins.unsafeDiscardOutputDependency hello.drvPath) "out"; + in builtins.trace a + (builtins.trace b + (assert a == b; null))' +} + +# Test with a regular old input-addresed derivation +# +# `builtins.outputOf` works without ca-derivations and doesn't create a +# placeholder but just returns the output path. +testStaticHello + +# Test with content addressed derivation. +NIX_TESTS_CA_BY_DEFAULT=1 testStaticHello + +# Test with derivation-producing derivation +# +# This is hardly different from the preceding cases, except that we're +# only taking 1 outputOf out of 2 possible outputOfs. Note that +# `.outPath` could be defined as `outputOf drvPath`, which is what we're +# testing here. The other `outputOf` that we're not testing here is the +# use of _dynamic_ derivations. +nix eval --impure --expr \ + 'with (import ./text-hashed-output.nix); let + a = producingDrv.outPath; + b = builtins.outputOf (builtins.builtins.unsafeDiscardOutputDependency producingDrv.drvPath) "out"; + in builtins.trace a + (builtins.trace b + (assert a == b; null))' + +# Test with unbuilt output of derivation-producing derivation. +# +# This function similar to `testStaticHello` used above, but instead of +# checking the property on a constant derivation, we check it on a +# derivation that's from another derivation's output (outPath). +testDynamicHello () { + nix eval --impure --expr \ + 'with (import ./text-hashed-output.nix); let + a = builtins.outputOf producingDrv.outPath "out"; + b = builtins.outputOf (builtins.outputOf (builtins.unsafeDiscardOutputDependency producingDrv.drvPath) "out") "out"; + in builtins.trace a + (builtins.trace b + (assert a == b; null))' +} + +# inner dynamic derivation is input-addressed +testDynamicHello + +# inner dynamic derivation is content-addressed +NIX_TESTS_CA_BY_DEFAULT=1 testDynamicHello diff --git a/tests/dyn-drv/local.mk b/tests/dyn-drv/local.mk index b087ecd1c..0ce7cd37d 100644 --- a/tests/dyn-drv/local.mk +++ b/tests/dyn-drv/local.mk @@ -1,7 +1,9 @@ dyn-drv-tests := \ $(d)/text-hashed-output.sh \ $(d)/recursive-mod-json.sh \ - $(d)/build-built-drv.sh + $(d)/build-built-drv.sh \ + $(d)/eval-outputOf.sh \ + $(d)/dep-built-drv.sh install-tests-groups += dyn-drv diff --git a/tests/dyn-drv/recursive-mod-json.sh b/tests/dyn-drv/recursive-mod-json.sh index 070c5c2cb..0698b81bd 100644 --- a/tests/dyn-drv/recursive-mod-json.sh +++ b/tests/dyn-drv/recursive-mod-json.sh @@ -3,6 +3,8 @@ source common.sh # FIXME if [[ $(uname) != Linux ]]; then skipTest "Not running Linux"; fi +export NIX_TESTS_CA_BY_DEFAULT=1 + enableFeatures 'recursive-nix' restartDaemon diff --git a/tests/dyn-drv/text-hashed-output.nix b/tests/dyn-drv/text-hashed-output.nix index a700fd102..99203b518 100644 --- a/tests/dyn-drv/text-hashed-output.nix +++ b/tests/dyn-drv/text-hashed-output.nix @@ -12,9 +12,6 @@ rec { mkdir -p $out echo "Hello World" > $out/hello ''; - __contentAddressed = true; - outputHashMode = "recursive"; - outputHashAlgo = "sha256"; }; producingDrv = mkDerivation { name = "hello.drv"; @@ -26,4 +23,11 @@ rec { outputHashMode = "text"; outputHashAlgo = "sha256"; }; + wrapper = mkDerivation { + name = "use-dynamic-drv-in-non-dynamic-drv"; + buildCommand = '' + echo "Copying the output of the dynamic derivation" + cp -r ${builtins.outputOf producingDrv.outPath "out"} $out + ''; + }; } From 1ef8008ca7a7b38570f06abdbb7960e93ac044ef Mon Sep 17 00:00:00 2001 From: Alex Zero Date: Sun, 13 Aug 2023 01:31:32 +0100 Subject: [PATCH 027/299] Fix follow path checking at depths greater than 2 We need to recurse into the input tree to handle follows paths that trarverse multiple inputs that may or may not be follow paths themselves. --- src/libexpr/flake/lockfile.cc | 2 +- tests/flakes/follow-paths.sh | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/libexpr/flake/lockfile.cc b/src/libexpr/flake/lockfile.cc index ba2fd46f0..3c202967a 100644 --- a/src/libexpr/flake/lockfile.cc +++ b/src/libexpr/flake/lockfile.cc @@ -345,7 +345,7 @@ void LockFile::check() for (auto & [inputPath, input] : inputs) { if (auto follows = std::get_if<1>(&input)) { - if (!follows->empty() && !get(inputs, *follows)) + if (!follows->empty() && !findInput(*follows)) throw Error("input '%s' follows a non-existent input '%s'", printInputPath(inputPath), printInputPath(*follows)); diff --git a/tests/flakes/follow-paths.sh b/tests/flakes/follow-paths.sh index fe9b51c65..a70d9acb2 100644 --- a/tests/flakes/follow-paths.sh +++ b/tests/flakes/follow-paths.sh @@ -148,3 +148,66 @@ git -C $flakeFollowsA add flake.nix nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid'" nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid2'" + +# Now test follow path overloading +flakeFollowsOverloadA=$TEST_ROOT/follows/overload/flakeA +flakeFollowsOverloadB=$TEST_ROOT/follows/overload/flakeA/flakeB +flakeFollowsOverloadC=$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC +flakeFollowsOverloadD=$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC/flakeD + +# Test following path flakerefs. +createGitRepo $flakeFollowsOverloadA +mkdir -p $flakeFollowsOverloadB +mkdir -p $flakeFollowsOverloadC +mkdir -p $flakeFollowsOverloadD + +cat > $flakeFollowsOverloadD/flake.nix < $flakeFollowsOverloadC/flake.nix < $flakeFollowsOverloadB/flake.nix < $flakeFollowsOverloadA/flake.nix < Date: Mon, 14 Aug 2023 18:52:21 +0100 Subject: [PATCH 028/299] Add release notes for the previous commit --- doc/manual/src/release-notes/rl-next.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 7ddb5ca00..5a870f1ab 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -22,3 +22,5 @@ - Introduce a new [`outputOf`](@docroot@/language/builtins.md#builtins-outputOf) builtin. It is part of the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. + +- Flake follow paths at depths greater than 2 are now handled correctly, preventing "follows a non-existent input" errors. From b74962c92b2d8d9b957934e0aefcf4983169ae1e Mon Sep 17 00:00:00 2001 From: Sergei Trofimovich Date: Mon, 14 Aug 2023 22:07:37 +0100 Subject: [PATCH 029/299] src/libexpr/search-path.cc: avoid out-of-bounds read on string_view Without the change build with `-D_GLIBCXX_ASSERTIONS` exposes testsuite assertion: $ gdb src/libexpr/tests/libnixexpr-tests Reading symbols from src/libexpr/tests/libnixexpr-tests... (gdb) break __glibcxx_assert_fail (gdb) run (gdb) bt in std::__glibcxx_assert_fail(char const*, int, char const*, char const*)@plt () from /mnt/archive/big/git/nix/src/libexpr/libnixexpr.so in std::basic_string_view >::operator[] (this=0x7fffffff56c0, __pos=4) at /nix/store/r74fw2j8rx5idb0w8s1s6ynwwgs0qmh9-gcc-14.0.0/include/c++/14.0.0/string_view:258 in nix::SearchPath::Prefix::suffixIfPotentialMatch (this=0x7fffffff5780, path=...) at src/libexpr/search-path.cc:15 in nix::SearchPathElem_suffixIfPotentialMatch_partialPrefix_Test::TestBody (this=0x555555a17540) at src/libexpr/tests/search-path.cc:62 As string sizes are usigned types `(a - b) > 0` effectively means `a != b`. While the intention should be `a > b`. The change fixes test suite pass. --- src/libexpr/search-path.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/search-path.cc b/src/libexpr/search-path.cc index 36bb4c3a5..180d5f8b1 100644 --- a/src/libexpr/search-path.cc +++ b/src/libexpr/search-path.cc @@ -10,7 +10,7 @@ std::optional SearchPath::Prefix::suffixIfPotentialMatch( /* Non-empty prefix and suffix must be separated by a /, or the prefix is not a valid path prefix. */ - bool needSeparator = n > 0 && (path.size() - n) > 0; + bool needSeparator = n > 0 && n < path.size(); if (needSeparator && path[n] != '/') { return std::nullopt; From 20d9c672d14de3f05a791251a67618d679cf4173 Mon Sep 17 00:00:00 2001 From: Vertex <5567402+VertexA115@users.noreply.github.com> Date: Tue, 15 Aug 2023 10:10:27 +0100 Subject: [PATCH 030/299] Update tests/flakes/follow-paths.sh Co-authored-by: Robert Hensing --- tests/flakes/follow-paths.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/flakes/follow-paths.sh b/tests/flakes/follow-paths.sh index a70d9acb2..f9d5c8c80 100644 --- a/tests/flakes/follow-paths.sh +++ b/tests/flakes/follow-paths.sh @@ -150,6 +150,25 @@ nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override fo nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid2'" # Now test follow path overloading +# This tests a lockfile checking regression https://github.com/NixOS/nix/pull/8819 +# +# We construct the following graph, where p->q means p has input q. +# A double edge means that the edge gets overridden using `follows`. +# +# A +# / \ +# / \ +# v v +# B ==> C --- follows declared in A +# \\ / +# \\/ --- follows declared in B +# v +# D +# +# The message was +# error: input 'B/D' follows a non-existent input 'B/C/D' +# +# Note that for `B` to resolve its follow for `D`, it needs `C/D`, for which it needs to resolve the follow on `C` first. flakeFollowsOverloadA=$TEST_ROOT/follows/overload/flakeA flakeFollowsOverloadB=$TEST_ROOT/follows/overload/flakeA/flakeB flakeFollowsOverloadC=$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC From b13fc7101fb06a65935d145081319145f7fef6f9 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 14 Aug 2023 12:24:26 +0200 Subject: [PATCH 031/299] Add positive source filter Source filtering is a really cool Nix feature that lets us avoid a lot of rebuilds, which speeds up the iteration cycle a lot in cases where the relevant source files aren't actually modified. We used to have a source filter that marked a few files as irrelevant, but this is the wrong approach, as we have many more files that are irrelevant. We may call this negative filtering. This commit switches the source filtering to positive filtering, which is a lot more robust. Instead of marking which files we don't need we marked the files that we do need. It's a superior approach because it is fail safe. Instead of allowing build performance problems to creep in over time, we require that all source inputs are declared. I shouldn't have to explain that declaring inputs is a good practice, so I'll stop over-explaining here. I do have to acknowledge that this will cause a build failure when the filter is incomplete. This is *good*, because it's the only realistic way we could be reminded of these problems. These events will be infrequent, so the small cost of extending the filter is worth it, compared to the hidden cost of longer dev cycles for things like tests, docker image, etc, etc. (Also rebuilding Nix for stupid unnecessary reasons makes my blood boil) --- flake.nix | 61 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index bdbf54169..28e581309 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,52 @@ }) stdenvs); + # Experimental fileset library: https://github.com/NixOS/nixpkgs/pull/222981 + # Not an "idiomatic" flake input because: + # - Propagation to dependent locks: https://github.com/NixOS/nix/issues/7730 + # - Subflake would download redundant and huge parent flake + # - No git tree hash support: https://github.com/NixOS/nix/issues/6044 + inherit (import (builtins.fetchTarball { url = "https://github.com/NixOS/nixpkgs/archive/1bdcd7fc8a6a40b2e805bad759b36e64e911036b.tar.gz"; sha256 = "sha256:14ljlpdsp4x7h1fkhbmc4bd3vsqnx8zdql4h3037wh09ad6a0893"; })) + fileset; + + baseFiles = + # .gitignore has already been processed, so any changes in it are irrelevant + # at this point. It is not represented verbatim for test purposes because + # that would interfere with repo semantics. + fileset.fileFilter (f: f.name != ".gitignore") ./.; + + nixSrc = fileset.toSource { + root = ./.; + fileset = fileset.intersect baseFiles ( + fileset.difference + (fileset.unions [ + ./.version + ./boehmgc-coroutine-sp-fallback.diff + ./bootstrap.sh + ./configure.ac + ./doc + ./local.mk + ./m4 + ./Makefile + ./Makefile.config.in + ./misc + ./mk + ./precompiled-headers.h + ./src + ./tests + ./COPYING + ./scripts/local.mk + (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) + # TODO: do we really need README.md? It doesn't seem used in the build. + ./README.md + ]) + (fileset.unions [ + # Removed file sets + ./tests/nixos + ./tests/installer + ]) + ); + }; # Memoize nixpkgs for different platforms for efficiency. nixpkgsFor = forAllSystems @@ -209,7 +255,7 @@ "-${client.version}-against-${daemon.version}"; inherit version; - src = self; + src = nixSrc; VERSION_SUFFIX = versionSuffix; @@ -320,18 +366,11 @@ }; let canRunInstalled = currentStdenv.buildPlatform.canExecute currentStdenv.hostPlatform; - - sourceByRegexInverted = rxs: origSrc: final.lib.cleanSourceWith { - filter = (path: type: - let relPath = final.lib.removePrefix (toString origSrc + "/") (toString path); - in ! lib.any (re: builtins.match re relPath != null) rxs); - src = origSrc; - }; in currentStdenv.mkDerivation (finalAttrs: { name = "nix-${version}"; inherit version; - src = sourceByRegexInverted [ "tests/nixos/.*" "tests/installer/.*" ] self; + src = nixSrc; VERSION_SUFFIX = versionSuffix; outputs = [ "out" "dev" "doc" ]; @@ -529,7 +568,7 @@ releaseTools.coverageAnalysis { name = "nix-coverage-${version}"; - src = self; + src = nixSrc; configureFlags = testConfigureFlags; @@ -557,7 +596,7 @@ pname = "nix-internal-api-docs"; inherit version; - src = self; + src = nixSrc; configureFlags = testConfigureFlags ++ internalApiDocsConfigureFlags; From 63e0b5d081fed582ac6a0a66f402dc525953524b Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 16 Aug 2023 15:46:37 +0200 Subject: [PATCH 032/299] GC root for fetched nixpkgs/lib content --- ascii-table.nix | 99 + asserts.nix | 53 + attrsets.nix | 1018 ++++++++++ cli.nix | 83 + customisation.nix | 315 ++++ debug.nix | 253 +++ default.nix | 167 ++ deprecated.nix | 307 +++ derivations.nix | 101 + fetchers.nix | 13 + fileset.nix | 993 ++++++++++ filesystem.nix | 194 ++ fixed-points.nix | 113 ++ flake.nix | 5 + generators.nix | 526 ++++++ kernel.nix | 27 + licenses.nix | 1104 +++++++++++ lists.nix | 724 +++++++ meta.nix | 148 ++ minver.nix | 2 + modules.nix | 1287 +++++++++++++ options.nix | 432 +++++ path/README.md | 196 ++ path/default.nix | 433 +++++ path/tests/default.nix | 34 + path/tests/generate.awk | 64 + path/tests/prop.nix | 60 + path/tests/prop.sh | 179 ++ path/tests/unit.nix | 193 ++ source-types.nix | 19 + sources.nix | 292 +++ strings-with-deps.nix | 84 + strings.nix | 1243 +++++++++++++ systems/architectures.nix | 114 ++ systems/default.nix | 228 +++ systems/doubles.nix | 119 ++ systems/examples.nix | 335 ++++ systems/flake-systems.nix | 29 + systems/inspect.nix | 117 ++ systems/parse.nix | 503 +++++ systems/platforms.nix | 565 ++++++ tests/check-eval.nix | 7 + tests/filesystem.sh | 84 + tests/maintainer-module.nix | 32 + tests/maintainers.nix | 53 + tests/misc.nix | 1657 +++++++++++++++++ tests/modules.sh | 408 ++++ ...adhoc-freeformType-survives-type-merge.nix | 14 + .../alias-with-priority-can-override.nix | 55 + tests/modules/alias-with-priority.nix | 55 + tests/modules/attrsOf-conditional-check.nix | 7 + tests/modules/attrsOf-lazy-check.nix | 7 + tests/modules/class-check.nix | 76 + tests/modules/declare-attrsOf.nix | 13 + .../modules/declare-attrsOfSub-any-enable.nix | 29 + ...e-bare-submodule-deep-option-duplicate.nix | 10 + .../declare-bare-submodule-deep-option.nix | 10 + .../declare-bare-submodule-nested-option.nix | 19 + tests/modules/declare-bare-submodule.nix | 18 + .../modules/declare-coerced-value-unsound.nix | 10 + tests/modules/declare-coerced-value.nix | 10 + tests/modules/declare-either.nix | 5 + tests/modules/declare-enable-nested.nix | 14 + tests/modules/declare-enable.nix | 14 + tests/modules/declare-int-between-value.nix | 9 + .../declare-int-positive-value-nested.nix | 9 + tests/modules/declare-int-positive-value.nix | 9 + tests/modules/declare-int-unsigned-value.nix | 9 + tests/modules/declare-lazyAttrsOf.nix | 6 + tests/modules/declare-mkPackageOption.nix | 19 + tests/modules/declare-oneOf.nix | 9 + tests/modules/declare-set.nix | 12 + .../declare-submodule-via-evalModules.nix | 28 + .../modules/declare-submoduleWith-modules.nix | 28 + .../declare-submoduleWith-noshorthand.nix | 13 + tests/modules/declare-submoduleWith-path.nix | 12 + .../declare-submoduleWith-shorthand.nix | 14 + .../modules/declare-submoduleWith-special.nix | 17 + tests/modules/declare-variants.nix | 9 + tests/modules/default.nix | 8 + tests/modules/deferred-module-error.nix | 20 + tests/modules/deferred-module.nix | 58 + tests/modules/define-_module-args-custom.nix | 7 + .../modules/define-attrsOfSub-bar-enable.nix | 3 + tests/modules/define-attrsOfSub-bar.nix | 3 + .../define-attrsOfSub-foo-enable-force.nix | 5 + .../define-attrsOfSub-foo-enable-if.nix | 5 + .../modules/define-attrsOfSub-foo-enable.nix | 3 + .../define-attrsOfSub-foo-force-enable.nix | 7 + .../define-attrsOfSub-foo-if-enable.nix | 7 + tests/modules/define-attrsOfSub-foo.nix | 3 + .../define-attrsOfSub-force-foo-enable.nix | 7 + .../define-attrsOfSub-if-foo-enable.nix | 7 + .../modules/define-bare-submodule-values.nix | 4 + tests/modules/define-enable-abort.nix | 3 + tests/modules/define-enable-force.nix | 5 + tests/modules/define-enable-throw.nix | 3 + .../modules/define-enable-with-custom-arg.nix | 7 + .../define-enable-with-top-level-mkIf.nix | 5 + tests/modules/define-enable.nix | 3 + .../define-force-attrsOfSub-foo-enable.nix | 5 + tests/modules/define-force-enable.nix | 5 + .../define-freeform-keywords-shorthand.nix | 15 + .../define-if-attrsOfSub-foo-enable.nix | 5 + tests/modules/define-module-check.nix | 3 + .../define-option-dependently-nested.nix | 16 + tests/modules/define-option-dependently.nix | 16 + tests/modules/define-settingsDict-a-is-b.nix | 3 + ...define-shorthandOnlyDefinesConfig-true.nix | 1 + .../define-submoduleWith-noshorthand.nix | 3 + .../define-submoduleWith-shorthand.nix | 3 + tests/modules/define-value-int-negative.nix | 3 + tests/modules/define-value-int-positive.nix | 3 + tests/modules/define-value-int-zero.nix | 3 + tests/modules/define-value-list.nix | 3 + .../modules/define-value-string-arbitrary.nix | 3 + tests/modules/define-value-string-bigint.nix | 3 + .../define-value-string-properties.nix | 12 + tests/modules/define-value-string.nix | 3 + tests/modules/define-variant.nix | 22 + tests/modules/disable-declare-enable.nix | 5 + .../disable-define-enable-string-path.nix | 5 + tests/modules/disable-define-enable.nix | 5 + tests/modules/disable-enable-modules.nix | 5 + tests/modules/disable-module-bad-key.nix | 16 + tests/modules/disable-module-with-key.nix | 34 + .../disable-module-with-toString-key.nix | 34 + tests/modules/disable-recursive/bar.nix | 5 + .../modules/disable-recursive/disable-bar.nix | 7 + .../modules/disable-recursive/disable-foo.nix | 7 + tests/modules/disable-recursive/foo.nix | 5 + tests/modules/disable-recursive/main.nix | 8 + tests/modules/doRename-basic.nix | 11 + tests/modules/doRename-warnings.nix | 14 + tests/modules/emptyValues.nix | 36 + .../modules/extendModules-168767-imports.nix | 41 + tests/modules/freeform-attrsOf.nix | 3 + tests/modules/freeform-lazyAttrsOf.nix | 3 + tests/modules/freeform-nested.nix | 14 + tests/modules/freeform-str-dep-unstr.nix | 8 + tests/modules/freeform-submodules.nix | 22 + tests/modules/freeform-unstr-dep-str.nix | 8 + tests/modules/functionTo/list-order.nix | 25 + tests/modules/functionTo/merging-attrs.nix | 27 + tests/modules/functionTo/merging-list.nix | 24 + .../modules/functionTo/submodule-options.nix | 61 + tests/modules/functionTo/trivial.nix | 17 + tests/modules/functionTo/wrong-type.nix | 18 + tests/modules/import-configuration.nix | 12 + tests/modules/import-custom-arg.nix | 6 + tests/modules/import-from-store.nix | 11 + tests/modules/merge-module-with-key.nix | 49 + tests/modules/module-class-is-darwin.nix | 4 + tests/modules/module-class-is-nixos.nix | 4 + tests/modules/module-imports-_type-check.nix | 3 + tests/modules/optionTypeFile.nix | 28 + tests/modules/optionTypeMerging.nix | 27 + tests/modules/raw.nix | 30 + tests/modules/shorthand-meta.nix | 19 + tests/modules/submoduleFiles.nix | 21 + .../types-anything/attrs-coercible.nix | 12 + tests/modules/types-anything/equal-atoms.nix | 26 + tests/modules/types-anything/functions.nix | 23 + tests/modules/types-anything/lists.nix | 16 + tests/modules/types-anything/mk-mods.nix | 44 + tests/modules/types-anything/nested-attrs.nix | 22 + tests/release.nix | 64 + tests/sources.sh | 66 + tests/systems.nix | 42 + tests/teams.nix | 50 + tests/test-to-plist-expected.plist | 46 + trivial.nix | 522 ++++++ types.nix | 908 +++++++++ versions.nix | 64 + zip-int-bits.nix | 39 + 175 files changed, 18510 insertions(+) create mode 100644 ascii-table.nix create mode 100644 asserts.nix create mode 100644 attrsets.nix create mode 100644 cli.nix create mode 100644 customisation.nix create mode 100644 debug.nix create mode 100644 default.nix create mode 100644 deprecated.nix create mode 100644 derivations.nix create mode 100644 fetchers.nix create mode 100644 fileset.nix create mode 100644 filesystem.nix create mode 100644 fixed-points.nix create mode 100644 flake.nix create mode 100644 generators.nix create mode 100644 kernel.nix create mode 100644 licenses.nix create mode 100644 lists.nix create mode 100644 meta.nix create mode 100644 minver.nix create mode 100644 modules.nix create mode 100644 options.nix create mode 100644 path/README.md create mode 100644 path/default.nix create mode 100644 path/tests/default.nix create mode 100644 path/tests/generate.awk create mode 100644 path/tests/prop.nix create mode 100755 path/tests/prop.sh create mode 100644 path/tests/unit.nix create mode 100644 source-types.nix create mode 100644 sources.nix create mode 100644 strings-with-deps.nix create mode 100644 strings.nix create mode 100644 systems/architectures.nix create mode 100644 systems/default.nix create mode 100644 systems/doubles.nix create mode 100644 systems/examples.nix create mode 100644 systems/flake-systems.nix create mode 100644 systems/inspect.nix create mode 100644 systems/parse.nix create mode 100644 systems/platforms.nix create mode 100644 tests/check-eval.nix create mode 100755 tests/filesystem.sh create mode 100644 tests/maintainer-module.nix create mode 100644 tests/maintainers.nix create mode 100644 tests/misc.nix create mode 100755 tests/modules.sh create mode 100644 tests/modules/adhoc-freeformType-survives-type-merge.nix create mode 100644 tests/modules/alias-with-priority-can-override.nix create mode 100644 tests/modules/alias-with-priority.nix create mode 100644 tests/modules/attrsOf-conditional-check.nix create mode 100644 tests/modules/attrsOf-lazy-check.nix create mode 100644 tests/modules/class-check.nix create mode 100644 tests/modules/declare-attrsOf.nix create mode 100644 tests/modules/declare-attrsOfSub-any-enable.nix create mode 100644 tests/modules/declare-bare-submodule-deep-option-duplicate.nix create mode 100644 tests/modules/declare-bare-submodule-deep-option.nix create mode 100644 tests/modules/declare-bare-submodule-nested-option.nix create mode 100644 tests/modules/declare-bare-submodule.nix create mode 100644 tests/modules/declare-coerced-value-unsound.nix create mode 100644 tests/modules/declare-coerced-value.nix create mode 100644 tests/modules/declare-either.nix create mode 100644 tests/modules/declare-enable-nested.nix create mode 100644 tests/modules/declare-enable.nix create mode 100644 tests/modules/declare-int-between-value.nix create mode 100644 tests/modules/declare-int-positive-value-nested.nix create mode 100644 tests/modules/declare-int-positive-value.nix create mode 100644 tests/modules/declare-int-unsigned-value.nix create mode 100644 tests/modules/declare-lazyAttrsOf.nix create mode 100644 tests/modules/declare-mkPackageOption.nix create mode 100644 tests/modules/declare-oneOf.nix create mode 100644 tests/modules/declare-set.nix create mode 100644 tests/modules/declare-submodule-via-evalModules.nix create mode 100644 tests/modules/declare-submoduleWith-modules.nix create mode 100644 tests/modules/declare-submoduleWith-noshorthand.nix create mode 100644 tests/modules/declare-submoduleWith-path.nix create mode 100644 tests/modules/declare-submoduleWith-shorthand.nix create mode 100644 tests/modules/declare-submoduleWith-special.nix create mode 100644 tests/modules/declare-variants.nix create mode 100644 tests/modules/default.nix create mode 100644 tests/modules/deferred-module-error.nix create mode 100644 tests/modules/deferred-module.nix create mode 100644 tests/modules/define-_module-args-custom.nix create mode 100644 tests/modules/define-attrsOfSub-bar-enable.nix create mode 100644 tests/modules/define-attrsOfSub-bar.nix create mode 100644 tests/modules/define-attrsOfSub-foo-enable-force.nix create mode 100644 tests/modules/define-attrsOfSub-foo-enable-if.nix create mode 100644 tests/modules/define-attrsOfSub-foo-enable.nix create mode 100644 tests/modules/define-attrsOfSub-foo-force-enable.nix create mode 100644 tests/modules/define-attrsOfSub-foo-if-enable.nix create mode 100644 tests/modules/define-attrsOfSub-foo.nix create mode 100644 tests/modules/define-attrsOfSub-force-foo-enable.nix create mode 100644 tests/modules/define-attrsOfSub-if-foo-enable.nix create mode 100644 tests/modules/define-bare-submodule-values.nix create mode 100644 tests/modules/define-enable-abort.nix create mode 100644 tests/modules/define-enable-force.nix create mode 100644 tests/modules/define-enable-throw.nix create mode 100644 tests/modules/define-enable-with-custom-arg.nix create mode 100644 tests/modules/define-enable-with-top-level-mkIf.nix create mode 100644 tests/modules/define-enable.nix create mode 100644 tests/modules/define-force-attrsOfSub-foo-enable.nix create mode 100644 tests/modules/define-force-enable.nix create mode 100644 tests/modules/define-freeform-keywords-shorthand.nix create mode 100644 tests/modules/define-if-attrsOfSub-foo-enable.nix create mode 100644 tests/modules/define-module-check.nix create mode 100644 tests/modules/define-option-dependently-nested.nix create mode 100644 tests/modules/define-option-dependently.nix create mode 100644 tests/modules/define-settingsDict-a-is-b.nix create mode 100644 tests/modules/define-shorthandOnlyDefinesConfig-true.nix create mode 100644 tests/modules/define-submoduleWith-noshorthand.nix create mode 100644 tests/modules/define-submoduleWith-shorthand.nix create mode 100644 tests/modules/define-value-int-negative.nix create mode 100644 tests/modules/define-value-int-positive.nix create mode 100644 tests/modules/define-value-int-zero.nix create mode 100644 tests/modules/define-value-list.nix create mode 100644 tests/modules/define-value-string-arbitrary.nix create mode 100644 tests/modules/define-value-string-bigint.nix create mode 100644 tests/modules/define-value-string-properties.nix create mode 100644 tests/modules/define-value-string.nix create mode 100644 tests/modules/define-variant.nix create mode 100644 tests/modules/disable-declare-enable.nix create mode 100644 tests/modules/disable-define-enable-string-path.nix create mode 100644 tests/modules/disable-define-enable.nix create mode 100644 tests/modules/disable-enable-modules.nix create mode 100644 tests/modules/disable-module-bad-key.nix create mode 100644 tests/modules/disable-module-with-key.nix create mode 100644 tests/modules/disable-module-with-toString-key.nix create mode 100644 tests/modules/disable-recursive/bar.nix create mode 100644 tests/modules/disable-recursive/disable-bar.nix create mode 100644 tests/modules/disable-recursive/disable-foo.nix create mode 100644 tests/modules/disable-recursive/foo.nix create mode 100644 tests/modules/disable-recursive/main.nix create mode 100644 tests/modules/doRename-basic.nix create mode 100644 tests/modules/doRename-warnings.nix create mode 100644 tests/modules/emptyValues.nix create mode 100644 tests/modules/extendModules-168767-imports.nix create mode 100644 tests/modules/freeform-attrsOf.nix create mode 100644 tests/modules/freeform-lazyAttrsOf.nix create mode 100644 tests/modules/freeform-nested.nix create mode 100644 tests/modules/freeform-str-dep-unstr.nix create mode 100644 tests/modules/freeform-submodules.nix create mode 100644 tests/modules/freeform-unstr-dep-str.nix create mode 100644 tests/modules/functionTo/list-order.nix create mode 100644 tests/modules/functionTo/merging-attrs.nix create mode 100644 tests/modules/functionTo/merging-list.nix create mode 100644 tests/modules/functionTo/submodule-options.nix create mode 100644 tests/modules/functionTo/trivial.nix create mode 100644 tests/modules/functionTo/wrong-type.nix create mode 100644 tests/modules/import-configuration.nix create mode 100644 tests/modules/import-custom-arg.nix create mode 100644 tests/modules/import-from-store.nix create mode 100644 tests/modules/merge-module-with-key.nix create mode 100644 tests/modules/module-class-is-darwin.nix create mode 100644 tests/modules/module-class-is-nixos.nix create mode 100644 tests/modules/module-imports-_type-check.nix create mode 100644 tests/modules/optionTypeFile.nix create mode 100644 tests/modules/optionTypeMerging.nix create mode 100644 tests/modules/raw.nix create mode 100644 tests/modules/shorthand-meta.nix create mode 100644 tests/modules/submoduleFiles.nix create mode 100644 tests/modules/types-anything/attrs-coercible.nix create mode 100644 tests/modules/types-anything/equal-atoms.nix create mode 100644 tests/modules/types-anything/functions.nix create mode 100644 tests/modules/types-anything/lists.nix create mode 100644 tests/modules/types-anything/mk-mods.nix create mode 100644 tests/modules/types-anything/nested-attrs.nix create mode 100644 tests/release.nix create mode 100755 tests/sources.sh create mode 100644 tests/systems.nix create mode 100644 tests/teams.nix create mode 100644 tests/test-to-plist-expected.plist create mode 100644 trivial.nix create mode 100644 types.nix create mode 100644 versions.nix create mode 100644 zip-int-bits.nix diff --git a/ascii-table.nix b/ascii-table.nix new file mode 100644 index 000000000..74989936e --- /dev/null +++ b/ascii-table.nix @@ -0,0 +1,99 @@ +{ "\t" = 9; + "\n" = 10; + "\r" = 13; + " " = 32; + "!" = 33; + "\"" = 34; + "#" = 35; + "$" = 36; + "%" = 37; + "&" = 38; + "'" = 39; + "(" = 40; + ")" = 41; + "*" = 42; + "+" = 43; + "," = 44; + "-" = 45; + "." = 46; + "/" = 47; + "0" = 48; + "1" = 49; + "2" = 50; + "3" = 51; + "4" = 52; + "5" = 53; + "6" = 54; + "7" = 55; + "8" = 56; + "9" = 57; + ":" = 58; + ";" = 59; + "<" = 60; + "=" = 61; + ">" = 62; + "?" = 63; + "@" = 64; + "A" = 65; + "B" = 66; + "C" = 67; + "D" = 68; + "E" = 69; + "F" = 70; + "G" = 71; + "H" = 72; + "I" = 73; + "J" = 74; + "K" = 75; + "L" = 76; + "M" = 77; + "N" = 78; + "O" = 79; + "P" = 80; + "Q" = 81; + "R" = 82; + "S" = 83; + "T" = 84; + "U" = 85; + "V" = 86; + "W" = 87; + "X" = 88; + "Y" = 89; + "Z" = 90; + "[" = 91; + "\\" = 92; + "]" = 93; + "^" = 94; + "_" = 95; + "`" = 96; + "a" = 97; + "b" = 98; + "c" = 99; + "d" = 100; + "e" = 101; + "f" = 102; + "g" = 103; + "h" = 104; + "i" = 105; + "j" = 106; + "k" = 107; + "l" = 108; + "m" = 109; + "n" = 110; + "o" = 111; + "p" = 112; + "q" = 113; + "r" = 114; + "s" = 115; + "t" = 116; + "u" = 117; + "v" = 118; + "w" = 119; + "x" = 120; + "y" = 121; + "z" = 122; + "{" = 123; + "|" = 124; + "}" = 125; + "~" = 126; +} diff --git a/asserts.nix b/asserts.nix new file mode 100644 index 000000000..98e0b490a --- /dev/null +++ b/asserts.nix @@ -0,0 +1,53 @@ +{ lib }: + +rec { + + /* Throw if pred is false, else return pred. + Intended to be used to augment asserts with helpful error messages. + + Example: + assertMsg false "nope" + stderr> error: nope + + assert assertMsg ("foo" == "bar") "foo is not bar, silly"; "" + stderr> error: foo is not bar, silly + + Type: + assertMsg :: Bool -> String -> Bool + */ + # TODO(Profpatsch): add tests that check stderr + assertMsg = + # Predicate that needs to succeed, otherwise `msg` is thrown + pred: + # Message to throw in case `pred` fails + msg: + pred || builtins.throw msg; + + /* Specialized `assertMsg` for checking if `val` is one of the elements + of the list `xs`. Useful for checking enums. + + Example: + let sslLibrary = "libressl"; + in assertOneOf "sslLibrary" sslLibrary [ "openssl" "bearssl" ] + stderr> error: sslLibrary must be one of [ + stderr> "openssl" + stderr> "bearssl" + stderr> ], but is: "libressl" + + Type: + assertOneOf :: String -> ComparableVal -> List ComparableVal -> Bool + */ + assertOneOf = + # The name of the variable the user entered `val` into, for inclusion in the error message + name: + # The value of what the user provided, to be compared against the values in `xs` + val: + # The list of valid values + xs: + assertMsg + (lib.elem val xs) + "${name} must be one of ${ + lib.generators.toPretty {} xs}, but is: ${ + lib.generators.toPretty {} val}"; + +} diff --git a/attrsets.nix b/attrsets.nix new file mode 100644 index 000000000..73b71f68d --- /dev/null +++ b/attrsets.nix @@ -0,0 +1,1018 @@ +{ lib }: +# Operations on attribute sets. + +let + inherit (builtins) head tail length; + inherit (lib.trivial) flip id mergeAttrs pipe; + inherit (lib.strings) concatStringsSep concatMapStringsSep escapeNixIdentifier sanitizeDerivationName; + inherit (lib.lists) foldr foldl' concatMap concatLists elemAt all partition groupBy take foldl; +in + +rec { + inherit (builtins) attrNames listToAttrs hasAttr isAttrs getAttr removeAttrs; + + + /* Return an attribute from nested attribute sets. + + Example: + x = { a = { b = 3; }; } + # ["a" "b"] is equivalent to x.a.b + # 6 is a default value to return if the path does not exist in attrset + attrByPath ["a" "b"] 6 x + => 3 + attrByPath ["z" "z"] 6 x + => 6 + + Type: + attrByPath :: [String] -> Any -> AttrSet -> Any + + */ + attrByPath = + # A list of strings representing the attribute path to return from `set` + attrPath: + # Default value if `attrPath` does not resolve to an existing value + default: + # The nested attribute set to select values from + set: + let attr = head attrPath; + in + if attrPath == [] then set + else if set ? ${attr} + then attrByPath (tail attrPath) default set.${attr} + else default; + + /* Return if an attribute from nested attribute set exists. + + Example: + x = { a = { b = 3; }; } + hasAttrByPath ["a" "b"] x + => true + hasAttrByPath ["z" "z"] x + => false + + Type: + hasAttrByPath :: [String] -> AttrSet -> Bool + */ + hasAttrByPath = + # A list of strings representing the attribute path to check from `set` + attrPath: + # The nested attribute set to check + e: + let attr = head attrPath; + in + if attrPath == [] then true + else if e ? ${attr} + then hasAttrByPath (tail attrPath) e.${attr} + else false; + + + /* Create a new attribute set with `value` set at the nested attribute location specified in `attrPath`. + + Example: + setAttrByPath ["a" "b"] 3 + => { a = { b = 3; }; } + + Type: + setAttrByPath :: [String] -> Any -> AttrSet + */ + setAttrByPath = + # A list of strings representing the attribute path to set + attrPath: + # The value to set at the location described by `attrPath` + value: + let + len = length attrPath; + atDepth = n: + if n == len + then value + else { ${elemAt attrPath n} = atDepth (n + 1); }; + in atDepth 0; + + /* Like `attrByPath`, but without a default value. If it doesn't find the + path it will throw an error. + + Example: + x = { a = { b = 3; }; } + getAttrFromPath ["a" "b"] x + => 3 + getAttrFromPath ["z" "z"] x + => error: cannot find attribute `z.z' + + Type: + getAttrFromPath :: [String] -> AttrSet -> Any + */ + getAttrFromPath = + # A list of strings representing the attribute path to get from `set` + attrPath: + # The nested attribute set to find the value in. + set: + let errorMsg = "cannot find attribute `" + concatStringsSep "." attrPath + "'"; + in attrByPath attrPath (abort errorMsg) set; + + /* Map each attribute in the given set and merge them into a new attribute set. + + Type: + concatMapAttrs :: (String -> a -> AttrSet) -> AttrSet -> AttrSet + + Example: + concatMapAttrs + (name: value: { + ${name} = value; + ${name + value} = value; + }) + { x = "a"; y = "b"; } + => { x = "a"; xa = "a"; y = "b"; yb = "b"; } + */ + concatMapAttrs = f: v: + foldl' mergeAttrs { } + (attrValues + (mapAttrs f v) + ); + + + /* Update or set specific paths of an attribute set. + + Takes a list of updates to apply and an attribute set to apply them to, + and returns the attribute set with the updates applied. Updates are + represented as `{ path = ...; update = ...; }` values, where `path` is a + list of strings representing the attribute path that should be updated, + and `update` is a function that takes the old value at that attribute path + as an argument and returns the new + value it should be. + + Properties: + + - Updates to deeper attribute paths are applied before updates to more + shallow attribute paths + + - Multiple updates to the same attribute path are applied in the order + they appear in the update list + + - If any but the last `path` element leads into a value that is not an + attribute set, an error is thrown + + - If there is an update for an attribute path that doesn't exist, + accessing the argument in the update function causes an error, but + intermediate attribute sets are implicitly created as needed + + Example: + updateManyAttrsByPath [ + { + path = [ "a" "b" ]; + update = old: { d = old.c; }; + } + { + path = [ "a" "b" "c" ]; + update = old: old + 1; + } + { + path = [ "x" "y" ]; + update = old: "xy"; + } + ] { a.b.c = 0; } + => { a = { b = { d = 1; }; }; x = { y = "xy"; }; } + + Type: updateManyAttrsByPath :: [{ path :: [String]; update :: (Any -> Any); }] -> AttrSet -> AttrSet + */ + updateManyAttrsByPath = let + # When recursing into attributes, instead of updating the `path` of each + # update using `tail`, which needs to allocate an entirely new list, + # we just pass a prefix length to use and make sure to only look at the + # path without the prefix length, so that we can reuse the original list + # entries. + go = prefixLength: hasValue: value: updates: + let + # Splits updates into ones on this level (split.right) + # And ones on levels further down (split.wrong) + split = partition (el: length el.path == prefixLength) updates; + + # Groups updates on further down levels into the attributes they modify + nested = groupBy (el: elemAt el.path prefixLength) split.wrong; + + # Applies only nested modification to the input value + withNestedMods = + # Return the value directly if we don't have any nested modifications + if split.wrong == [] then + if hasValue then value + else + # Throw an error if there is no value. This `head` call here is + # safe, but only in this branch since `go` could only be called + # with `hasValue == false` for nested updates, in which case + # it's also always called with at least one update + let updatePath = (head split.right).path; in + throw + ( "updateManyAttrsByPath: Path '${showAttrPath updatePath}' does " + + "not exist in the given value, but the first update to this " + + "path tries to access the existing value.") + else + # If there are nested modifications, try to apply them to the value + if ! hasValue then + # But if we don't have a value, just use an empty attribute set + # as the value, but simplify the code a bit + mapAttrs (name: go (prefixLength + 1) false null) nested + else if isAttrs value then + # If we do have a value and it's an attribute set, override it + # with the nested modifications + value // + mapAttrs (name: go (prefixLength + 1) (value ? ${name}) value.${name}) nested + else + # However if it's not an attribute set, we can't apply the nested + # modifications, throw an error + let updatePath = (head split.wrong).path; in + throw + ( "updateManyAttrsByPath: Path '${showAttrPath updatePath}' needs to " + + "be updated, but path '${showAttrPath (take prefixLength updatePath)}' " + + "of the given value is not an attribute set, so we can't " + + "update an attribute inside of it."); + + # We get the final result by applying all the updates on this level + # after having applied all the nested updates + # We use foldl instead of foldl' so that in case of multiple updates, + # intermediate values aren't evaluated if not needed + in foldl (acc: el: el.update acc) withNestedMods split.right; + + in updates: value: go 0 true value updates; + + /* Return the specified attributes from a set. + + Example: + attrVals ["a" "b" "c"] as + => [as.a as.b as.c] + + Type: + attrVals :: [String] -> AttrSet -> [Any] + */ + attrVals = + # The list of attributes to fetch from `set`. Each attribute name must exist on the attrbitue set + nameList: + # The set to get attribute values from + set: map (x: set.${x}) nameList; + + + /* Return the values of all attributes in the given set, sorted by + attribute name. + + Example: + attrValues {c = 3; a = 1; b = 2;} + => [1 2 3] + + Type: + attrValues :: AttrSet -> [Any] + */ + attrValues = builtins.attrValues or (attrs: attrVals (attrNames attrs) attrs); + + + /* Given a set of attribute names, return the set of the corresponding + attributes from the given set. + + Example: + getAttrs [ "a" "b" ] { a = 1; b = 2; c = 3; } + => { a = 1; b = 2; } + + Type: + getAttrs :: [String] -> AttrSet -> AttrSet + */ + getAttrs = + # A list of attribute names to get out of `set` + names: + # The set to get the named attributes from + attrs: genAttrs names (name: attrs.${name}); + + /* Collect each attribute named `attr` from a list of attribute + sets. Sets that don't contain the named attribute are ignored. + + Example: + catAttrs "a" [{a = 1;} {b = 0;} {a = 2;}] + => [1 2] + + Type: + catAttrs :: String -> [AttrSet] -> [Any] + */ + catAttrs = builtins.catAttrs or + (attr: l: concatLists (map (s: if s ? ${attr} then [s.${attr}] else []) l)); + + + /* Filter an attribute set by removing all attributes for which the + given predicate return false. + + Example: + filterAttrs (n: v: n == "foo") { foo = 1; bar = 2; } + => { foo = 1; } + + Type: + filterAttrs :: (String -> Any -> Bool) -> AttrSet -> AttrSet + */ + filterAttrs = + # Predicate taking an attribute name and an attribute value, which returns `true` to include the attribute, or `false` to exclude the attribute. + pred: + # The attribute set to filter + set: + listToAttrs (concatMap (name: let v = set.${name}; in if pred name v then [(nameValuePair name v)] else []) (attrNames set)); + + + /* Filter an attribute set recursively by removing all attributes for + which the given predicate return false. + + Example: + filterAttrsRecursive (n: v: v != null) { foo = { bar = null; }; } + => { foo = {}; } + + Type: + filterAttrsRecursive :: (String -> Any -> Bool) -> AttrSet -> AttrSet + */ + filterAttrsRecursive = + # Predicate taking an attribute name and an attribute value, which returns `true` to include the attribute, or `false` to exclude the attribute. + pred: + # The attribute set to filter + set: + listToAttrs ( + concatMap (name: + let v = set.${name}; in + if pred name v then [ + (nameValuePair name ( + if isAttrs v then filterAttrsRecursive pred v + else v + )) + ] else [] + ) (attrNames set) + ); + + /* + Like builtins.foldl' but for attribute sets. + Iterates over every name-value pair in the given attribute set. + The result of the callback function is often called `acc` for accumulator. It is passed between callbacks from left to right and the final `acc` is the return value of `foldlAttrs`. + + Attention: + There is a completely different function + `lib.foldAttrs` + which has nothing to do with this function, despite the similar name. + + Example: + foldlAttrs + (acc: name: value: { + sum = acc.sum + value; + names = acc.names ++ [name]; + }) + { sum = 0; names = []; } + { + foo = 1; + bar = 10; + } + -> + { + sum = 11; + names = ["bar" "foo"]; + } + + foldlAttrs + (throw "function not needed") + 123 + {}; + -> + 123 + + foldlAttrs + (_: _: v: v) + (throw "initial accumulator not needed") + { z = 3; a = 2; }; + -> + 3 + + The accumulator doesn't have to be an attrset. + It can be as simple as a number or string. + + foldlAttrs + (acc: _: v: acc * 10 + v) + 1 + { z = 1; a = 2; }; + -> + 121 + + Type: + foldlAttrs :: ( a -> String -> b -> a ) -> a -> { ... :: b } -> a + */ + foldlAttrs = f: init: set: + foldl' + (acc: name: f acc name set.${name}) + init + (attrNames set); + + /* Apply fold functions to values grouped by key. + + Example: + foldAttrs (item: acc: [item] ++ acc) [] [{ a = 2; } { a = 3; }] + => { a = [ 2 3 ]; } + + Type: + foldAttrs :: (Any -> Any -> Any) -> Any -> [AttrSets] -> Any + + */ + foldAttrs = + # A function, given a value and a collector combines the two. + op: + # The starting value. + nul: + # A list of attribute sets to fold together by key. + list_of_attrs: + foldr (n: a: + foldr (name: o: + o // { ${name} = op n.${name} (a.${name} or nul); } + ) a (attrNames n) + ) {} list_of_attrs; + + + /* Recursively collect sets that verify a given predicate named `pred` + from the set `attrs`. The recursion is stopped when the predicate is + verified. + + Example: + collect isList { a = { b = ["b"]; }; c = [1]; } + => [["b"] [1]] + + collect (x: x ? outPath) + { a = { outPath = "a/"; }; b = { outPath = "b/"; }; } + => [{ outPath = "a/"; } { outPath = "b/"; }] + + Type: + collect :: (AttrSet -> Bool) -> AttrSet -> [x] + */ + collect = + # Given an attribute's value, determine if recursion should stop. + pred: + # The attribute set to recursively collect. + attrs: + if pred attrs then + [ attrs ] + else if isAttrs attrs then + concatMap (collect pred) (attrValues attrs) + else + []; + + /* Return the cartesian product of attribute set value combinations. + + Example: + cartesianProductOfSets { a = [ 1 2 ]; b = [ 10 20 ]; } + => [ + { a = 1; b = 10; } + { a = 1; b = 20; } + { a = 2; b = 10; } + { a = 2; b = 20; } + ] + Type: + cartesianProductOfSets :: AttrSet -> [AttrSet] + */ + cartesianProductOfSets = + # Attribute set with attributes that are lists of values + attrsOfLists: + foldl' (listOfAttrs: attrName: + concatMap (attrs: + map (listValue: attrs // { ${attrName} = listValue; }) attrsOfLists.${attrName} + ) listOfAttrs + ) [{}] (attrNames attrsOfLists); + + + /* Utility function that creates a `{name, value}` pair as expected by `builtins.listToAttrs`. + + Example: + nameValuePair "some" 6 + => { name = "some"; value = 6; } + + Type: + nameValuePair :: String -> Any -> { name :: String; value :: Any; } + */ + nameValuePair = + # Attribute name + name: + # Attribute value + value: + { inherit name value; }; + + + /* Apply a function to each element in an attribute set, creating a new attribute set. + + Example: + mapAttrs (name: value: name + "-" + value) + { x = "foo"; y = "bar"; } + => { x = "x-foo"; y = "y-bar"; } + + Type: + mapAttrs :: (String -> Any -> Any) -> AttrSet -> AttrSet + */ + mapAttrs = builtins.mapAttrs or + (f: set: + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))); + + + /* Like `mapAttrs`, but allows the name of each attribute to be + changed in addition to the value. The applied function should + return both the new name and value as a `nameValuePair`. + + Example: + mapAttrs' (name: value: nameValuePair ("foo_" + name) ("bar-" + value)) + { x = "a"; y = "b"; } + => { foo_x = "bar-a"; foo_y = "bar-b"; } + + Type: + mapAttrs' :: (String -> Any -> { name :: String; value :: Any; }) -> AttrSet -> AttrSet + */ + mapAttrs' = + # A function, given an attribute's name and value, returns a new `nameValuePair`. + f: + # Attribute set to map over. + set: + listToAttrs (map (attr: f attr set.${attr}) (attrNames set)); + + + /* Call a function for each attribute in the given set and return + the result in a list. + + Example: + mapAttrsToList (name: value: name + value) + { x = "a"; y = "b"; } + => [ "xa" "yb" ] + + Type: + mapAttrsToList :: (String -> a -> b) -> AttrSet -> [b] + + */ + mapAttrsToList = + # A function, given an attribute's name and value, returns a new value. + f: + # Attribute set to map over. + attrs: + map (name: f name attrs.${name}) (attrNames attrs); + + + /* Like `mapAttrs`, except that it recursively applies itself to + the *leaf* attributes of a potentially-nested attribute set: + the second argument of the function will never be an attrset. + Also, the first argument of the argument function is a *list* + of the attribute names that form the path to the leaf attribute. + + For a function that gives you control over what counts as a leaf, + see `mapAttrsRecursiveCond`. + + Example: + mapAttrsRecursive (path: value: concatStringsSep "-" (path ++ [value])) + { n = { a = "A"; m = { b = "B"; c = "C"; }; }; d = "D"; } + => { n = { a = "n-a-A"; m = { b = "n-m-b-B"; c = "n-m-c-C"; }; }; d = "d-D"; } + + Type: + mapAttrsRecursive :: ([String] -> a -> b) -> AttrSet -> AttrSet + */ + mapAttrsRecursive = + # A function, given a list of attribute names and a value, returns a new value. + f: + # Set to recursively map over. + set: + mapAttrsRecursiveCond (as: true) f set; + + + /* Like `mapAttrsRecursive`, but it takes an additional predicate + function that tells it whether to recurse into an attribute + set. If it returns false, `mapAttrsRecursiveCond` does not + recurse, but does apply the map function. If it returns true, it + does recurse, and does not apply the map function. + + Example: + # To prevent recursing into derivations (which are attribute + # sets with the attribute "type" equal to "derivation"): + mapAttrsRecursiveCond + (as: !(as ? "type" && as.type == "derivation")) + (x: ... do something ...) + attrs + + Type: + mapAttrsRecursiveCond :: (AttrSet -> Bool) -> ([String] -> a -> b) -> AttrSet -> AttrSet + */ + mapAttrsRecursiveCond = + # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. + cond: + # A function, given a list of attribute names and a value, returns a new value. + f: + # Attribute set to recursively map over. + set: + let + recurse = path: + let + g = + name: value: + if isAttrs value && cond value + then recurse (path ++ [name]) value + else f (path ++ [name]) value; + in mapAttrs g; + in recurse [] set; + + + /* Generate an attribute set by mapping a function over a list of + attribute names. + + Example: + genAttrs [ "foo" "bar" ] (name: "x_" + name) + => { foo = "x_foo"; bar = "x_bar"; } + + Type: + genAttrs :: [ String ] -> (String -> Any) -> AttrSet + */ + genAttrs = + # Names of values in the resulting attribute set. + names: + # A function, given the name of the attribute, returns the attribute's value. + f: + listToAttrs (map (n: nameValuePair n (f n)) names); + + + /* Check whether the argument is a derivation. Any set with + `{ type = "derivation"; }` counts as a derivation. + + Example: + nixpkgs = import {} + isDerivation nixpkgs.ruby + => true + isDerivation "foobar" + => false + + Type: + isDerivation :: Any -> Bool + */ + isDerivation = + # Value to check. + value: value.type or null == "derivation"; + + /* Converts a store path to a fake derivation. + + Type: + toDerivation :: Path -> Derivation + */ + toDerivation = + # A store path to convert to a derivation. + path: + let + path' = builtins.storePath path; + res = + { type = "derivation"; + name = sanitizeDerivationName (builtins.substring 33 (-1) (baseNameOf path')); + outPath = path'; + outputs = [ "out" ]; + out = res; + outputName = "out"; + }; + in res; + + + /* If `cond` is true, return the attribute set `as`, + otherwise an empty attribute set. + + Example: + optionalAttrs (true) { my = "set"; } + => { my = "set"; } + optionalAttrs (false) { my = "set"; } + => { } + + Type: + optionalAttrs :: Bool -> AttrSet -> AttrSet + */ + optionalAttrs = + # Condition under which the `as` attribute set is returned. + cond: + # The attribute set to return if `cond` is `true`. + as: + if cond then as else {}; + + + /* Merge sets of attributes and use the function `f` to merge attributes + values. + + Example: + zipAttrsWithNames ["a"] (name: vs: vs) [{a = "x";} {a = "y"; b = "z";}] + => { a = ["x" "y"]; } + + Type: + zipAttrsWithNames :: [ String ] -> (String -> [ Any ] -> Any) -> [ AttrSet ] -> AttrSet + */ + zipAttrsWithNames = + # List of attribute names to zip. + names: + # A function, accepts an attribute name, all the values, and returns a combined value. + f: + # List of values from the list of attribute sets. + sets: + listToAttrs (map (name: { + inherit name; + value = f name (catAttrs name sets); + }) names); + + + /* Merge sets of attributes and use the function f to merge attribute values. + Like `lib.attrsets.zipAttrsWithNames` with all key names are passed for `names`. + + Implementation note: Common names appear multiple times in the list of + names, hopefully this does not affect the system because the maximal + laziness avoid computing twice the same expression and `listToAttrs` does + not care about duplicated attribute names. + + Example: + zipAttrsWith (name: values: values) [{a = "x";} {a = "y"; b = "z";}] + => { a = ["x" "y"]; b = ["z"]; } + + Type: + zipAttrsWith :: (String -> [ Any ] -> Any) -> [ AttrSet ] -> AttrSet + */ + zipAttrsWith = + builtins.zipAttrsWith or (f: sets: zipAttrsWithNames (concatMap attrNames sets) f sets); + + + /* Merge sets of attributes and combine each attribute value in to a list. + + Like `lib.attrsets.zipAttrsWith` with `(name: values: values)` as the function. + + Example: + zipAttrs [{a = "x";} {a = "y"; b = "z";}] + => { a = ["x" "y"]; b = ["z"]; } + + Type: + zipAttrs :: [ AttrSet ] -> AttrSet + */ + zipAttrs = + # List of attribute sets to zip together. + sets: + zipAttrsWith (name: values: values) sets; + + + /* Does the same as the update operator '//' except that attributes are + merged until the given predicate is verified. The predicate should + accept 3 arguments which are the path to reach the attribute, a part of + the first attribute set and a part of the second attribute set. When + the predicate is satisfied, the value of the first attribute set is + replaced by the value of the second attribute set. + + Example: + recursiveUpdateUntil (path: l: r: path == ["foo"]) { + # first attribute set + foo.bar = 1; + foo.baz = 2; + bar = 3; + } { + #second attribute set + foo.bar = 1; + foo.quz = 2; + baz = 4; + } + + => { + foo.bar = 1; # 'foo.*' from the second set + foo.quz = 2; # + bar = 3; # 'bar' from the first set + baz = 4; # 'baz' from the second set + } + + Type: + recursiveUpdateUntil :: ( [ String ] -> AttrSet -> AttrSet -> Bool ) -> AttrSet -> AttrSet -> AttrSet + */ + recursiveUpdateUntil = + # Predicate, taking the path to the current attribute as a list of strings for attribute names, and the two values at that path from the original arguments. + pred: + # Left attribute set of the merge. + lhs: + # Right attribute set of the merge. + rhs: + let f = attrPath: + zipAttrsWith (n: values: + let here = attrPath ++ [n]; in + if length values == 1 + || pred here (elemAt values 1) (head values) then + head values + else + f here values + ); + in f [] [rhs lhs]; + + + /* A recursive variant of the update operator ‘//’. The recursion + stops when one of the attribute values is not an attribute set, + in which case the right hand side value takes precedence over the + left hand side value. + + Example: + recursiveUpdate { + boot.loader.grub.enable = true; + boot.loader.grub.device = "/dev/hda"; + } { + boot.loader.grub.device = ""; + } + + returns: { + boot.loader.grub.enable = true; + boot.loader.grub.device = ""; + } + + Type: + recursiveUpdate :: AttrSet -> AttrSet -> AttrSet + */ + recursiveUpdate = + # Left attribute set of the merge. + lhs: + # Right attribute set of the merge. + rhs: + recursiveUpdateUntil (path: lhs: rhs: !(isAttrs lhs && isAttrs rhs)) lhs rhs; + + + /* Returns true if the pattern is contained in the set. False otherwise. + + Example: + matchAttrs { cpu = {}; } { cpu = { bits = 64; }; } + => true + + Type: + matchAttrs :: AttrSet -> AttrSet -> Bool + */ + matchAttrs = + # Attribute set structure to match + pattern: + # Attribute set to find patterns in + attrs: + assert isAttrs pattern; + all id (attrValues (zipAttrsWithNames (attrNames pattern) (n: values: + let pat = head values; val = elemAt values 1; in + if length values == 1 then false + else if isAttrs pat then isAttrs val && matchAttrs pat val + else pat == val + ) [pattern attrs])); + + + /* Override only the attributes that are already present in the old set + useful for deep-overriding. + + Example: + overrideExisting {} { a = 1; } + => {} + overrideExisting { b = 2; } { a = 1; } + => { b = 2; } + overrideExisting { a = 3; b = 2; } { a = 1; } + => { a = 1; b = 2; } + + Type: + overrideExisting :: AttrSet -> AttrSet -> AttrSet + */ + overrideExisting = + # Original attribute set + old: + # Attribute set with attributes to override in `old`. + new: + mapAttrs (name: value: new.${name} or value) old; + + + /* Turns a list of strings into a human-readable description of those + strings represented as an attribute path. The result of this function is + not intended to be machine-readable. + Create a new attribute set with `value` set at the nested attribute location specified in `attrPath`. + + Example: + showAttrPath [ "foo" "10" "bar" ] + => "foo.\"10\".bar" + showAttrPath [] + => "" + + Type: + showAttrPath :: [String] -> String + */ + showAttrPath = + # Attribute path to render to a string + path: + if path == [] then "" + else concatMapStringsSep "." escapeNixIdentifier path; + + + /* Get a package output. + If no output is found, fallback to `.out` and then to the default. + + Example: + getOutput "dev" pkgs.openssl + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-dev" + + Type: + getOutput :: String -> Derivation -> String + */ + getOutput = output: pkg: + if ! pkg ? outputSpecified || ! pkg.outputSpecified + then pkg.${output} or pkg.out or pkg + else pkg; + + /* Get a package's `bin` output. + If the output does not exist, fallback to `.out` and then to the default. + + Example: + getBin pkgs.openssl + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r" + + Type: + getBin :: Derivation -> String + */ + getBin = getOutput "bin"; + + + /* Get a package's `lib` output. + If the output does not exist, fallback to `.out` and then to the default. + + Example: + getLib pkgs.openssl + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-lib" + + Type: + getLib :: Derivation -> String + */ + getLib = getOutput "lib"; + + + /* Get a package's `dev` output. + If the output does not exist, fallback to `.out` and then to the default. + + Example: + getDev pkgs.openssl + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-dev" + + Type: + getDev :: Derivation -> String + */ + getDev = getOutput "dev"; + + + /* Get a package's `man` output. + If the output does not exist, fallback to `.out` and then to the default. + + Example: + getMan pkgs.openssl + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-man" + + Type: + getMan :: Derivation -> String + */ + getMan = getOutput "man"; + + /* Pick the outputs of packages to place in `buildInputs` + + Type: chooseDevOutputs :: [Derivation] -> [String] + + */ + chooseDevOutputs = + # List of packages to pick `dev` outputs from + drvs: + builtins.map getDev drvs; + + /* Make various Nix tools consider the contents of the resulting + attribute set when looking for what to build, find, etc. + + This function only affects a single attribute set; it does not + apply itself recursively for nested attribute sets. + + Example: + { pkgs ? import {} }: + { + myTools = pkgs.lib.recurseIntoAttrs { + inherit (pkgs) hello figlet; + }; + } + + Type: + recurseIntoAttrs :: AttrSet -> AttrSet + + */ + recurseIntoAttrs = + # An attribute set to scan for derivations. + attrs: + attrs // { recurseForDerivations = true; }; + + /* Undo the effect of recurseIntoAttrs. + + Type: + dontRecurseIntoAttrs :: AttrSet -> AttrSet + */ + dontRecurseIntoAttrs = + # An attribute set to not scan for derivations. + attrs: + attrs // { recurseForDerivations = false; }; + + /* `unionOfDisjoint x y` is equal to `x // y // z` where the + attrnames in `z` are the intersection of the attrnames in `x` and + `y`, and all values `assert` with an error message. This + operator is commutative, unlike (//). + + Type: unionOfDisjoint :: AttrSet -> AttrSet -> AttrSet + */ + unionOfDisjoint = x: y: + let + intersection = builtins.intersectAttrs x y; + collisions = lib.concatStringsSep " " (builtins.attrNames intersection); + mask = builtins.mapAttrs (name: value: builtins.throw + "unionOfDisjoint: collision on ${name}; complete list: ${collisions}") + intersection; + in + (x // y) // mask; + + # DEPRECATED + zipWithNames = zipAttrsWithNames; + + # DEPRECATED + zip = builtins.trace + "lib.zip is deprecated, use lib.zipAttrsWith instead" zipAttrsWith; +} diff --git a/cli.nix b/cli.nix new file mode 100644 index 000000000..c96d4dbb0 --- /dev/null +++ b/cli.nix @@ -0,0 +1,83 @@ +{ lib }: + +rec { + /* Automatically convert an attribute set to command-line options. + + This helps protect against malformed command lines and also to reduce + boilerplate related to command-line construction for simple use cases. + + `toGNUCommandLine` returns a list of nix strings. + `toGNUCommandLineShell` returns an escaped shell string. + + Example: + cli.toGNUCommandLine {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + } + => [ + "-X" "PUT" + "--data" "{\"id\":0}" + "--retry" "3" + "--url" "https://example.com/foo" + "--url" "https://example.com/bar" + "--verbose" + ] + + cli.toGNUCommandLineShell {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + } + => "'-X' 'PUT' '--data' '{\"id\":0}' '--retry' '3' '--url' 'https://example.com/foo' '--url' 'https://example.com/bar' '--verbose'"; + */ + toGNUCommandLineShell = + options: attrs: lib.escapeShellArgs (toGNUCommandLine options attrs); + + toGNUCommandLine = { + # how to string-format the option name; + # by default one character is a short option (`-`), + # more than one characters a long option (`--`). + mkOptionName ? + k: if builtins.stringLength k == 1 + then "-${k}" + else "--${k}", + + # how to format a boolean value to a command list; + # by default it’s a flag option + # (only the option name if true, left out completely if false). + mkBool ? k: v: lib.optional v (mkOptionName k), + + # how to format a list value to a command list; + # by default the option name is repeated for each value + # and `mkOption` is applied to the values themselves. + mkList ? k: v: lib.concatMap (mkOption k) v, + + # how to format any remaining value to a command list; + # on the toplevel, booleans and lists are handled by `mkBool` and `mkList`, + # though they can still appear as values of a list. + # By default, everything is printed verbatim and complex types + # are forbidden (lists, attrsets, functions). `null` values are omitted. + mkOption ? + k: v: if v == null + then [] + else [ (mkOptionName k) (lib.generators.mkValueStringDefault {} v) ] + }: + options: + let + render = k: v: + if builtins.isBool v then mkBool k v + else if builtins.isList v then mkList k v + else mkOption k v; + + in + builtins.concatLists (lib.mapAttrsToList render options); +} diff --git a/customisation.nix b/customisation.nix new file mode 100644 index 000000000..fe32e890f --- /dev/null +++ b/customisation.nix @@ -0,0 +1,315 @@ +{ lib }: + +rec { + + + /* `overrideDerivation drv f` takes a derivation (i.e., the result + of a call to the builtin function `derivation`) and returns a new + derivation in which the attributes of the original are overridden + according to the function `f`. The function `f` is called with + the original derivation attributes. + + `overrideDerivation` allows certain "ad-hoc" customisation + scenarios (e.g. in ~/.config/nixpkgs/config.nix). For instance, + if you want to "patch" the derivation returned by a package + function in Nixpkgs to build another version than what the + function itself provides, you can do something like this: + + mySed = overrideDerivation pkgs.gnused (oldAttrs: { + name = "sed-4.2.2-pre"; + src = fetchurl { + url = ftp://alpha.gnu.org/gnu/sed/sed-4.2.2-pre.tar.bz2; + sha256 = "11nq06d131y4wmf3drm0yk502d2xc6n5qy82cg88rb9nqd2lj41k"; + }; + patches = []; + }); + + For another application, see build-support/vm, where this + function is used to build arbitrary derivations inside a QEMU + virtual machine. + + Note that in order to preserve evaluation errors, the new derivation's + outPath depends on the old one's, which means that this function cannot + be used in circular situations when the old derivation also depends on the + new one. + + You should in general prefer `drv.overrideAttrs` over this function; + see the nixpkgs manual for more information on overriding. + */ + overrideDerivation = drv: f: + let + newDrv = derivation (drv.drvAttrs // (f drv)); + in lib.flip (extendDerivation (builtins.seq drv.drvPath true)) newDrv ( + { meta = drv.meta or {}; + passthru = if drv ? passthru then drv.passthru else {}; + } + // + (drv.passthru or {}) + // + # TODO(@Artturin): remove before release 23.05 and only have __spliced. + (lib.optionalAttrs (drv ? crossDrv && drv ? nativeDrv) { + crossDrv = overrideDerivation drv.crossDrv f; + nativeDrv = overrideDerivation drv.nativeDrv f; + }) + // + lib.optionalAttrs (drv ? __spliced) { + __spliced = {} // (lib.mapAttrs (_: sDrv: overrideDerivation sDrv f) drv.__spliced); + }); + + + /* `makeOverridable` takes a function from attribute set to attribute set and + injects `override` attribute which can be used to override arguments of + the function. + + nix-repl> x = {a, b}: { result = a + b; } + + nix-repl> y = lib.makeOverridable x { a = 1; b = 2; } + + nix-repl> y + { override = «lambda»; overrideDerivation = «lambda»; result = 3; } + + nix-repl> y.override { a = 10; } + { override = «lambda»; overrideDerivation = «lambda»; result = 12; } + + Please refer to "Nixpkgs Contributors Guide" section + ".overrideDerivation" to learn about `overrideDerivation` and caveats + related to its use. + */ + makeOverridable = f: origArgs: + let + result = f origArgs; + + # Creates a functor with the same arguments as f + copyArgs = g: lib.setFunctionArgs g (lib.functionArgs f); + # Changes the original arguments with (potentially a function that returns) a set of new attributes + overrideWith = newArgs: origArgs // (if lib.isFunction newArgs then newArgs origArgs else newArgs); + + # Re-call the function but with different arguments + overrideArgs = copyArgs (newArgs: makeOverridable f (overrideWith newArgs)); + # Change the result of the function call by applying g to it + overrideResult = g: makeOverridable (copyArgs (args: g (f args))) origArgs; + in + if builtins.isAttrs result then + result // { + override = overrideArgs; + overrideDerivation = fdrv: overrideResult (x: overrideDerivation x fdrv); + ${if result ? overrideAttrs then "overrideAttrs" else null} = fdrv: + overrideResult (x: x.overrideAttrs fdrv); + } + else if lib.isFunction result then + # Transform the result into a functor while propagating its arguments + lib.setFunctionArgs result (lib.functionArgs result) // { + override = overrideArgs; + } + else result; + + + /* Call the package function in the file `fn` with the required + arguments automatically. The function is called with the + arguments `args`, but any missing arguments are obtained from + `autoArgs`. This function is intended to be partially + parameterised, e.g., + + callPackage = callPackageWith pkgs; + pkgs = { + libfoo = callPackage ./foo.nix { }; + libbar = callPackage ./bar.nix { }; + }; + + If the `libbar` function expects an argument named `libfoo`, it is + automatically passed as an argument. Overrides or missing + arguments can be supplied in `args`, e.g. + + libbar = callPackage ./bar.nix { + libfoo = null; + enableX11 = true; + }; + */ + callPackageWith = autoArgs: fn: args: + let + f = if lib.isFunction fn then fn else import fn; + fargs = lib.functionArgs f; + + # All arguments that will be passed to the function + # This includes automatic ones and ones passed explicitly + allArgs = builtins.intersectAttrs fargs autoArgs // args; + + # A list of argument names that the function requires, but + # wouldn't be passed to it + missingArgs = lib.attrNames + # Filter out arguments that have a default value + (lib.filterAttrs (name: value: ! value) + # Filter out arguments that would be passed + (removeAttrs fargs (lib.attrNames allArgs))); + + # Get a list of suggested argument names for a given missing one + getSuggestions = arg: lib.pipe (autoArgs // args) [ + lib.attrNames + # Only use ones that are at most 2 edits away. While mork would work, + # levenshteinAtMost is only fast for 2 or less. + (lib.filter (lib.strings.levenshteinAtMost 2 arg)) + # Put strings with shorter distance first + (lib.sort (x: y: lib.strings.levenshtein x arg < lib.strings.levenshtein y arg)) + # Only take the first couple results + (lib.take 3) + # Quote all entries + (map (x: "\"" + x + "\"")) + ]; + + prettySuggestions = suggestions: + if suggestions == [] then "" + else if lib.length suggestions == 1 then ", did you mean ${lib.elemAt suggestions 0}?" + else ", did you mean ${lib.concatStringsSep ", " (lib.init suggestions)} or ${lib.last suggestions}?"; + + errorForArg = arg: + let + loc = builtins.unsafeGetAttrPos arg fargs; + # loc' can be removed once lib/minver.nix is >2.3.4, since that includes + # https://github.com/NixOS/nix/pull/3468 which makes loc be non-null + loc' = if loc != null then loc.file + ":" + toString loc.line + else if ! lib.isFunction fn then + toString fn + lib.optionalString (lib.sources.pathIsDirectory fn) "/default.nix" + else ""; + in "Function called without required argument \"${arg}\" at " + + "${loc'}${prettySuggestions (getSuggestions arg)}"; + + # Only show the error for the first missing argument + error = errorForArg (lib.head missingArgs); + + in if missingArgs == [] then makeOverridable f allArgs else abort error; + + + /* Like callPackage, but for a function that returns an attribute + set of derivations. The override function is added to the + individual attributes. */ + callPackagesWith = autoArgs: fn: args: + let + f = if lib.isFunction fn then fn else import fn; + auto = builtins.intersectAttrs (lib.functionArgs f) autoArgs; + origArgs = auto // args; + pkgs = f origArgs; + mkAttrOverridable = name: _: makeOverridable (newArgs: (f newArgs).${name}) origArgs; + in + if lib.isDerivation pkgs then throw + ("function `callPackages` was called on a *single* derivation " + + ''"${pkgs.name or ""}";'' + + " did you mean to use `callPackage` instead?") + else lib.mapAttrs mkAttrOverridable pkgs; + + + /* Add attributes to each output of a derivation without changing + the derivation itself and check a given condition when evaluating. */ + extendDerivation = condition: passthru: drv: + let + outputs = drv.outputs or [ "out" ]; + + commonAttrs = drv // (builtins.listToAttrs outputsList) // + ({ all = map (x: x.value) outputsList; }) // passthru; + + outputToAttrListElement = outputName: + { name = outputName; + value = commonAttrs // { + inherit (drv.${outputName}) type outputName; + outputSpecified = true; + drvPath = assert condition; drv.${outputName}.drvPath; + outPath = assert condition; drv.${outputName}.outPath; + } // + # TODO: give the derivation control over the outputs. + # `overrideAttrs` may not be the only attribute that needs + # updating when switching outputs. + lib.optionalAttrs (passthru?overrideAttrs) { + # TODO: also add overrideAttrs when overrideAttrs is not custom, e.g. when not splicing. + overrideAttrs = f: (passthru.overrideAttrs f).${outputName}; + }; + }; + + outputsList = map outputToAttrListElement outputs; + in commonAttrs // { + drvPath = assert condition; drv.drvPath; + outPath = assert condition; drv.outPath; + }; + + /* Strip a derivation of all non-essential attributes, returning + only those needed by hydra-eval-jobs. Also strictly evaluate the + result to ensure that there are no thunks kept alive to prevent + garbage collection. */ + hydraJob = drv: + let + outputs = drv.outputs or ["out"]; + + commonAttrs = + { inherit (drv) name system meta; inherit outputs; } + // lib.optionalAttrs (drv._hydraAggregate or false) { + _hydraAggregate = true; + constituents = map hydraJob (lib.flatten drv.constituents); + } + // (lib.listToAttrs outputsList); + + makeOutput = outputName: + let output = drv.${outputName}; in + { name = outputName; + value = commonAttrs // { + outPath = output.outPath; + drvPath = output.drvPath; + type = "derivation"; + inherit outputName; + }; + }; + + outputsList = map makeOutput outputs; + + drv' = (lib.head outputsList).value; + in if drv == null then null else + lib.deepSeq drv' drv'; + + /* Make a set of packages with a common scope. All packages called + with the provided `callPackage` will be evaluated with the same + arguments. Any package in the set may depend on any other. The + `overrideScope'` function allows subsequent modification of the package + set in a consistent way, i.e. all packages in the set will be + called with the overridden packages. The package sets may be + hierarchical: the packages in the set are called with the scope + provided by `newScope` and the set provides a `newScope` attribute + which can form the parent scope for later package sets. */ + makeScope = newScope: f: + let self = f self // { + newScope = scope: newScope (self // scope); + callPackage = self.newScope {}; + overrideScope = g: lib.warn + "`overrideScope` (from `lib.makeScope`) is deprecated. Do `overrideScope' (self: super: { … })` instead of `overrideScope (super: self: { … })`. All other overrides have the parameters in that order, including other definitions of `overrideScope`. This was the only definition violating the pattern." + (makeScope newScope (lib.fixedPoints.extends (lib.flip g) f)); + overrideScope' = g: makeScope newScope (lib.fixedPoints.extends g f); + packages = f; + }; + in self; + + /* Like the above, but aims to support cross compilation. It's still ugly, but + hopefully it helps a little bit. */ + makeScopeWithSplicing = splicePackages: newScope: otherSplices: keep: extra: f: + let + spliced0 = splicePackages { + pkgsBuildBuild = otherSplices.selfBuildBuild; + pkgsBuildHost = otherSplices.selfBuildHost; + pkgsBuildTarget = otherSplices.selfBuildTarget; + pkgsHostHost = otherSplices.selfHostHost; + pkgsHostTarget = self; # Not `otherSplices.selfHostTarget`; + pkgsTargetTarget = otherSplices.selfTargetTarget; + }; + spliced = extra spliced0 // spliced0 // keep self; + self = f self // { + newScope = scope: newScope (spliced // scope); + callPackage = newScope spliced; # == self.newScope {}; + # N.B. the other stages of the package set spliced in are *not* + # overridden. + overrideScope = g: makeScopeWithSplicing + splicePackages + newScope + otherSplices + keep + extra + (lib.fixedPoints.extends g f); + packages = f; + }; + in self; + +} diff --git a/debug.nix b/debug.nix new file mode 100644 index 000000000..a851cd747 --- /dev/null +++ b/debug.nix @@ -0,0 +1,253 @@ +/* Collection of functions useful for debugging + broken nix expressions. + + * `trace`-like functions take two values, print + the first to stderr and return the second. + * `traceVal`-like functions take one argument + which both printed and returned. + * `traceSeq`-like functions fully evaluate their + traced value before printing (not just to “weak + head normal form” like trace does by default). + * Functions that end in `-Fn` take an additional + function as their first argument, which is applied + to the traced value before it is printed. +*/ +{ lib }: +let + inherit (lib) + isInt + attrNames + isList + isAttrs + substring + addErrorContext + attrValues + concatLists + concatStringsSep + const + elem + generators + head + id + isDerivation + isFunction + mapAttrs + trace; +in + +rec { + + # -- TRACING -- + + /* Conditionally trace the supplied message, based on a predicate. + + Type: traceIf :: bool -> string -> a -> a + + Example: + traceIf true "hello" 3 + trace: hello + => 3 + */ + traceIf = + # Predicate to check + pred: + # Message that should be traced + msg: + # Value to return + x: if pred then trace msg x else x; + + /* Trace the supplied value after applying a function to it, and + return the original value. + + Type: traceValFn :: (a -> b) -> a -> a + + Example: + traceValFn (v: "mystring ${v}") "foo" + trace: mystring foo + => "foo" + */ + traceValFn = + # Function to apply + f: + # Value to trace and return + x: trace (f x) x; + + /* Trace the supplied value and return it. + + Type: traceVal :: a -> a + + Example: + traceVal 42 + # trace: 42 + => 42 + */ + traceVal = traceValFn id; + + /* `builtins.trace`, but the value is `builtins.deepSeq`ed first. + + Type: traceSeq :: a -> b -> b + + Example: + trace { a.b.c = 3; } null + trace: { a = ; } + => null + traceSeq { a.b.c = 3; } null + trace: { a = { b = { c = 3; }; }; } + => null + */ + traceSeq = + # The value to trace + x: + # The value to return + y: trace (builtins.deepSeq x x) y; + + /* Like `traceSeq`, but only evaluate down to depth n. + This is very useful because lots of `traceSeq` usages + lead to an infinite recursion. + + Example: + traceSeqN 2 { a.b.c = 3; } null + trace: { a = { b = {…}; }; } + => null + + Type: traceSeqN :: Int -> a -> b -> b + */ + traceSeqN = depth: x: y: + let snip = v: if isList v then noQuotes "[…]" v + else if isAttrs v then noQuotes "{…}" v + else v; + noQuotes = str: v: { __pretty = const str; val = v; }; + modify = n: fn: v: if (n == 0) then fn v + else if isList v then map (modify (n - 1) fn) v + else if isAttrs v then mapAttrs + (const (modify (n - 1) fn)) v + else v; + in trace (generators.toPretty { allowPrettyValues = true; } + (modify depth snip x)) y; + + /* A combination of `traceVal` and `traceSeq` that applies a + provided function to the value to be traced after `deepSeq`ing + it. + */ + traceValSeqFn = + # Function to apply + f: + # Value to trace + v: traceValFn f (builtins.deepSeq v v); + + /* A combination of `traceVal` and `traceSeq`. */ + traceValSeq = traceValSeqFn id; + + /* A combination of `traceVal` and `traceSeqN` that applies a + provided function to the value to be traced. */ + traceValSeqNFn = + # Function to apply + f: + depth: + # Value to trace + v: traceSeqN depth (f v) v; + + /* A combination of `traceVal` and `traceSeqN`. */ + traceValSeqN = traceValSeqNFn id; + + /* Trace the input and output of a function `f` named `name`, + both down to `depth`. + + This is useful for adding around a function call, + to see the before/after of values as they are transformed. + + Example: + traceFnSeqN 2 "id" (x: x) { a.b.c = 3; } + trace: { fn = "id"; from = { a.b = {…}; }; to = { a.b = {…}; }; } + => { a.b.c = 3; } + */ + traceFnSeqN = depth: name: f: v: + let res = f v; + in lib.traceSeqN + (depth + 1) + { + fn = name; + from = v; + to = res; + } + res; + + + # -- TESTING -- + + /* Evaluates a set of tests. + + A test is an attribute set `{expr, expected}`, + denoting an expression and its expected result. + + The result is a `list` of __failed tests__, each represented as + `{name, expected, result}`, + + - expected + - What was passed as `expected` + - result + - The actual `result` of the test + + Used for regression testing of the functions in lib; see + tests.nix for more examples. + + Important: Only attributes that start with `test` are executed. + + - If you want to run only a subset of the tests add the attribute `tests = ["testName"];` + + Example: + + runTests { + testAndOk = { + expr = lib.and true false; + expected = false; + }; + testAndFail = { + expr = lib.and true false; + expected = true; + }; + } + -> + [ + { + name = "testAndFail"; + expected = true; + result = false; + } + ] + + Type: + runTests :: { + tests = [ String ]; + ${testName} :: { + expr :: a; + expected :: a; + }; + } + -> + [ + { + name :: String; + expected :: a; + result :: a; + } + ] + */ + runTests = + # Tests to run + tests: concatLists (attrValues (mapAttrs (name: test: + let testsToRun = if tests ? tests then tests.tests else []; + in if (substring 0 4 name == "test" || elem name testsToRun) + && ((testsToRun == []) || elem name tests.tests) + && (test.expr != test.expected) + + then [ { inherit name; expected = test.expected; result = test.expr; } ] + else [] ) tests)); + + /* Create a test assuming that list elements are `true`. + + Example: + { testX = allTrue [ true ]; } + */ + testAllTrue = expr: { inherit expr; expected = map (x: true) expr; }; +} diff --git a/default.nix b/default.nix new file mode 100644 index 000000000..7f7f73168 --- /dev/null +++ b/default.nix @@ -0,0 +1,167 @@ +/* Library of low-level helper functions for nix expressions. + * + * Please implement (mostly) exhaustive unit tests + * for new functions in `./tests.nix`. + */ +let + + inherit (import ./fixed-points.nix { inherit lib; }) makeExtensible; + + lib = makeExtensible (self: let + callLibs = file: import file { lib = self; }; + in { + + # often used, or depending on very little + trivial = callLibs ./trivial.nix; + fixedPoints = callLibs ./fixed-points.nix; + + # datatypes + attrsets = callLibs ./attrsets.nix; + lists = callLibs ./lists.nix; + strings = callLibs ./strings.nix; + stringsWithDeps = callLibs ./strings-with-deps.nix; + + # packaging + customisation = callLibs ./customisation.nix; + derivations = callLibs ./derivations.nix; + maintainers = import ../maintainers/maintainer-list.nix; + teams = callLibs ../maintainers/team-list.nix; + meta = callLibs ./meta.nix; + versions = callLibs ./versions.nix; + + # module system + modules = callLibs ./modules.nix; + options = callLibs ./options.nix; + types = callLibs ./types.nix; + + # constants + licenses = callLibs ./licenses.nix; + sourceTypes = callLibs ./source-types.nix; + systems = callLibs ./systems; + + # serialization + cli = callLibs ./cli.nix; + generators = callLibs ./generators.nix; + + # misc + asserts = callLibs ./asserts.nix; + debug = callLibs ./debug.nix; + misc = callLibs ./deprecated.nix; + + # domain-specific + fetchers = callLibs ./fetchers.nix; + + # Eval-time filesystem handling + path = callLibs ./path; + filesystem = callLibs ./filesystem.nix; + fileset = callLibs ./fileset.nix; + sources = callLibs ./sources.nix; + + # back-compat aliases + platforms = self.systems.doubles; + + # linux kernel configuration + kernel = callLibs ./kernel.nix; + + inherit (builtins) add addErrorContext attrNames concatLists + deepSeq elem elemAt filter genericClosure genList getAttr + hasAttr head isAttrs isBool isInt isList isPath isString length + lessThan listToAttrs pathExists readFile replaceStrings seq + stringLength sub substring tail trace; + inherit (self.trivial) id const pipe concat or and bitAnd bitOr bitXor + bitNot boolToString mergeAttrs flip mapNullable inNixShell isFloat min max + importJSON importTOML warn warnIf warnIfNot throwIf throwIfNot checkListOfEnum + info showWarnings nixpkgsVersion version isInOldestRelease + mod compare splitByAndCompare + functionArgs setFunctionArgs isFunction toFunction + toHexString toBaseDigits inPureEvalMode; + inherit (self.fixedPoints) fix fix' converge extends composeExtensions + composeManyExtensions makeExtensible makeExtensibleWithCustomName; + inherit (self.attrsets) attrByPath hasAttrByPath setAttrByPath + getAttrFromPath attrVals attrValues getAttrs catAttrs filterAttrs + filterAttrsRecursive foldlAttrs foldAttrs collect nameValuePair mapAttrs + mapAttrs' mapAttrsToList concatMapAttrs mapAttrsRecursive mapAttrsRecursiveCond + genAttrs isDerivation toDerivation optionalAttrs + zipAttrsWithNames zipAttrsWith zipAttrs recursiveUpdateUntil + recursiveUpdate matchAttrs overrideExisting showAttrPath getOutput getBin + getLib getDev getMan chooseDevOutputs zipWithNames zip + recurseIntoAttrs dontRecurseIntoAttrs cartesianProductOfSets + updateManyAttrsByPath; + inherit (self.lists) singleton forEach foldr fold foldl foldl' imap0 imap1 + concatMap flatten remove findSingle findFirst any all count + optional optionals toList range replicate partition zipListsWith zipLists + reverseList listDfs toposort sort naturalSort compareLists take + drop sublist last init crossLists unique intersectLists + subtractLists mutuallyExclusive groupBy groupBy'; + inherit (self.strings) concatStrings concatMapStrings concatImapStrings + intersperse concatStringsSep concatMapStringsSep + concatImapStringsSep concatLines makeSearchPath makeSearchPathOutput + makeLibraryPath makeBinPath optionalString + hasInfix hasPrefix hasSuffix stringToCharacters stringAsChars escape + escapeShellArg escapeShellArgs + isStorePath isStringLike + isValidPosixName toShellVar toShellVars + escapeRegex escapeURL escapeXML replaceChars lowerChars + upperChars toLower toUpper addContextFrom splitString + removePrefix removeSuffix versionOlder versionAtLeast + getName getVersion + mesonOption mesonBool mesonEnable + nameFromURL enableFeature enableFeatureAs withFeature + withFeatureAs fixedWidthString fixedWidthNumber + toInt toIntBase10 readPathsFromFile fileContents; + inherit (self.stringsWithDeps) textClosureList textClosureMap + noDepEntry fullDepEntry packEntry stringAfter; + inherit (self.customisation) overrideDerivation makeOverridable + callPackageWith callPackagesWith extendDerivation hydraJob + makeScope makeScopeWithSplicing; + inherit (self.derivations) lazyDerivation; + inherit (self.meta) addMetaAttrs dontDistribute setName updateName + appendToName mapDerivationAttrset setPrio lowPrio lowPrioSet hiPrio + hiPrioSet getLicenseFromSpdxId getExe; + inherit (self.filesystem) pathType pathIsDirectory pathIsRegularFile; + inherit (self.sources) cleanSourceFilter + cleanSource sourceByRegex sourceFilesBySuffices + commitIdFromGitRepo cleanSourceWith pathHasContext + canCleanSource pathIsGitRepo; + inherit (self.modules) evalModules setDefaultModuleLocation + unifyModuleSyntax applyModuleArgsIfFunction mergeModules + mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions + pushDownProperties dischargeProperties filterOverrides + sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride + mkOptionDefault mkDefault mkImageMediaOverride mkForce mkVMOverride + mkFixStrictness mkOrder mkBefore mkAfter mkAliasDefinitions + mkAliasAndWrapDefinitions fixMergeModules mkRemovedOptionModule + mkRenamedOptionModule mkRenamedOptionModuleWith + mkMergedOptionModule mkChangedOptionModule + mkAliasOptionModule mkDerivedConfig doRename + mkAliasOptionModuleMD; + inherit (self.options) isOption mkEnableOption mkSinkUndeclaredOptions + mergeDefaultOption mergeOneOption mergeEqualOption mergeUniqueOption + getValues getFiles + optionAttrSetToDocList optionAttrSetToDocList' + scrubOptionValue literalExpression literalExample literalDocBook + showOption showOptionWithDefLocs showFiles + unknownModule mkOption mkPackageOption mkPackageOptionMD + mdDoc literalMD; + inherit (self.types) isType setType defaultTypeMerge defaultFunctor + isOptionType mkOptionType; + inherit (self.asserts) + assertMsg assertOneOf; + inherit (self.debug) traceIf traceVal traceValFn + traceSeq traceSeqN traceValSeq + traceValSeqFn traceValSeqN traceValSeqNFn traceFnSeqN + runTests testAllTrue; + inherit (self.misc) maybeEnv defaultMergeArg defaultMerge foldArgs + maybeAttrNullable maybeAttr ifEnable checkFlag getValue + checkReqs uniqList uniqListExt condConcat lazyGenericClosure + innerModifySumArgs modifySumArgs innerClosePropagation + closePropagation mapAttrsFlatten nvs setAttr setAttrMerge + mergeAttrsWithFunc mergeAttrsConcatenateValues + mergeAttrsNoOverride mergeAttrByFunc mergeAttrsByFuncDefaults + mergeAttrsByFuncDefaultsClean mergeAttrBy + fakeHash fakeSha256 fakeSha512 + nixType imap; + inherit (self.versions) + splitVersion; + }); +in lib diff --git a/deprecated.nix b/deprecated.nix new file mode 100644 index 000000000..ed14e04bb --- /dev/null +++ b/deprecated.nix @@ -0,0 +1,307 @@ +{ lib }: +let + inherit (builtins) head tail isList isAttrs isInt attrNames; + +in + +with lib.lists; +with lib.attrsets; +with lib.strings; + +rec { + + # returns default if env var is not set + maybeEnv = name: default: + let value = builtins.getEnv name; in + if value == "" then default else value; + + defaultMergeArg = x : y: if builtins.isAttrs y then + y + else + (y x); + defaultMerge = x: y: x // (defaultMergeArg x y); + foldArgs = merger: f: init: x: + let arg = (merger init (defaultMergeArg init x)); + # now add the function with composed args already applied to the final attrs + base = (setAttrMerge "passthru" {} (f arg) + ( z: z // { + function = foldArgs merger f arg; + args = (lib.attrByPath ["passthru" "args"] {} z) // x; + } )); + withStdOverrides = base // { + override = base.passthru.function; + }; + in + withStdOverrides; + + + # shortcut for attrByPath ["name"] default attrs + maybeAttrNullable = maybeAttr; + + # shortcut for attrByPath ["name"] default attrs + maybeAttr = name: default: attrs: attrs.${name} or default; + + + # Return the second argument if the first one is true or the empty version + # of the second argument. + ifEnable = cond: val: + if cond then val + else if builtins.isList val then [] + else if builtins.isAttrs val then {} + # else if builtins.isString val then "" + else if val == true || val == false then false + else null; + + + # Return true only if there is an attribute and it is true. + checkFlag = attrSet: name: + if name == "true" then true else + if name == "false" then false else + if (elem name (attrByPath ["flags"] [] attrSet)) then true else + attrByPath [name] false attrSet ; + + + # Input : attrSet, [ [name default] ... ], name + # Output : its value or default. + getValue = attrSet: argList: name: + ( attrByPath [name] (if checkFlag attrSet name then true else + if argList == [] then null else + let x = builtins.head argList; in + if (head x) == name then + (head (tail x)) + else (getValue attrSet + (tail argList) name)) attrSet ); + + + # Input : attrSet, [[name default] ...], [ [flagname reqs..] ... ] + # Output : are reqs satisfied? It's asserted. + checkReqs = attrSet: argList: condList: + ( + foldr lib.and true + (map (x: let name = (head x); in + + ((checkFlag attrSet name) -> + (foldr lib.and true + (map (y: let val=(getValue attrSet argList y); in + (val!=null) && (val!=false)) + (tail x))))) condList)); + + + # This function has O(n^2) performance. + uniqList = { inputList, acc ? [] }: + let go = xs: acc: + if xs == [] + then [] + else let x = head xs; + y = if elem x acc then [] else [x]; + in y ++ go (tail xs) (y ++ acc); + in go inputList acc; + + uniqListExt = { inputList, + outputList ? [], + getter ? (x: x), + compare ? (x: y: x==y) }: + if inputList == [] then outputList else + let x = head inputList; + isX = y: (compare (getter y) (getter x)); + newOutputList = outputList ++ + (if any isX outputList then [] else [x]); + in uniqListExt { outputList = newOutputList; + inputList = (tail inputList); + inherit getter compare; + }; + + condConcat = name: list: checker: + if list == [] then name else + if checker (head list) then + condConcat + (name + (head (tail list))) + (tail (tail list)) + checker + else condConcat + name (tail (tail list)) checker; + + lazyGenericClosure = {startSet, operator}: + let + work = list: doneKeys: result: + if list == [] then + result + else + let x = head list; key = x.key; in + if elem key doneKeys then + work (tail list) doneKeys result + else + work (tail list ++ operator x) ([key] ++ doneKeys) ([x] ++ result); + in + work startSet [] []; + + innerModifySumArgs = f: x: a: b: if b == null then (f a b) // x else + innerModifySumArgs f x (a // b); + modifySumArgs = f: x: innerModifySumArgs f x {}; + + + innerClosePropagation = acc: xs: + if xs == [] + then acc + else let y = head xs; + ys = tail xs; + in if ! isAttrs y + then innerClosePropagation acc ys + else let acc' = [y] ++ acc; + in innerClosePropagation + acc' + (uniqList { inputList = (maybeAttrNullable "propagatedBuildInputs" [] y) + ++ (maybeAttrNullable "propagatedNativeBuildInputs" [] y) + ++ ys; + acc = acc'; + } + ); + + closePropagationSlow = list: (uniqList {inputList = (innerClosePropagation [] list);}); + + # This is an optimisation of lib.closePropagation which avoids the O(n^2) behavior + # Using a list of derivations, it generates the full closure of the propagatedXXXBuildInputs + # The ordering / sorting / comparison is done based on the `outPath` + # attribute of each derivation. + # On some benchmarks, it performs up to 15 times faster than lib.closePropagation. + # See https://github.com/NixOS/nixpkgs/pull/194391 for details. + closePropagationFast = list: + builtins.map (x: x.val) (builtins.genericClosure { + startSet = builtins.map (x: { + key = x.outPath; + val = x; + }) (builtins.filter (x: x != null) list); + operator = item: + if !builtins.isAttrs item.val then + [ ] + else + builtins.concatMap (x: + if x != null then [{ + key = x.outPath; + val = x; + }] else + [ ]) ((item.val.propagatedBuildInputs or [ ]) + ++ (item.val.propagatedNativeBuildInputs or [ ])); + }); + + closePropagation = if builtins ? genericClosure + then closePropagationFast + else closePropagationSlow; + + # calls a function (f attr value ) for each record item. returns a list + mapAttrsFlatten = f: r: map (attr: f attr r.${attr}) (attrNames r); + + # attribute set containing one attribute + nvs = name: value: listToAttrs [ (nameValuePair name value) ]; + # adds / replaces an attribute of an attribute set + setAttr = set: name: v: set // (nvs name v); + + # setAttrMerge (similar to mergeAttrsWithFunc but only merges the values of a particular name) + # setAttrMerge "a" [] { a = [2];} (x: x ++ [3]) -> { a = [2 3]; } + # setAttrMerge "a" [] { } (x: x ++ [3]) -> { a = [ 3]; } + setAttrMerge = name: default: attrs: f: + setAttr attrs name (f (maybeAttr name default attrs)); + + # Using f = a: b = b the result is similar to // + # merge attributes with custom function handling the case that the attribute + # exists in both sets + mergeAttrsWithFunc = f: set1: set2: + foldr (n: set: if set ? ${n} + then setAttr set n (f set.${n} set2.${n}) + else set ) + (set2 // set1) (attrNames set2); + + # merging two attribute set concatenating the values of same attribute names + # eg { a = 7; } { a = [ 2 3 ]; } becomes { a = [ 7 2 3 ]; } + mergeAttrsConcatenateValues = mergeAttrsWithFunc ( a: b: (toList a) ++ (toList b) ); + + # merges attributes using //, if a name exists in both attributes + # an error will be triggered unless its listed in mergeLists + # so you can mergeAttrsNoOverride { buildInputs = [a]; } { buildInputs = [a]; } {} to get + # { buildInputs = [a b]; } + # merging buildPhase doesn't really make sense. The cases will be rare where appending /prefixing will fit your needs? + # in these cases the first buildPhase will override the second one + # ! deprecated, use mergeAttrByFunc instead + mergeAttrsNoOverride = { mergeLists ? ["buildInputs" "propagatedBuildInputs"], + overrideSnd ? [ "buildPhase" ] + }: attrs1: attrs2: + foldr (n: set: + setAttr set n ( if set ? ${n} + then # merge + if elem n mergeLists # attribute contains list, merge them by concatenating + then attrs2.${n} ++ attrs1.${n} + else if elem n overrideSnd + then attrs1.${n} + else throw "error mergeAttrsNoOverride, attribute ${n} given in both attributes - no merge func defined" + else attrs2.${n} # add attribute not existing in attr1 + )) attrs1 (attrNames attrs2); + + + # example usage: + # mergeAttrByFunc { + # inherit mergeAttrBy; # defined below + # buildInputs = [ a b ]; + # } { + # buildInputs = [ c d ]; + # }; + # will result in + # { mergeAttrsBy = [...]; buildInputs = [ a b c d ]; } + # is used by defaultOverridableDelayableArgs and can be used when composing using + # foldArgs, composedArgsAndFun or applyAndFun. Example: composableDerivation in all-packages.nix + mergeAttrByFunc = x: y: + let + mergeAttrBy2 = { mergeAttrBy = lib.mergeAttrs; } + // (maybeAttr "mergeAttrBy" {} x) + // (maybeAttr "mergeAttrBy" {} y); in + foldr lib.mergeAttrs {} [ + x y + (mapAttrs ( a: v: # merge special names using given functions + if x ? ${a} + then if y ? ${a} + then v x.${a} y.${a} # both have attr, use merge func + else x.${a} # only x has attr + else y.${a} # only y has attr) + ) (removeAttrs mergeAttrBy2 + # don't merge attrs which are neither in x nor y + (filter (a: ! x ? ${a} && ! y ? ${a}) + (attrNames mergeAttrBy2)) + ) + ) + ]; + mergeAttrsByFuncDefaults = foldl mergeAttrByFunc { inherit mergeAttrBy; }; + mergeAttrsByFuncDefaultsClean = list: removeAttrs (mergeAttrsByFuncDefaults list) ["mergeAttrBy"]; + + # sane defaults (same name as attr name so that inherit can be used) + mergeAttrBy = # { buildInputs = concatList; [...]; passthru = mergeAttr; [..]; } + listToAttrs (map (n: nameValuePair n lib.concat) + [ "nativeBuildInputs" "buildInputs" "propagatedBuildInputs" "configureFlags" "prePhases" "postAll" "patches" ]) + // listToAttrs (map (n: nameValuePair n lib.mergeAttrs) [ "passthru" "meta" "cfg" "flags" ]) + // listToAttrs (map (n: nameValuePair n (a: b: "${a}\n${b}") ) [ "preConfigure" "postInstall" ]) + ; + + nixType = x: + if isAttrs x then + if x ? outPath then "derivation" + else "attrs" + else if lib.isFunction x then "function" + else if isList x then "list" + else if x == true then "bool" + else if x == false then "bool" + else if x == null then "null" + else if isInt x then "int" + else "string"; + + /* deprecated: + + For historical reasons, imap has an index starting at 1. + + But for consistency with the rest of the library we want an index + starting at zero. + */ + imap = imap1; + + # Fake hashes. Can be used as hash placeholders, when computing hash ahead isn't trivial + fakeHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + fakeSha256 = "0000000000000000000000000000000000000000000000000000000000000000"; + fakeSha512 = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; +} diff --git a/derivations.nix b/derivations.nix new file mode 100644 index 000000000..5b7ed1868 --- /dev/null +++ b/derivations.nix @@ -0,0 +1,101 @@ +{ lib }: + +let + inherit (lib) throwIfNot; +in +{ + /* + Restrict a derivation to a predictable set of attribute names, so + that the returned attrset is not strict in the actual derivation, + saving a lot of computation when the derivation is non-trivial. + + This is useful in situations where a derivation might only be used for its + passthru attributes, improving evaluation performance. + + The returned attribute set is lazy in `derivation`. Specifically, this + means that the derivation will not be evaluated in at least the + situations below. + + For illustration and/or testing, we define derivation such that its + evaluation is very noticeable. + + let derivation = throw "This won't be evaluated."; + + In the following expressions, `derivation` will _not_ be evaluated: + + (lazyDerivation { inherit derivation; }).type + + attrNames (lazyDerivation { inherit derivation; }) + + (lazyDerivation { inherit derivation; } // { foo = true; }).foo + + (lazyDerivation { inherit derivation; meta.foo = true; }).meta + + In these expressions, `derivation` _will_ be evaluated: + + "${lazyDerivation { inherit derivation }}" + + (lazyDerivation { inherit derivation }).outPath + + (lazyDerivation { inherit derivation }).meta + + And the following expressions are not valid, because the refer to + implementation details and/or attributes that may not be present on + some derivations: + + (lazyDerivation { inherit derivation }).buildInputs + + (lazyDerivation { inherit derivation }).passthru + + (lazyDerivation { inherit derivation }).pythonPath + + */ + lazyDerivation = + args@{ + # The derivation to be wrapped. + derivation + , # Optional meta attribute. + # + # While this function is primarily about derivations, it can improve + # the `meta` package attribute, which is usually specified through + # `mkDerivation`. + meta ? null + , # Optional extra values to add to the returned attrset. + # + # This can be used for adding package attributes, such as `tests`. + passthru ? { } + }: + let + # These checks are strict in `drv` and some `drv` attributes, but the + # attrset spine returned by lazyDerivation does not depend on it. + # Instead, the individual derivation attributes do depend on it. + checked = + throwIfNot (derivation.type or null == "derivation") + "lazySimpleDerivation: input must be a derivation." + throwIfNot + (derivation.outputs == [ "out" ]) + # Supporting multiple outputs should be a matter of inheriting more attrs. + "The derivation ${derivation.name or ""} has multiple outputs. This is not supported by lazySimpleDerivation yet. Support could be added, and be useful as long as the set of outputs is known in advance, without evaluating the actual derivation." + derivation; + in + { + # Hardcoded `type` + # + # `lazyDerivation` requires its `derivation` argument to be a derivation, + # so if it is not, that is a programming error by the caller and not + # something that `lazyDerivation` consumers should be able to correct + # for after the fact. + # So, to improve laziness, we assume correctness here and check it only + # when actual derivation values are accessed later. + type = "derivation"; + + # A fixed set of derivation values, so that `lazyDerivation` can return + # its attrset before evaluating `derivation`. + # This must only list attributes that are available on _all_ derivations. + inherit (checked) outputs out outPath outputName drvPath name system; + + # The meta attribute can either be taken from the derivation, or if the + # `lazyDerivation` caller knew a shortcut, be taken from there. + meta = args.meta or checked.meta; + } // passthru; +} diff --git a/fetchers.nix b/fetchers.nix new file mode 100644 index 000000000..1107353b5 --- /dev/null +++ b/fetchers.nix @@ -0,0 +1,13 @@ +# snippets that can be shared by multiple fetchers (pkgs/build-support) +{ lib }: +{ + + proxyImpureEnvVars = [ + # We borrow these environment variables from the caller to allow + # easy proxy configuration. This is impure, but a fixed-output + # derivation like fetchurl is allowed to do so since its result is + # by definition pure. + "http_proxy" "https_proxy" "ftp_proxy" "all_proxy" "no_proxy" + ]; + +} diff --git a/fileset.nix b/fileset.nix new file mode 100644 index 000000000..3eb909162 --- /dev/null +++ b/fileset.nix @@ -0,0 +1,993 @@ +# Add this to docs somewhere: If you pass the same arguments to the same functions, you will get the same result +{ lib }: +let + # TODO: Add builtins.traceVerbose or so to all the operations for debugging + # TODO: Document limitation that empty directories won't be included + # TODO: Point out that equality comparison using `==` doesn't quite work because there's multiple representations for all files in a directory: "directory" and `readDir path`. + # TODO: subset and superset check functions. Can easily be implemented with `difference` and `isEmpty` + # TODO: Write down complexity of each operation, most should be O(1)! + # TODO: Implement an operation for optionally including a file if it exists. + # TODO: Derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets + + inherit (builtins) + isAttrs + isString + isPath + isList + typeOf + readDir + match + pathExists + seq + ; + + inherit (lib.trivial) + mapNullable + ; + + inherit (lib.lists) + head + tail + foldl' + range + length + elemAt + all + imap0 + drop + commonPrefix + ; + + inherit (lib.strings) + isCoercibleToString + ; + + inherit (lib.attrsets) + mapAttrs + attrNames + attrValues + ; + + inherit (lib.filesystem) + pathType + ; + + inherit (lib.sources) + cleanSourceWith + ; + + inherit (lib.path) + append + deconstruct + construct + hasPrefix + removePrefix + ; + + inherit (lib.path.components) + fromSubpath + ; + + # Internal file set structure: + # + # # A set of files + # = { + # _type = "fileset"; + # + # # The base path, only files under this path can be represented + # # Always a directory + # _base = ; + # + # # A tree representation of all included files + # _tree = ; + # }; + # + # # A directory entry value + # = + # # A nested directory + # + # + # # A nested file + # | + # + # # A removed file or directory + # # This is represented like this instead of removing the entry from the attribute set because: + # # - It improves laziness + # # - It allows keeping the attribute set as a `builtins.readDir` cache + # | null + # + # # A directory + # = + # # The inclusion state for every directory entry + # { = ; } + # + # # All files in a directory, recursively. + # # Semantically this is equivalent to `builtins.readDir path`, but lazier, because + # # operations that don't require the entry listing can avoid it. + # # This string is chosen to be compatible with `builtins.readDir` for a simpler implementation + # "directory"; + # + # # A file + # = + # # A file with this filetype + # # These strings match `builtins.readDir` for a simpler implementation + # "regular" | "symlink" | "unknown" + + # Create a fileset structure + # Type: Path -> -> + _create = base: tree: { + _type = "fileset"; + # All attributes are internal + _base = base; + _tree = tree; + # Double __ to make it be evaluated and ordered first + __noEval = throw '' + File sets are not intended to be directly inspected or evaluated. Instead prefer: + - If you want to print a file set, use the `lib.fileset.trace` or `lib.fileset.pretty` function. + - If you want to check file sets for equality, use the `lib.fileset.equals` function. + ''; + }; + + # Create a file set from a path + # Type: Path -> + _singleton = path: + let + type = pathType path; + in + if type == "directory" then + _create path type + else + # Always coerce to a directory + # If we don't do this we run into problems like: + # - What should `toSource { base = ./default.nix; fileset = difference ./default.nix ./default.nix; }` do? + # - Importing an empty directory wouldn't make much sense because our `base` is a file + # - Neither can we create a store path containing nothing at all + # - The only option is to throw an error that `base` should be a directory + # - Should `fileFilter (file: file.name == "default.nix") ./default.nix` run the predicate on the ./default.nix file? + # - If no, should the result include or exclude ./default.nix? In any case, it would be confusing and inconsistent + # - If yes, it needs to consider ./. to have influence the filesystem result, because file names are part of the parent directory, so filter would change the necessary base + _create (dirOf path) + (_nestTree + (dirOf path) + [ (baseNameOf path) ] + type + ); + + # Turn a builtins.filterSource-based source filter on a root path into a file set containing only files included by the filter + # Type: Path -> (String -> String -> Bool) -> + _fromSource = root: filter: + let + recurse = focusPath: type: + # FIXME: Generally we shouldn't use toString on paths, though it might be correct + # here since we're trying to mimic the impure behavior of `builtins.filterPath` + if ! filter (toString focusPath) type then + null + else if type == "directory" then + mapAttrs + (name: recurse (append focusPath name)) + (readDir focusPath) + else + type; + + + rootPathType = pathType root; + tree = + if rootPathType == "directory" then + recurse root rootPathType + else + rootPathType; + in + _create root tree; + + # Coerce a value to a fileset + # Type: String -> String -> Any -> + _coerce = function: context: value: + if value._type or "" == "fileset" then + value + else if ! isPath value then + if value._isLibCleanSourceWith or false then + throw '' + lib.fileset.${function}: Expected ${context} to be a path, but it's a value produced by `lib.sources` instead. + Such a value is only supported when converted to a file set using `lib.fileset.fromSource`.'' + else if isCoercibleToString value then + throw '' + lib.fileset.${function}: Expected ${context} to be a path, but it's a string-coercible value instead, possibly a Nix store path. + Such a value is not supported, `lib.fileset` only supports local file filtering.'' + else + throw "lib.fileset.${function}: Expected ${context} to be a path, but got a ${typeOf value}." + else if ! pathExists value then + throw "lib.fileset.${function}: Expected ${context} \"${toString value}\" to be a path that exists, but it doesn't." + else + _singleton value; + + # Nest a tree under some further components + # Type: Path -> [ String ] -> -> + _nestTree = targetBase: extraComponents: tree: + let + recurse = index: focusPath: + if index == length extraComponents then + tree + else + let + focusedName = elemAt extraComponents index; + in + mapAttrs + (name: _: + if name == focusedName then + recurse (index + 1) (append focusPath name) + else + null + ) + (readDir focusPath); + in + recurse 0 targetBase; + + # Expand "directory" to { = ; } + # Type: Path -> -> { = ; } + _directoryEntries = path: value: + if isAttrs value then + value + else + readDir path; + + # The following tables are a bit complicated, but they nicely explain the + # corresponding implementations, here's the legend: + # + # lhs\rhs: The values for the left hand side and right hand side arguments + # null: null, an excluded file/directory + # attrs: satisfies `isAttrs value`, an explicitly listed directory containing nested trees + # dir: "directory", a recursively included directory + # str: "regular", "symlink" or "unknown", a filetype string + # rec: A result computed by recursing + # -: Can't occur because one argument is a directory while the other is a file + # : Indicates that the result is computed by the branch with that number + + # The union of two 's + # Type: -> -> + # + # lhs\rhs | null | attrs | dir | str | + # ------- | ------- | ------- | ----- | ----- | + # null | 2 null | 2 attrs | 2 dir | 2 str | + # attrs | 3 attrs | 1 rec | 2 dir | - | + # dir | 3 dir | 3 dir | 2 dir | - | + # str | 3 str | - | - | 2 str | + _unionTree = lhs: rhs: + # Branch 1 + if isAttrs lhs && isAttrs rhs then + mapAttrs (name: _unionTree lhs.${name}) rhs + # Branch 2 + else if lhs == null || isString rhs then + rhs + # Branch 3 + else + lhs; + + # The intersection of two 's + # Type: -> -> + # + # lhs\rhs | null | attrs | dir | str | + # ------- | ------- | ------- | ------- | ------ | + # null | 2 null | 2 null | 2 null | 2 null | + # attrs | 3 null | 1 rec | 2 attrs | - | + # dir | 3 null | 3 attrs | 2 dir | - | + # str | 3 null | - | - | 2 str | + _intersectTree = lhs: rhs: + # Branch 1 + if isAttrs lhs && isAttrs rhs then + mapAttrs (name: _intersectTree lhs.${name}) rhs + # Branch 2 + else if lhs == null || isString rhs then + lhs + # Branch 3 + else + rhs; + + # The difference between two 's + # Type: Path -> -> -> + # + # lhs\rhs | null | attrs | dir | str | + # ------- | ------- | ------- | ------ | ------ | + # null | 1 null | 1 null | 1 null | 1 null | + # attrs | 2 attrs | 3 rec | 1 null | - | + # dir | 2 dir | 3 rec | 1 null | - | + # str | 2 str | - | - | 1 null | + _differenceTree = path: lhs: rhs: + # Branch 1 + if isString rhs || lhs == null then + null + # Branch 2 + else if rhs == null then + lhs + # Branch 3 + else + mapAttrs (name: lhsValue: + _differenceTree (append path name) lhsValue rhs.${name} + ) (_directoryEntries path lhs); + + # Whether two 's are equal + # Type: Path -> -> -> + # + # | lhs\rhs | null | attrs | dir | str | + # | ------- | ------- | ------- | ------ | ------- | + # | null | 1 true | 1 rec | 1 rec | 1 false | + # | attrs | 2 rec | 3 rec | 3 rec | - | + # | dir | 2 rec | 3 rec | 4 true | - | + # | str | 2 false | - | - | 4 true | + _equalsTree = path: lhs: rhs: + # Branch 1 + if lhs == null then + _isEmptyTree path rhs + # Branch 2 + else if rhs == null then + _isEmptyTree path lhs + # Branch 3 + else if isAttrs lhs || isAttrs rhs then + let + lhs' = _directoryEntries path lhs; + rhs' = _directoryEntries path rhs; + in + all (name: + _equalsTree (append path name) lhs'.${name} rhs'.${name} + ) (attrNames lhs') + # Branch 4 + else + true; + + # Whether a tree is empty, containing no files + # Type: Path -> -> Bool + _isEmptyTree = path: tree: + if isAttrs tree || tree == "directory" then + let + entries = _directoryEntries path tree; + in + all (name: _isEmptyTree (append path name) entries.${name}) (attrNames entries) + else + tree == null; + + # Simplifies a tree, optionally expanding all "directory"'s into complete listings + # Type: Bool -> Path -> -> + _simplifyTree = expand: base: tree: + let + recurse = focusPath: tree: + if tree == "directory" && expand || isAttrs tree then + let + expanded = _directoryEntries focusPath tree; + transformedSubtrees = mapAttrs (name: recurse (append focusPath name)) expanded; + values = attrValues transformedSubtrees; + in + if all (value: value == "emptyDir") values then + "emptyDir" + else if all (value: isNull value || value == "emptyDir") values then + null + else if !expand && all (value: isString value || value == "emptyDir") values then + "directory" + else + mapAttrs (name: value: if value == "emptyDir" then null else value) transformedSubtrees + else + tree; + result = recurse base tree; + in + if result == "emptyDir" then + null + else + result; + + _prettyTreeSuffix = tree: + if isAttrs tree then + "" + else if tree == "directory" then + " (recursive directory)" + else + " (${tree})"; + + # Pretty-print all files included in the file set. + # Type: (b -> String -> b) -> b -> Path -> FileSet -> b + _prettyFoldl' = f: start: base: tree: + let + traceTreeAttrs = start: indent: tree: + # Nix should really be evaluating foldl''s second argument before starting the iteration + # See the same problem in Haskell: + # - https://stackoverflow.com/a/14282642 + # - https://gitlab.haskell.org/ghc/ghc/-/issues/12173 + # - https://well-typed.com/blog/90/#a-postscript-which-foldl + # - https://old.reddit.com/r/haskell/comments/21wvk7/foldl_is_broken/ + seq start + (foldl' (prev: name: + let + subtree = tree.${name}; + + intermediate = + f prev "${indent}- ${name}${_prettyTreeSuffix subtree}"; + in + if subtree == null then + # Don't print anything at all if this subtree is empty + prev + else if isAttrs subtree then + # A directory with explicit entries + # Do print this node, but also recurse + traceTreeAttrs intermediate "${indent} " subtree + else + # Either a file, or a recursively included directory + # Do print this node but no further recursion needed + intermediate + ) start (attrNames tree)); + + intermediate = + if tree == null then + f start "${toString base} (empty)" + else + f start "${toString base}${_prettyTreeSuffix tree}"; + in + if isAttrs tree then + traceTreeAttrs intermediate "" tree + else + intermediate; + + # Coerce and normalise the bases of multiple file set values passed to user-facing functions + # Type: String -> [ { context :: String, value :: Any } ] -> { commonBase :: Path, trees :: [ ] } + _normaliseBase = function: list: + let + processed = map ({ context, value }: + let + fileset = _coerce function context value; + in { + inherit fileset context; + baseParts = deconstruct fileset._base; + } + ) list; + + first = head processed; + + commonComponents = foldl' (components: el: + if first.baseParts.root != el.baseParts.root then + throw "lib.fileset.${function}: Expected file sets to have the same filesystem root, but ${first.context} has root \"${toString first.baseParts.root}\" while ${el.context} has root \"${toString el.baseParts.root}\"." + else + commonPrefix components el.baseParts.components + ) first.baseParts.components (tail processed); + + commonBase = construct { + root = first.baseParts.root; + components = commonComponents; + }; + + commonComponentsLength = length commonComponents; + + trees = map (value: + _nestTree + commonBase + (drop commonComponentsLength value.baseParts.components) + value.fileset._tree + ) processed; + in + { + inherit commonBase trees; + }; + +in { + + /* + Import a file set into the Nix store, making it usable inside derivations. + Return a source-like value that can be coerced to a Nix store path. + + This function takes an attribute set with these attributes as an argument: + + - `root` (required): The local path that should be the root of the result. + `fileset` must not be influenceable by paths outside `root`, meaning `lib.fileset.getInfluenceBase fileset` must be under `root`. + + Warning: Setting `root` to `lib.fileset.getInfluenceBase fileset` directly would make the resulting Nix store path file structure dependent on how `fileset` is declared. + This makes it non-trivial to predict where specific paths are located in the result. + + - `fileset` (required): The set of files to import into the Nix store. + Use the other `lib.fileset` functions to define `fileset`. + Only directories containing at least one file are included in the result, unless `extraExistingDirs` is used to ensure the existence of specific directories even without any files. + + - `extraExistingDirs` (optional, default `[]`): Additionally ensure the existence of these directory paths in the result, even they don't contain any files in `fileset`. + + Type: + toSource :: { + root :: Path, + fileset :: FileSet, + extraExistingDirs :: [ Path ] ? [ ], + } -> SourceLike + */ + toSource = { root, fileset, extraExistingDirs ? [ ] }: + let + maybeFileset = fileset; + in + let + fileset = _coerce "toSource" "`fileset` attribute" maybeFileset; + + # Directories that recursively have no files in them will always be `null` + sparseTree = + let + recurse = focusPath: tree: + if tree == "directory" || isAttrs tree then + let + entries = _directoryEntries focusPath tree; + sparseSubtrees = mapAttrs (name: recurse (append focusPath name)) entries; + values = attrValues sparseSubtrees; + in + if all isNull values then + null + else if all isString values then + "directory" + else + sparseSubtrees + else + tree; + resultingTree = recurse fileset._base fileset._tree; + # The fileset's _base might be below the root of the `toSource`, so we need to lift the tree up to `root` + extraRootNesting = removePrefix root fileset._base; + in _nestTree root extraRootNesting resultingTree; + + sparseExtendedTree = + if ! isList extraExistingDirs then + throw "lib.fileset.toSource: Expected the `extraExistingDirs` attribute to be a list, but it's a ${typeOf extraExistingDirs} instead." + else + lib.foldl' (tree: i: + let + dir = elemAt extraExistingDirs i; + + # We're slightly abusing the internal functions and structure to ensure that the extra directory is represented in the sparse tree. + value = mapAttrs (name: value: null) (readDir dir); + extraTree = _nestTree root (removePrefix root dir) value; + result = _unionTree tree extraTree; + in + if ! isPath dir then + throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths, but element at index ${toString i} is a ${typeOf dir} instead." + else if ! pathExists dir then + throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths that exist, but the path at index ${toString i} \"${toString dir}\" does not." + else if pathType dir != "directory" then + throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths pointing to directories, but the path at index ${toString i} \"${toString dir}\" points to a file instead." + else if ! hasPrefix root dir then + throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths under the `root` attribute \"${toString root}\", but the path at index ${toString i} \"${toString dir}\" is not." + else + result + ) sparseTree (range 0 (length extraExistingDirs - 1)); + + rootComponentsLength = length (deconstruct root).components; + + # This function is called often for the filter, so it should be fast + inSet = components: + let + recurse = index: localTree: + if index == length components then + localTree != null + else if localTree ? ${elemAt components index} then + recurse (index + 1) localTree.${elemAt components index} + else + localTree == "directory"; + in recurse rootComponentsLength sparseExtendedTree; + + in + if ! isPath root then + if root._isLibCleanSourceWith or false then + throw '' + lib.fileset.toSource: Expected attribute `root` to be a path, but it's a value produced by `lib.sources` instead. + Such a value is only supported when converted to a file set using `lib.fileset.fromSource` and passed to the `fileset` attribute, where it may also be combined using other functions from `lib.fileset`.'' + else if isCoercibleToString root then + throw '' + lib.fileset.toSource: Expected attribute `root` to be a path, but it's a string-like value instead, possibly a Nix store path. + Such a value is not supported, `lib.fileset` only supports local file filtering.'' + else + throw "lib.fileset.toSource: Expected attribute `root` to be a path, but it's a ${typeOf root} instead." + else if ! pathExists root then + throw "lib.fileset.toSource: Expected attribute `root` \"${toString root}\" to be a path that exists, but it doesn't." + else if pathType root != "directory" then + throw "lib.fileset.toSource: Expected attribute `root` \"${toString root}\" to be a path pointing to a directory, but it's pointing to a file instead." + else if ! hasPrefix root fileset._base then + throw "lib.fileset.toSource: Expected attribute `fileset` to not be influenceable by any paths outside `root`, but `lib.fileset.getInfluenceBase fileset` \"${toString fileset._base}\" is outside `root`." + else + cleanSourceWith { + name = "source"; + src = root; + filter = pathString: _: inSet (fromSubpath "./${pathString}"); + }; + + /* + Create a file set from a filtered local source as produced by the `lib.sources` functions. + This does not import anything into the store. + + Type: + fromSource :: SourceLike -> FileSet + + Example: + fromSource (lib.sources.cleanSource ./.) + */ + fromSource = source: + if ! source._isLibCleanSourceWith or false || ! source ? origSrc || ! source ? filter then + throw "lib.fileset.fromSource: Expected the argument to be a value produced from `lib.sources`, but got a ${typeOf source} instead." + else if ! isPath source.origSrc then + throw "lib.fileset.fromSource: Expected the argument to be source-like value of a local path." + else + _fromSource source.origSrc source.filter; + + /* + Coerce a value to a file set: + + - If the value is a file set already, return it directly + + - If the value is a path pointing to a file, return a file set with that single file + + - If the value is a path pointing to a directory, return a file set with all files contained in that directory + + This function is mostly not needed because all functions in `lib.fileset` will implicitly apply it for arguments that are expected to be a file set. + + Type: + coerce :: Any -> FileSet + */ + coerce = value: _coerce "coerce" "argument" value; + + + /* + Create a file set containing all files contained in a path (see `coerce`), or no files if the path doesn't exist. + + This is useful when you want to include a file only if it actually exists. + + Type: + optional :: Path -> FileSet + */ + optional = path: + if ! isPath path then + throw "lib.fileset.optional: Expected argument to be a path, but got a ${typeOf path}." + else if pathExists path then + _singleton path + else + _create path null; + + /* + Return the common ancestor directory of all file set operations used to construct this file set, meaning that nothing outside the this directory can influence the set of files included. + + Type: + getInfluenceBase :: FileSet -> Path + + Example: + getInfluenceBase ./Makefile + => ./. + + getInfluenceBase ./src + => ./src + + getInfluenceBase (fileFilter (file: false) ./.) + => ./. + + getInfluenceBase (union ./Makefile ../default.nix) + => ../. + */ + getInfluenceBase = maybeFileset: + let + fileset = _coerce "getInfluenceBase" "argument" maybeFileset; + in + fileset._base; + + /* + Incrementally evaluate and trace a file set in a pretty way. + Functionally this is the same as splitting the result from `lib.fileset.pretty` into lines and tracing those. + However this function can do the same thing incrementally, so it can already start printing the result as the first lines are known. + + The `expand` argument (false by default) controls whether all files should be printed individually. + + Type: + trace :: { expand :: Bool ? false } -> FileSet -> Any -> Any + + Example: + trace {} (unions [ ./foo.nix ./bar/baz.c ./qux ]) + => + trace: /home/user/src/myProject + trace: - bar + trace: - baz.c (regular) + trace: - foo.nix (regular) + trace: - qux (recursive directory) + null + + trace { expand = true; } (unions [ ./foo.nix ./bar/baz.c ./qux ]) + => + trace: /home/user/src/myProject + trace: - bar + trace: - baz.c (regular) + trace: - foo.nix (regular) + trace: - qux + trace: - florp.c (regular) + trace: - florp.h (regular) + null + */ + trace = { expand ? false }: maybeFileset: + let + fileset = _coerce "trace" "second argument" maybeFileset; + simpleTree = _simplifyTree expand fileset._base fileset._tree; + in + _prettyFoldl' (acc: el: builtins.trace el acc) (x: x) + fileset._base + simpleTree; + + /* + The same as `lib.fileset.trace`, but instead of taking an argument for the value to return, the given file set is returned instead. + + Type: + traceVal :: { expand :: Bool ? false } -> FileSet -> FileSet + */ + traceVal = { expand ? false }: maybeFileset: + let + fileset = _coerce "traceVal" "second argument" maybeFileset; + simpleTree = _simplifyTree expand fileset._base fileset._tree; + in + _prettyFoldl' (acc: el: builtins.trace el acc) fileset + fileset._base + simpleTree; + + /* + The same as `lib.fileset.trace`, but instead of tracing each line, the result is returned as a string. + + Type: + pretty :: { expand :: Bool ? false } -> FileSet -> String + */ + pretty = { expand ? false }: maybeFileset: + let + fileset = _coerce "pretty" "second argument" maybeFileset; + simpleTree = _simplifyTree expand fileset._base fileset._tree; + in + _prettyFoldl' (acc: el: "${acc}\n${el}") "" + fileset._base + simpleTree; + + /* + The file set containing all files that are in either of two given file sets. + Recursively, the first argument is evaluated first, only evaluating the second argument if necessary. + + union a b = a ⋃ b + + Type: + union :: FileSet -> FileSet -> FileSet + */ + union = lhs: rhs: + let + normalised = _normaliseBase "union" [ + { + context = "first argument"; + value = lhs; + } + { + context = "second argument"; + value = rhs; + } + ]; + in + _create normalised.commonBase + (_unionTree + (elemAt normalised.trees 0) + (elemAt normalised.trees 1) + ); + + /* + The file containing all files from that are in any of the given file sets. + Recursively, the elements are evaluated from left to right, only evaluating arguments on the right if necessary. + + Type: + unions :: [FileSet] -> FileSet + */ + unions = list: + let + annotated = imap0 (i: el: { + context = "element ${toString i} of the argument"; + value = el; + }) list; + + normalised = _normaliseBase "unions" annotated; + + tree = foldl' _unionTree (head normalised.trees) (tail normalised.trees); + in + if ! isList list then + throw "lib.fileset.unions: Expected argument to be a list, but got a ${typeOf list}." + else if length list == 0 then + throw "lib.fileset.unions: Expected argument to be a list with at least one element, but it contains no elements." + else + _create normalised.commonBase tree; + + /* + The file set containing all files that are in both given sets. + Recursively, the first argument is evaluated first, only evaluating the second argument if necessary. + + intersect a b == a ⋂ b + + Type: + intersect :: FileSet -> FileSet -> FileSet + */ + intersect = lhs: rhs: + let + normalised = _normaliseBase "intersect" [ + { + context = "first argument"; + value = lhs; + } + { + context = "second argument"; + value = rhs; + } + ]; + in + _create normalised.commonBase + (_intersectTree + (elemAt normalised.trees 0) + (elemAt normalised.trees 1) + ); + + /* + The file set containing all files that are in all the given sets. + Recursively, the elements are evaluated from left to right, only evaluating arguments on the right if necessary. + + Type: + intersects :: [FileSet] -> FileSet + */ + intersects = list: + let + annotated = imap0 (i: el: { + context = "element ${toString i} of the argument"; + value = el; + }) list; + + normalised = _normaliseBase "intersects" annotated; + + tree = foldl' _intersectTree (head normalised.trees) (tail normalised.trees); + in + if ! isList list then + throw "lib.fileset.intersects: Expected argument to be a list, but got a ${typeOf list}." + else if length list == 0 then + throw "lib.fileset.intersects: Expected argument to be a list with at least one element, but it contains no elements." + else + _create normalised.commonBase tree; + + /* + The file set containing all files that are in the first file set but not in the second. + Recursively, the second argument is evaluated first, only evaluating the first argument if necessary. + + difference a b == a ∖ b + + Type: + difference :: FileSet -> FileSet -> FileSet + */ + difference = lhs: rhs: + let + normalised = _normaliseBase "difference" [ + { + context = "first argument"; + value = lhs; + } + { + context = "second argument"; + value = rhs; + } + ]; + in + _create normalised.commonBase + (_differenceTree normalised.commonBase + (elemAt normalised.trees 0) + (elemAt normalised.trees 1) + ); + + /* + Filter a file set to only contain files matching some predicate. + + The predicate is called with an attribute set containing these attributes: + + - `name`: The filename + + - `type`: The type of the file, either "regular", "symlink" or "unknown" + + - `ext`: The file extension or `null` if the file has none. + + More formally: + - `ext` contains no `.` + - `.${ext}` is a suffix of the `name` + + - Potentially other attributes in the future + + Type: + fileFilter :: + ({ + name :: String, + type :: String, + ext :: String | Null, + ... + } -> Bool) + -> FileSet + -> FileSet + */ + fileFilter = predicate: maybeFileset: + let + fileset = _coerce "fileFilter" "second argument" maybeFileset; + recurse = focusPath: tree: + mapAttrs (name: subtree: + if isAttrs subtree || subtree == "directory" then + recurse (append focusPath name) subtree + else if + predicate { + inherit name; + type = subtree; + ext = mapNullable head (match ".*\\.(.*)" name); + # To ensure forwards compatibility with more arguments being added in the future, + # adding an attribute which can't be deconstructed :) + "This attribute is passed to prevent `lib.fileset.fileFilter` predicate functions from breaking when more attributes are added in the future. Please add `...` to the function to handle this and future additional arguments." = null; + } + then + subtree + else + null + ) (_directoryEntries focusPath tree); + in + _create fileset._base (recurse fileset._base fileset._tree); + + /* + A file set containing all files that are contained in a directory whose name satisfies the given predicate. + Only directories under the given path are checked, this is to ensure that components outside of the given path cannot influence the result. + Consequently this function does not accept a file set as an argument. + If you need to filter files in a file set based on components, use `intersect myFileSet (directoryFilter myPredicate myPath)` instead. + + Type: + directoryFilter :: (String -> Bool) -> Path -> FileSet + + Example: + # Select all files in hidden directories within ./. + directoryFilter (hasPrefix ".") ./. + + # Select all files in directories named `build` within ./src + directoryFilter (name: name == "build") ./src + */ + directoryFilter = predicate: path: + let + recurse = focusPath: + mapAttrs (name: type: + if type == "directory" then + if predicate name then + type + else + recurse (append focusPath name) + else + null + ) (readDir focusPath); + in + if path._type or null == "fileset" then + throw '' + lib.fileset.directoryFilter: Expected second argument to be a path, but it's a file set. + If you need to filter files in a file set, use `intersect myFileSet (directoryFilter myPredicate myPath)` instead.'' + else if ! isPath path then + throw "lib.fileset.directoryFilter: Expected second argument to be a path, but got a ${typeOf path}." + else if pathType path != "directory" then + throw "lib.fileset.directoryFilter: Expected second argument \"${toString path}\" to be a directory, but it's not." + else + _create path (recurse path); + + /* + Check whether two file sets contain the same files. + + Type: + equals :: FileSet -> FileSet -> Bool + */ + equals = lhs: rhs: + let + normalised = _normaliseBase "equals" [ + { + context = "first argument"; + value = lhs; + } + { + context = "second argument"; + value = rhs; + } + ]; + in + _equalsTree normalised.commonBase + (elemAt normalised.trees 0) + (elemAt normalised.trees 1); + + /* + Check whether a file set contains no files. + + Type: + isEmpty :: FileSet -> Bool + */ + isEmpty = maybeFileset: + let + fileset = _coerce "isEmpty" "argument" maybeFileset; + in + _isEmptyTree fileset._base fileset._tree; +} diff --git a/filesystem.nix b/filesystem.nix new file mode 100644 index 000000000..5d048c0a1 --- /dev/null +++ b/filesystem.nix @@ -0,0 +1,194 @@ +# Functions for querying information about the filesystem +# without copying any files to the Nix store. +{ lib }: + +# Tested in lib/tests/filesystem.sh +let + inherit (builtins) + readDir + pathExists + storeDir + ; + + inherit (lib.trivial) + inPureEvalMode + ; + + inherit (lib.path) + deconstruct + ; + + inherit (lib.path.components) + fromSubpath + ; + + inherit (lib.lists) + take + length + ; + + inherit (lib.filesystem) + pathType + ; +in + +{ + + /* + The type of a path. The path needs to exist and be accessible. + The result is either "directory" for a directory, "regular" for a regular file, "symlink" for a symlink, or "unknown" for anything else. + + Type: + pathType :: Path -> String + + Example: + pathType /. + => "directory" + + pathType /some/file.nix + => "regular" + */ + pathType = + let + storeDirComponents = fromSubpath "./${storeDir}"; + + legacy = path: + if ! pathExists path + # Fail irrecoverably to mimic the historic behavior of this function and + # the new builtins.readFileType + then abort "lib.filesystem.pathType: Path ${toString path} does not exist." + # The filesystem root is the only path where `dirOf / == /` and + # `baseNameOf /` is not valid. We can detect this and directly return + # "directory", since we know the filesystem root can't be anything else. + else if dirOf path == path + then "directory" + else (readDir (dirOf path)).${baseNameOf path}; + + legacyPureEval = path: + let + # This is a workaround for the fact that `lib.filesystem.pathType` doesn't work correctly on /nix/store/...-name paths in pure evaluation mode. For file set combinators it's safe to assume that + pathParts = deconstruct path; + assumeDirectory = + storeDirComponents == take (length pathParts.components - 1) pathParts.components; + in + if assumeDirectory then + "directory" + else + legacy path; + + in + if builtins ? readFileType then + builtins.readFileType + # Nix <2.14 compatibility shim + else if inPureEvalMode then + legacyPureEval + else + legacy; + + /* + Whether a path exists and is a directory. + + Type: + pathIsDirectory :: Path -> Bool + + Example: + pathIsDirectory /. + => true + + pathIsDirectory /this/does/not/exist + => false + + pathIsDirectory /some/file.nix + => false + */ + pathIsDirectory = path: + pathExists path && pathType path == "directory"; + + /* + Whether a path exists and is a regular file, meaning not a symlink or any other special file type. + + Type: + pathIsRegularFile :: Path -> Bool + + Example: + pathIsRegularFile /. + => false + + pathIsRegularFile /this/does/not/exist + => false + + pathIsRegularFile /some/file.nix + => true + */ + pathIsRegularFile = path: + pathExists path && pathType path == "regular"; + + /* + A map of all haskell packages defined in the given path, + identified by having a cabal file with the same name as the + directory itself. + + Type: Path -> Map String Path + */ + haskellPathsInDir = + # The directory within to search + root: + let # Files in the root + root-files = builtins.attrNames (builtins.readDir root); + # Files with their full paths + root-files-with-paths = + map (file: + { name = file; value = root + "/${file}"; } + ) root-files; + # Subdirectories of the root with a cabal file. + cabal-subdirs = + builtins.filter ({ name, value }: + builtins.pathExists (value + "/${name}.cabal") + ) root-files-with-paths; + in builtins.listToAttrs cabal-subdirs; + /* + Find the first directory containing a file matching 'pattern' + upward from a given 'file'. + Returns 'null' if no directories contain a file matching 'pattern'. + + Type: RegExp -> Path -> Nullable { path : Path; matches : [ MatchResults ]; } + */ + locateDominatingFile = + # The pattern to search for + pattern: + # The file to start searching upward from + file: + let go = path: + let files = builtins.attrNames (builtins.readDir path); + matches = builtins.filter (match: match != null) + (map (builtins.match pattern) files); + in + if builtins.length matches != 0 + then { inherit path matches; } + else if path == /. + then null + else go (dirOf path); + parent = dirOf file; + isDir = + let base = baseNameOf file; + type = (builtins.readDir parent).${base} or null; + in file == /. || type == "directory"; + in go (if isDir then file else parent); + + + /* + Given a directory, return a flattened list of all files within it recursively. + + Type: Path -> [ Path ] + */ + listFilesRecursive = + # The path to recursively list + dir: + lib.flatten (lib.mapAttrsToList (name: type: + if type == "directory" then + lib.filesystem.listFilesRecursive (dir + "/${name}") + else + dir + "/${name}" + ) (builtins.readDir dir)); + +} diff --git a/fixed-points.nix b/fixed-points.nix new file mode 100644 index 000000000..926428293 --- /dev/null +++ b/fixed-points.nix @@ -0,0 +1,113 @@ +{ lib, ... }: +rec { + # Compute the fixed point of the given function `f`, which is usually an + # attribute set that expects its final, non-recursive representation as an + # argument: + # + # f = self: { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; } + # + # Nix evaluates this recursion until all references to `self` have been + # resolved. At that point, the final result is returned and `f x = x` holds: + # + # nix-repl> fix f + # { bar = "bar"; foo = "foo"; foobar = "foobar"; } + # + # Type: fix :: (a -> a) -> a + # + # See https://en.wikipedia.org/wiki/Fixed-point_combinator for further + # details. + fix = f: let x = f x; in x; + + # A variant of `fix` that records the original recursive attribute set in the + # result. This is useful in combination with the `extends` function to + # implement deep overriding. See pkgs/development/haskell-modules/default.nix + # for a concrete example. + fix' = f: let x = f x // { __unfix__ = f; }; in x; + + # Return the fixpoint that `f` converges to when called recursively, starting + # with the input `x`. + # + # nix-repl> converge (x: x / 2) 16 + # 0 + converge = f: x: + let + x' = f x; + in + if x' == x + then x + else converge f x'; + + # Modify the contents of an explicitly recursive attribute set in a way that + # honors `self`-references. This is accomplished with a function + # + # g = self: super: { foo = super.foo + " + "; } + # + # that has access to the unmodified input (`super`) as well as the final + # non-recursive representation of the attribute set (`self`). `extends` + # differs from the native `//` operator insofar as that it's applied *before* + # references to `self` are resolved: + # + # nix-repl> fix (extends g f) + # { bar = "bar"; foo = "foo + "; foobar = "foo + bar"; } + # + # The name of the function is inspired by object-oriented inheritance, i.e. + # think of it as an infix operator `g extends f` that mimics the syntax from + # Java. It may seem counter-intuitive to have the "base class" as the second + # argument, but it's nice this way if several uses of `extends` are cascaded. + # + # To get a better understanding how `extends` turns a function with a fix + # point (the package set we start with) into a new function with a different fix + # point (the desired packages set) lets just see, how `extends g f` + # unfolds with `g` and `f` defined above: + # + # extends g f = self: let super = f self; in super // g self super; + # = self: let super = { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; }; in super // g self super + # = self: { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; } // g self { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; } + # = self: { foo = "foo"; bar = "bar"; foobar = self.foo + self.bar; } // { foo = "foo" + " + "; } + # = self: { foo = "foo + "; bar = "bar"; foobar = self.foo + self.bar; } + # + extends = f: rattrs: self: let super = rattrs self; in super // f self super; + + # Compose two extending functions of the type expected by 'extends' + # into one where changes made in the first are available in the + # 'super' of the second + composeExtensions = + f: g: final: prev: + let fApplied = f final prev; + prev' = prev // fApplied; + in fApplied // g final prev'; + + # Compose several extending functions of the type expected by 'extends' into + # one where changes made in preceding functions are made available to + # subsequent ones. + # + # composeManyExtensions : [packageSet -> packageSet -> packageSet] -> packageSet -> packageSet -> packageSet + # ^final ^prev ^overrides ^final ^prev ^overrides + composeManyExtensions = + lib.foldr (x: y: composeExtensions x y) (final: prev: {}); + + # Create an overridable, recursive attribute set. For example: + # + # nix-repl> obj = makeExtensible (self: { }) + # + # nix-repl> obj + # { __unfix__ = «lambda»; extend = «lambda»; } + # + # nix-repl> obj = obj.extend (self: super: { foo = "foo"; }) + # + # nix-repl> obj + # { __unfix__ = «lambda»; extend = «lambda»; foo = "foo"; } + # + # nix-repl> obj = obj.extend (self: super: { foo = super.foo + " + "; bar = "bar"; foobar = self.foo + self.bar; }) + # + # nix-repl> obj + # { __unfix__ = «lambda»; bar = "bar"; extend = «lambda»; foo = "foo + "; foobar = "foo + bar"; } + makeExtensible = makeExtensibleWithCustomName "extend"; + + # Same as `makeExtensible` but the name of the extending attribute is + # customized. + makeExtensibleWithCustomName = extenderName: rattrs: + fix' (self: (rattrs self) // { + ${extenderName} = f: makeExtensibleWithCustomName extenderName (extends f rattrs); + }); +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..0b5e54d54 --- /dev/null +++ b/flake.nix @@ -0,0 +1,5 @@ +{ + description = "Library of low-level helper functions for nix expressions."; + + outputs = { self }: { lib = import ./.; }; +} diff --git a/generators.nix b/generators.nix new file mode 100644 index 000000000..496845fc9 --- /dev/null +++ b/generators.nix @@ -0,0 +1,526 @@ +/* Functions that generate widespread file + * formats from nix data structures. + * + * They all follow a similar interface: + * generator { config-attrs } data + * + * `config-attrs` are “holes” in the generators + * with sensible default implementations that + * can be overwritten. The default implementations + * are mostly generators themselves, called with + * their respective default values; they can be reused. + * + * Tests can be found in ./tests/misc.nix + * Documentation in the manual, #sec-generators + */ +{ lib }: +with (lib).trivial; +let + libStr = lib.strings; + libAttr = lib.attrsets; + + inherit (lib) isFunction; +in + +rec { + + ## -- HELPER FUNCTIONS & DEFAULTS -- + + /* Convert a value to a sensible default string representation. + * The builtin `toString` function has some strange defaults, + * suitable for bash scripts but not much else. + */ + mkValueStringDefault = {}: v: with builtins; + let err = t: v: abort + ("generators.mkValueStringDefault: " + + "${t} not supported: ${toPretty {} v}"); + in if isInt v then toString v + # convert derivations to store paths + else if lib.isDerivation v then toString v + # we default to not quoting strings + else if isString v then v + # isString returns "1", which is not a good default + else if true == v then "true" + # here it returns to "", which is even less of a good default + else if false == v then "false" + else if null == v then "null" + # if you have lists you probably want to replace this + else if isList v then err "lists" v + # same as for lists, might want to replace + else if isAttrs v then err "attrsets" v + # functions can’t be printed of course + else if isFunction v then err "functions" v + # Floats currently can't be converted to precise strings, + # condition warning on nix version once this isn't a problem anymore + # See https://github.com/NixOS/nix/pull/3480 + else if isFloat v then libStr.floatToString v + else err "this value is" (toString v); + + + /* Generate a line of key k and value v, separated by + * character sep. If sep appears in k, it is escaped. + * Helper for synaxes with different separators. + * + * mkValueString specifies how values should be formatted. + * + * mkKeyValueDefault {} ":" "f:oo" "bar" + * > "f\:oo:bar" + */ + mkKeyValueDefault = { + mkValueString ? mkValueStringDefault {} + }: sep: k: v: + "${libStr.escape [sep] k}${sep}${mkValueString v}"; + + + ## -- FILE FORMAT GENERATORS -- + + + /* Generate a key-value-style config file from an attrset. + * + * mkKeyValue is the same as in toINI. + */ + toKeyValue = { + mkKeyValue ? mkKeyValueDefault {} "=", + listsAsDuplicateKeys ? false + }: + let mkLine = k: v: mkKeyValue k v + "\n"; + mkLines = if listsAsDuplicateKeys + then k: v: map (mkLine k) (if lib.isList v then v else [v]) + else k: v: [ (mkLine k v) ]; + in attrs: libStr.concatStrings (lib.concatLists (libAttr.mapAttrsToList mkLines attrs)); + + + /* Generate an INI-style config file from an + * attrset of sections to an attrset of key-value pairs. + * + * generators.toINI {} { + * foo = { hi = "${pkgs.hello}"; ciao = "bar"; }; + * baz = { "also, integers" = 42; }; + * } + * + *> [baz] + *> also, integers=42 + *> + *> [foo] + *> ciao=bar + *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10 + * + * The mk* configuration attributes can generically change + * the way sections and key-value strings are generated. + * + * For more examples see the test cases in ./tests/misc.nix. + */ + toINI = { + # apply transformations (e.g. escapes) to section names + mkSectionName ? (name: libStr.escape [ "[" "]" ] name), + # format a setting line from key and value + mkKeyValue ? mkKeyValueDefault {} "=", + # allow lists as values for duplicate keys + listsAsDuplicateKeys ? false + }: attrsOfAttrs: + let + # map function to string for each key val + mapAttrsToStringsSep = sep: mapFn: attrs: + libStr.concatStringsSep sep + (libAttr.mapAttrsToList mapFn attrs); + mkSection = sectName: sectValues: '' + [${mkSectionName sectName}] + '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues; + in + # map input to ini sections + mapAttrsToStringsSep "\n" mkSection attrsOfAttrs; + + /* Generate an INI-style config file from an attrset + * specifying the global section (no header), and an + * attrset of sections to an attrset of key-value pairs. + * + * generators.toINIWithGlobalSection {} { + * globalSection = { + * someGlobalKey = "hi"; + * }; + * sections = { + * foo = { hi = "${pkgs.hello}"; ciao = "bar"; }; + * baz = { "also, integers" = 42; }; + * } + * + *> someGlobalKey=hi + *> + *> [baz] + *> also, integers=42 + *> + *> [foo] + *> ciao=bar + *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10 + * + * The mk* configuration attributes can generically change + * the way sections and key-value strings are generated. + * + * For more examples see the test cases in ./tests/misc.nix. + * + * If you don’t need a global section, you can also use + * `generators.toINI` directly, which only takes + * the part in `sections`. + */ + toINIWithGlobalSection = { + # apply transformations (e.g. escapes) to section names + mkSectionName ? (name: libStr.escape [ "[" "]" ] name), + # format a setting line from key and value + mkKeyValue ? mkKeyValueDefault {} "=", + # allow lists as values for duplicate keys + listsAsDuplicateKeys ? false + }: { globalSection, sections }: + ( if globalSection == {} + then "" + else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection) + + "\n") + + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections); + + /* Generate a git-config file from an attrset. + * + * It has two major differences from the regular INI format: + * + * 1. values are indented with tabs + * 2. sections can have sub-sections + * + * generators.toGitINI { + * url."ssh://git@github.com/".insteadOf = "https://github.com"; + * user.name = "edolstra"; + * } + * + *> [url "ssh://git@github.com/"] + *> insteadOf = https://github.com/ + *> + *> [user] + *> name = edolstra + */ + toGitINI = attrs: + with builtins; + let + mkSectionName = name: + let + containsQuote = libStr.hasInfix ''"'' name; + sections = libStr.splitString "." name; + section = head sections; + subsections = tail sections; + subsection = concatStringsSep "." subsections; + in if containsQuote || subsections == [ ] then + name + else + ''${section} "${subsection}"''; + + # generation for multiple ini values + mkKeyValue = k: v: + let mkKeyValue = mkKeyValueDefault { } " = " k; + in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v)); + + # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI + gitFlattenAttrs = let + recurse = path: value: + if isAttrs value && !lib.isDerivation value then + lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value + else if length path > 1 then { + ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value; + } else { + ${head path} = value; + }; + in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs)); + + toINI_ = toINI { inherit mkKeyValue mkSectionName; }; + in + toINI_ (gitFlattenAttrs attrs); + + /* Generates JSON from an arbitrary (non-function) value. + * For more information see the documentation of the builtin. + */ + toJSON = {}: builtins.toJSON; + + + /* YAML has been a strict superset of JSON since 1.2, so we + * use toJSON. Before it only had a few differences referring + * to implicit typing rules, so it should work with older + * parsers as well. + */ + toYAML = toJSON; + + withRecursion = + { + /* If this option is not null, the given value will stop evaluating at a certain depth */ + depthLimit + /* If this option is true, an error will be thrown, if a certain given depth is exceeded */ + , throwOnDepthLimit ? true + }: + assert builtins.isInt depthLimit; + let + specialAttrs = [ + "__functor" + "__functionArgs" + "__toString" + "__pretty" + ]; + stepIntoAttr = evalNext: name: + if builtins.elem name specialAttrs + then id + else evalNext; + transform = depth: + if depthLimit != null && depth > depthLimit then + if throwOnDepthLimit + then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!" + else const "" + else id; + mapAny = with builtins; depth: v: + let + evalNext = x: mapAny (depth + 1) (transform (depth + 1) x); + in + if isAttrs v then mapAttrs (stepIntoAttr evalNext) v + else if isList v then map evalNext v + else transform (depth + 1) v; + in + mapAny 0; + + /* Pretty print a value, akin to `builtins.trace`. + * Should probably be a builtin as well. + * The pretty-printed string should be suitable for rendering default values + * in the NixOS manual. In particular, it should be as close to a valid Nix expression + * as possible. + */ + toPretty = { + /* If this option is true, attrsets like { __pretty = fn; val = …; } + will use fn to convert val to a pretty printed representation. + (This means fn is type Val -> String.) */ + allowPrettyValues ? false, + /* If this option is true, the output is indented with newlines for attribute sets and lists */ + multiline ? true, + /* Initial indentation level */ + indent ? "" + }: + let + go = indent: v: with builtins; + let isPath = v: typeOf v == "path"; + introSpace = if multiline then "\n${indent} " else " "; + outroSpace = if multiline then "\n${indent}" else " "; + in if isInt v then toString v + # toString loses precision on floats, so we use toJSON instead. This isn't perfect + # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for + # pretty-printing purposes this is acceptable. + else if isFloat v then builtins.toJSON v + else if isString v then + let + lines = filter (v: ! isList v) (builtins.split "\n" v); + escapeSingleline = libStr.escape [ "\\" "\"" "\${" ]; + escapeMultiline = libStr.replaceStrings [ "\${" "''" ] [ "''\${" "'''" ]; + singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\""; + multilineResult = let + escapedLines = map escapeMultiline lines; + # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer" + # indentation level. Otherwise, '' is appended to the last line. + lastLine = lib.last escapedLines; + in "''" + introSpace + concatStringsSep introSpace (lib.init escapedLines) + + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''"; + in + if multiline && length lines > 1 then multilineResult else singlelineResult + else if true == v then "true" + else if false == v then "false" + else if null == v then "null" + else if isPath v then toString v + else if isList v then + if v == [] then "[ ]" + else "[" + introSpace + + libStr.concatMapStringsSep introSpace (go (indent + " ")) v + + outroSpace + "]" + else if isFunction v then + let fna = lib.functionArgs v; + showFnas = concatStringsSep ", " (libAttr.mapAttrsToList + (name: hasDefVal: if hasDefVal then name + "?" else name) + fna); + in if fna == {} then "" + else "" + else if isAttrs v then + # apply pretty values if allowed + if allowPrettyValues && v ? __pretty && v ? val + then v.__pretty v.val + else if v == {} then "{ }" + else if v ? type && v.type == "derivation" then + "" + else "{" + introSpace + + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList + (name: value: + "${libStr.escapeNixIdentifier name} = ${ + builtins.addErrorContext "while evaluating an attribute `${name}`" + (go (indent + " ") value) + };") v) + + outroSpace + "}" + else abort "generators.toPretty: should never happen (v = ${v})"; + in go indent; + + # PLIST handling + toPlist = {}: v: let + isFloat = builtins.isFloat or (x: false); + isPath = x: builtins.typeOf x == "path"; + expr = ind: x: with builtins; + if x == null then "" else + if isBool x then bool ind x else + if isInt x then int ind x else + if isString x then str ind x else + if isList x then list ind x else + if isAttrs x then attrs ind x else + if isPath x then str ind (toString x) else + if isFloat x then float ind x else + abort "generators.toPlist: should never happen (v = ${v})"; + + literal = ind: x: ind + x; + + bool = ind: x: literal ind (if x then "" else ""); + int = ind: x: literal ind "${toString x}"; + str = ind: x: literal ind "${x}"; + key = ind: x: literal ind "${x}"; + float = ind: x: literal ind "${toString x}"; + + indent = ind: expr "\t${ind}"; + + item = ind: libStr.concatMapStringsSep "\n" (indent ind); + + list = ind: x: libStr.concatStringsSep "\n" [ + (literal ind "") + (item ind x) + (literal ind "") + ]; + + attrs = ind: x: libStr.concatStringsSep "\n" [ + (literal ind "") + (attr ind x) + (literal ind "") + ]; + + attr = let attrFilter = name: value: name != "_module" && value != null; + in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList + (name: value: lib.optionals (attrFilter name value) [ + (key "\t${ind}" name) + (expr "\t${ind}" value) + ]) x)); + + in '' + + +${expr "" v} +''; + + /* Translate a simple Nix expression to Dhall notation. + * Note that integers are translated to Integer and never + * the Natural type. + */ + toDhall = { }@args: v: + with builtins; + let concatItems = lib.strings.concatStringsSep ", "; + in if isAttrs v then + "{ ${ + concatItems (lib.attrsets.mapAttrsToList + (key: value: "${key} = ${toDhall args value}") v) + } }" + else if isList v then + "[ ${concatItems (map (toDhall args) v)} ]" + else if isInt v then + "${if v < 0 then "" else "+"}${toString v}" + else if isBool v then + (if v then "True" else "False") + else if isFunction v then + abort "generators.toDhall: cannot convert a function to Dhall" + else if v == null then + abort "generators.toDhall: cannot convert a null to Dhall" + else + builtins.toJSON v; + + /* + Translate a simple Nix expression to Lua representation with occasional + Lua-inlines that can be constructed by mkLuaInline function. + + Configuration: + * multiline - by default is true which results in indented block-like view. + * indent - initial indent. + * asBindings - by default generate single value, but with this use attrset to set global vars. + + Attention: + Regardless of multiline parameter there is no trailing newline. + + Example: + generators.toLua {} + { + cmd = [ "typescript-language-server" "--stdio" ]; + settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)''; + } + -> + { + ["cmd"] = { + "typescript-language-server", + "--stdio" + }, + ["settings"] = { + ["workspace"] = { + ["library"] = (vim.api.nvim_get_runtime_file("", true)) + } + } + } + + Type: + toLua :: AttrSet -> Any -> String + */ + toLua = { + /* If this option is true, the output is indented with newlines for attribute sets and lists */ + multiline ? true, + /* Initial indentation level */ + indent ? "", + /* Interpret as variable bindings */ + asBindings ? false, + }@args: v: + with builtins; + let + innerIndent = "${indent} "; + introSpace = if multiline then "\n${innerIndent}" else " "; + outroSpace = if multiline then "\n${indent}" else " "; + innerArgs = args // { + indent = if asBindings then indent else innerIndent; + asBindings = false; + }; + concatItems = concatStringsSep ",${introSpace}"; + isLuaInline = { _type ? null, ... }: _type == "lua-inline"; + + generatedBindings = + assert lib.assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}"; + libStr.concatStrings ( + lib.attrsets.mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v + ); + + # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names + matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*"; + badVarNames = filter (name: matchVarName name == null) (attrNames v); + in + if asBindings then + generatedBindings + else if v == null then + "nil" + else if isInt v || isFloat v || isString v || isBool v then + builtins.toJSON v + else if isList v then + (if v == [ ] then "{}" else + "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}") + else if isAttrs v then + ( + if isLuaInline v then + "(${v.expr})" + else if v == { } then + "{}" + else + "{${introSpace}${concatItems ( + lib.attrsets.mapAttrsToList (key: value: "[${builtins.toJSON key}] = ${toLua innerArgs value}") v + )}${outroSpace}}" + ) + else + abort "generators.toLua: type ${typeOf v} is unsupported"; + + /* + Mark string as Lua expression to be inlined when processed by toLua. + + Type: + mkLuaInline :: String -> AttrSet + */ + mkLuaInline = expr: { _type = "lua-inline"; inherit expr; }; +} diff --git a/kernel.nix b/kernel.nix new file mode 100644 index 000000000..33da9663a --- /dev/null +++ b/kernel.nix @@ -0,0 +1,27 @@ +{ lib }: + +with lib; +{ + + + # Keeping these around in case we decide to change this horrible implementation :) + option = x: + x // { optional = true; }; + + yes = { tristate = "y"; optional = false; }; + no = { tristate = "n"; optional = false; }; + module = { tristate = "m"; optional = false; }; + unset = { tristate = null; optional = false; }; + freeform = x: { freeform = x; optional = false; }; + + /* + Common patterns/legacy used in common-config/hardened/config.nix + */ + whenHelpers = version: { + whenAtLeast = ver: mkIf (versionAtLeast version ver); + whenOlder = ver: mkIf (versionOlder version ver); + # range is (inclusive, exclusive) + whenBetween = verLow: verHigh: mkIf (versionAtLeast version verLow && versionOlder version verHigh); + }; + +} diff --git a/licenses.nix b/licenses.nix new file mode 100644 index 000000000..1d2565fba --- /dev/null +++ b/licenses.nix @@ -0,0 +1,1104 @@ +{ lib }: + +lib.mapAttrs (lname: lset: let + defaultLicense = rec { + shortName = lname; + free = true; # Most of our licenses are Free, explicitly declare unfree additions as such! + deprecated = false; + }; + + mkLicense = licenseDeclaration: let + applyDefaults = license: defaultLicense // license; + applySpdx = license: + if license ? spdxId + then license // { url = "https://spdx.org/licenses/${license.spdxId}.html"; } + else license; + applyRedistributable = license: { redistributable = license.free; } // license; + in lib.pipe licenseDeclaration [ + applyDefaults + applySpdx + applyRedistributable + ]; +in mkLicense lset) ({ + /* License identifiers from spdx.org where possible. + * If you cannot find your license here, then look for a similar license or + * add it to this list. The URL mentioned above is a good source for inspiration. + */ + + abstyles = { + spdxId = "Abstyles"; + fullName = "Abstyles License"; + }; + + afl20 = { + spdxId = "AFL-2.0"; + fullName = "Academic Free License v2.0"; + }; + + afl21 = { + spdxId = "AFL-2.1"; + fullName = "Academic Free License v2.1"; + }; + + afl3 = { + spdxId = "AFL-3.0"; + fullName = "Academic Free License v3.0"; + }; + + agpl3Only = { + spdxId = "AGPL-3.0-only"; + fullName = "GNU Affero General Public License v3.0 only"; + }; + + agpl3Plus = { + spdxId = "AGPL-3.0-or-later"; + fullName = "GNU Affero General Public License v3.0 or later"; + }; + + aladdin = { + spdxId = "Aladdin"; + fullName = "Aladdin Free Public License"; + free = false; + }; + + amazonsl = { + fullName = "Amazon Software License"; + url = "https://aws.amazon.com/asl/"; + free = false; + }; + + amd = { + fullName = "AMD License Agreement"; + url = "https://developer.amd.com/amd-license-agreement/"; + free = false; + }; + + aom = { + fullName = "Alliance for Open Media Patent License 1.0"; + url = "https://aomedia.org/license/patent-license/"; + }; + + apsl10 = { + spdxId = "APSL-1.0"; + fullName = "Apple Public Source License 1.0"; + }; + + apsl20 = { + spdxId = "APSL-2.0"; + fullName = "Apple Public Source License 2.0"; + }; + + arphicpl = { + fullName = "Arphic Public License"; + url = "https://www.freedesktop.org/wiki/Arphic_Public_License/"; + }; + + artistic1 = { + spdxId = "Artistic-1.0"; + fullName = "Artistic License 1.0"; + }; + + artistic2 = { + spdxId = "Artistic-2.0"; + fullName = "Artistic License 2.0"; + }; + + asl20 = { + spdxId = "Apache-2.0"; + fullName = "Apache License 2.0"; + }; + + asl20-llvm = { + spdxId = "Apache-2.0 WITH LLVM-exception"; + fullName = "Apache License 2.0 with LLVM Exceptions"; + }; + + bitstreamVera = { + spdxId = "Bitstream-Vera"; + fullName = "Bitstream Vera Font License"; + }; + + bitTorrent10 = { + spdxId = "BitTorrent-1.0"; + fullName = " BitTorrent Open Source License v1.0"; + }; + + bitTorrent11 = { + spdxId = "BitTorrent-1.1"; + fullName = " BitTorrent Open Source License v1.1"; + }; + + bola11 = { + url = "https://blitiri.com.ar/p/bola/"; + fullName = "Buena Onda License Agreement 1.1"; + }; + + boost = { + spdxId = "BSL-1.0"; + fullName = "Boost Software License 1.0"; + }; + + beerware = { + spdxId = "Beerware"; + fullName = "Beerware License"; + }; + + blueOak100 = { + spdxId = "BlueOak-1.0.0"; + fullName = "Blue Oak Model License 1.0.0"; + }; + + bsd0 = { + spdxId = "0BSD"; + fullName = "BSD Zero Clause License"; + }; + + bsd1 = { + spdxId = "BSD-1-Clause"; + fullName = "BSD 1-Clause License"; + }; + + bsd2 = { + spdxId = "BSD-2-Clause"; + fullName = ''BSD 2-clause "Simplified" License''; + }; + + bsd2Patent = { + spdxId = "BSD-2-Clause-Patent"; + fullName = "BSD-2-Clause Plus Patent License"; + }; + + bsd2WithViews = { + spdxId = "BSD-2-Clause-Views"; + fullName = "BSD 2-Clause with views sentence"; + }; + + bsd3 = { + spdxId = "BSD-3-Clause"; + fullName = ''BSD 3-clause "New" or "Revised" License''; + }; + + bsdOriginal = { + spdxId = "BSD-4-Clause"; + fullName = ''BSD 4-clause "Original" or "Old" License''; + }; + + bsdOriginalShortened = { + spdxId = "BSD-4-Clause-Shortened"; + fullName = "BSD 4 Clause Shortened"; + }; + + bsdOriginalUC = { + spdxId = "BSD-4-Clause-UC"; + fullName = "BSD 4-Clause University of California-Specific"; + }; + + bsdProtection = { + spdxId = "BSD-Protection"; + fullName = "BSD Protection License"; + }; + + bsl11 = { + fullName = "Business Source License 1.1"; + url = "https://mariadb.com/bsl11"; + free = false; + redistributable = true; + }; + + caossl = { + fullName = "Computer Associates Open Source Licence Version 1.0"; + url = "http://jxplorer.org/licence.html"; + }; + + cal10 = { + fullName = "Cryptographic Autonomy License version 1.0 (CAL-1.0)"; + url = "https://opensource.org/licenses/CAL-1.0"; + }; + + caldera = { + spdxId = "Caldera"; + fullName = "Caldera License"; + url = "http://www.lemis.com/grog/UNIX/ancient-source-all.pdf"; + }; + + capec = { + fullName = "Common Attack Pattern Enumeration and Classification"; + url = "https://capec.mitre.org/about/termsofuse.html"; + }; + + clArtistic = { + spdxId = "ClArtistic"; + fullName = "Clarified Artistic License"; + }; + + cc0 = { + spdxId = "CC0-1.0"; + fullName = "Creative Commons Zero v1.0 Universal"; + }; + + cc-by-nc-nd-30 = { + spdxId = "CC-BY-NC-ND-3.0"; + fullName = "Creative Commons Attribution Non Commercial No Derivative Works 3.0 Unported"; + free = false; + }; + + cc-by-nc-nd-40 = { + spdxId = "CC-BY-NC-ND-4.0"; + fullName = "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International"; + free = false; + }; + + cc-by-nc-sa-20 = { + spdxId = "CC-BY-NC-SA-2.0"; + fullName = "Creative Commons Attribution Non Commercial Share Alike 2.0"; + free = false; + }; + + cc-by-nc-sa-25 = { + spdxId = "CC-BY-NC-SA-2.5"; + fullName = "Creative Commons Attribution Non Commercial Share Alike 2.5"; + free = false; + }; + + cc-by-nc-sa-30 = { + spdxId = "CC-BY-NC-SA-3.0"; + fullName = "Creative Commons Attribution Non Commercial Share Alike 3.0"; + free = false; + }; + + cc-by-nc-sa-40 = { + spdxId = "CC-BY-NC-SA-4.0"; + fullName = "Creative Commons Attribution Non Commercial Share Alike 4.0"; + free = false; + }; + + cc-by-nc-30 = { + spdxId = "CC-BY-NC-3.0"; + fullName = "Creative Commons Attribution Non Commercial 3.0 Unported"; + free = false; + }; + + cc-by-nc-40 = { + spdxId = "CC-BY-NC-4.0"; + fullName = "Creative Commons Attribution Non Commercial 4.0 International"; + free = false; + }; + + cc-by-nd-30 = { + spdxId = "CC-BY-ND-3.0"; + fullName = "Creative Commons Attribution-No Derivative Works v3.00"; + free = false; + }; + + cc-by-sa-25 = { + spdxId = "CC-BY-SA-2.5"; + fullName = "Creative Commons Attribution Share Alike 2.5"; + }; + + cc-by-30 = { + spdxId = "CC-BY-3.0"; + fullName = "Creative Commons Attribution 3.0"; + }; + + cc-by-sa-30 = { + spdxId = "CC-BY-SA-3.0"; + fullName = "Creative Commons Attribution Share Alike 3.0"; + }; + + cc-by-40 = { + spdxId = "CC-BY-4.0"; + fullName = "Creative Commons Attribution 4.0"; + }; + + cc-by-sa-40 = { + spdxId = "CC-BY-SA-4.0"; + fullName = "Creative Commons Attribution Share Alike 4.0"; + }; + + cddl = { + spdxId = "CDDL-1.0"; + fullName = "Common Development and Distribution License 1.0"; + }; + + cecill20 = { + spdxId = "CECILL-2.0"; + fullName = "CeCILL Free Software License Agreement v2.0"; + }; + + cecill21 = { + spdxId = "CECILL-2.1"; + fullName = "CeCILL Free Software License Agreement v2.1"; + }; + + cecill-b = { + spdxId = "CECILL-B"; + fullName = "CeCILL-B Free Software License Agreement"; + }; + + cecill-c = { + spdxId = "CECILL-C"; + fullName = "CeCILL-C Free Software License Agreement"; + }; + + cpal10 = { + spdxId = "CPAL-1.0"; + fullName = "Common Public Attribution License 1.0"; + }; + + cpl10 = { + spdxId = "CPL-1.0"; + fullName = "Common Public License 1.0"; + }; + + curl = { + spdxId = "curl"; + fullName = "curl License"; + }; + + doc = { + spdxId = "DOC"; + fullName = "DOC License"; + }; + + drl10 = { + spdxId = "DRL-1.0"; + fullName = "Detection Rule License 1.0"; + }; + + eapl = { + fullName = "EPSON AVASYS PUBLIC LICENSE"; + url = "https://avasys.jp/hp/menu000000700/hpg000000603.htm"; + free = false; + }; + + ecl20 = { + fullName = "Educational Community License, Version 2.0"; + url = "https://opensource.org/licenses/ECL-2.0"; + shortName = "ECL 2.0"; + spdxId = "ECL-2.0"; + }; + + efl10 = { + spdxId = "EFL-1.0"; + fullName = "Eiffel Forum License v1.0"; + }; + + efl20 = { + spdxId = "EFL-2.0"; + fullName = "Eiffel Forum License v2.0"; + }; + + elastic = { + fullName = "ELASTIC LICENSE"; + url = "https://github.com/elastic/elasticsearch/blob/master/licenses/ELASTIC-LICENSE.txt"; + free = false; + }; + + epl10 = { + spdxId = "EPL-1.0"; + fullName = "Eclipse Public License 1.0"; + }; + + epl20 = { + spdxId = "EPL-2.0"; + fullName = "Eclipse Public License 2.0"; + }; + + epson = { + fullName = "Seiko Epson Corporation Software License Agreement for Linux"; + url = "https://download.ebz.epson.net/dsc/du/02/eula/global/LINUX_EN.html"; + free = false; + }; + + eupl11 = { + spdxId = "EUPL-1.1"; + fullName = "European Union Public License 1.1"; + }; + + eupl12 = { + spdxId = "EUPL-1.2"; + fullName = "European Union Public License 1.2"; + }; + + fdl11Only = { + spdxId = "GFDL-1.1-only"; + fullName = "GNU Free Documentation License v1.1 only"; + }; + + fdl11Plus = { + spdxId = "GFDL-1.1-or-later"; + fullName = "GNU Free Documentation License v1.1 or later"; + }; + + fdl12Only = { + spdxId = "GFDL-1.2-only"; + fullName = "GNU Free Documentation License v1.2 only"; + }; + + fdl12Plus = { + spdxId = "GFDL-1.2-or-later"; + fullName = "GNU Free Documentation License v1.2 or later"; + }; + + fdl13Only = { + spdxId = "GFDL-1.3-only"; + fullName = "GNU Free Documentation License v1.3 only"; + }; + + fdl13Plus = { + spdxId = "GFDL-1.3-or-later"; + fullName = "GNU Free Documentation License v1.3 or later"; + }; + + ffsl = { + fullName = "Floodgap Free Software License"; + url = "https://www.floodgap.com/software/ffsl/license.html"; + free = false; + }; + + free = { + fullName = "Unspecified free software license"; + }; + + ftl = { + spdxId = "FTL"; + fullName = "Freetype Project License"; + }; + + g4sl = { + fullName = "Geant4 Software License"; + url = "https://geant4.web.cern.ch/geant4/license/LICENSE.html"; + }; + + geogebra = { + fullName = "GeoGebra Non-Commercial License Agreement"; + url = "https://www.geogebra.org/license"; + free = false; + }; + + generaluser = { + fullName = "GeneralUser GS License v2.0"; + url = "http://www.schristiancollins.com/generaluser.php"; # license included in sources + }; + + gpl1Only = { + spdxId = "GPL-1.0-only"; + fullName = "GNU General Public License v1.0 only"; + }; + + gpl1Plus = { + spdxId = "GPL-1.0-or-later"; + fullName = "GNU General Public License v1.0 or later"; + }; + + gpl2Only = { + spdxId = "GPL-2.0-only"; + fullName = "GNU General Public License v2.0 only"; + }; + + gpl2Classpath = { + spdxId = "GPL-2.0-with-classpath-exception"; + fullName = "GNU General Public License v2.0 only (with Classpath exception)"; + }; + + gpl2ClasspathPlus = { + fullName = "GNU General Public License v2.0 or later (with Classpath exception)"; + url = "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception"; + }; + + gpl2Oss = { + fullName = "GNU General Public License version 2 only (with OSI approved licenses linking exception)"; + url = "https://www.mysql.com/about/legal/licensing/foss-exception"; + }; + + gpl2Plus = { + spdxId = "GPL-2.0-or-later"; + fullName = "GNU General Public License v2.0 or later"; + }; + + gpl3Only = { + spdxId = "GPL-3.0-only"; + fullName = "GNU General Public License v3.0 only"; + }; + + gpl3Plus = { + spdxId = "GPL-3.0-or-later"; + fullName = "GNU General Public License v3.0 or later"; + }; + + gpl3ClasspathPlus = { + fullName = "GNU General Public License v3.0 or later (with Classpath exception)"; + url = "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception"; + }; + + hpnd = { + spdxId = "HPND"; + fullName = "Historic Permission Notice and Disclaimer"; + }; + + hpndSellVariant = { + fullName = "Historical Permission Notice and Disclaimer - sell variant"; + spdxId = "HPND-sell-variant"; + }; + + # Intel's license, seems free + iasl = { + fullName = "iASL"; + url = "https://old.calculate-linux.org/packages/licenses/iASL"; + }; + + ijg = { + spdxId = "IJG"; + fullName = "Independent JPEG Group License"; + }; + + imagemagick = { + fullName = "ImageMagick License"; + spdxId = "imagemagick"; + }; + + imlib2 = { + spdxId = "Imlib2"; + fullName = "Imlib2 License"; + }; + + info-zip = { + spdxId = "Info-ZIP"; + fullName = "Info-ZIP License"; + url = "http://www.info-zip.org/pub/infozip/license.html"; + }; + + inria-compcert = { + fullName = "INRIA Non-Commercial License Agreement for the CompCert verified compiler"; + url = "https://compcert.org/doc/LICENSE.txt"; + free = false; + }; + + inria-icesl = { + fullName = "INRIA Non-Commercial License Agreement for IceSL"; + url = "https://icesl.loria.fr/assets/pdf/EULA_IceSL_binary.pdf"; + free = false; + }; + + ipa = { + spdxId = "IPA"; + fullName = "IPA Font License"; + }; + + ipl10 = { + spdxId = "IPL-1.0"; + fullName = "IBM Public License v1.0"; + }; + + isc = { + spdxId = "ISC"; + fullName = "ISC License"; + }; + + # Proprietary binaries; free to redistribute without modification. + databricks = { + fullName = "Databricks Proprietary License"; + url = "https://pypi.org/project/databricks-connect"; + free = false; + }; + + databricks-dbx = { + fullName = "DataBricks eXtensions aka dbx License"; + url = "https://github.com/databrickslabs/dbx/blob/743b579a4ac44531f764c6e522dbe5a81a7dc0e4/LICENSE"; + free = false; + redistributable = false; + }; + + fair = { + fullName = "Fair License"; + spdxId = "Fair"; + free = true; + }; + + issl = { + fullName = "Intel Simplified Software License"; + url = "https://software.intel.com/en-us/license/intel-simplified-software-license"; + free = false; + }; + + lal12 = { + spdxId = "LAL-1.2"; + fullName = "Licence Art Libre 1.2"; + }; + + lal13 = { + spdxId = "LAL-1.3"; + fullName = "Licence Art Libre 1.3"; + }; + + lens = { + fullName = "Lens Terms of Service Agreement"; + url = "https://k8slens.dev/licenses/tos"; + free = false; + }; + + lgpl2Only = { + spdxId = "LGPL-2.0-only"; + fullName = "GNU Library General Public License v2 only"; + }; + + lgpl2Plus = { + spdxId = "LGPL-2.0-or-later"; + fullName = "GNU Library General Public License v2 or later"; + }; + + lgpl21Only = { + spdxId = "LGPL-2.1-only"; + fullName = "GNU Lesser General Public License v2.1 only"; + }; + + lgpl21Plus = { + spdxId = "LGPL-2.1-or-later"; + fullName = "GNU Lesser General Public License v2.1 or later"; + }; + + lgpl3Only = { + spdxId = "LGPL-3.0-only"; + fullName = "GNU Lesser General Public License v3.0 only"; + }; + + lgpl3Plus = { + spdxId = "LGPL-3.0-or-later"; + fullName = "GNU Lesser General Public License v3.0 or later"; + }; + + lgpllr = { + spdxId = "LGPLLR"; + fullName = "Lesser General Public License For Linguistic Resources"; + }; + + libpng = { + spdxId = "Libpng"; + fullName = "libpng License"; + }; + + libpng2 = { + spdxId = "libpng-2.0"; # Used since libpng 1.6.36. + fullName = "PNG Reference Library version 2"; + }; + + libssh2 = { + fullName = "libssh2 License"; + url = "https://www.libssh2.org/license.html"; + }; + + libtiff = { + spdxId = "libtiff"; + fullName = "libtiff License"; + }; + + llgpl21 = { + fullName = "Lisp LGPL; GNU Lesser General Public License version 2.1 with Franz Inc. preamble for clarification of LGPL terms in context of Lisp"; + url = "https://opensource.franz.com/preamble.html"; + }; + + lppl12 = { + spdxId = "LPPL-1.2"; + fullName = "LaTeX Project Public License v1.2"; + }; + + lppl13c = { + spdxId = "LPPL-1.3c"; + fullName = "LaTeX Project Public License v1.3c"; + }; + + lpl-102 = { + spdxId = "LPL-1.02"; + fullName = "Lucent Public License v1.02"; + }; + + miros = { + fullName = "MirOS License"; + url = "https://opensource.org/licenses/MirOS"; + }; + + # spdx.org does not (yet) differentiate between the X11 and Expat versions + # for details see https://en.wikipedia.org/wiki/MIT_License#Various_versions + mit = { + spdxId = "MIT"; + fullName = "MIT License"; + }; + # https://spdx.org/licenses/MIT-feh.html + mit-feh = { + spdxId = "MIT-feh"; + fullName = "feh License"; + }; + + mitAdvertising = { + spdxId = "MIT-advertising"; + fullName = "Enlightenment License (e16)"; + }; + + mit0 = { + spdxId = "MIT-0"; + fullName = "MIT No Attribution"; + }; + + mpl10 = { + spdxId = "MPL-1.0"; + fullName = "Mozilla Public License 1.0"; + }; + + mpl11 = { + spdxId = "MPL-1.1"; + fullName = "Mozilla Public License 1.1"; + }; + + mpl20 = { + spdxId = "MPL-2.0"; + fullName = "Mozilla Public License 2.0"; + }; + + mspl = { + spdxId = "MS-PL"; + fullName = "Microsoft Public License"; + }; + + nasa13 = { + spdxId = "NASA-1.3"; + fullName = "NASA Open Source Agreement 1.3"; + free = false; + }; + + ncsa = { + spdxId = "NCSA"; + fullName = "University of Illinois/NCSA Open Source License"; + }; + + nlpl = { + spdxId = "NLPL"; + fullName = "No Limit Public License"; + }; + + nposl3 = { + spdxId = "NPOSL-3.0"; + fullName = "Non-Profit Open Software License 3.0"; + }; + + obsidian = { + fullName = "Obsidian End User Agreement"; + url = "https://obsidian.md/eula"; + free = false; + }; + + ocamlpro_nc = { + fullName = "OCamlPro Non Commercial license version 1"; + url = "https://alt-ergo.ocamlpro.com/http/alt-ergo-2.2.0/OCamlPro-Non-Commercial-License.pdf"; + free = false; + }; + + odbl = { + spdxId = "ODbL-1.0"; + fullName = "Open Data Commons Open Database License v1.0"; + }; + + ofl = { + spdxId = "OFL-1.1"; + fullName = "SIL Open Font License 1.1"; + }; + + oml = { + spdxId = "OML"; + fullName = "Open Market License"; + }; + + openldap = { + spdxId = "OLDAP-2.8"; + fullName = "Open LDAP Public License v2.8"; + }; + + openssl = { + spdxId = "OpenSSL"; + fullName = "OpenSSL License"; + }; + + osl2 = { + spdxId = "OSL-2.0"; + fullName = "Open Software License 2.0"; + }; + + osl21 = { + spdxId = "OSL-2.1"; + fullName = "Open Software License 2.1"; + }; + + osl3 = { + spdxId = "OSL-3.0"; + fullName = "Open Software License 3.0"; + }; + + parity70 = { + spdxId = "Parity-7.0.0"; + fullName = "Parity Public License 7.0.0"; + url = "https://paritylicense.com/versions/7.0.0.html"; + }; + + php301 = { + spdxId = "PHP-3.01"; + fullName = "PHP License v3.01"; + }; + + postgresql = { + spdxId = "PostgreSQL"; + fullName = "PostgreSQL License"; + }; + + postman = { + fullName = "Postman EULA"; + url = "https://www.getpostman.com/licenses/postman_base_app"; + free = false; + }; + + psfl = { + spdxId = "Python-2.0"; + fullName = "Python Software Foundation License version 2"; + url = "https://docs.python.org/license.html"; + }; + + publicDomain = { + fullName = "Public Domain"; + }; + + purdueBsd = { + fullName = " Purdue BSD-Style License"; # also know as lsof license + url = "https://enterprise.dejacode.com/licenses/public/purdue-bsd"; + }; + + prosperity30 = { + fullName = "Prosperity-3.0.0"; + free = false; + url = "https://prosperitylicense.com/versions/3.0.0.html"; + }; + + qhull = { + spdxId = "Qhull"; + fullName = "Qhull License"; + }; + + qpl = { + spdxId = "QPL-1.0"; + fullName = "Q Public License 1.0"; + }; + + qwt = { + fullName = "Qwt License, Version 1.0"; + url = "https://qwt.sourceforge.io/qwtlicense.html"; + }; + + ruby = { + spdxId = "Ruby"; + fullName = "Ruby License"; + }; + + sendmail = { + spdxId = "Sendmail"; + fullName = "Sendmail License"; + }; + + sgi-b-20 = { + spdxId = "SGI-B-2.0"; + fullName = "SGI Free Software License B v2.0"; + }; + + # Gentoo seems to treat it as a license: + # https://gitweb.gentoo.org/repo/gentoo.git/tree/licenses/SGMLUG?id=7d999af4a47bf55e53e54713d98d145f935935c1 + sgmlug = { + fullName = "SGML UG SGML Parser Materials license"; + }; + + sleepycat = { + spdxId = "Sleepycat"; + fullName = "Sleepycat License"; + }; + + smail = { + shortName = "smail"; + fullName = "SMAIL General Public License"; + url = "https://sources.debian.org/copyright/license/debianutils/4.9.1/"; + }; + + sspl = { + shortName = "SSPL"; + fullName = "Server Side Public License"; + url = "https://www.mongodb.com/licensing/server-side-public-license"; + free = false; + # NOTE Debatable. + # The license a slightly modified AGPL but still considered unfree by the + # OSI for what seem like political reasons + redistributable = true; # Definitely redistributable though, it's an AGPL derivative + }; + + stk = { + shortName = "stk"; + fullName = "Synthesis Tool Kit 4.3"; + url = "https://github.com/thestk/stk/blob/master/LICENSE"; + }; + + tsl = { + shortName = "TSL"; + fullName = "Timescale License Agreegment"; + url = "https://github.com/timescale/timescaledb/blob/main/tsl/LICENSE-TIMESCALE"; + unfree = true; + }; + + tcltk = { + spdxId = "TCL"; + fullName = "TCL/TK License"; + }; + + ucd = { + fullName = "Unicode Character Database License"; + url = "https://fedoraproject.org/wiki/Licensing:UCD"; + }; + + ufl = { + fullName = "Ubuntu Font License 1.0"; + url = "https://ubuntu.com/legal/font-licence"; + }; + + unfree = { + fullName = "Unfree"; + free = false; + }; + + unfreeRedistributable = { + fullName = "Unfree redistributable"; + free = false; + redistributable = true; + }; + + unfreeRedistributableFirmware = { + fullName = "Unfree redistributable firmware"; + redistributable = true; + # Note: we currently consider these "free" for inclusion in the + # channel and NixOS images. + }; + + unicode-dfs-2015 = { + spdxId = "Unicode-DFS-2015"; + fullName = "Unicode License Agreement - Data Files and Software (2015)"; + }; + + unicode-dfs-2016 = { + spdxId = "Unicode-DFS-2016"; + fullName = "Unicode License Agreement - Data Files and Software (2016)"; + }; + + unlicense = { + spdxId = "Unlicense"; + fullName = "The Unlicense"; + }; + + upl = { + fullName = "Universal Permissive License"; + url = "https://oss.oracle.com/licenses/upl/"; + }; + + vim = { + spdxId = "Vim"; + fullName = "Vim License"; + }; + + virtualbox-puel = { + fullName = "Oracle VM VirtualBox Extension Pack Personal Use and Evaluation License (PUEL)"; + url = "https://www.virtualbox.org/wiki/VirtualBox_PUEL"; + free = false; + }; + + vol-sl = { + fullName = "Volatility Software License, Version 1.0"; + url = "https://www.volatilityfoundation.org/license/vsl-v1.0"; + }; + + vsl10 = { + spdxId = "VSL-1.0"; + fullName = "Vovida Software License v1.0"; + }; + + watcom = { + spdxId = "Watcom-1.0"; + fullName = "Sybase Open Watcom Public License 1.0"; + }; + + w3c = { + spdxId = "W3C"; + fullName = "W3C Software Notice and License"; + }; + + wadalab = { + fullName = "Wadalab Font License"; + url = "https://fedoraproject.org/wiki/Licensing:Wadalab?rd=Licensing/Wadalab"; + }; + + wtfpl = { + spdxId = "WTFPL"; + fullName = "Do What The F*ck You Want To Public License"; + }; + + wxWindows = { + spdxId = "wxWindows"; + fullName = "wxWindows Library Licence, Version 3.1"; + }; + + x11 = { + spdxId = "X11"; + fullName = "X11 License"; + }; + + xfig = { + fullName = "xfig"; + url = "http://mcj.sourceforge.net/authors.html#xfig"; # https is broken + }; + + zlib = { + spdxId = "Zlib"; + fullName = "zlib License"; + }; + + zpl20 = { + spdxId = "ZPL-2.0"; + fullName = "Zope Public License 2.0"; + }; + + zpl21 = { + spdxId = "ZPL-2.1"; + fullName = "Zope Public License 2.1"; + }; +} // { + # TODO: remove legacy aliases + agpl3 = { + spdxId = "AGPL-3.0"; + fullName = "GNU Affero General Public License v3.0"; + deprecated = true; + }; + gpl2 = { + spdxId = "GPL-2.0"; + fullName = "GNU General Public License v2.0"; + deprecated = true; + }; + gpl3 = { + spdxId = "GPL-3.0"; + fullName = "GNU General Public License v3.0"; + deprecated = true; + }; + lgpl2 = { + spdxId = "LGPL-2.0"; + fullName = "GNU Library General Public License v2"; + deprecated = true; + }; + lgpl21 = { + spdxId = "LGPL-2.1"; + fullName = "GNU Lesser General Public License v2.1"; + deprecated = true; + }; + lgpl3 = { + spdxId = "LGPL-3.0"; + fullName = "GNU Lesser General Public License v3.0"; + deprecated = true; + }; +}) diff --git a/lists.nix b/lists.nix new file mode 100644 index 000000000..bea9bf43c --- /dev/null +++ b/lists.nix @@ -0,0 +1,724 @@ +# General list operations. + +{ lib }: +let + inherit (lib.strings) toInt; + inherit (lib.trivial) compare min; + inherit (lib.attrsets) mapAttrs; +in +rec { + + inherit (builtins) head tail length isList elemAt concatLists filter elem genList map; + + /* Create a list consisting of a single element. `singleton x` is + sometimes more convenient with respect to indentation than `[x]` + when x spans multiple lines. + + Type: singleton :: a -> [a] + + Example: + singleton "foo" + => [ "foo" ] + */ + singleton = x: [x]; + + /* Apply the function to each element in the list. Same as `map`, but arguments + flipped. + + Type: forEach :: [a] -> (a -> b) -> [b] + + Example: + forEach [ 1 2 ] (x: + toString x + ) + => [ "1" "2" ] + */ + forEach = xs: f: map f xs; + + /* “right fold” a binary function `op` between successive elements of + `list` with `nul` as the starting value, i.e., + `foldr op nul [x_1 x_2 ... x_n] == op x_1 (op x_2 ... (op x_n nul))`. + + Type: foldr :: (a -> b -> b) -> b -> [a] -> b + + Example: + concat = foldr (a: b: a + b) "z" + concat [ "a" "b" "c" ] + => "abcz" + # different types + strange = foldr (int: str: toString (int + 1) + str) "a" + strange [ 1 2 3 4 ] + => "2345a" + */ + foldr = op: nul: list: + let + len = length list; + fold' = n: + if n == len + then nul + else op (elemAt list n) (fold' (n + 1)); + in fold' 0; + + /* `fold` is an alias of `foldr` for historic reasons */ + # FIXME(Profpatsch): deprecate? + fold = foldr; + + + /* “left fold”, like `foldr`, but from the left: + `foldl op nul [x_1 x_2 ... x_n] == op (... (op (op nul x_1) x_2) ... x_n)`. + + Type: foldl :: (b -> a -> b) -> b -> [a] -> b + + Example: + lconcat = foldl (a: b: a + b) "z" + lconcat [ "a" "b" "c" ] + => "zabc" + # different types + lstrange = foldl (str: int: str + toString (int + 1)) "a" + lstrange [ 1 2 3 4 ] + => "a2345" + */ + foldl = op: nul: list: + let + foldl' = n: + if n == -1 + then nul + else op (foldl' (n - 1)) (elemAt list n); + in foldl' (length list - 1); + + /* Strict version of `foldl`. + + The difference is that evaluation is forced upon access. Usually used + with small whole results (in contrast with lazily-generated list or large + lists where only a part is consumed.) + + Type: foldl' :: (b -> a -> b) -> b -> [a] -> b + */ + foldl' = builtins.foldl' or foldl; + + /* Map with index starting from 0 + + Type: imap0 :: (int -> a -> b) -> [a] -> [b] + + Example: + imap0 (i: v: "${v}-${toString i}") ["a" "b"] + => [ "a-0" "b-1" ] + */ + imap0 = f: list: genList (n: f n (elemAt list n)) (length list); + + /* Map with index starting from 1 + + Type: imap1 :: (int -> a -> b) -> [a] -> [b] + + Example: + imap1 (i: v: "${v}-${toString i}") ["a" "b"] + => [ "a-1" "b-2" ] + */ + imap1 = f: list: genList (n: f (n + 1) (elemAt list n)) (length list); + + /* Map and concatenate the result. + + Type: concatMap :: (a -> [b]) -> [a] -> [b] + + Example: + concatMap (x: [x] ++ ["z"]) ["a" "b"] + => [ "a" "z" "b" "z" ] + */ + concatMap = builtins.concatMap or (f: list: concatLists (map f list)); + + /* Flatten the argument into a single list; that is, nested lists are + spliced into the top-level lists. + + Example: + flatten [1 [2 [3] 4] 5] + => [1 2 3 4 5] + flatten 1 + => [1] + */ + flatten = x: + if isList x + then concatMap (y: flatten y) x + else [x]; + + /* Remove elements equal to 'e' from a list. Useful for buildInputs. + + Type: remove :: a -> [a] -> [a] + + Example: + remove 3 [ 1 3 4 3 ] + => [ 1 4 ] + */ + remove = + # Element to remove from the list + e: filter (x: x != e); + + /* Find the sole element in the list matching the specified + predicate, returns `default` if no such element exists, or + `multiple` if there are multiple matching elements. + + Type: findSingle :: (a -> bool) -> a -> a -> [a] -> a + + Example: + findSingle (x: x == 3) "none" "multiple" [ 1 3 3 ] + => "multiple" + findSingle (x: x == 3) "none" "multiple" [ 1 3 ] + => 3 + findSingle (x: x == 3) "none" "multiple" [ 1 9 ] + => "none" + */ + findSingle = + # Predicate + pred: + # Default value to return if element was not found. + default: + # Default value to return if more than one element was found + multiple: + # Input list + list: + let found = filter pred list; len = length found; + in if len == 0 then default + else if len != 1 then multiple + else head found; + + /* Find the first element in the list matching the specified + predicate or return `default` if no such element exists. + + Type: findFirst :: (a -> bool) -> a -> [a] -> a + + Example: + findFirst (x: x > 3) 7 [ 1 6 4 ] + => 6 + findFirst (x: x > 9) 7 [ 1 6 4 ] + => 7 + */ + findFirst = + # Predicate + pred: + # Default value to return + default: + # Input list + list: + let + # A naive recursive implementation would be much simpler, but + # would also overflow the evaluator stack. We use `foldl'` as a workaround + # because it reuses the same stack space, evaluating the function for one + # element after another. We can't return early, so this means that we + # sacrifice early cutoff, but that appears to be an acceptable cost. A + # clever scheme with "exponential search" is possible, but appears over- + # engineered for now. See https://github.com/NixOS/nixpkgs/pull/235267 + + # Invariant: + # - if index < 0 then el == elemAt list (- index - 1) and all elements before el didn't satisfy pred + # - if index >= 0 then pred (elemAt list index) and all elements before (elemAt list index) didn't satisfy pred + # + # We start with index -1 and the 0'th element of the list, which satisfies the invariant + resultIndex = foldl' (index: el: + if index < 0 then + # No match yet before the current index, we need to check the element + if pred el then + # We have a match! Turn it into the actual index to prevent future iterations from modifying it + - index - 1 + else + # Still no match, update the index to the next element (we're counting down, so minus one) + index - 1 + else + # There's already a match, propagate the index without evaluating anything + index + ) (-1) list; + in + if resultIndex < 0 then + default + else + elemAt list resultIndex; + + /* Return true if function `pred` returns true for at least one + element of `list`. + + Type: any :: (a -> bool) -> [a] -> bool + + Example: + any isString [ 1 "a" { } ] + => true + any isString [ 1 { } ] + => false + */ + any = builtins.any or (pred: foldr (x: y: if pred x then true else y) false); + + /* Return true if function `pred` returns true for all elements of + `list`. + + Type: all :: (a -> bool) -> [a] -> bool + + Example: + all (x: x < 3) [ 1 2 ] + => true + all (x: x < 3) [ 1 2 3 ] + => false + */ + all = builtins.all or (pred: foldr (x: y: if pred x then y else false) true); + + /* Count how many elements of `list` match the supplied predicate + function. + + Type: count :: (a -> bool) -> [a] -> int + + Example: + count (x: x == 3) [ 3 2 3 4 6 ] + => 2 + */ + count = + # Predicate + pred: foldl' (c: x: if pred x then c + 1 else c) 0; + + /* Return a singleton list or an empty list, depending on a boolean + value. Useful when building lists with optional elements + (e.g. `++ optional (system == "i686-linux") firefox`). + + Type: optional :: bool -> a -> [a] + + Example: + optional true "foo" + => [ "foo" ] + optional false "foo" + => [ ] + */ + optional = cond: elem: if cond then [elem] else []; + + /* Return a list or an empty list, depending on a boolean value. + + Type: optionals :: bool -> [a] -> [a] + + Example: + optionals true [ 2 3 ] + => [ 2 3 ] + optionals false [ 2 3 ] + => [ ] + */ + optionals = + # Condition + cond: + # List to return if condition is true + elems: if cond then elems else []; + + + /* If argument is a list, return it; else, wrap it in a singleton + list. If you're using this, you should almost certainly + reconsider if there isn't a more "well-typed" approach. + + Example: + toList [ 1 2 ] + => [ 1 2 ] + toList "hi" + => [ "hi "] + */ + toList = x: if isList x then x else [x]; + + /* Return a list of integers from `first` up to and including `last`. + + Type: range :: int -> int -> [int] + + Example: + range 2 4 + => [ 2 3 4 ] + range 3 2 + => [ ] + */ + range = + # First integer in the range + first: + # Last integer in the range + last: + if first > last then + [] + else + genList (n: first + n) (last - first + 1); + + /* Return a list with `n` copies of an element. + + Type: replicate :: int -> a -> [a] + + Example: + replicate 3 "a" + => [ "a" "a" "a" ] + replicate 2 true + => [ true true ] + */ + replicate = n: elem: genList (_: elem) n; + + /* Splits the elements of a list in two lists, `right` and + `wrong`, depending on the evaluation of a predicate. + + Type: (a -> bool) -> [a] -> { right :: [a]; wrong :: [a]; } + + Example: + partition (x: x > 2) [ 5 1 2 3 4 ] + => { right = [ 5 3 4 ]; wrong = [ 1 2 ]; } + */ + partition = builtins.partition or (pred: + foldr (h: t: + if pred h + then { right = [h] ++ t.right; wrong = t.wrong; } + else { right = t.right; wrong = [h] ++ t.wrong; } + ) { right = []; wrong = []; }); + + /* Splits the elements of a list into many lists, using the return value of a predicate. + Predicate should return a string which becomes keys of attrset `groupBy` returns. + + `groupBy'` allows to customise the combining function and initial value + + Example: + groupBy (x: boolToString (x > 2)) [ 5 1 2 3 4 ] + => { true = [ 5 3 4 ]; false = [ 1 2 ]; } + groupBy (x: x.name) [ {name = "icewm"; script = "icewm &";} + {name = "xfce"; script = "xfce4-session &";} + {name = "icewm"; script = "icewmbg &";} + {name = "mate"; script = "gnome-session &";} + ] + => { icewm = [ { name = "icewm"; script = "icewm &"; } + { name = "icewm"; script = "icewmbg &"; } ]; + mate = [ { name = "mate"; script = "gnome-session &"; } ]; + xfce = [ { name = "xfce"; script = "xfce4-session &"; } ]; + } + + groupBy' builtins.add 0 (x: boolToString (x > 2)) [ 5 1 2 3 4 ] + => { true = 12; false = 3; } + */ + groupBy' = op: nul: pred: lst: mapAttrs (name: foldl op nul) (groupBy pred lst); + + groupBy = builtins.groupBy or ( + pred: foldl' (r: e: + let + key = pred e; + in + r // { ${key} = (r.${key} or []) ++ [e]; } + ) {}); + + /* Merges two lists of the same size together. If the sizes aren't the same + the merging stops at the shortest. How both lists are merged is defined + by the first argument. + + Type: zipListsWith :: (a -> b -> c) -> [a] -> [b] -> [c] + + Example: + zipListsWith (a: b: a + b) ["h" "l"] ["e" "o"] + => ["he" "lo"] + */ + zipListsWith = + # Function to zip elements of both lists + f: + # First list + fst: + # Second list + snd: + genList + (n: f (elemAt fst n) (elemAt snd n)) (min (length fst) (length snd)); + + /* Merges two lists of the same size together. If the sizes aren't the same + the merging stops at the shortest. + + Type: zipLists :: [a] -> [b] -> [{ fst :: a; snd :: b; }] + + Example: + zipLists [ 1 2 ] [ "a" "b" ] + => [ { fst = 1; snd = "a"; } { fst = 2; snd = "b"; } ] + */ + zipLists = zipListsWith (fst: snd: { inherit fst snd; }); + + /* Reverse the order of the elements of a list. + + Type: reverseList :: [a] -> [a] + + Example: + + reverseList [ "b" "o" "j" ] + => [ "j" "o" "b" ] + */ + reverseList = xs: + let l = length xs; in genList (n: elemAt xs (l - n - 1)) l; + + /* Depth-First Search (DFS) for lists `list != []`. + + `before a b == true` means that `b` depends on `a` (there's an + edge from `b` to `a`). + + Example: + listDfs true hasPrefix [ "/home/user" "other" "/" "/home" ] + == { minimal = "/"; # minimal element + visited = [ "/home/user" ]; # seen elements (in reverse order) + rest = [ "/home" "other" ]; # everything else + } + + listDfs true hasPrefix [ "/home/user" "other" "/" "/home" "/" ] + == { cycle = "/"; # cycle encountered at this element + loops = [ "/" ]; # and continues to these elements + visited = [ "/" "/home/user" ]; # elements leading to the cycle (in reverse order) + rest = [ "/home" "other" ]; # everything else + + */ + listDfs = stopOnCycles: before: list: + let + dfs' = us: visited: rest: + let + c = filter (x: before x us) visited; + b = partition (x: before x us) rest; + in if stopOnCycles && (length c > 0) + then { cycle = us; loops = c; inherit visited rest; } + else if length b.right == 0 + then # nothing is before us + { minimal = us; inherit visited rest; } + else # grab the first one before us and continue + dfs' (head b.right) + ([ us ] ++ visited) + (tail b.right ++ b.wrong); + in dfs' (head list) [] (tail list); + + /* Sort a list based on a partial ordering using DFS. This + implementation is O(N^2), if your ordering is linear, use `sort` + instead. + + `before a b == true` means that `b` should be after `a` + in the result. + + Example: + + toposort hasPrefix [ "/home/user" "other" "/" "/home" ] + == { result = [ "/" "/home" "/home/user" "other" ]; } + + toposort hasPrefix [ "/home/user" "other" "/" "/home" "/" ] + == { cycle = [ "/home/user" "/" "/" ]; # path leading to a cycle + loops = [ "/" ]; } # loops back to these elements + + toposort hasPrefix [ "other" "/home/user" "/home" "/" ] + == { result = [ "other" "/" "/home" "/home/user" ]; } + + toposort (a: b: a < b) [ 3 2 1 ] == { result = [ 1 2 3 ]; } + + */ + toposort = before: list: + let + dfsthis = listDfs true before list; + toporest = toposort before (dfsthis.visited ++ dfsthis.rest); + in + if length list < 2 + then # finish + { result = list; } + else if dfsthis ? cycle + then # there's a cycle, starting from the current vertex, return it + { cycle = reverseList ([ dfsthis.cycle ] ++ dfsthis.visited); + inherit (dfsthis) loops; } + else if toporest ? cycle + then # there's a cycle somewhere else in the graph, return it + toporest + # Slow, but short. Can be made a bit faster with an explicit stack. + else # there are no cycles + { result = [ dfsthis.minimal ] ++ toporest.result; }; + + /* Sort a list based on a comparator function which compares two + elements and returns true if the first argument is strictly below + the second argument. The returned list is sorted in an increasing + order. The implementation does a quick-sort. + + Example: + sort (a: b: a < b) [ 5 3 7 ] + => [ 3 5 7 ] + */ + sort = builtins.sort or ( + strictLess: list: + let + len = length list; + first = head list; + pivot' = n: acc@{ left, right }: let el = elemAt list n; next = pivot' (n + 1); in + if n == len + then acc + else if strictLess first el + then next { inherit left; right = [ el ] ++ right; } + else + next { left = [ el ] ++ left; inherit right; }; + pivot = pivot' 1 { left = []; right = []; }; + in + if len < 2 then list + else (sort strictLess pivot.left) ++ [ first ] ++ (sort strictLess pivot.right)); + + /* Compare two lists element-by-element. + + Example: + compareLists compare [] [] + => 0 + compareLists compare [] [ "a" ] + => -1 + compareLists compare [ "a" ] [] + => 1 + compareLists compare [ "a" "b" ] [ "a" "c" ] + => -1 + */ + compareLists = cmp: a: b: + if a == [] + then if b == [] + then 0 + else -1 + else if b == [] + then 1 + else let rel = cmp (head a) (head b); in + if rel == 0 + then compareLists cmp (tail a) (tail b) + else rel; + + /* Sort list using "Natural sorting". + Numeric portions of strings are sorted in numeric order. + + Example: + naturalSort ["disk11" "disk8" "disk100" "disk9"] + => ["disk8" "disk9" "disk11" "disk100"] + naturalSort ["10.46.133.149" "10.5.16.62" "10.54.16.25"] + => ["10.5.16.62" "10.46.133.149" "10.54.16.25"] + naturalSort ["v0.2" "v0.15" "v0.0.9"] + => [ "v0.0.9" "v0.2" "v0.15" ] + */ + naturalSort = lst: + let + vectorise = s: map (x: if isList x then toInt (head x) else x) (builtins.split "(0|[1-9][0-9]*)" s); + prepared = map (x: [ (vectorise x) x ]) lst; # remember vectorised version for O(n) regex splits + less = a: b: (compareLists compare (head a) (head b)) < 0; + in + map (x: elemAt x 1) (sort less prepared); + + /* Return the first (at most) N elements of a list. + + Type: take :: int -> [a] -> [a] + + Example: + take 2 [ "a" "b" "c" "d" ] + => [ "a" "b" ] + take 2 [ ] + => [ ] + */ + take = + # Number of elements to take + count: sublist 0 count; + + /* Remove the first (at most) N elements of a list. + + Type: drop :: int -> [a] -> [a] + + Example: + drop 2 [ "a" "b" "c" "d" ] + => [ "c" "d" ] + drop 2 [ ] + => [ ] + */ + drop = + # Number of elements to drop + count: + # Input list + list: sublist count (length list) list; + + commonPrefixLength = lhs: rhs: + let + minLength = min (length lhs) (length rhs); + recurse = index: + if index >= minLength || elemAt lhs index != elemAt rhs index then + index + else + recurse (index + 1); + in recurse 0; + + commonPrefix = lhs: rhs: + take (commonPrefixLength lhs rhs) lhs; + + /* Return a list consisting of at most `count` elements of `list`, + starting at index `start`. + + Type: sublist :: int -> int -> [a] -> [a] + + Example: + sublist 1 3 [ "a" "b" "c" "d" "e" ] + => [ "b" "c" "d" ] + sublist 1 3 [ ] + => [ ] + */ + sublist = + # Index at which to start the sublist + start: + # Number of elements to take + count: + # Input list + list: + let len = length list; in + genList + (n: elemAt list (n + start)) + (if start >= len then 0 + else if start + count > len then len - start + else count); + + /* Return the last element of a list. + + This function throws an error if the list is empty. + + Type: last :: [a] -> a + + Example: + last [ 1 2 3 ] + => 3 + */ + last = list: + assert lib.assertMsg (list != []) "lists.last: list must not be empty!"; + elemAt list (length list - 1); + + /* Return all elements but the last. + + This function throws an error if the list is empty. + + Type: init :: [a] -> [a] + + Example: + init [ 1 2 3 ] + => [ 1 2 ] + */ + init = list: + assert lib.assertMsg (list != []) "lists.init: list must not be empty!"; + take (length list - 1) list; + + + /* Return the image of the cross product of some lists by a function. + + Example: + crossLists (x:y: "${toString x}${toString y}") [[1 2] [3 4]] + => [ "13" "14" "23" "24" ] + */ + crossLists = builtins.trace + "lib.crossLists is deprecated, use lib.cartesianProductOfSets instead" + (f: foldl (fs: args: concatMap (f: map f args) fs) [f]); + + + /* Remove duplicate elements from the list. O(n^2) complexity. + + Type: unique :: [a] -> [a] + + Example: + unique [ 3 2 3 4 ] + => [ 3 2 4 ] + */ + unique = foldl' (acc: e: if elem e acc then acc else acc ++ [ e ]) []; + + /* Intersects list 'e' and another list. O(nm) complexity. + + Example: + intersectLists [ 1 2 3 ] [ 6 3 2 ] + => [ 3 2 ] + */ + intersectLists = e: filter (x: elem x e); + + /* Subtracts list 'e' from another list. O(nm) complexity. + + Example: + subtractLists [ 3 2 ] [ 1 2 3 4 5 3 ] + => [ 1 4 5 ] + */ + subtractLists = e: filter (x: !(elem x e)); + + /* Test if two lists have no common element. + It should be slightly more efficient than (intersectLists a b == []) + */ + mutuallyExclusive = a: b: length a == 0 || !(any (x: elem x a) b); + +} diff --git a/meta.nix b/meta.nix new file mode 100644 index 000000000..5fd55c4e9 --- /dev/null +++ b/meta.nix @@ -0,0 +1,148 @@ +/* Some functions for manipulating meta attributes, as well as the + name attribute. */ + +{ lib }: + +rec { + + + /* Add to or override the meta attributes of the given + derivation. + + Example: + addMetaAttrs {description = "Bla blah";} somePkg + */ + addMetaAttrs = newAttrs: drv: + drv // { meta = (drv.meta or {}) // newAttrs; }; + + + /* Disable Hydra builds of given derivation. + */ + dontDistribute = drv: addMetaAttrs { hydraPlatforms = []; } drv; + + + /* Change the symbolic name of a package for presentation purposes + (i.e., so that nix-env users can tell them apart). + */ + setName = name: drv: drv // {inherit name;}; + + + /* Like `setName`, but takes the previous name as an argument. + + Example: + updateName (oldName: oldName + "-experimental") somePkg + */ + updateName = updater: drv: drv // {name = updater (drv.name);}; + + + /* Append a suffix to the name of a package (before the version + part). */ + appendToName = suffix: updateName (name: + let x = builtins.parseDrvName name; in "${x.name}-${suffix}-${x.version}"); + + + /* Apply a function to each derivation and only to derivations in an attrset. + */ + mapDerivationAttrset = f: set: lib.mapAttrs (name: pkg: if lib.isDerivation pkg then (f pkg) else pkg) set; + + /* Set the nix-env priority of the package. + */ + setPrio = priority: addMetaAttrs { inherit priority; }; + + /* Decrease the nix-env priority of the package, i.e., other + versions/variants of the package will be preferred. + */ + lowPrio = setPrio 10; + + /* Apply lowPrio to an attrset with derivations + */ + lowPrioSet = set: mapDerivationAttrset lowPrio set; + + + /* Increase the nix-env priority of the package, i.e., this + version/variant of the package will be preferred. + */ + hiPrio = setPrio (-10); + + /* Apply hiPrio to an attrset with derivations + */ + hiPrioSet = set: mapDerivationAttrset hiPrio set; + + + /* Check to see if a platform is matched by the given `meta.platforms` + element. + + A `meta.platform` pattern is either + + 1. (legacy) a system string. + + 2. (modern) a pattern for the entire platform structure (see `lib.systems.inspect.platformPatterns`). + + 3. (modern) a pattern for the platform `parsed` field (see `lib.systems.inspect.patterns`). + + We can inject these into a pattern for the whole of a structured platform, + and then match that. + */ + platformMatch = platform: elem: let + pattern = + if builtins.isString elem + then { system = elem; } + else if elem?parsed + then elem + else { parsed = elem; }; + in lib.matchAttrs pattern platform; + + /* Check if a package is available on a given platform. + + A package is available on a platform if both + + 1. One of `meta.platforms` pattern matches the given + platform, or `meta.platforms` is not present. + + 2. None of `meta.badPlatforms` pattern matches the given platform. + */ + availableOn = platform: pkg: + ((!pkg?meta.platforms) || lib.any (platformMatch platform) pkg.meta.platforms) && + lib.all (elem: !platformMatch platform elem) (pkg.meta.badPlatforms or []); + + /* Get the corresponding attribute in lib.licenses + from the SPDX ID. + For SPDX IDs, see + https://spdx.org/licenses + + Type: + getLicenseFromSpdxId :: str -> AttrSet + + Example: + lib.getLicenseFromSpdxId "MIT" == lib.licenses.mit + => true + lib.getLicenseFromSpdxId "mIt" == lib.licenses.mit + => true + lib.getLicenseFromSpdxId "MY LICENSE" + => trace: warning: getLicenseFromSpdxId: No license matches the given SPDX ID: MY LICENSE + => { shortName = "MY LICENSE"; } + */ + getLicenseFromSpdxId = + let + spdxLicenses = lib.mapAttrs (id: ls: assert lib.length ls == 1; builtins.head ls) + (lib.groupBy (l: lib.toLower l.spdxId) (lib.filter (l: l ? spdxId) (lib.attrValues lib.licenses))); + in licstr: + spdxLicenses.${ lib.toLower licstr } or ( + lib.warn "getLicenseFromSpdxId: No license matches the given SPDX ID: ${licstr}" + { shortName = licstr; } + ); + + /* Get the path to the main program of a derivation with either + meta.mainProgram or pname or name + + Type: getExe :: derivation -> string + + Example: + getExe pkgs.hello + => "/nix/store/g124820p9hlv4lj8qplzxw1c44dxaw1k-hello-2.12/bin/hello" + getExe pkgs.mustache-go + => "/nix/store/am9ml4f4ywvivxnkiaqwr0hyxka1xjsf-mustache-go-1.3.0/bin/mustache" + */ + getExe = x: + "${lib.getBin x}/bin/${x.meta.mainProgram or (lib.getName x)}"; +} diff --git a/minver.nix b/minver.nix new file mode 100644 index 000000000..507d45bba --- /dev/null +++ b/minver.nix @@ -0,0 +1,2 @@ +# Expose the minimum required version for evaluating Nixpkgs +"2.3" diff --git a/modules.nix b/modules.nix new file mode 100644 index 000000000..4dc8c663b --- /dev/null +++ b/modules.nix @@ -0,0 +1,1287 @@ +{ lib }: + +let + inherit (lib) + all + any + attrByPath + attrNames + catAttrs + concatLists + concatMap + concatStringsSep + elem + filter + foldl' + getAttrFromPath + head + id + imap1 + isAttrs + isBool + isFunction + isList + isPath + isString + length + mapAttrs + mapAttrsToList + mapAttrsRecursiveCond + min + optional + optionalAttrs + optionalString + recursiveUpdate + reverseList sort + setAttrByPath + types + warnIf + zipAttrsWith + ; + inherit (lib.options) + isOption + mkOption + showDefs + showFiles + showOption + unknownModule + ; + inherit (lib.strings) + isConvertibleWithToString + ; + + showDeclPrefix = loc: decl: prefix: + " - option(s) with prefix `${showOption (loc ++ [prefix])}' in module `${decl._file}'"; + showRawDecls = loc: decls: + concatStringsSep "\n" + (sort (a: b: a < b) + (concatMap + (decl: map + (showDeclPrefix loc decl) + (attrNames decl.options) + ) + decls + )); + + /* See https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules + or file://./../doc/module-system/module-system.chapter.md + + !!! Please think twice before adding to this argument list! The more + that is specified here instead of in the modules themselves the harder + it is to transparently move a set of modules to be a submodule of another + config (as the proper arguments need to be replicated at each call to + evalModules) and the less declarative the module set is. */ + evalModules = evalModulesArgs@ + { modules + , prefix ? [] + , # This should only be used for special arguments that need to be evaluated + # when resolving module structure (like in imports). For everything else, + # there's _module.args. If specialArgs.modulesPath is defined it will be + # used as the base path for disabledModules. + specialArgs ? {} + , # `class`: + # A nominal type for modules. When set and non-null, this adds a check to + # make sure that only compatible modules are imported. + # This would be remove in the future, Prefer _module.args option instead. + class ? null + , args ? {} + , # This would be remove in the future, Prefer _module.check option instead. + check ? true + }: + let + withWarnings = x: + lib.warnIf (evalModulesArgs?args) "The args argument to evalModules is deprecated. Please set config._module.args instead." + lib.warnIf (evalModulesArgs?check) "The check argument to evalModules is deprecated. Please set config._module.check instead." + x; + + legacyModules = + optional (evalModulesArgs?args) { + config = { + _module.args = args; + }; + } + ++ optional (evalModulesArgs?check) { + config = { + _module.check = mkDefault check; + }; + }; + regularModules = modules ++ legacyModules; + + # This internal module declare internal options under the `_module' + # attribute. These options are fragile, as they are used by the + # module system to change the interpretation of modules. + # + # When extended with extendModules or moduleType, a fresh instance of + # this module is used, to avoid conflicts and allow chaining of + # extendModules. + internalModule = rec { + _file = "lib/modules.nix"; + + key = _file; + + options = { + _module.args = mkOption { + # Because things like `mkIf` are entirely useless for + # `_module.args` (because there's no way modules can check which + # arguments were passed), we'll use `lazyAttrsOf` which drops + # support for that, in turn it's lazy in its values. This means e.g. + # a `_module.args.pkgs = import (fetchTarball { ... }) {}` won't + # start a download when `pkgs` wasn't evaluated. + type = types.lazyAttrsOf types.raw; + # Only render documentation once at the root of the option tree, + # not for all individual submodules. + # Allow merging option decls to make this internal regardless. + ${if prefix == [] + then null # unset => visible + else "internal"} = true; + # TODO: hidden during the markdown transition to not expose downstream + # users of the docs infra to markdown if they're not ready for it. + # we don't make this visible conditionally because it can impact + # performance (https://github.com/NixOS/nixpkgs/pull/208407#issuecomment-1368246192) + visible = false; + # TODO: Change the type of this option to a submodule with a + # freeformType, so that individual arguments can be documented + # separately + description = lib.mdDoc '' + Additional arguments passed to each module in addition to ones + like `lib`, `config`, + and `pkgs`, `modulesPath`. + + This option is also available to all submodules. Submodules do not + inherit args from their parent module, nor do they provide args to + their parent module or sibling submodules. The sole exception to + this is the argument `name` which is provided by + parent modules to a submodule and contains the attribute name + the submodule is bound to, or a unique generated name if it is + not bound to an attribute. + + Some arguments are already passed by default, of which the + following *cannot* be changed with this option: + - {var}`lib`: The nixpkgs library. + - {var}`config`: The results of all options after merging the values from all modules together. + - {var}`options`: The options declared in all modules. + - {var}`specialArgs`: The `specialArgs` argument passed to `evalModules`. + - All attributes of {var}`specialArgs` + + Whereas option values can generally depend on other option values + thanks to laziness, this does not apply to `imports`, which + must be computed statically before anything else. + + For this reason, callers of the module system can provide `specialArgs` + which are available during import resolution. + + For NixOS, `specialArgs` includes + {var}`modulesPath`, which allows you to import + extra modules from the nixpkgs package tree without having to + somehow make the module aware of the location of the + `nixpkgs` or NixOS directories. + ``` + { modulesPath, ... }: { + imports = [ + (modulesPath + "/profiles/minimal.nix") + ]; + } + ``` + + For NixOS, the default value for this option includes at least this argument: + - {var}`pkgs`: The nixpkgs package set according to + the {option}`nixpkgs.pkgs` option. + ''; + }; + + _module.check = mkOption { + type = types.bool; + internal = true; + default = true; + description = lib.mdDoc "Whether to check whether all option definitions have matching declarations."; + }; + + _module.freeformType = mkOption { + type = types.nullOr types.optionType; + internal = true; + default = null; + description = lib.mdDoc '' + If set, merge all definitions that don't have an associated option + together using this type. The result then gets combined with the + values of all declared options to produce the final ` + config` value. + + If this is `null`, definitions without an option + will throw an error unless {option}`_module.check` is + turned off. + ''; + }; + + _module.specialArgs = mkOption { + readOnly = true; + internal = true; + description = lib.mdDoc '' + Externally provided module arguments that can't be modified from + within a configuration, but can be used in module imports. + ''; + }; + }; + + config = { + _module.args = { + inherit extendModules; + moduleType = type; + }; + _module.specialArgs = specialArgs; + }; + }; + + merged = + let collected = collectModules + class + (specialArgs.modulesPath or "") + (regularModules ++ [ internalModule ]) + ({ inherit lib options config specialArgs; } // specialArgs); + in mergeModules prefix (reverseList collected); + + options = merged.matchedOptions; + + config = + let + + # For definitions that have an associated option + declaredConfig = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options; + + # If freeformType is set, this is for definitions that don't have an associated option + freeformConfig = + let + defs = map (def: { + file = def.file; + value = setAttrByPath def.prefix def.value; + }) merged.unmatchedDefns; + in if defs == [] then {} + else declaredConfig._module.freeformType.merge prefix defs; + + in if declaredConfig._module.freeformType == null then declaredConfig + # Because all definitions that had an associated option ended in + # declaredConfig, freeformConfig can only contain the non-option + # paths, meaning recursiveUpdate will never override any value + else recursiveUpdate freeformConfig declaredConfig; + + checkUnmatched = + if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then + let + firstDef = head merged.unmatchedDefns; + baseMsg = + let + optText = showOption (prefix ++ firstDef.prefix); + defText = + builtins.addErrorContext + "while evaluating the error message for definitions for `${optText}', which is an option that does not exist" + (builtins.addErrorContext + "while evaluating a definition from `${firstDef.file}'" + ( showDefs [ firstDef ]) + ); + in + "The option `${optText}' does not exist. Definition values:${defText}"; + in + if attrNames options == [ "_module" ] + then + let + optionName = showOption prefix; + in + if optionName == "" + then throw '' + ${baseMsg} + + It seems as if you're trying to declare an option by placing it into `config' rather than `options'! + '' + else + throw '' + ${baseMsg} + + However there are no options defined in `${showOption prefix}'. Are you sure you've + declared your options properly? This can happen if you e.g. declared your options in `types.submodule' + under `config' rather than `options'. + '' + else throw baseMsg + else null; + + checked = builtins.seq checkUnmatched; + + extendModules = extendArgs@{ + modules ? [], + specialArgs ? {}, + prefix ? [], + }: + evalModules (evalModulesArgs // { + inherit class; + modules = regularModules ++ modules; + specialArgs = evalModulesArgs.specialArgs or {} // specialArgs; + prefix = extendArgs.prefix or evalModulesArgs.prefix or []; + }); + + type = lib.types.submoduleWith { + inherit modules specialArgs class; + }; + + result = withWarnings { + _type = "configuration"; + options = checked options; + config = checked (removeAttrs config [ "_module" ]); + _module = checked (config._module); + inherit extendModules type; + class = class; + }; + in result; + + # collectModules :: (class: String) -> (modulesPath: String) -> (modules: [ Module ]) -> (args: Attrs) -> [ Module ] + # + # Collects all modules recursively through `import` statements, filtering out + # all modules in disabledModules. + collectModules = class: let + + # Like unifyModuleSyntax, but also imports paths and calls functions if necessary + loadModule = args: fallbackFile: fallbackKey: m: + if isFunction m then + unifyModuleSyntax fallbackFile fallbackKey (applyModuleArgs fallbackKey m args) + else if isAttrs m then + if m._type or "module" == "module" then + unifyModuleSyntax fallbackFile fallbackKey m + else if m._type == "if" || m._type == "override" then + loadModule args fallbackFile fallbackKey { config = m; } + else + throw ( + "Could not load a value as a module, because it is of type ${lib.strings.escapeNixString m._type}" + + lib.optionalString (fallbackFile != unknownModule) ", in file ${toString fallbackFile}." + + lib.optionalString (m._type == "configuration") " If you do intend to import this configuration, please only import the modules that make up the configuration. You may have to create a `let` binding, file or attribute to give yourself access to the relevant modules.\nWhile loading a configuration into the module system is a very sensible idea, it can not be done cleanly in practice." + # Extended explanation: That's because a finalized configuration is more than just a set of modules. For instance, it has its own `specialArgs` that, by the nature of `specialArgs` can't be loaded through `imports` or the the `modules` argument. So instead, we have to ask you to extract the relevant modules and use those instead. This way, we keep the module system comparatively simple, and hopefully avoid a bad surprise down the line. + ) + else if isList m then + let defs = [{ file = fallbackFile; value = m; }]; in + throw "Module imports can't be nested lists. Perhaps you meant to remove one level of lists? Definitions: ${showDefs defs}" + else unifyModuleSyntax (toString m) (toString m) (applyModuleArgsIfFunction (toString m) (import m) args); + + checkModule = + if class != null + then + m: + if m._class != null -> m._class == class + then m + else + throw "The module ${m._file or m.key} was imported into ${class} instead of ${m._class}." + else + m: m; + + /* + Collects all modules recursively into the form + + { + disabled = [ ]; + # All modules of the main module list + modules = [ + { + key = ; + module = ; + # All modules imported by the module for key1 + modules = [ + { + key = ; + module = ; + # All modules imported by the module for key1-1 + modules = [ ... ]; + } + ... + ]; + } + ... + ]; + } + */ + collectStructuredModules = + let + collectResults = modules: { + disabled = concatLists (catAttrs "disabled" modules); + inherit modules; + }; + in parentFile: parentKey: initialModules: args: collectResults (imap1 (n: x: + let + module = checkModule (loadModule args parentFile "${parentKey}:anon-${toString n}" x); + collectedImports = collectStructuredModules module._file module.key module.imports args; + in { + key = module.key; + module = module; + modules = collectedImports.modules; + disabled = (if module.disabledModules != [] then [{ file = module._file; disabled = module.disabledModules; }] else []) ++ collectedImports.disabled; + }) initialModules); + + # filterModules :: String -> { disabled, modules } -> [ Module ] + # + # Filters a structure as emitted by collectStructuredModules by removing all disabled + # modules recursively. It returns the final list of unique-by-key modules + filterModules = modulesPath: { disabled, modules }: + let + moduleKey = file: m: + if isString m + then + if builtins.substring 0 1 m == "/" + then m + else toString modulesPath + "/" + m + + else if isConvertibleWithToString m + then + if m?key && m.key != toString m + then + throw "Module `${file}` contains a disabledModules item that is an attribute set that can be converted to a string (${toString m}) but also has a `.key` attribute (${m.key}) with a different value. This makes it ambiguous which module should be disabled." + else + toString m + + else if m?key + then + m.key + + else if isAttrs m + then throw "Module `${file}` contains a disabledModules item that is an attribute set, presumably a module, that does not have a `key` attribute. This means that the module system doesn't have any means to identify the module that should be disabled. Make sure that you've put the correct value in disabledModules: a string path relative to modulesPath, a path value, or an attribute set with a `key` attribute." + else throw "Each disabledModules item must be a path, string, or a attribute set with a key attribute, or a value supported by toString. However, one of the disabledModules items in `${toString file}` is none of that, but is of type ${builtins.typeOf m}."; + + disabledKeys = concatMap ({ file, disabled }: map (moduleKey file) disabled) disabled; + keyFilter = filter (attrs: ! elem attrs.key disabledKeys); + in map (attrs: attrs.module) (builtins.genericClosure { + startSet = keyFilter modules; + operator = attrs: keyFilter attrs.modules; + }); + + in modulesPath: initialModules: args: + filterModules modulesPath (collectStructuredModules unknownModule "" initialModules args); + + /* Wrap a module with a default location for reporting errors. */ + setDefaultModuleLocation = file: m: + { _file = file; imports = [ m ]; }; + + /* Massage a module into canonical form, that is, a set consisting + of ‘options’, ‘config’ and ‘imports’ attributes. */ + unifyModuleSyntax = file: key: m: + let + addMeta = config: if m ? meta + then mkMerge [ config { meta = m.meta; } ] + else config; + addFreeformType = config: if m ? freeformType + then mkMerge [ config { _module.freeformType = m.freeformType; } ] + else config; + in + if m ? config || m ? options then + let badAttrs = removeAttrs m ["_class" "_file" "key" "disabledModules" "imports" "options" "config" "meta" "freeformType"]; in + if badAttrs != {} then + throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by introducing a top-level `config' or `options' attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: ${toString (attrNames badAttrs)}) into the explicit `config' attribute." + else + { _file = toString m._file or file; + _class = m._class or null; + key = toString m.key or key; + disabledModules = m.disabledModules or []; + imports = m.imports or []; + options = m.options or {}; + config = addFreeformType (addMeta (m.config or {})); + } + else + # shorthand syntax + lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module." + { _file = toString m._file or file; + _class = m._class or null; + key = toString m.key or key; + disabledModules = m.disabledModules or []; + imports = m.require or [] ++ m.imports or []; + options = {}; + config = addFreeformType (removeAttrs m ["_class" "_file" "key" "disabledModules" "require" "imports" "freeformType"]); + }; + + applyModuleArgsIfFunction = key: f: args@{ config, options, lib, ... }: + if isFunction f then applyModuleArgs key f args else f; + + applyModuleArgs = key: f: args@{ config, options, lib, ... }: + let + # Module arguments are resolved in a strict manner when attribute set + # deconstruction is used. As the arguments are now defined with the + # config._module.args option, the strictness used on the attribute + # set argument would cause an infinite loop, if the result of the + # option is given as argument. + # + # To work-around the strictness issue on the deconstruction of the + # attributes set argument, we create a new attribute set which is + # constructed to satisfy the expected set of attributes. Thus calling + # a module will resolve strictly the attributes used as argument but + # not their values. The values are forwarding the result of the + # evaluation of the option. + context = name: ''while evaluating the module argument `${name}' in "${key}":''; + extraArgs = builtins.mapAttrs (name: _: + builtins.addErrorContext (context name) + (args.${name} or config._module.args.${name}) + ) (lib.functionArgs f); + + # Note: we append in the opposite order such that we can add an error + # context on the explicit arguments of "args" too. This update + # operator is used to make the "args@{ ... }: with args.lib;" notation + # works. + in f (args // extraArgs); + + /* Merge a list of modules. This will recurse over the option + declarations in all modules, combining them into a single set. + At the same time, for each option declaration, it will merge the + corresponding option definitions in all machines, returning them + in the ‘value’ attribute of each option. + + This returns a set like + { + # A recursive set of options along with their final values + matchedOptions = { + foo = { _type = "option"; value = "option value of foo"; ... }; + bar.baz = { _type = "option"; value = "option value of bar.baz"; ... }; + ... + }; + # A list of definitions that weren't matched by any option + unmatchedDefns = [ + { file = "file.nix"; prefix = [ "qux" ]; value = "qux"; } + ... + ]; + } + */ + mergeModules = prefix: modules: + mergeModules' prefix modules + (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules); + + mergeModules' = prefix: options: configs: + let + /* byName is like foldAttrs, but will look for attributes to merge in the + specified attribute name. + + byName "foo" (module: value: ["module.hidden=${module.hidden},value=${value}"]) + [ + { + hidden="baz"; + foo={qux="bar"; gla="flop";}; + } + { + hidden="fli"; + foo={qux="gne"; gli="flip";}; + } + ] + ===> + { + gla = [ "module.hidden=baz,value=flop" ]; + gli = [ "module.hidden=fli,value=flip" ]; + qux = [ "module.hidden=baz,value=bar" "module.hidden=fli,value=gne" ]; + } + */ + byName = attr: f: modules: + zipAttrsWith (n: concatLists) + (map (module: let subtree = module.${attr}; in + if !(builtins.isAttrs subtree) then + throw (if attr == "config" then '' + You're trying to define a value of type `${builtins.typeOf subtree}' + rather than an attribute set for the option + `${builtins.concatStringsSep "." prefix}'! + + This usually happens if `${builtins.concatStringsSep "." prefix}' has option + definitions inside that are not matched. Please check how to properly define + this option by e.g. referring to `man 5 configuration.nix'! + '' else '' + An option declaration for `${builtins.concatStringsSep "." prefix}' has type + `${builtins.typeOf subtree}' rather than an attribute set. + Did you mean to define this outside of `options'? + '') + else + mapAttrs (n: f module) subtree + ) modules); + # an attrset 'name' => list of submodules that declare ‘name’. + declsByName = byName "options" (module: option: + [{ inherit (module) _file; options = option; }] + ) options; + # an attrset 'name' => list of submodules that define ‘name’. + defnsByName = byName "config" (module: value: + map (config: { inherit (module) file; inherit config; }) (pushDownProperties value) + ) configs; + # extract the definitions for each loc + defnsByName' = byName "config" (module: value: + [{ inherit (module) file; inherit value; }] + ) configs; + + # Convert an option tree decl to a submodule option decl + optionTreeToOption = decl: + if isOption decl.options + then decl + else decl // { + options = mkOption { + type = types.submoduleWith { + modules = [ { options = decl.options; } ]; + # `null` is not intended for use by modules. It is an internal + # value that means "whatever the user has declared elsewhere". + # This might become obsolete with https://github.com/NixOS/nixpkgs/issues/162398 + shorthandOnlyDefinesConfig = null; + }; + }; + }; + + resultsByName = mapAttrs (name: decls: + # We're descending into attribute ‘name’. + let + loc = prefix ++ [name]; + defns = defnsByName.${name} or []; + defns' = defnsByName'.${name} or []; + optionDecls = filter (m: isOption m.options) decls; + in + if length optionDecls == length decls then + let opt = fixupOptionType loc (mergeOptionDecls loc decls); + in { + matchedOptions = evalOptionValue loc opt defns'; + unmatchedDefns = []; + } + else if optionDecls != [] then + if all (x: x.options.type.name == "submodule") optionDecls + # Raw options can only be merged into submodules. Merging into + # attrsets might be nice, but ambiguous. Suppose we have + # attrset as a `attrsOf submodule`. User declares option + # attrset.foo.bar, this could mean: + # a. option `bar` is only available in `attrset.foo` + # b. option `foo.bar` is available in all `attrset.*` + # c. reject and require "" as a reminder that it behaves like (b). + # d. magically combine (a) and (c). + # All of the above are merely syntax sugar though. + then + let opt = fixupOptionType loc (mergeOptionDecls loc (map optionTreeToOption decls)); + in { + matchedOptions = evalOptionValue loc opt defns'; + unmatchedDefns = []; + } + else + let + nonOptions = filter (m: !isOption m.options) decls; + in + throw "The option `${showOption loc}' in module `${(lib.head optionDecls)._file}' would be a parent of the following options, but its type `${(lib.head optionDecls).options.type.description or ""}' does not support nested options.\n${ + showRawDecls loc nonOptions + }" + else + mergeModules' loc decls defns) declsByName; + + matchedOptions = mapAttrs (n: v: v.matchedOptions) resultsByName; + + # an attrset 'name' => list of unmatched definitions for 'name' + unmatchedDefnsByName = + # Propagate all unmatched definitions from nested option sets + mapAttrs (n: v: v.unmatchedDefns) resultsByName + # Plus the definitions for the current prefix that don't have a matching option + // removeAttrs defnsByName' (attrNames matchedOptions); + in { + inherit matchedOptions; + + # Transforms unmatchedDefnsByName into a list of definitions + unmatchedDefns = + if configs == [] + then + # When no config values exist, there can be no unmatched config, so + # we short circuit and avoid evaluating more _options_ than necessary. + [] + else + concatLists (mapAttrsToList (name: defs: + map (def: def // { + # Set this so we know when the definition first left unmatched territory + prefix = [name] ++ (def.prefix or []); + }) defs + ) unmatchedDefnsByName); + }; + + /* Merge multiple option declarations into a single declaration. In + general, there should be only one declaration of each option. + The exception is the ‘options’ attribute, which specifies + sub-options. These can be specified multiple times to allow one + module to add sub-options to an option declared somewhere else + (e.g. multiple modules define sub-options for ‘fileSystems’). + + 'loc' is the list of attribute names where the option is located. + + 'opts' is a list of modules. Each module has an options attribute which + correspond to the definition of 'loc' in 'opt.file'. */ + mergeOptionDecls = + loc: opts: + foldl' (res: opt: + let t = res.type; + t' = opt.options.type; + mergedType = t.typeMerge t'.functor; + typesMergeable = mergedType != null; + typeSet = if (bothHave "type") && typesMergeable + then { type = mergedType; } + else {}; + bothHave = k: opt.options ? ${k} && res ? ${k}; + in + if bothHave "default" || + bothHave "example" || + bothHave "description" || + bothHave "apply" || + (bothHave "type" && (! typesMergeable)) + then + throw "The option `${showOption loc}' in `${opt._file}' is already declared in ${showFiles res.declarations}." + else + let + getSubModules = opt.options.type.getSubModules or null; + submodules = + if getSubModules != null then map (setDefaultModuleLocation opt._file) getSubModules ++ res.options + else res.options; + in opt.options // res // + { declarations = res.declarations ++ [opt._file]; + options = submodules; + } // typeSet + ) { inherit loc; declarations = []; options = []; } opts; + + /* Merge all the definitions of an option to produce the final + config value. */ + evalOptionValue = loc: opt: defs: + let + # Add in the default value for this option, if any. + defs' = + (optional (opt ? default) + { file = head opt.declarations; value = mkOptionDefault opt.default; }) ++ defs; + + # Handle properties, check types, and merge everything together. + res = + if opt.readOnly or false && length defs' > 1 then + let + # For a better error message, evaluate all readOnly definitions as + # if they were the only definition. + separateDefs = map (def: def // { + value = (mergeDefinitions loc opt.type [ def ]).mergedValue; + }) defs'; + in throw "The option `${showOption loc}' is read-only, but it's set multiple times. Definition values:${showDefs separateDefs}" + else + mergeDefinitions loc opt.type defs'; + + # Apply the 'apply' function to the merged value. This allows options to + # yield a value computed from the definitions + value = if opt ? apply then opt.apply res.mergedValue else res.mergedValue; + + warnDeprecation = + warnIf (opt.type.deprecationMessage != null) + "The type `types.${opt.type.name}' of option `${showOption loc}' defined in ${showFiles opt.declarations} is deprecated. ${opt.type.deprecationMessage}"; + + in warnDeprecation opt // + { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value; + inherit (res.defsFinal') highestPrio; + definitions = map (def: def.value) res.defsFinal; + files = map (def: def.file) res.defsFinal; + definitionsWithLocations = res.defsFinal; + inherit (res) isDefined; + # This allows options to be correctly displayed using `${options.path.to.it}` + __toString = _: showOption loc; + }; + + # Merge definitions of a value of a given type. + mergeDefinitions = loc: type: defs: rec { + defsFinal' = + let + # Process mkMerge and mkIf properties. + defs' = concatMap (m: + map (value: { inherit (m) file; inherit value; }) (builtins.addErrorContext "while evaluating definitions from `${m.file}':" (dischargeProperties m.value)) + ) defs; + + # Process mkOverride properties. + defs'' = filterOverrides' defs'; + + # Sort mkOrder properties. + defs''' = + # Avoid sorting if we don't have to. + if any (def: def.value._type or "" == "order") defs''.values + then sortProperties defs''.values + else defs''.values; + in { + values = defs'''; + inherit (defs'') highestPrio; + }; + defsFinal = defsFinal'.values; + + # Type-check the remaining definitions, and merge them. Or throw if no definitions. + mergedValue = + if isDefined then + if all (def: type.check def.value) defsFinal then type.merge loc defsFinal + else let allInvalid = filter (def: ! type.check def.value) defsFinal; + in throw "A definition for option `${showOption loc}' is not of type `${type.description}'. Definition values:${showDefs allInvalid}" + else + # (nixos-option detects this specific error message and gives it special + # handling. If changed here, please change it there too.) + throw "The option `${showOption loc}' is used but not defined."; + + isDefined = defsFinal != []; + + optionalValue = + if isDefined then { value = mergedValue; } + else {}; + }; + + /* Given a config set, expand mkMerge properties, and push down the + other properties into the children. The result is a list of + config sets that do not have properties at top-level. For + example, + + mkMerge [ { boot = set1; } (mkIf cond { boot = set2; services = set3; }) ] + + is transformed into + + [ { boot = set1; } { boot = mkIf cond set2; services = mkIf cond set3; } ]. + + This transform is the critical step that allows mkIf conditions + to refer to the full configuration without creating an infinite + recursion. + */ + pushDownProperties = cfg: + if cfg._type or "" == "merge" then + concatMap pushDownProperties cfg.contents + else if cfg._type or "" == "if" then + map (mapAttrs (n: v: mkIf cfg.condition v)) (pushDownProperties cfg.content) + else if cfg._type or "" == "override" then + map (mapAttrs (n: v: mkOverride cfg.priority v)) (pushDownProperties cfg.content) + else # FIXME: handle mkOrder? + [ cfg ]; + + /* Given a config value, expand mkMerge properties, and discharge + any mkIf conditions. That is, this is the place where mkIf + conditions are actually evaluated. The result is a list of + config values. For example, ‘mkIf false x’ yields ‘[]’, + ‘mkIf true x’ yields ‘[x]’, and + + mkMerge [ 1 (mkIf true 2) (mkIf true (mkIf false 3)) ] + + yields ‘[ 1 2 ]’. + */ + dischargeProperties = def: + if def._type or "" == "merge" then + concatMap dischargeProperties def.contents + else if def._type or "" == "if" then + if isBool def.condition then + if def.condition then + dischargeProperties def.content + else + [ ] + else + throw "‘mkIf’ called with a non-Boolean condition" + else + [ def ]; + + /* Given a list of config values, process the mkOverride properties, + that is, return the values that have the highest (that is, + numerically lowest) priority, and strip the mkOverride + properties. For example, + + [ { file = "/1"; value = mkOverride 10 "a"; } + { file = "/2"; value = mkOverride 20 "b"; } + { file = "/3"; value = "z"; } + { file = "/4"; value = mkOverride 10 "d"; } + ] + + yields + + [ { file = "/1"; value = "a"; } + { file = "/4"; value = "d"; } + ] + + Note that "z" has the default priority 100. + */ + filterOverrides = defs: (filterOverrides' defs).values; + + filterOverrides' = defs: + let + getPrio = def: if def.value._type or "" == "override" then def.value.priority else defaultOverridePriority; + highestPrio = foldl' (prio: def: min (getPrio def) prio) 9999 defs; + strip = def: if def.value._type or "" == "override" then def // { value = def.value.content; } else def; + in { + values = concatMap (def: if getPrio def == highestPrio then [(strip def)] else []) defs; + inherit highestPrio; + }; + + /* Sort a list of properties. The sort priority of a property is + defaultOrderPriority by default, but can be overridden by wrapping the property + using mkOrder. */ + sortProperties = defs: + let + strip = def: + if def.value._type or "" == "order" + then def // { value = def.value.content; inherit (def.value) priority; } + else def; + defs' = map strip defs; + compare = a: b: (a.priority or defaultOrderPriority) < (b.priority or defaultOrderPriority); + in sort compare defs'; + + # This calls substSubModules, whose entire purpose is only to ensure that + # option declarations in submodules have accurate position information. + # TODO: Merge this into mergeOptionDecls + fixupOptionType = loc: opt: + if opt.type.getSubModules or null == null + then opt // { type = opt.type or types.unspecified; } + else opt // { type = opt.type.substSubModules opt.options; options = []; }; + + + /* Properties. */ + + mkIf = condition: content: + { _type = "if"; + inherit condition content; + }; + + mkAssert = assertion: message: content: + mkIf + (if assertion then true else throw "\nFailed assertion: ${message}") + content; + + mkMerge = contents: + { _type = "merge"; + inherit contents; + }; + + mkOverride = priority: content: + { _type = "override"; + inherit priority content; + }; + + mkOptionDefault = mkOverride 1500; # priority of option defaults + mkDefault = mkOverride 1000; # used in config sections of non-user modules to set a default + defaultOverridePriority = 100; + mkImageMediaOverride = mkOverride 60; # image media profiles can be derived by inclusion into host config, hence needing to override host config, but do allow user to mkForce + mkForce = mkOverride 50; + mkVMOverride = mkOverride 10; # used by ‘nixos-rebuild build-vm’ + + defaultPriority = lib.warnIf (lib.isInOldestRelease 2305) "lib.modules.defaultPriority is deprecated, please use lib.modules.defaultOverridePriority instead." defaultOverridePriority; + + mkFixStrictness = lib.warn "lib.mkFixStrictness has no effect and will be removed. It returns its argument unmodified, so you can just remove any calls." id; + + mkOrder = priority: content: + { _type = "order"; + inherit priority content; + }; + + mkBefore = mkOrder 500; + defaultOrderPriority = 1000; + mkAfter = mkOrder 1500; + + # Convenient property used to transfer all definitions and their + # properties from one option to another. This property is useful for + # renaming options, and also for including properties from another module + # system, including sub-modules. + # + # { config, options, ... }: + # + # { + # # 'bar' might not always be defined in the current module-set. + # config.foo.enable = mkAliasDefinitions (options.bar.enable or {}); + # + # # 'barbaz' has to be defined in the current module-set. + # config.foobar.paths = mkAliasDefinitions options.barbaz.paths; + # } + # + # Note, this is different than taking the value of the option and using it + # as a definition, as the new definition will not keep the mkOverride / + # mkDefault properties of the previous option. + # + mkAliasDefinitions = mkAliasAndWrapDefinitions id; + mkAliasAndWrapDefinitions = wrap: option: + mkAliasIfDef option (wrap (mkMerge option.definitions)); + + # Similar to mkAliasAndWrapDefinitions but copies over the priority from the + # option as well. + # + # If a priority is not set, it assumes a priority of defaultOverridePriority. + mkAliasAndWrapDefsWithPriority = wrap: option: + let + prio = option.highestPrio or defaultOverridePriority; + defsWithPrio = map (mkOverride prio) option.definitions; + in mkAliasIfDef option (wrap (mkMerge defsWithPrio)); + + mkAliasIfDef = option: + mkIf (isOption option && option.isDefined); + + /* Compatibility. */ + fixMergeModules = modules: args: evalModules { inherit modules args; check = false; }; + + + /* Return a module that causes a warning to be shown if the + specified option is defined. For example, + + mkRemovedOptionModule [ "boot" "loader" "grub" "bootDevice" ] "" + + causes a assertion if the user defines boot.loader.grub.bootDevice. + + replacementInstructions is a string that provides instructions on + how to achieve the same functionality without the removed option, + or alternatively a reasoning why the functionality is not needed. + replacementInstructions SHOULD be provided! + */ + mkRemovedOptionModule = optionName: replacementInstructions: + { options, ... }: + { options = setAttrByPath optionName (mkOption { + visible = false; + apply = x: throw "The option `${showOption optionName}' can no longer be used since it's been removed. ${replacementInstructions}"; + }); + config.assertions = + let opt = getAttrFromPath optionName options; in [{ + assertion = !opt.isDefined; + message = '' + The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it. + ${replacementInstructions} + ''; + }]; + }; + + /* Return a module that causes a warning to be shown if the + specified "from" option is defined; the defined value is however + forwarded to the "to" option. This can be used to rename options + while providing backward compatibility. For example, + + mkRenamedOptionModule [ "boot" "copyKernels" ] [ "boot" "loader" "grub" "copyKernels" ] + + forwards any definitions of boot.copyKernels to + boot.loader.grub.copyKernels while printing a warning. + + This also copies over the priority from the aliased option to the + non-aliased option. + */ + mkRenamedOptionModule = from: to: doRename { + inherit from to; + visible = false; + warn = true; + use = builtins.trace "Obsolete option `${showOption from}' is used. It was renamed to `${showOption to}'."; + }; + + mkRenamedOptionModuleWith = { + /* Old option path as list of strings. */ + from, + /* New option path as list of strings. */ + to, + + /* + Release number of the first release that contains the rename, ignoring backports. + Set it to the upcoming release, matching the nixpkgs/.version file. + */ + sinceRelease, + + }: doRename { + inherit from to; + visible = false; + warn = lib.isInOldestRelease sinceRelease; + use = lib.warnIf (lib.isInOldestRelease sinceRelease) + "Obsolete option `${showOption from}' is used. It was renamed to `${showOption to}'."; + }; + + /* Return a module that causes a warning to be shown if any of the "from" + option is defined; the defined values can be used in the "mergeFn" to set + the "to" value. + This function can be used to merge multiple options into one that has a + different type. + + "mergeFn" takes the module "config" as a parameter and must return a value + of "to" option type. + + mkMergedOptionModule + [ [ "a" "b" "c" ] + [ "d" "e" "f" ] ] + [ "x" "y" "z" ] + (config: + let value = p: getAttrFromPath p config; + in + if (value [ "a" "b" "c" ]) == true then "foo" + else if (value [ "d" "e" "f" ]) == true then "bar" + else "baz") + + - options.a.b.c is a removed boolean option + - options.d.e.f is a removed boolean option + - options.x.y.z is a new str option that combines a.b.c and d.e.f + functionality + + This show a warning if any a.b.c or d.e.f is set, and set the value of + x.y.z to the result of the merge function + */ + mkMergedOptionModule = from: to: mergeFn: + { config, options, ... }: + { + options = foldl' recursiveUpdate {} (map (path: setAttrByPath path (mkOption { + visible = false; + # To use the value in mergeFn without triggering errors + default = "_mkMergedOptionModule"; + })) from); + + config = { + warnings = filter (x: x != "") (map (f: + let val = getAttrFromPath f config; + opt = getAttrFromPath f options; + in + optionalString + (val != "_mkMergedOptionModule") + "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly." + ) from); + } // setAttrByPath to (mkMerge + (optional + (any (f: (getAttrFromPath f config) != "_mkMergedOptionModule") from) + (mergeFn config))); + }; + + /* Single "from" version of mkMergedOptionModule. + Return a module that causes a warning to be shown if the "from" option is + defined; the defined value can be used in the "mergeFn" to set the "to" + value. + This function can be used to change an option into another that has a + different type. + + "mergeFn" takes the module "config" as a parameter and must return a value of + "to" option type. + + mkChangedOptionModule [ "a" "b" "c" ] [ "x" "y" "z" ] + (config: + let value = getAttrFromPath [ "a" "b" "c" ] config; + in + if value > 100 then "high" + else "normal") + + - options.a.b.c is a removed int option + - options.x.y.z is a new str option that supersedes a.b.c + + This show a warning if a.b.c is set, and set the value of x.y.z to the + result of the change function + */ + mkChangedOptionModule = from: to: changeFn: + mkMergedOptionModule [ from ] to changeFn; + + /* Like ‘mkRenamedOptionModule’, but doesn't show a warning. */ + mkAliasOptionModule = from: to: doRename { + inherit from to; + visible = true; + warn = false; + use = id; + }; + + /* Transitional version of mkAliasOptionModule that uses MD docs. */ + mkAliasOptionModuleMD = from: to: doRename { + inherit from to; + visible = true; + warn = false; + use = id; + markdown = true; + }; + + /* mkDerivedConfig : Option a -> (a -> Definition b) -> Definition b + + Create config definitions with the same priority as the definition of another option. + This should be used for option definitions where one option sets the value of another as a convenience. + For instance a config file could be set with a `text` or `source` option, where text translates to a `source` + value using `mkDerivedConfig options.text (pkgs.writeText "filename.conf")`. + + It takes care of setting the right priority using `mkOverride`. + */ + # TODO: make the module system error message include information about `opt` in + # error messages about conflicts. E.g. introduce a variation of `mkOverride` which + # adds extra location context to the definition object. This will allow context to be added + # to all messages that report option locations "this value was derived from + # which was defined in ". It can provide a trace of options that contributed + # to definitions. + mkDerivedConfig = opt: f: + mkOverride + (opt.highestPrio or defaultOverridePriority) + (f opt.value); + + doRename = { from, to, visible, warn, use, withPriority ? true, markdown ? false }: + { config, options, ... }: + let + fromOpt = getAttrFromPath from options; + toOf = attrByPath to + (abort "Renaming error: option `${showOption to}' does not exist."); + toType = let opt = attrByPath to {} options; in opt.type or (types.submodule {}); + in + { + options = setAttrByPath from (mkOption { + inherit visible; + description = if markdown + then lib.mdDoc "Alias of {option}`${showOption to}`." + else "Alias of ."; + apply = x: use (toOf config); + } // optionalAttrs (toType != null) { + type = toType; + }); + config = mkMerge [ + (optionalAttrs (options ? warnings) { + warnings = optional (warn && fromOpt.isDefined) + "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'."; + }) + (if withPriority + then mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt + else mkAliasAndWrapDefinitions (setAttrByPath to) fromOpt) + ]; + }; + + /* Use this function to import a JSON file as NixOS configuration. + + modules.importJSON :: path -> attrs + */ + importJSON = file: { + _file = file; + config = lib.importJSON file; + }; + + /* Use this function to import a TOML file as NixOS configuration. + + modules.importTOML :: path -> attrs + */ + importTOML = file: { + _file = file; + config = lib.importTOML file; + }; + + private = lib.mapAttrs + (k: lib.warn "External use of `lib.modules.${k}` is deprecated. If your use case isn't covered by non-deprecated functions, we'd like to know more and perhaps support your use case well, instead of providing access to these low level functions. In this case please open an issue in https://github.com/nixos/nixpkgs/issues/.") + { + inherit + applyModuleArgsIfFunction + dischargeProperties + evalOptionValue + mergeModules + mergeModules' + pushDownProperties + unifyModuleSyntax + ; + collectModules = collectModules null; + }; + +in +private // +{ + # NOTE: not all of these functions are necessarily public interfaces; some + # are just needed by types.nix, but are not meant to be consumed + # externally. + inherit + defaultOrderPriority + defaultOverridePriority + defaultPriority + doRename + evalModules + filterOverrides + filterOverrides' + fixMergeModules + fixupOptionType # should be private? + importJSON + importTOML + mergeDefinitions + mergeOptionDecls # should be private? + mkAfter + mkAliasAndWrapDefinitions + mkAliasAndWrapDefsWithPriority + mkAliasDefinitions + mkAliasIfDef + mkAliasOptionModule + mkAliasOptionModuleMD + mkAssert + mkBefore + mkChangedOptionModule + mkDefault + mkDerivedConfig + mkFixStrictness + mkForce + mkIf + mkImageMediaOverride + mkMerge + mkMergedOptionModule + mkOptionDefault + mkOrder + mkOverride + mkRemovedOptionModule + mkRenamedOptionModule + mkRenamedOptionModuleWith + mkVMOverride + setDefaultModuleLocation + sortProperties; +} diff --git a/options.nix b/options.nix new file mode 100644 index 000000000..af7914bb5 --- /dev/null +++ b/options.nix @@ -0,0 +1,432 @@ +# Nixpkgs/NixOS option handling. +{ lib }: + +let + inherit (lib) + all + collect + concatLists + concatMap + concatMapStringsSep + filter + foldl' + head + tail + isAttrs + isBool + isDerivation + isFunction + isInt + isList + isString + length + mapAttrs + optional + optionals + take + ; + inherit (lib.attrsets) + attrByPath + optionalAttrs + ; + inherit (lib.strings) + concatMapStrings + concatStringsSep + ; + inherit (lib.types) + mkOptionType + ; + inherit (lib.lists) + last + ; + prioritySuggestion = '' + Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions. + ''; +in +rec { + + /* Returns true when the given argument is an option + + Type: isOption :: a -> bool + + Example: + isOption 1 // => false + isOption (mkOption {}) // => true + */ + isOption = lib.isType "option"; + + /* Creates an Option attribute set. mkOption accepts an attribute set with the following keys: + + All keys default to `null` when not given. + + Example: + mkOption { } // => { _type = "option"; } + mkOption { default = "foo"; } // => { _type = "option"; default = "foo"; } + */ + mkOption = + { + # Default value used when no definition is given in the configuration. + default ? null, + # Textual representation of the default, for the manual. + defaultText ? null, + # Example value used in the manual. + example ? null, + # String describing the option. + description ? null, + # Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-options-doc/default.nix). + relatedPackages ? null, + # Option type, providing type-checking and value merging. + type ? null, + # Function that converts the option value to something else. + apply ? null, + # Whether the option is for NixOS developers only. + internal ? null, + # Whether the option shows up in the manual. Default: true. Use false to hide the option and any sub-options from submodules. Use "shallow" to hide only sub-options. + visible ? null, + # Whether the option can be set only once + readOnly ? null, + } @ attrs: + attrs // { _type = "option"; }; + + /* Creates an Option attribute set for a boolean value option i.e an + option to be toggled on or off: + + Example: + mkEnableOption "foo" + => { _type = "option"; default = false; description = "Whether to enable foo."; example = true; type = { ... }; } + */ + mkEnableOption = + # Name for the created option + name: mkOption { + default = false; + example = true; + description = + if name ? _type && name._type == "mdDoc" + then lib.mdDoc "Whether to enable ${name.text}." + else "Whether to enable ${name}."; + type = lib.types.bool; + }; + + /* Creates an Option attribute set for an option that specifies the + package a module should use for some purpose. + + The package is specified in the third argument under `default` as a list of strings + representing its attribute path in nixpkgs (or another package set). + Because of this, you need to pass nixpkgs itself (or a subset) as the first argument. + + The second argument may be either a string or a list of strings. + It provides the display name of the package in the description of the generated option + (using only the last element if the passed value is a list) + and serves as the fallback value for the `default` argument. + + To include extra information in the description, pass `extraDescription` to + append arbitrary text to the generated description. + You can also pass an `example` value, either a literal string or an attribute path. + + The default argument can be omitted if the provided name is + an attribute of pkgs (if name is a string) or a + valid attribute path in pkgs (if name is a list). + + If you wish to explicitly provide no default, pass `null` as `default`. + + Type: mkPackageOption :: pkgs -> (string|[string]) -> { default? :: [string], example? :: null|string|[string], extraDescription? :: string } -> option + + Example: + mkPackageOption pkgs "hello" { } + => { _type = "option"; default = «derivation /nix/store/3r2vg51hlxj3cx5vscp0vkv60bqxkaq0-hello-2.10.drv»; defaultText = { ... }; description = "The hello package to use."; type = { ... }; } + + Example: + mkPackageOption pkgs "GHC" { + default = [ "ghc" ]; + example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])"; + } + => { _type = "option"; default = «derivation /nix/store/jxx55cxsjrf8kyh3fp2ya17q99w7541r-ghc-8.10.7.drv»; defaultText = { ... }; description = "The GHC package to use."; example = { ... }; type = { ... }; } + + Example: + mkPackageOption pkgs [ "python39Packages" "pytorch" ] { + extraDescription = "This is an example and doesn't actually do anything."; + } + => { _type = "option"; default = «derivation /nix/store/gvqgsnc4fif9whvwd9ppa568yxbkmvk8-python3.9-pytorch-1.10.2.drv»; defaultText = { ... }; description = "The pytorch package to use. This is an example and doesn't actually do anything."; type = { ... }; } + + */ + mkPackageOption = + # Package set (a specific version of nixpkgs or a subset) + pkgs: + # Name for the package, shown in option description + name: + { + # Whether the package can be null, for example to disable installing a package altogether. + nullable ? false, + # The attribute path where the default package is located (may be omitted) + default ? name, + # A string or an attribute path to use as an example (may be omitted) + example ? null, + # Additional text to include in the option description (may be omitted) + extraDescription ? "", + }: + let + name' = if isList name then last name else name; + in mkOption ({ + type = with lib.types; (if nullable then nullOr else lib.id) package; + description = "The ${name'} package to use." + + (if extraDescription == "" then "" else " ") + extraDescription; + } // (if default != null then let + default' = if isList default then default else [ default ]; + defaultPath = concatStringsSep "." default'; + defaultValue = attrByPath default' + (throw "${defaultPath} cannot be found in pkgs") pkgs; + in { + default = defaultValue; + defaultText = literalExpression ("pkgs." + defaultPath); + } else if nullable then { + default = null; + } else { }) // lib.optionalAttrs (example != null) { + example = literalExpression + (if isList example then "pkgs." + concatStringsSep "." example else example); + }); + + /* Like mkPackageOption, but emit an mdDoc description instead of DocBook. */ + mkPackageOptionMD = pkgs: name: extra: + let option = mkPackageOption pkgs name extra; + in option // { description = lib.mdDoc option.description; }; + + /* This option accepts anything, but it does not produce any result. + + This is useful for sharing a module across different module sets + without having to implement similar features as long as the + values of the options are not accessed. */ + mkSinkUndeclaredOptions = attrs: mkOption ({ + internal = true; + visible = false; + default = false; + description = "Sink for option definitions."; + type = mkOptionType { + name = "sink"; + check = x: true; + merge = loc: defs: false; + }; + apply = x: throw "Option value is not readable because the option is not declared."; + } // attrs); + + mergeDefaultOption = loc: defs: + let list = getValues defs; in + if length list == 1 then head list + else if all isFunction list then x: mergeDefaultOption loc (map (f: f x) list) + else if all isList list then concatLists list + else if all isAttrs list then foldl' lib.mergeAttrs {} list + else if all isBool list then foldl' lib.or false list + else if all isString list then lib.concatStrings list + else if all isInt list && all (x: x == head list) list then head list + else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}"; + + mergeOneOption = mergeUniqueOption { message = ""; }; + + mergeUniqueOption = { message }: loc: defs: + if length defs == 1 + then (head defs).value + else assert length defs > 1; + throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}"; + + /* "Merge" option definitions by checking that they all have the same value. */ + mergeEqualOption = loc: defs: + if defs == [] then abort "This case should never happen." + # Return early if we only have one element + # This also makes it work for functions, because the foldl' below would try + # to compare the first element with itself, which is false for functions + else if length defs == 1 then (head defs).value + else (foldl' (first: def: + if def.value != first.value then + throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}\n${prioritySuggestion}" + else + first) (head defs) (tail defs)).value; + + /* Extracts values of all "value" keys of the given list. + + Type: getValues :: [ { value :: a; } ] -> [a] + + Example: + getValues [ { value = 1; } { value = 2; } ] // => [ 1 2 ] + getValues [ ] // => [ ] + */ + getValues = map (x: x.value); + + /* Extracts values of all "file" keys of the given list + + Type: getFiles :: [ { file :: a; } ] -> [a] + + Example: + getFiles [ { file = "file1"; } { file = "file2"; } ] // => [ "file1" "file2" ] + getFiles [ ] // => [ ] + */ + getFiles = map (x: x.file); + + # Generate documentation template from the list of option declaration like + # the set generated with filterOptionSets. + optionAttrSetToDocList = optionAttrSetToDocList' []; + + optionAttrSetToDocList' = _: options: + concatMap (opt: + let + name = showOption opt.loc; + docOption = { + loc = opt.loc; + inherit name; + description = opt.description or null; + declarations = filter (x: x != unknownModule) opt.declarations; + internal = opt.internal or false; + visible = + if (opt?visible && opt.visible == "shallow") + then true + else opt.visible or true; + readOnly = opt.readOnly or false; + type = opt.type.description or "unspecified"; + } + // optionalAttrs (opt ? example) { + example = + builtins.addErrorContext "while evaluating the example of option `${name}`" ( + renderOptionValue opt.example + ); + } + // optionalAttrs (opt ? defaultText || opt ? default) { + default = + builtins.addErrorContext "while evaluating the ${if opt?defaultText then "defaultText" else "default value"} of option `${name}`" ( + renderOptionValue (opt.defaultText or opt.default) + ); + } + // optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) { inherit (opt) relatedPackages; }; + + subOptions = + let ss = opt.type.getSubOptions opt.loc; + in if ss != {} then optionAttrSetToDocList' opt.loc ss else []; + subOptionsVisible = docOption.visible && opt.visible or null != "shallow"; + in + # To find infinite recursion in NixOS option docs: + # builtins.trace opt.loc + [ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options); + + + /* This function recursively removes all derivation attributes from + `x` except for the `name` attribute. + + This is to make the generation of `options.xml` much more + efficient: the XML representation of derivations is very large + (on the order of megabytes) and is not actually used by the + manual generator. + + This function was made obsolete by renderOptionValue and is kept for + compatibility with out-of-tree code. + */ + scrubOptionValue = x: + if isDerivation x then + { type = "derivation"; drvPath = x.name; outPath = x.name; name = x.name; } + else if isList x then map scrubOptionValue x + else if isAttrs x then mapAttrs (n: v: scrubOptionValue v) (removeAttrs x ["_args"]) + else x; + + + /* Ensures that the given option value (default or example) is a `_type`d string + by rendering Nix values to `literalExpression`s. + */ + renderOptionValue = v: + if v ? _type && v ? text then v + else literalExpression (lib.generators.toPretty { + multiline = true; + allowPrettyValues = true; + } v); + + + /* For use in the `defaultText` and `example` option attributes. Causes the + given string to be rendered verbatim in the documentation as Nix code. This + is necessary for complex values, e.g. functions, or values that depend on + other values or packages. + */ + literalExpression = text: + if ! isString text then throw "literalExpression expects a string." + else { _type = "literalExpression"; inherit text; }; + + literalExample = lib.warn "literalExample is deprecated, use literalExpression instead, or use literalDocBook for a non-Nix description." literalExpression; + + + /* For use in the `defaultText` and `example` option attributes. Causes the + given DocBook text to be inserted verbatim in the documentation, for when + a `literalExpression` would be too hard to read. + */ + literalDocBook = text: + if ! isString text then throw "literalDocBook expects a string." + else + lib.warnIf (lib.isInOldestRelease 2211) + "literalDocBook is deprecated, use literalMD instead" + { _type = "literalDocBook"; inherit text; }; + + /* Transition marker for documentation that's already migrated to markdown + syntax. + */ + mdDoc = text: + if ! isString text then throw "mdDoc expects a string." + else { _type = "mdDoc"; inherit text; }; + + /* For use in the `defaultText` and `example` option attributes. Causes the + given MD text to be inserted verbatim in the documentation, for when + a `literalExpression` would be too hard to read. + */ + literalMD = text: + if ! isString text then throw "literalMD expects a string." + else { _type = "literalMD"; inherit text; }; + + # Helper functions. + + /* Convert an option, described as a list of the option parts to a + human-readable version. + + Example: + (showOption ["foo" "bar" "baz"]) == "foo.bar.baz" + (showOption ["foo" "bar.baz" "tux"]) == "foo.\"bar.baz\".tux" + (showOption ["windowManager" "2bwm" "enable"]) == "windowManager.\"2bwm\".enable" + + Placeholders will not be quoted as they are not actual values: + (showOption ["foo" "*" "bar"]) == "foo.*.bar" + (showOption ["foo" "" "bar"]) == "foo..bar" + */ + showOption = parts: let + escapeOptionPart = part: + let + # We assume that these are "special values" and not real configuration data. + # If it is real configuration data, it is rendered incorrectly. + specialIdentifiers = [ + "" # attrsOf (submodule {}) + "*" # listOf (submodule {}) + "" # functionTo + ]; + in if builtins.elem part specialIdentifiers + then part + else lib.strings.escapeNixIdentifier part; + in (concatStringsSep ".") (map escapeOptionPart parts); + showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files); + + showDefs = defs: concatMapStrings (def: + let + # Pretty print the value for display, if successful + prettyEval = builtins.tryEval + (lib.generators.toPretty { } + (lib.generators.withRecursion { depthLimit = 10; throwOnDepthLimit = false; } def.value)); + # Split it into its lines + lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value); + # Only display the first 5 lines, and indent them for better visibility + value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "..."); + result = + # Don't print any value if evaluating the value strictly fails + if ! prettyEval.success then "" + # Put it on a new line if it consists of multiple + else if length lines > 1 then ":\n " + value + else ": " + value; + in "\n- In `${def.file}'${result}" + ) defs; + + showOptionWithDefLocs = opt: '' + ${showOption opt.loc}, with values defined in: + ${concatMapStringsSep "\n" (defFile: " - ${defFile}") opt.files} + ''; + + unknownModule = ""; + +} diff --git a/path/README.md b/path/README.md new file mode 100644 index 000000000..87e552d12 --- /dev/null +++ b/path/README.md @@ -0,0 +1,196 @@ +# Path library + +This document explains why the `lib.path` library is designed the way it is. + +The purpose of this library is to process [filesystem paths]. It does not read files from the filesystem. +It exists to support the native Nix [path value type] with extra functionality. + +[filesystem paths]: https://en.m.wikipedia.org/wiki/Path_(computing) +[path value type]: https://nixos.org/manual/nix/stable/language/values.html#type-path + +As an extension of the path value type, it inherits the same intended use cases and limitations: +- Only use paths to access files at evaluation time, such as the local project source. +- Paths cannot point to derivations, so they are unfit to represent dependencies. +- A path implicitly imports the referenced files into the Nix store when interpolated to a string. Therefore paths are not suitable to access files at build- or run-time, as you risk importing the path from the evaluation system instead. + +Overall, this library works with two types of paths: +- Absolute paths are represented with the Nix [path value type]. Nix automatically normalises these paths. +- Subpaths are represented with the [string value type] since path value types don't support relative paths. This library normalises these paths as safely as possible. Absolute paths in strings are not supported. + + A subpath refers to a specific file or directory within an absolute base directory. + It is a stricter form of a relative path, notably [without support for `..` components][parents] since those could escape the base directory. + +[string value type]: https://nixos.org/manual/nix/stable/language/values.html#type-string + +This library is designed to be as safe and intuitive as possible, throwing errors when operations are attempted that would produce surprising results, and giving the expected result otherwise. + +This library is designed to work well as a dependency for the `lib.filesystem` and `lib.sources` library components. Contrary to these library components, `lib.path` does not read any paths from the filesystem. + +This library makes only these assumptions about paths and no others: +- `dirOf path` returns the path to the parent directory of `path`, unless `path` is the filesystem root, in which case `path` is returned. + - There can be multiple filesystem roots: `p == dirOf p` and `q == dirOf q` does not imply `p == q`. + - While there's only a single filesystem root in stable Nix, the [lazy trees feature](https://github.com/NixOS/nix/pull/6530) introduces [additional filesystem roots](https://github.com/NixOS/nix/pull/6530#discussion_r1041442173). +- `path + ("/" + string)` returns the path to the `string` subdirectory in `path`. + - If `string` contains no `/` characters, then `dirOf (path + ("/" + string)) == path`. + - If `string` contains no `/` characters, then `baseNameOf (path + ("/" + string)) == string`. +- `path1 == path2` returns `true` only if `path1` points to the same filesystem path as `path2`. + +Notably we do not make the assumption that we can turn paths into strings using `toString path`. + +## Design decisions + +Each subsection here contains a decision along with arguments and counter-arguments for (+) and against (-) that decision. + +### Leading dots for relative paths +[leading-dots]: #leading-dots-for-relative-paths + +Observing: Since subpaths are a form of relative paths, they can have a leading `./` to indicate it being a relative path, this is generally not necessary for tools though. + +Considering: Paths should be as explicit, consistent and unambiguous as possible. + +Decision: Returned subpaths should always have a leading `./`. + +
+Arguments + +- (+) In shells, just running `foo` as a command wouldn't execute the file `foo`, whereas `./foo` would execute the file. In contrast, `foo/bar` does execute that file without the need for `./`. This can lead to confusion about when a `./` needs to be prefixed. If a `./` is always included, this becomes a non-issue. This effectively then means that paths don't overlap with command names. +- (+) Prepending with `./` makes the subpaths always valid as relative Nix path expressions. +- (+) Using paths in command line arguments could give problems if not escaped properly, e.g. if a path was `--version`. This is not a problem with `./--version`. This effectively then means that paths don't overlap with GNU-style command line options. +- (-) `./` is not required to resolve relative paths, resolution always has an implicit `./` as prefix. +- (-) It's less noisy without the `./`, e.g. in error messages. + - (+) But similarly, it could be confusing whether something was even a path. + e.g. `foo` could be anything, but `./foo` is more clearly a path. +- (+) Makes it more uniform with absolute paths (those always start with `/`). + - (-) That is not relevant for practical purposes. +- (+) `find` also outputs results with `./`. + - (-) But only if you give it an argument of `.`. If you give it the argument `some-directory`, it won't prefix that. +- (-) `realpath --relative-to` doesn't prefix relative paths with `./`. + - (+) There is no need to return the same result as `realpath`. + +
+ +### Representation of the current directory +[curdir]: #representation-of-the-current-directory + +Observing: The subpath that produces the base directory can be represented with `.` or `./` or `./.`. + +Considering: Paths should be as consistent and unambiguous as possible. + +Decision: It should be `./.`. + +
+Arguments + +- (+) `./` would be inconsistent with [the decision to not persist trailing slashes][trailing-slashes]. +- (-) `.` is how `realpath` normalises paths. +- (+) `.` can be interpreted as a shell command (it's a builtin for sourcing files in `bash` and `zsh`). +- (+) `.` would be the only path without a `/`. It could not be used as a Nix path expression, since those require at least one `/` to be parsed as such. +- (-) `./.` is rather long. + - (-) We don't require users to type this though, as it's only output by the library. + As inputs all three variants are supported for subpaths (and we can't do anything about absolute paths) +- (-) `builtins.dirOf "foo" == "."`, so `.` would be consistent with that. +- (+) `./.` is consistent with the [decision to have leading `./`][leading-dots]. +- (+) `./.` is a valid Nix path expression, although this property does not hold for every relative path or subpath. + +
+ +### Subpath representation +[relrepr]: #subpath-representation + +Observing: Subpaths such as `foo/bar` can be represented in various ways: +- string: `"foo/bar"` +- list with all the components: `[ "foo" "bar" ]` +- attribute set: `{ type = "relative-path"; components = [ "foo" "bar" ]; }` + +Considering: Paths should be as safe to use as possible. We should generate string outputs in the library and not encourage users to do that themselves. + +Decision: Paths are represented as strings. + +
+Arguments + +- (+) It's simpler for the users of the library. One doesn't have to convert a path a string before it can be used. + - (+) Naively converting the list representation to a string with `concatStringsSep "/"` would break for `[]`, requiring library users to be more careful. +- (+) It doesn't encourage people to do their own path processing and instead use the library. + With a list representation it would seem easy to just use `lib.lists.init` to get the parent directory, but then it breaks for `.`, which would be represented as `[ ]`. +- (+) `+` is convenient and doesn't work on lists and attribute sets. + - (-) Shouldn't use `+` anyways, we export safer functions for path manipulation. + +
+ +### Parent directory +[parents]: #parent-directory + +Observing: Relative paths can have `..` components, which refer to the parent directory. + +Considering: Paths should be as safe and unambiguous as possible. + +Decision: `..` path components in string paths are not supported, neither as inputs nor as outputs. Hence, string paths are called subpaths, rather than relative paths. + +
+Arguments + +- (+) If we wanted relative paths to behave according to the "physical" interpretation (as a directory tree with relations between nodes), it would require resolving symlinks, since e.g. `foo/..` would not be the same as `.` if `foo` is a symlink. + - (-) The "logical" interpretation is also valid (treating paths as a sequence of names), and is used by some software. It is simpler, and not using symlinks at all is safer. + - (+) Mixing both models can lead to surprises. + - (+) We can't resolve symlinks without filesystem access. + - (+) Nix also doesn't support reading symlinks at evaluation time. + - (-) We could just not handle such cases, e.g. `equals "foo" "foo/bar/.. == false`. The paths are different, we don't need to check whether the paths point to the same thing. + - (+) Assume we said `relativeTo /foo /bar == "../bar"`. If this is used like `/bar/../foo` in the end, and `bar` turns out to be a symlink to somewhere else, this won't be accurate. + - (-) We could decide to not support such ambiguous operations, or mark them as such, e.g. the normal `relativeTo` will error on such a case, but there could be `extendedRelativeTo` supporting that. +- (-) `..` are a part of paths, a path library should therefore support it. + - (+) If we can convincingly argue that all such use cases are better done e.g. with runtime tools, the library not supporting it can nudge people towards using those. +- (-) We could allow "..", but only in the prefix. + - (+) Then we'd have to throw an error for doing `append /some/path "../foo"`, making it non-composable. + - (+) The same is for returning paths with `..`: `relativeTo /foo /bar => "../bar"` would produce a non-composable path. +- (+) We argue that `..` is not needed at the Nix evaluation level, since we'd always start evaluation from the project root and don't go up from there. + - (+) `..` is supported in Nix paths, turning them into absolute paths. + - (-) This is ambiguous in the presence of symlinks. +- (+) If you need `..` for building or runtime, you can use build-/run-time tooling to create those (e.g. `realpath` with `--relative-to`), or use absolute paths instead. + This also gives you the ability to correctly handle symlinks. + +
+ +### Trailing slashes +[trailing-slashes]: #trailing-slashes + +Observing: Subpaths can contain trailing slashes, like `foo/`, indicating that the path points to a directory and not a file. + +Considering: Paths should be as consistent as possible, there should only be a single normalisation for the same path. + +Decision: All functions remove trailing slashes in their results. + +
+Arguments + +- (+) It allows normalisations to be unique, in that there's only a single normalisation for the same path. If trailing slashes were preserved, both `foo/bar` and `foo/bar/` would be valid but different normalisations for the same path. +- Comparison to other frameworks to figure out the least surprising behavior: + - (+) Nix itself doesn't support trailing slashes when parsing and doesn't preserve them when appending paths. + - (-) [Rust's std::path](https://doc.rust-lang.org/std/path/index.html) does preserve them during [construction](https://doc.rust-lang.org/std/path/struct.Path.html#method.new). + - (+) Doesn't preserve them when returning individual [components](https://doc.rust-lang.org/std/path/struct.Path.html#method.components). + - (+) Doesn't preserve them when [canonicalizing](https://doc.rust-lang.org/std/path/struct.Path.html#method.canonicalize). + - (+) [Python 3's pathlib](https://docs.python.org/3/library/pathlib.html#module-pathlib) doesn't preserve them during [construction](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath). + - Notably it represents the individual components as a list internally. + - (-) [Haskell's filepath](https://hackage.haskell.org/package/filepath-1.4.100.0) has [explicit support](https://hackage.haskell.org/package/filepath-1.4.100.0/docs/System-FilePath.html#g:6) for handling trailing slashes. + - (-) Does preserve them for [normalisation](https://hackage.haskell.org/package/filepath-1.4.100.0/docs/System-FilePath.html#v:normalise). + - (-) [NodeJS's Path library](https://nodejs.org/api/path.html) preserves trailing slashes for [normalisation](https://nodejs.org/api/path.html#pathnormalizepath). + - (+) For [parsing a path](https://nodejs.org/api/path.html#pathparsepath) into its significant elements, trailing slashes are not preserved. +- (+) Nix's builtin function `dirOf` gives an unexpected result for paths with trailing slashes: `dirOf "foo/bar/" == "foo/bar"`. + Inconsistently, `baseNameOf` works correctly though: `baseNameOf "foo/bar/" == "bar"`. + - (-) We are writing a path library to improve handling of paths though, so we shouldn't use these functions and discourage their use. +- (-) Unexpected result when normalising intermediate paths, like `relative.normalise ("foo" + "/") + "bar" == "foobar"`. + - (+) This is not a practical use case though. + - (+) Don't use `+` to append paths, this library has a `join` function for that. + - (-) Users might use `+` out of habit though. +- (+) The `realpath` command also removes trailing slashes. +- (+) Even with a trailing slash, the path is the same, it's only an indication that it's a directory. + +
+ +## Other implementations and references + +- [Rust](https://doc.rust-lang.org/std/path/struct.Path.html) +- [Python](https://docs.python.org/3/library/pathlib.html) +- [Haskell](https://hackage.haskell.org/package/filepath-1.4.100.0/docs/System-FilePath.html) +- [Nodejs](https://nodejs.org/api/path.html) +- [POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/nframe.html) diff --git a/path/default.nix b/path/default.nix new file mode 100644 index 000000000..ce5d77599 --- /dev/null +++ b/path/default.nix @@ -0,0 +1,433 @@ +# Functions for working with paths, see ./path.md +{ lib }: +let + + inherit (builtins) + isString + isPath + split + match + typeOf + ; + + inherit (lib.lists) + length + head + last + genList + elemAt + all + concatMap + foldl' + take + drop + ; + + inherit (lib.strings) + concatStringsSep + substring + ; + + inherit (lib.asserts) + assertMsg + ; + + inherit (lib.path.subpath) + isValid + ; + + # Return the reason why a subpath is invalid, or `null` if it's valid + subpathInvalidReason = value: + if ! isString value then + "The given value is of type ${builtins.typeOf value}, but a string was expected" + else if value == "" then + "The given string is empty" + else if substring 0 1 value == "/" then + "The given string \"${value}\" starts with a `/`, representing an absolute path" + # We don't support ".." components, see ./path.md#parent-directory + else if match "(.*/)?\\.\\.(/.*)?" value != null then + "The given string \"${value}\" contains a `..` component, which is not allowed in subpaths" + else null; + + # Split and normalise a relative path string into its components. + # Error for ".." components and doesn't include "." components + splitRelPath = path: + let + # Split the string into its parts using regex for efficiency. This regex + # matches patterns like "/", "/./", "/././", with arbitrarily many "/"s + # together. These are the main special cases: + # - Leading "./" gets split into a leading "." part + # - Trailing "/." or "/" get split into a trailing "." or "" + # part respectively + # + # These are the only cases where "." and "" parts can occur + parts = split "/+(\\./+)*" path; + + # `split` creates a list of 2 * k + 1 elements, containing the k + + # 1 parts, interleaved with k matches where k is the number of + # (non-overlapping) matches. This calculation here gets the number of parts + # back from the list length + # floor( (2 * k + 1) / 2 ) + 1 == floor( k + 1/2 ) + 1 == k + 1 + partCount = length parts / 2 + 1; + + # To assemble the final list of components we want to: + # - Skip a potential leading ".", normalising "./foo" to "foo" + # - Skip a potential trailing "." or "", normalising "foo/" and "foo/." to + # "foo". See ./path.md#trailing-slashes + skipStart = if head parts == "." then 1 else 0; + skipEnd = if last parts == "." || last parts == "" then 1 else 0; + + # We can now know the length of the result by removing the number of + # skipped parts from the total number + componentCount = partCount - skipEnd - skipStart; + + in + # Special case of a single "." path component. Such a case leaves a + # componentCount of -1 due to the skipStart/skipEnd not verifying that + # they don't refer to the same character + if path == "." then [] + + # Generate the result list directly. This is more efficient than a + # combination of `filter`, `init` and `tail`, because here we don't + # allocate any intermediate lists + else genList (index: + # To get to the element we need to add the number of parts we skip and + # multiply by two due to the interleaved layout of `parts` + elemAt parts ((skipStart + index) * 2) + ) componentCount; + + # Join relative path components together + joinRelPath = components: + # Always return relative paths with `./` as a prefix (./path.md#leading-dots-for-relative-paths) + "./" + + # An empty string is not a valid relative path, so we need to return a `.` when we have no components + (if components == [] then "." else concatStringsSep "/" components); + + # Deconstruct a path value type into: + # - root: The filesystem root of the path, generally `/` + # - components: All the path's components + # + # This is similar to `splitString "/" (toString path)` but safer + # because it can distinguish different filesystem roots + deconstructPath = + let + recurse = components: path: + # If the parent of a path is the path itself, then it's a filesystem root + if path == dirOf path then { root = path; inherit components; } + else recurse ([ (baseNameOf path) ] ++ components) (dirOf path); + in recurse []; + + # Used as an abstraction between `hasPrefix`, `hasProperPrefix` and `removePrefix` + # + # Takes four arguments: + # - context: A string describing the callee, for error messages + # - continue: A function with four arguments + # - path1: A path + # - path2: Another path + # + # The function checks whether `path1` is a path and computes its + # decomposition (`deconPath1`) before taking `path2` as an argument, which + # allows the computation to be cached in a thunk between multiple calls. + # With `path2` also provided, it checks whether it's also a path, computes + # the decomposition (`deconPath2`), checks whether both paths have the same + # filesystem root, and then calls `continue path1 deconPath1 path2 deconPath2`, + # allowing the caller to decide what to do with these values. + withTwoDeconstructedPaths = context: continue: + path1: + assert assertMsg + (isPath path1) + "${context}: First argument is of type ${typeOf path1}, but a path was expected"; + let + deconPath1 = deconstructPath path1; + in + path2: + assert assertMsg + (isPath path2) + "${context}: Second argument is of type ${typeOf path2}, but a path was expected"; + let + deconPath2 = deconstructPath path2; + in + assert assertMsg + (deconPath1.root == deconPath2.root) '' + ${context}: Filesystem roots must be the same for both paths, but paths with different roots were given: + first argument: "${toString path1}" (root "${toString deconPath1.root}") + second argument: "${toString path2}" (root "${toString deconPath2.root}")''; + continue path1 deconPath1 path2 deconPath2; + +in /* No rec! Add dependencies on this file at the top. */ { + + /* Append a subpath string to a path. + + Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results. + More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"), + and that the second argument is a valid subpath string (see `lib.path.subpath.isValid`). + + Type: + append :: Path -> String -> Path + + Example: + append /foo "bar/baz" + => /foo/bar/baz + + # subpaths don't need to be normalised + append /foo "./bar//baz/./" + => /foo/bar/baz + + # can append to root directory + append /. "foo/bar" + => /foo/bar + + # first argument needs to be a path value type + append "/foo" "bar" + => + + # second argument needs to be a valid subpath string + append /foo /bar + => + append /foo "" + => + append /foo "/bar" + => + append /foo "../bar" + => + */ + append = + # The absolute path to append to + path: + # The subpath string to append + subpath: + assert assertMsg (isPath path) '' + lib.path.append: The first argument is of type ${builtins.typeOf path}, but a path was expected''; + assert assertMsg (isValid subpath) '' + lib.path.append: Second argument is not a valid subpath string: + ${subpathInvalidReason subpath}''; + path + ("/" + subpath); + + hasPrefix = withTwoDeconstructedPaths "lib.path.hasPrefix" (prefix: deconPrefix: path: deconPath: + take (length deconPrefix.components) deconPath.components == deconPrefix.components + ); + + removePrefix = withTwoDeconstructedPaths "lib.path.removePrefix" (prefix: deconPrefix: path: deconPath: + if take (length deconPrefix.components) deconPath.components == deconPrefix.components + then drop (length deconPrefix.components) deconPath.components + else throw '' + lib.path.removePrefix: The first prefix path argument (${toString prefix}) is not a prefix of the second path argument (${toString path})'' + ); + + /* Whether a value is a valid subpath string. + + - The value is a string + + - The string is not empty + + - The string doesn't start with a `/` + + - The string doesn't contain any `..` path components + + Type: + subpath.isValid :: String -> Bool + + Example: + # Not a string + subpath.isValid null + => false + + # Empty string + subpath.isValid "" + => false + + # Absolute path + subpath.isValid "/foo" + => false + + # Contains a `..` path component + subpath.isValid "../foo" + => false + + # Valid subpath + subpath.isValid "foo/bar" + => true + + # Doesn't need to be normalised + subpath.isValid "./foo//bar/" + => true + */ + subpath.isValid = + # The value to check + value: + subpathInvalidReason value == null; + + + /* Join subpath strings together using `/`, returning a normalised subpath string. + + Like `concatStringsSep "/"` but safer, specifically: + + - All elements must be valid subpath strings, see `lib.path.subpath.isValid` + + - The result gets normalised, see `lib.path.subpath.normalise` + + - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."` + + Laws: + + - Associativity: + + subpath.join [ x (subpath.join [ y z ]) ] == subpath.join [ (subpath.join [ x y ]) z ] + + - Identity - `"./."` is the neutral element for normalised paths: + + subpath.join [ ] == "./." + subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p + subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p + + - Normalisation - the result is normalised according to `lib.path.subpath.normalise`: + + subpath.join ps == subpath.normalise (subpath.join ps) + + - For non-empty lists, the implementation is equivalent to normalising the result of `concatStringsSep "/"`. + Note that the above laws can be derived from this one. + + ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps) + + Type: + subpath.join :: [ String ] -> String + + Example: + subpath.join [ "foo" "bar/baz" ] + => "./foo/bar/baz" + + # normalise the result + subpath.join [ "./foo" "." "bar//./baz/" ] + => "./foo/bar/baz" + + # passing an empty list results in the current directory + subpath.join [ ] + => "./." + + # elements must be valid subpath strings + subpath.join [ /foo ] + => + subpath.join [ "" ] + => + subpath.join [ "/foo" ] + => + subpath.join [ "../foo" ] + => + */ + subpath.join = + # The list of subpaths to join together + subpaths: + # Fast in case all paths are valid + if all isValid subpaths + then joinRelPath (concatMap splitRelPath subpaths) + else + # Otherwise we take our time to gather more info for a better error message + # Strictly go through each path, throwing on the first invalid one + # Tracks the list index in the fold accumulator + foldl' (i: path: + if isValid path + then i + 1 + else throw '' + lib.path.subpath.join: Element at index ${toString i} is not a valid subpath string: + ${subpathInvalidReason path}'' + ) 0 subpaths; + + /* Normalise a subpath. Throw an error if the subpath isn't valid, see + `lib.path.subpath.isValid` + + - Limit repeating `/` to a single one + + - Remove redundant `.` components + + - Remove trailing `/` and `/.` + + - Add leading `./` + + Laws: + + - Idempotency - normalising multiple times gives the same result: + + subpath.normalise (subpath.normalise p) == subpath.normalise p + + - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node: + + subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q}) + + - Don't change the result when appended to a Nix path value: + + base + ("/" + p) == base + ("/" + subpath.normalise p) + + - Don't change the path according to `realpath`: + + $(realpath ${p}) == $(realpath ${subpath.normalise p}) + + - Only error on invalid subpaths: + + builtins.tryEval (subpath.normalise p)).success == subpath.isValid p + + Type: + subpath.normalise :: String -> String + + Example: + # limit repeating `/` to a single one + subpath.normalise "foo//bar" + => "./foo/bar" + + # remove redundant `.` components + subpath.normalise "foo/./bar" + => "./foo/bar" + + # add leading `./` + subpath.normalise "foo/bar" + => "./foo/bar" + + # remove trailing `/` + subpath.normalise "foo/bar/" + => "./foo/bar" + + # remove trailing `/.` + subpath.normalise "foo/bar/." + => "./foo/bar" + + # Return the current directory as `./.` + subpath.normalise "." + => "./." + + # error on `..` path components + subpath.normalise "foo/../bar" + => + + # error on empty string + subpath.normalise "" + => + + # error on absolute path + subpath.normalise "/foo" + => + */ + subpath.normalise = + # The subpath string to normalise + subpath: + assert assertMsg (isValid subpath) '' + lib.path.subpath.normalise: Argument is not a valid subpath string: + ${subpathInvalidReason subpath}''; + joinRelPath (splitRelPath subpath); + + commonAncestor = a: b: + let + a' = deconstructPath a; + b' = deconstructPath b; + in + if a'.root != b'.root then + throw "lib.path.commonAncestor: Given paths don't have the same filesystem root" + else + a'.root + ("/" + concatStringsSep "/" (lib.lists.commonPrefix a'.components b'.components)); + + deconstruct = path: deconstructPath path; + construct = { root, components }: root + ("/" + concatStringsSep "/" components); + + components = { + toSubpath = joinRelPath; + fromSubpath = splitRelPath; + }; +} diff --git a/path/tests/default.nix b/path/tests/default.nix new file mode 100644 index 000000000..9a31e4282 --- /dev/null +++ b/path/tests/default.nix @@ -0,0 +1,34 @@ +{ + nixpkgs ? ../../.., + system ? builtins.currentSystem, + pkgs ? import nixpkgs { + config = {}; + overlays = []; + inherit system; + }, + libpath ? ../.., + # Random seed + seed ? null, +}: +pkgs.runCommand "lib-path-tests" { + nativeBuildInputs = with pkgs; [ + nix + jq + bc + ]; +} '' + # Needed to make Nix evaluation work + export NIX_STATE_DIR=$(mktemp -d) + + cp -r ${libpath} lib + export TEST_LIB=$PWD/lib + + echo "Running unit tests lib/path/tests/unit.nix" + nix-instantiate --eval lib/path/tests/unit.nix \ + --argstr libpath "$TEST_LIB" + + echo "Running property tests lib/path/tests/prop.sh" + bash lib/path/tests/prop.sh ${toString seed} + + touch $out +'' diff --git a/path/tests/generate.awk b/path/tests/generate.awk new file mode 100644 index 000000000..811dd0c46 --- /dev/null +++ b/path/tests/generate.awk @@ -0,0 +1,64 @@ +# Generate random path-like strings, separated by null characters. +# +# Invocation: +# +# awk -f ./generate.awk -v = | tr '\0' '\n' +# +# Customizable variables (all default to 0): +# - seed: Deterministic random seed to use for generation +# - count: Number of paths to generate +# - extradotweight: Give extra weight to dots being generated +# - extraslashweight: Give extra weight to slashes being generated +# - extranullweight: Give extra weight to null being generated, making paths shorter +BEGIN { + # Random seed, passed explicitly for reproducibility + srand(seed) + + # Don't include special characters below 32 + minascii = 32 + # Don't include DEL at 128 + maxascii = 127 + upperascii = maxascii - minascii + + # add extra weight for ., in addition to the one weight from the ascii range + upperdot = upperascii + extradotweight + + # add extra weight for /, in addition to the one weight from the ascii range + upperslash = upperdot + extraslashweight + + # add extra weight for null, indicating the end of the string + # Must be at least 1 to have strings end at all + total = upperslash + 1 + extranullweight + + # new=1 indicates that it's a new string + new=1 + while (count > 0) { + + # Random integer between [0, total) + value = int(rand() * total) + + if (value < upperascii) { + # Ascii range + printf("%c", value + minascii) + new=0 + + } else if (value < upperdot) { + # Dot range + printf "." + new=0 + + } else if (value < upperslash) { + # If it's the start of a new path, only generate a / in 10% of cases + # This is always an invalid subpath, which is not a very interesting case + if (new && rand() > 0.1) continue + printf "/" + + } else { + # Do not generate empty strings + if (new) continue + printf "\x00" + count-- + new=1 + } + } +} diff --git a/path/tests/prop.nix b/path/tests/prop.nix new file mode 100644 index 000000000..67e5c1e9d --- /dev/null +++ b/path/tests/prop.nix @@ -0,0 +1,60 @@ +# Given a list of path-like strings, check some properties of the path library +# using those paths and return a list of attribute sets of the following form: +# +# { = ; } +# +# If `normalise` fails to evaluate, the attribute value is set to `""`. +# If not, the resulting value is normalised again and an appropriate attribute set added to the output list. +{ + # The path to the nixpkgs lib to use + libpath, + # A flat directory containing files with randomly-generated + # path-like values + dir, +}: +let + lib = import libpath; + + # read each file into a string + strings = map (name: + builtins.readFile (dir + "/${name}") + ) (builtins.attrNames (builtins.readDir dir)); + + inherit (lib.path.subpath) normalise isValid; + inherit (lib.asserts) assertMsg; + + normaliseAndCheck = str: + let + originalValid = isValid str; + + tryOnce = builtins.tryEval (normalise str); + tryTwice = builtins.tryEval (normalise tryOnce.value); + + absConcatOrig = /. + ("/" + str); + absConcatNormalised = /. + ("/" + tryOnce.value); + in + # Check the lib.path.subpath.normalise property to only error on invalid subpaths + assert assertMsg + (originalValid -> tryOnce.success) + "Even though string \"${str}\" is valid as a subpath, the normalisation for it failed"; + assert assertMsg + (! originalValid -> ! tryOnce.success) + "Even though string \"${str}\" is invalid as a subpath, the normalisation for it succeeded"; + + # Check normalisation idempotency + assert assertMsg + (originalValid -> tryTwice.success) + "For valid subpath \"${str}\", the normalisation \"${tryOnce.value}\" was not a valid subpath"; + assert assertMsg + (originalValid -> tryOnce.value == tryTwice.value) + "For valid subpath \"${str}\", normalising it once gives \"${tryOnce.value}\" but normalising it twice gives a different result: \"${tryTwice.value}\""; + + # Check that normalisation doesn't change a string when appended to an absolute Nix path value + assert assertMsg + (originalValid -> absConcatOrig == absConcatNormalised) + "For valid subpath \"${str}\", appending to an absolute Nix path value gives \"${absConcatOrig}\", but appending the normalised result \"${tryOnce.value}\" gives a different value \"${absConcatNormalised}\""; + + # Return an empty string when failed + if tryOnce.success then tryOnce.value else ""; + +in lib.genAttrs strings normaliseAndCheck diff --git a/path/tests/prop.sh b/path/tests/prop.sh new file mode 100755 index 000000000..e48c6667f --- /dev/null +++ b/path/tests/prop.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +# Property tests for the `lib.path` library +# +# It generates random path-like strings and runs the functions on +# them, checking that the expected laws of the functions hold + +set -euo pipefail +shopt -s inherit_errexit + +# https://stackoverflow.com/a/246128 +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +if test -z "${TEST_LIB:-}"; then + TEST_LIB=$SCRIPT_DIR/../.. +fi + +tmp="$(mktemp -d)" +clean_up() { + rm -rf "$tmp" +} +trap clean_up EXIT +mkdir -p "$tmp/work" +cd "$tmp/work" + +# Defaulting to a random seed but the first argument can override this +seed=${1:-$RANDOM} +echo >&2 "Using seed $seed, use \`lib/path/tests/prop.sh $seed\` to reproduce this result" + +# The number of random paths to generate. This specific number was chosen to +# be fast enough while still generating enough variety to detect bugs. +count=500 + +debug=0 +# debug=1 # print some extra info +# debug=2 # print generated values + +# Fine tuning parameters to balance the number of generated invalid paths +# to the variance in generated paths. +extradotweight=64 # Larger value: more dots +extraslashweight=64 # Larger value: more slashes +extranullweight=16 # Larger value: shorter strings + +die() { + echo >&2 "test case failed: " "$@" + exit 1 +} + +if [[ "$debug" -ge 1 ]]; then + echo >&2 "Generating $count random path-like strings" +fi + +# Read stream of null-terminated strings entry-by-entry into bash, +# write it to a file and the `strings` array. +declare -a strings=() +mkdir -p "$tmp/strings" +while IFS= read -r -d $'\0' str; do + printf "%s" "$str" > "$tmp/strings/${#strings[@]}" + strings+=("$str") +done < <(awk \ + -f "$SCRIPT_DIR"/generate.awk \ + -v seed="$seed" \ + -v count="$count" \ + -v extradotweight="$extradotweight" \ + -v extraslashweight="$extraslashweight" \ + -v extranullweight="$extranullweight") + +if [[ "$debug" -ge 1 ]]; then + echo >&2 "Trying to normalise the generated path-like strings with Nix" +fi + +# Precalculate all normalisations with a single Nix call. Calling Nix for each +# string individually would take way too long +nix-instantiate --eval --strict --json \ + --argstr libpath "$TEST_LIB" \ + --argstr dir "$tmp/strings" \ + "$SCRIPT_DIR"/prop.nix \ + >"$tmp/result.json" + +# Uses some jq magic to turn the resulting attribute set into an associative +# bash array assignment +declare -A normalised_result="($(jq ' + to_entries + | map("[\(.key | @sh)]=\(.value | @sh)") + | join(" \n")' -r < "$tmp/result.json"))" + +# Looks up a normalisation result for a string +# Checks that the normalisation is only failing iff it's an invalid subpath +# For valid subpaths, returns 0 and prints the normalisation result +# For invalid subpaths, returns 1 +normalise() { + local str=$1 + # Uses the same check for validity as in the library implementation + if [[ "$str" == "" || "$str" == /* || "$str" =~ ^(.*/)?\.\.(/.*)?$ ]]; then + valid= + else + valid=1 + fi + + normalised=${normalised_result[$str]} + # An empty string indicates failure, this is encoded in ./prop.nix + if [[ -n "$normalised" ]]; then + if [[ -n "$valid" ]]; then + echo "$normalised" + else + die "For invalid subpath \"$str\", lib.path.subpath.normalise returned this result: \"$normalised\"" + fi + else + if [[ -n "$valid" ]]; then + die "For valid subpath \"$str\", lib.path.subpath.normalise failed" + else + if [[ "$debug" -ge 2 ]]; then + echo >&2 "String \"$str\" is not a valid subpath" + fi + # Invalid and it correctly failed, we let the caller continue if they catch the exit code + return 1 + fi + fi +} + +# Intermediate result populated by test_idempotency_realpath +# and used in test_normalise_uniqueness +# +# Contains a mapping from a normalised subpath to the realpath result it represents +declare -A norm_to_real + +test_idempotency_realpath() { + if [[ "$debug" -ge 1 ]]; then + echo >&2 "Checking idempotency of each result and making sure the realpath result isn't changed" + fi + + # Count invalid subpaths to display stats + invalid=0 + for str in "${strings[@]}"; do + if ! result=$(normalise "$str"); then + ((invalid++)) || true + continue + fi + + # Check the law that it doesn't change the result of a realpath + mkdir -p -- "$str" "$result" + real_orig=$(realpath -- "$str") + real_norm=$(realpath -- "$result") + + if [[ "$real_orig" != "$real_norm" ]]; then + die "realpath of the original string \"$str\" (\"$real_orig\") is not the same as realpath of the normalisation \"$result\" (\"$real_norm\")" + fi + + if [[ "$debug" -ge 2 ]]; then + echo >&2 "String \"$str\" gets normalised to \"$result\" and file path \"$real_orig\"" + fi + norm_to_real["$result"]="$real_orig" + done + if [[ "$debug" -ge 1 ]]; then + echo >&2 "$(bc <<< "scale=1; 100 / $count * $invalid")% of the total $count generated strings were invalid subpath strings, and were therefore ignored" + fi +} + +test_normalise_uniqueness() { + if [[ "$debug" -ge 1 ]]; then + echo >&2 "Checking for the uniqueness law" + fi + + for norm_p in "${!norm_to_real[@]}"; do + real_p=${norm_to_real["$norm_p"]} + for norm_q in "${!norm_to_real[@]}"; do + real_q=${norm_to_real["$norm_q"]} + # Checks normalisation uniqueness law for each pair of values + if [[ "$norm_p" != "$norm_q" && "$real_p" == "$real_q" ]]; then + die "Normalisations \"$norm_p\" and \"$norm_q\" are different, but the realpath of them is the same: \"$real_p\"" + fi + done + done +} + +test_idempotency_realpath +test_normalise_uniqueness + +echo >&2 tests ok diff --git a/path/tests/unit.nix b/path/tests/unit.nix new file mode 100644 index 000000000..61c4ab4d6 --- /dev/null +++ b/path/tests/unit.nix @@ -0,0 +1,193 @@ +# Unit tests for lib.path functions. Use `nix-build` in this directory to +# run these +{ libpath }: +let + lib = import libpath; + inherit (lib.path) append subpath; + + cases = lib.runTests { + # Test examples from the lib.path.append documentation + testAppendExample1 = { + expr = append /foo "bar/baz"; + expected = /foo/bar/baz; + }; + testAppendExample2 = { + expr = append /foo "./bar//baz/./"; + expected = /foo/bar/baz; + }; + testAppendExample3 = { + expr = append /. "foo/bar"; + expected = /foo/bar; + }; + testAppendExample4 = { + expr = (builtins.tryEval (append "/foo" "bar")).success; + expected = false; + }; + testAppendExample5 = { + expr = (builtins.tryEval (append /foo /bar)).success; + expected = false; + }; + testAppendExample6 = { + expr = (builtins.tryEval (append /foo "")).success; + expected = false; + }; + testAppendExample7 = { + expr = (builtins.tryEval (append /foo "/bar")).success; + expected = false; + }; + testAppendExample8 = { + expr = (builtins.tryEval (append /foo "../bar")).success; + expected = false; + }; + + # Test examples from the lib.path.subpath.isValid documentation + testSubpathIsValidExample1 = { + expr = subpath.isValid null; + expected = false; + }; + testSubpathIsValidExample2 = { + expr = subpath.isValid ""; + expected = false; + }; + testSubpathIsValidExample3 = { + expr = subpath.isValid "/foo"; + expected = false; + }; + testSubpathIsValidExample4 = { + expr = subpath.isValid "../foo"; + expected = false; + }; + testSubpathIsValidExample5 = { + expr = subpath.isValid "foo/bar"; + expected = true; + }; + testSubpathIsValidExample6 = { + expr = subpath.isValid "./foo//bar/"; + expected = true; + }; + # Some extra tests + testSubpathIsValidTwoDotsEnd = { + expr = subpath.isValid "foo/.."; + expected = false; + }; + testSubpathIsValidTwoDotsMiddle = { + expr = subpath.isValid "foo/../bar"; + expected = false; + }; + testSubpathIsValidTwoDotsPrefix = { + expr = subpath.isValid "..foo"; + expected = true; + }; + testSubpathIsValidTwoDotsSuffix = { + expr = subpath.isValid "foo.."; + expected = true; + }; + testSubpathIsValidTwoDotsPrefixComponent = { + expr = subpath.isValid "foo/..bar/baz"; + expected = true; + }; + testSubpathIsValidTwoDotsSuffixComponent = { + expr = subpath.isValid "foo/bar../baz"; + expected = true; + }; + testSubpathIsValidThreeDots = { + expr = subpath.isValid "..."; + expected = true; + }; + testSubpathIsValidFourDots = { + expr = subpath.isValid "...."; + expected = true; + }; + testSubpathIsValidThreeDotsComponent = { + expr = subpath.isValid "foo/.../bar"; + expected = true; + }; + testSubpathIsValidFourDotsComponent = { + expr = subpath.isValid "foo/..../bar"; + expected = true; + }; + + # Test examples from the lib.path.subpath.join documentation + testSubpathJoinExample1 = { + expr = subpath.join [ "foo" "bar/baz" ]; + expected = "./foo/bar/baz"; + }; + testSubpathJoinExample2 = { + expr = subpath.join [ "./foo" "." "bar//./baz/" ]; + expected = "./foo/bar/baz"; + }; + testSubpathJoinExample3 = { + expr = subpath.join [ ]; + expected = "./."; + }; + testSubpathJoinExample4 = { + expr = (builtins.tryEval (subpath.join [ /foo ])).success; + expected = false; + }; + testSubpathJoinExample5 = { + expr = (builtins.tryEval (subpath.join [ "" ])).success; + expected = false; + }; + testSubpathJoinExample6 = { + expr = (builtins.tryEval (subpath.join [ "/foo" ])).success; + expected = false; + }; + testSubpathJoinExample7 = { + expr = (builtins.tryEval (subpath.join [ "../foo" ])).success; + expected = false; + }; + + # Test examples from the lib.path.subpath.normalise documentation + testSubpathNormaliseExample1 = { + expr = subpath.normalise "foo//bar"; + expected = "./foo/bar"; + }; + testSubpathNormaliseExample2 = { + expr = subpath.normalise "foo/./bar"; + expected = "./foo/bar"; + }; + testSubpathNormaliseExample3 = { + expr = subpath.normalise "foo/bar"; + expected = "./foo/bar"; + }; + testSubpathNormaliseExample4 = { + expr = subpath.normalise "foo/bar/"; + expected = "./foo/bar"; + }; + testSubpathNormaliseExample5 = { + expr = subpath.normalise "foo/bar/."; + expected = "./foo/bar"; + }; + testSubpathNormaliseExample6 = { + expr = subpath.normalise "."; + expected = "./."; + }; + testSubpathNormaliseExample7 = { + expr = (builtins.tryEval (subpath.normalise "foo/../bar")).success; + expected = false; + }; + testSubpathNormaliseExample8 = { + expr = (builtins.tryEval (subpath.normalise "")).success; + expected = false; + }; + testSubpathNormaliseExample9 = { + expr = (builtins.tryEval (subpath.normalise "/foo")).success; + expected = false; + }; + # Some extra tests + testSubpathNormaliseIsValidDots = { + expr = subpath.normalise "./foo/.bar/.../baz...qux"; + expected = "./foo/.bar/.../baz...qux"; + }; + testSubpathNormaliseWrongType = { + expr = (builtins.tryEval (subpath.normalise null)).success; + expected = false; + }; + testSubpathNormaliseTwoDots = { + expr = (builtins.tryEval (subpath.normalise "..")).success; + expected = false; + }; + }; +in + if cases == [] then "Unit tests successful" + else throw "Path unit tests failed: ${lib.generators.toPretty {} cases}" diff --git a/source-types.nix b/source-types.nix new file mode 100644 index 000000000..c4f263dcf --- /dev/null +++ b/source-types.nix @@ -0,0 +1,19 @@ +{ lib }: + +let + defaultSourceType = tname: { + shortName = tname; + isSource = false; + }; +in lib.mapAttrs (tname: tset: defaultSourceType tname // tset) { + + fromSource = { + isSource = true; + }; + + binaryNativeCode = {}; + + binaryBytecode = {}; + + binaryFirmware = {}; +} diff --git a/sources.nix b/sources.nix new file mode 100644 index 000000000..d990777c6 --- /dev/null +++ b/sources.nix @@ -0,0 +1,292 @@ +# Functions for copying sources to the Nix store. +{ lib }: + +# Tested in lib/tests/sources.sh +let + inherit (builtins) + match + readDir + split + storeDir + tryEval + ; + inherit (lib) + boolToString + filter + getAttr + isString + pathExists + readFile + ; + inherit (lib.filesystem) + pathType + pathIsDirectory + pathIsRegularFile + ; + + /* + A basic filter for `cleanSourceWith` that removes + directories of version control system, backup files (*~) + and some generated files. + */ + cleanSourceFilter = name: type: let baseName = baseNameOf (toString name); in ! ( + # Filter out version control software files/directories + (baseName == ".git" || type == "directory" && (baseName == ".svn" || baseName == "CVS" || baseName == ".hg")) || + # Filter out editor backup / swap files. + lib.hasSuffix "~" baseName || + match "^\\.sw[a-z]$" baseName != null || + match "^\\..*\\.sw[a-z]$" baseName != null || + + # Filter out generates files. + lib.hasSuffix ".o" baseName || + lib.hasSuffix ".so" baseName || + # Filter out nix-build result symlinks + (type == "symlink" && lib.hasPrefix "result" baseName) || + # Filter out sockets and other types of files we can't have in the store. + (type == "unknown") + ); + + /* + Filters a source tree removing version control files and directories using cleanSourceFilter. + + Example: + cleanSource ./. + */ + cleanSource = src: cleanSourceWith { filter = cleanSourceFilter; inherit src; }; + + /* + Like `builtins.filterSource`, except it will compose with itself, + allowing you to chain multiple calls together without any + intermediate copies being put in the nix store. + + Example: + lib.cleanSourceWith { + filter = f; + src = lib.cleanSourceWith { + filter = g; + src = ./.; + }; + } + # Succeeds! + + builtins.filterSource f (builtins.filterSource g ./.) + # Fails! + + */ + cleanSourceWith = + { + # A path or cleanSourceWith result to filter and/or rename. + src, + # Optional with default value: constant true (include everything) + # The function will be combined with the && operator such + # that src.filter is called lazily. + # For implementing a filter, see + # https://nixos.org/nix/manual/#builtin-filterSource + # Type: A function (path -> type -> bool) + filter ? _path: _type: true, + # Optional name to use as part of the store path. + # This defaults to `src.name` or otherwise `"source"`. + name ? null + }: + let + orig = toSourceAttributes src; + in fromSourceAttributes { + inherit (orig) origSrc; + filter = path: type: filter path type && orig.filter path type; + name = if name != null then name else orig.name; + }; + + /* + Add logging to a source, for troubleshooting the filtering behavior. + Type: + sources.trace :: sourceLike -> Source + */ + trace = + # Source to debug. The returned source will behave like this source, but also log its filter invocations. + src: + let + attrs = toSourceAttributes src; + in + fromSourceAttributes ( + attrs // { + filter = path: type: + let + r = attrs.filter path type; + in + builtins.trace "${attrs.name}.filter ${path} = ${boolToString r}" r; + } + ) // { + satisfiesSubpathInvariant = src ? satisfiesSubpathInvariant && src.satisfiesSubpathInvariant; + }; + + /* + Filter sources by a list of regular expressions. + + Example: src = sourceByRegex ./my-subproject [".*\.py$" "^database.sql$"] + */ + sourceByRegex = src: regexes: + let + isFiltered = src ? _isLibCleanSourceWith; + origSrc = if isFiltered then src.origSrc else src; + in lib.cleanSourceWith { + filter = (path: type: + let relPath = lib.removePrefix (toString origSrc + "/") (toString path); + in lib.any (re: match re relPath != null) regexes); + inherit src; + }; + + /* + Get all files ending with the specified suffices from the given + source directory or its descendants, omitting files that do not match + any suffix. The result of the example below will include files like + `./dir/module.c` and `./dir/subdir/doc.xml` if present. + + Type: sourceLike -> [String] -> Source + + Example: + sourceFilesBySuffices ./. [ ".xml" ".c" ] + */ + sourceFilesBySuffices = + # Path or source containing the files to be returned + src: + # A list of file suffix strings + exts: + let filter = name: type: + let base = baseNameOf (toString name); + in type == "directory" || lib.any (ext: lib.hasSuffix ext base) exts; + in cleanSourceWith { inherit filter src; }; + + pathIsGitRepo = path: (_commitIdFromGitRepoOrError path)?value; + + /* + Get the commit id of a git repo. + + Example: commitIdFromGitRepo + */ + commitIdFromGitRepo = path: + let commitIdOrError = _commitIdFromGitRepoOrError path; + in commitIdOrError.value or (throw commitIdOrError.error); + + # Get the commit id of a git repo. + + # Returns `{ value = commitHash }` or `{ error = "... message ..." }`. + + # Example: commitIdFromGitRepo + # not exported, used for commitIdFromGitRepo + _commitIdFromGitRepoOrError = + let readCommitFromFile = file: path: + let fileName = path + "/${file}"; + packedRefsName = path + "/packed-refs"; + absolutePath = base: path: + if lib.hasPrefix "/" path + then path + else toString (/. + "${base}/${path}"); + in if pathIsRegularFile path + # Resolve git worktrees. See gitrepository-layout(5) + then + let m = match "^gitdir: (.*)$" (lib.fileContents path); + in if m == null + then { error = "File contains no gitdir reference: " + path; } + else + let gitDir = absolutePath (dirOf path) (lib.head m); + commonDir'' = if pathIsRegularFile "${gitDir}/commondir" + then lib.fileContents "${gitDir}/commondir" + else gitDir; + commonDir' = lib.removeSuffix "/" commonDir''; + commonDir = absolutePath gitDir commonDir'; + refFile = lib.removePrefix "${commonDir}/" "${gitDir}/${file}"; + in readCommitFromFile refFile commonDir + + else if pathIsRegularFile fileName + # Sometimes git stores the commitId directly in the file but + # sometimes it stores something like: «ref: refs/heads/branch-name» + then + let fileContent = lib.fileContents fileName; + matchRef = match "^ref: (.*)$" fileContent; + in if matchRef == null + then { value = fileContent; } + else readCommitFromFile (lib.head matchRef) path + + else if pathIsRegularFile packedRefsName + # Sometimes, the file isn't there at all and has been packed away in the + # packed-refs file, so we have to grep through it: + then + let fileContent = readFile packedRefsName; + matchRef = match "([a-z0-9]+) ${file}"; + isRef = s: isString s && (matchRef s) != null; + # there is a bug in libstdc++ leading to stackoverflow for long strings: + # https://github.com/NixOS/nix/issues/2147#issuecomment-659868795 + refs = filter isRef (split "\n" fileContent); + in if refs == [] + then { error = "Could not find " + file + " in " + packedRefsName; } + else { value = lib.head (matchRef (lib.head refs)); } + + else { error = "Not a .git directory: " + toString path; }; + in readCommitFromFile "HEAD"; + + pathHasContext = builtins.hasContext or (lib.hasPrefix storeDir); + + canCleanSource = src: src ? _isLibCleanSourceWith || !(pathHasContext (toString src)); + + # -------------------------------------------------------------------------- # + # Internal functions + # + + # toSourceAttributes : sourceLike -> SourceAttrs + # + # Convert any source-like object into a simple, singular representation. + # We don't expose this representation in order to avoid having a fifth path- + # like class of objects in the wild. + # (Existing ones being: paths, strings, sources and x//{outPath}) + # So instead of exposing internals, we build a library of combinator functions. + toSourceAttributes = src: + let + isFiltered = src ? _isLibCleanSourceWith; + in + { + # The original path + origSrc = if isFiltered then src.origSrc else src; + filter = if isFiltered then src.filter else _: _: true; + name = if isFiltered then src.name else "source"; + }; + + # fromSourceAttributes : SourceAttrs -> Source + # + # Inverse of toSourceAttributes for Source objects. + fromSourceAttributes = { origSrc, filter, name }: + { + _isLibCleanSourceWith = true; + inherit origSrc filter name; + outPath = builtins.path { inherit filter name; path = origSrc; }; + }; + +in { + + pathType = lib.warnIf (lib.isInOldestRelease 2305) + "lib.sources.pathType has been moved to lib.filesystem.pathType." + lib.filesystem.pathType; + + pathIsDirectory = lib.warnIf (lib.isInOldestRelease 2305) + "lib.sources.pathIsDirectory has been moved to lib.filesystem.pathIsDirectory." + lib.filesystem.pathIsDirectory; + + pathIsRegularFile = lib.warnIf (lib.isInOldestRelease 2305) + "lib.sources.pathIsRegularFile has been moved to lib.filesystem.pathIsRegularFile." + lib.filesystem.pathIsRegularFile; + + inherit + pathIsGitRepo + commitIdFromGitRepo + + cleanSource + cleanSourceWith + cleanSourceFilter + pathHasContext + canCleanSource + + sourceByRegex + sourceFilesBySuffices + + trace + ; +} diff --git a/strings-with-deps.nix b/strings-with-deps.nix new file mode 100644 index 000000000..7b88b018d --- /dev/null +++ b/strings-with-deps.nix @@ -0,0 +1,84 @@ +{ lib }: +/* +Usage: + + You define you custom builder script by adding all build steps to a list. + for example: + builder = writeScript "fsg-4.4-builder" + (textClosure [doUnpack addInputs preBuild doMake installPhase doForceShare]); + + a step is defined by noDepEntry, fullDepEntry or packEntry. + To ensure that prerequisite are met those are added before the task itself by + textClosureDupList. Duplicated items are removed again. + + See trace/nixpkgs/trunk/pkgs/top-level/builder-defs.nix for some predefined build steps + + Attention: + + let + pkgs = (import ) {}; + in let + inherit (pkgs.stringsWithDeps) fullDepEntry packEntry noDepEntry textClosureMap; + inherit (pkgs.lib) id; + + nameA = noDepEntry "Text a"; + nameB = fullDepEntry "Text b" ["nameA"]; + nameC = fullDepEntry "Text c" ["nameA"]; + + stages = { + nameHeader = noDepEntry "#! /bin/sh \n"; + inherit nameA nameB nameC; + }; + in + textClosureMap id stages + [ "nameHeader" "nameA" "nameB" "nameC" + nameC # <- added twice. add a dep entry if you know that it will be added once only [1] + "nameB" # <- this will not be added again because the attr name (reference) is used + ] + + # result: Str("#! /bin/sh \n\nText a\nText b\nText c\nText c",[]) + + [1] maybe this behaviour should be removed to keep things simple (?) +*/ + +let + inherit (lib) + concatStringsSep + head + isAttrs + listToAttrs + tail + ; +in +rec { + + /* !!! The interface of this function is kind of messed up, since + it's way too overloaded and almost but not quite computes a + topological sort of the depstrings. */ + + textClosureList = predefined: arg: + let + f = done: todo: + if todo == [] then {result = []; inherit done;} + else + let entry = head todo; in + if isAttrs entry then + let x = f done entry.deps; + y = f x.done (tail todo); + in { result = x.result ++ [entry.text] ++ y.result; + done = y.done; + } + else if done ? ${entry} then f done (tail todo) + else f (done // listToAttrs [{name = entry; value = 1;}]) ([predefined.${entry}] ++ tail todo); + in (f {} arg).result; + + textClosureMap = f: predefined: names: + concatStringsSep "\n" (map f (textClosureList predefined names)); + + noDepEntry = text: {inherit text; deps = [];}; + fullDepEntry = text: deps: {inherit text deps;}; + packEntry = deps: {inherit deps; text="";}; + + stringAfter = deps: text: { inherit text deps; }; + +} diff --git a/strings.nix b/strings.nix new file mode 100644 index 000000000..e875520c6 --- /dev/null +++ b/strings.nix @@ -0,0 +1,1243 @@ +/* String manipulation functions. */ +{ lib }: +let + + inherit (builtins) length; + + inherit (lib.trivial) warnIf; + +asciiTable = import ./ascii-table.nix; + +in + +rec { + + inherit (builtins) + compareVersions + elem + elemAt + filter + fromJSON + head + isInt + isList + isAttrs + isPath + isString + match + parseDrvName + readFile + replaceStrings + split + storeDir + stringLength + substring + tail + toJSON + typeOf + unsafeDiscardStringContext + ; + + /* Concatenate a list of strings. + + Type: concatStrings :: [string] -> string + + Example: + concatStrings ["foo" "bar"] + => "foobar" + */ + concatStrings = builtins.concatStringsSep ""; + + /* Map a function over a list and concatenate the resulting strings. + + Type: concatMapStrings :: (a -> string) -> [a] -> string + + Example: + concatMapStrings (x: "a" + x) ["foo" "bar"] + => "afooabar" + */ + concatMapStrings = f: list: concatStrings (map f list); + + /* Like `concatMapStrings` except that the f functions also gets the + position as a parameter. + + Type: concatImapStrings :: (int -> a -> string) -> [a] -> string + + Example: + concatImapStrings (pos: x: "${toString pos}-${x}") ["foo" "bar"] + => "1-foo2-bar" + */ + concatImapStrings = f: list: concatStrings (lib.imap1 f list); + + /* Place an element between each element of a list + + Type: intersperse :: a -> [a] -> [a] + + Example: + intersperse "/" ["usr" "local" "bin"] + => ["usr" "/" "local" "/" "bin"]. + */ + intersperse = + # Separator to add between elements + separator: + # Input list + list: + if list == [] || length list == 1 + then list + else tail (lib.concatMap (x: [separator x]) list); + + /* Concatenate a list of strings with a separator between each element + + Type: concatStringsSep :: string -> [string] -> string + + Example: + concatStringsSep "/" ["usr" "local" "bin"] + => "usr/local/bin" + */ + concatStringsSep = builtins.concatStringsSep or (separator: list: + lib.foldl' (x: y: x + y) "" (intersperse separator list)); + + /* Maps a function over a list of strings and then concatenates the + result with the specified separator interspersed between + elements. + + Type: concatMapStringsSep :: string -> (a -> string) -> [a] -> string + + Example: + concatMapStringsSep "-" (x: toUpper x) ["foo" "bar" "baz"] + => "FOO-BAR-BAZ" + */ + concatMapStringsSep = + # Separator to add between elements + sep: + # Function to map over the list + f: + # List of input strings + list: concatStringsSep sep (map f list); + + /* Same as `concatMapStringsSep`, but the mapping function + additionally receives the position of its argument. + + Type: concatIMapStringsSep :: string -> (int -> a -> string) -> [a] -> string + + Example: + concatImapStringsSep "-" (pos: x: toString (x / pos)) [ 6 6 6 ] + => "6-3-2" + */ + concatImapStringsSep = + # Separator to add between elements + sep: + # Function that receives elements and their positions + f: + # List of input strings + list: concatStringsSep sep (lib.imap1 f list); + + /* Concatenate a list of strings, adding a newline at the end of each one. + Defined as `concatMapStrings (s: s + "\n")`. + + Type: concatLines :: [string] -> string + + Example: + concatLines [ "foo" "bar" ] + => "foo\nbar\n" + */ + concatLines = concatMapStrings (s: s + "\n"); + + /* Construct a Unix-style, colon-separated search path consisting of + the given `subDir` appended to each of the given paths. + + Type: makeSearchPath :: string -> [string] -> string + + Example: + makeSearchPath "bin" ["/root" "/usr" "/usr/local"] + => "/root/bin:/usr/bin:/usr/local/bin" + makeSearchPath "bin" [""] + => "/bin" + */ + makeSearchPath = + # Directory name to append + subDir: + # List of base paths + paths: + concatStringsSep ":" (map (path: path + "/" + subDir) (filter (x: x != null) paths)); + + /* Construct a Unix-style search path by appending the given + `subDir` to the specified `output` of each of the packages. If no + output by the given name is found, fallback to `.out` and then to + the default. + + Type: string -> string -> [package] -> string + + Example: + makeSearchPathOutput "dev" "bin" [ pkgs.openssl pkgs.zlib ] + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-dev/bin:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8/bin" + */ + makeSearchPathOutput = + # Package output to use + output: + # Directory name to append + subDir: + # List of packages + pkgs: makeSearchPath subDir (map (lib.getOutput output) pkgs); + + /* Construct a library search path (such as RPATH) containing the + libraries for a set of packages + + Example: + makeLibraryPath [ "/usr" "/usr/local" ] + => "/usr/lib:/usr/local/lib" + pkgs = import { } + makeLibraryPath [ pkgs.openssl pkgs.zlib ] + => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r/lib:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8/lib" + */ + makeLibraryPath = makeSearchPathOutput "lib" "lib"; + + /* Construct a binary search path (such as $PATH) containing the + binaries for a set of packages. + + Example: + makeBinPath ["/root" "/usr" "/usr/local"] + => "/root/bin:/usr/bin:/usr/local/bin" + */ + makeBinPath = makeSearchPathOutput "bin" "bin"; + + /* Normalize path, removing extraneous /s + + Type: normalizePath :: string -> string + + Example: + normalizePath "/a//b///c/" + => "/a/b/c/" + */ + normalizePath = s: + warnIf + (isPath s) + '' + lib.strings.normalizePath: The argument (${toString s}) is a path value, but only strings are supported. + Path values are always normalised in Nix, so there's no need to call this function on them. + This function also copies the path to the Nix store and returns the store path, the same as "''${path}" will, which may not be what you want. + This behavior is deprecated and will throw an error in the future.'' + ( + builtins.foldl' + (x: y: if y == "/" && hasSuffix "/" x then x else x+y) + "" + (stringToCharacters s) + ); + + /* Depending on the boolean `cond', return either the given string + or the empty string. Useful to concatenate against a bigger string. + + Type: optionalString :: bool -> string -> string + + Example: + optionalString true "some-string" + => "some-string" + optionalString false "some-string" + => "" + */ + optionalString = + # Condition + cond: + # String to return if condition is true + string: if cond then string else ""; + + /* Determine whether a string has given prefix. + + Type: hasPrefix :: string -> string -> bool + + Example: + hasPrefix "foo" "foobar" + => true + hasPrefix "foo" "barfoo" + => false + */ + hasPrefix = + # Prefix to check for + pref: + # Input string + str: + # Before 23.05, paths would be copied to the store before converting them + # to strings and comparing. This was surprising and confusing. + warnIf + (isPath pref) + '' + lib.strings.hasPrefix: The first argument (${toString pref}) is a path value, but only strings are supported. + There is almost certainly a bug in the calling code, since this function always returns `false` in such a case. + This function also copies the path to the Nix store, which may not be what you want. + This behavior is deprecated and will throw an error in the future.'' + (substring 0 (stringLength pref) str == pref); + + /* Determine whether a string has given suffix. + + Type: hasSuffix :: string -> string -> bool + + Example: + hasSuffix "foo" "foobar" + => false + hasSuffix "foo" "barfoo" + => true + */ + hasSuffix = + # Suffix to check for + suffix: + # Input string + content: + let + lenContent = stringLength content; + lenSuffix = stringLength suffix; + in + # Before 23.05, paths would be copied to the store before converting them + # to strings and comparing. This was surprising and confusing. + warnIf + (isPath suffix) + '' + lib.strings.hasSuffix: The first argument (${toString suffix}) is a path value, but only strings are supported. + There is almost certainly a bug in the calling code, since this function always returns `false` in such a case. + This function also copies the path to the Nix store, which may not be what you want. + This behavior is deprecated and will throw an error in the future.'' + ( + lenContent >= lenSuffix + && substring (lenContent - lenSuffix) lenContent content == suffix + ); + + /* Determine whether a string contains the given infix + + Type: hasInfix :: string -> string -> bool + + Example: + hasInfix "bc" "abcd" + => true + hasInfix "ab" "abcd" + => true + hasInfix "cd" "abcd" + => true + hasInfix "foo" "abcd" + => false + */ + hasInfix = infix: content: + # Before 23.05, paths would be copied to the store before converting them + # to strings and comparing. This was surprising and confusing. + warnIf + (isPath infix) + '' + lib.strings.hasInfix: The first argument (${toString infix}) is a path value, but only strings are supported. + There is almost certainly a bug in the calling code, since this function always returns `false` in such a case. + This function also copies the path to the Nix store, which may not be what you want. + This behavior is deprecated and will throw an error in the future.'' + (builtins.match ".*${escapeRegex infix}.*" "${content}" != null); + + /* Convert a string to a list of characters (i.e. singleton strings). + This allows you to, e.g., map a function over each character. However, + note that this will likely be horribly inefficient; Nix is not a + general purpose programming language. Complex string manipulations + should, if appropriate, be done in a derivation. + Also note that Nix treats strings as a list of bytes and thus doesn't + handle unicode. + + Type: stringToCharacters :: string -> [string] + + Example: + stringToCharacters "" + => [ ] + stringToCharacters "abc" + => [ "a" "b" "c" ] + stringToCharacters "🦄" + => [ "�" "�" "�" "�" ] + */ + stringToCharacters = s: + map (p: substring p 1 s) (lib.range 0 (stringLength s - 1)); + + /* Manipulate a string character by character and replace them by + strings before concatenating the results. + + Type: stringAsChars :: (string -> string) -> string -> string + + Example: + stringAsChars (x: if x == "a" then "i" else x) "nax" + => "nix" + */ + stringAsChars = + # Function to map over each individual character + f: + # Input string + s: concatStrings ( + map f (stringToCharacters s) + ); + + /* Convert char to ascii value, must be in printable range + + Type: charToInt :: string -> int + + Example: + charToInt "A" + => 65 + charToInt "(" + => 40 + + */ + charToInt = c: builtins.getAttr c asciiTable; + + /* Escape occurrence of the elements of `list` in `string` by + prefixing it with a backslash. + + Type: escape :: [string] -> string -> string + + Example: + escape ["(" ")"] "(foo)" + => "\\(foo\\)" + */ + escape = list: replaceStrings list (map (c: "\\${c}") list); + + /* Escape occurrence of the element of `list` in `string` by + converting to its ASCII value and prefixing it with \\x. + Only works for printable ascii characters. + + Type: escapeC = [string] -> string -> string + + Example: + escapeC [" "] "foo bar" + => "foo\\x20bar" + + */ + escapeC = list: replaceStrings list (map (c: "\\x${ toLower (lib.toHexString (charToInt c))}") list); + + /* Escape the string so it can be safely placed inside a URL + query. + + Type: escapeURL :: string -> string + + Example: + escapeURL "foo/bar baz" + => "foo%2Fbar%20baz" + */ + escapeURL = let + unreserved = [ "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S" "T" "U" "V" "W" "X" "Y" "Z" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "-" "_" "." "~" ]; + toEscape = builtins.removeAttrs asciiTable unreserved; + in + replaceStrings (builtins.attrNames toEscape) (lib.mapAttrsToList (_: c: "%${fixedWidthString 2 "0" (lib.toHexString c)}") toEscape); + + /* Quote string to be used safely within the Bourne shell. + + Type: escapeShellArg :: string -> string + + Example: + escapeShellArg "esc'ape\nme" + => "'esc'\\''ape\nme'" + */ + escapeShellArg = arg: "'${replaceStrings ["'"] ["'\\''"] (toString arg)}'"; + + /* Quote all arguments to be safely passed to the Bourne shell. + + Type: escapeShellArgs :: [string] -> string + + Example: + escapeShellArgs ["one" "two three" "four'five"] + => "'one' 'two three' 'four'\\''five'" + */ + escapeShellArgs = concatMapStringsSep " " escapeShellArg; + + /* Test whether the given name is a valid POSIX shell variable name. + + Type: string -> bool + + Example: + isValidPosixName "foo_bar000" + => true + isValidPosixName "0-bad.jpg" + => false + */ + isValidPosixName = name: match "[a-zA-Z_][a-zA-Z0-9_]*" name != null; + + /* Translate a Nix value into a shell variable declaration, with proper escaping. + + The value can be a string (mapped to a regular variable), a list of strings + (mapped to a Bash-style array) or an attribute set of strings (mapped to a + Bash-style associative array). Note that "string" includes string-coercible + values like paths or derivations. + + Strings are translated into POSIX sh-compatible code; lists and attribute sets + assume a shell that understands Bash syntax (e.g. Bash or ZSH). + + Type: string -> (string | listOf string | attrsOf string) -> string + + Example: + '' + ${toShellVar "foo" "some string"} + [[ "$foo" == "some string" ]] + '' + */ + toShellVar = name: value: + lib.throwIfNot (isValidPosixName name) "toShellVar: ${name} is not a valid shell variable name" ( + if isAttrs value && ! isStringLike value then + "declare -A ${name}=(${ + concatStringsSep " " (lib.mapAttrsToList (n: v: + "[${escapeShellArg n}]=${escapeShellArg v}" + ) value) + })" + else if isList value then + "declare -a ${name}=(${escapeShellArgs value})" + else + "${name}=${escapeShellArg value}" + ); + + /* Translate an attribute set into corresponding shell variable declarations + using `toShellVar`. + + Type: attrsOf (string | listOf string | attrsOf string) -> string + + Example: + let + foo = "value"; + bar = foo; + in '' + ${toShellVars { inherit foo bar; }} + [[ "$foo" == "$bar" ]] + '' + */ + toShellVars = vars: concatStringsSep "\n" (lib.mapAttrsToList toShellVar vars); + + /* Turn a string into a Nix expression representing that string + + Type: string -> string + + Example: + escapeNixString "hello\${}\n" + => "\"hello\\\${}\\n\"" + */ + escapeNixString = s: escape ["$"] (toJSON s); + + /* Turn a string into an exact regular expression + + Type: string -> string + + Example: + escapeRegex "[^a-z]*" + => "\\[\\^a-z]\\*" + */ + escapeRegex = escape (stringToCharacters "\\[{()^$?*+|."); + + /* Quotes a string if it can't be used as an identifier directly. + + Type: string -> string + + Example: + escapeNixIdentifier "hello" + => "hello" + escapeNixIdentifier "0abc" + => "\"0abc\"" + */ + escapeNixIdentifier = s: + # Regex from https://github.com/NixOS/nix/blob/d048577909e383439c2549e849c5c2f2016c997e/src/libexpr/lexer.l#L91 + if match "[a-zA-Z_][a-zA-Z0-9_'-]*" s != null + then s else escapeNixString s; + + /* Escapes a string such that it is safe to include verbatim in an XML + document. + + Type: string -> string + + Example: + escapeXML ''"test" 'test' < & >'' + => ""test" 'test' < & >" + */ + escapeXML = builtins.replaceStrings + ["\"" "'" "<" ">" "&"] + [""" "'" "<" ">" "&"]; + + # warning added 12-12-2022 + replaceChars = lib.warn "replaceChars is a deprecated alias of replaceStrings, replace usages of it with replaceStrings." builtins.replaceStrings; + + # Case conversion utilities. + lowerChars = stringToCharacters "abcdefghijklmnopqrstuvwxyz"; + upperChars = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /* Converts an ASCII string to lower-case. + + Type: toLower :: string -> string + + Example: + toLower "HOME" + => "home" + */ + toLower = replaceStrings upperChars lowerChars; + + /* Converts an ASCII string to upper-case. + + Type: toUpper :: string -> string + + Example: + toUpper "home" + => "HOME" + */ + toUpper = replaceStrings lowerChars upperChars; + + /* Appends string context from another string. This is an implementation + detail of Nix and should be used carefully. + + Strings in Nix carry an invisible `context` which is a list of strings + representing store paths. If the string is later used in a derivation + attribute, the derivation will properly populate the inputDrvs and + inputSrcs. + + Example: + pkgs = import { }; + addContextFrom pkgs.coreutils "bar" + => "bar" + */ + addContextFrom = a: b: substring 0 0 a + b; + + /* Cut a string with a separator and produces a list of strings which + were separated by this separator. + + Example: + splitString "." "foo.bar.baz" + => [ "foo" "bar" "baz" ] + splitString "/" "/usr/local/bin" + => [ "" "usr" "local" "bin" ] + */ + splitString = sep: s: + let + splits = builtins.filter builtins.isString (builtins.split (escapeRegex (toString sep)) (toString s)); + in + map (addContextFrom s) splits; + + /* Return a string without the specified prefix, if the prefix matches. + + Type: string -> string -> string + + Example: + removePrefix "foo." "foo.bar.baz" + => "bar.baz" + removePrefix "xxx" "foo.bar.baz" + => "foo.bar.baz" + */ + removePrefix = + # Prefix to remove if it matches + prefix: + # Input string + str: + # Before 23.05, paths would be copied to the store before converting them + # to strings and comparing. This was surprising and confusing. + warnIf + (isPath prefix) + '' + lib.strings.removePrefix: The first argument (${toString prefix}) is a path value, but only strings are supported. + There is almost certainly a bug in the calling code, since this function never removes any prefix in such a case. + This function also copies the path to the Nix store, which may not be what you want. + This behavior is deprecated and will throw an error in the future.'' + (let + preLen = stringLength prefix; + sLen = stringLength str; + in + if substring 0 preLen str == prefix then + substring preLen (sLen - preLen) str + else + str); + + /* Return a string without the specified suffix, if the suffix matches. + + Type: string -> string -> string + + Example: + removeSuffix "front" "homefront" + => "home" + removeSuffix "xxx" "homefront" + => "homefront" + */ + removeSuffix = + # Suffix to remove if it matches + suffix: + # Input string + str: + # Before 23.05, paths would be copied to the store before converting them + # to strings and comparing. This was surprising and confusing. + warnIf + (isPath suffix) + '' + lib.strings.removeSuffix: The first argument (${toString suffix}) is a path value, but only strings are supported. + There is almost certainly a bug in the calling code, since this function never removes any suffix in such a case. + This function also copies the path to the Nix store, which may not be what you want. + This behavior is deprecated and will throw an error in the future.'' + (let + sufLen = stringLength suffix; + sLen = stringLength str; + in + if sufLen <= sLen && suffix == substring (sLen - sufLen) sufLen str then + substring 0 (sLen - sufLen) str + else + str); + + /* Return true if string v1 denotes a version older than v2. + + Example: + versionOlder "1.1" "1.2" + => true + versionOlder "1.1" "1.1" + => false + */ + versionOlder = v1: v2: compareVersions v2 v1 == 1; + + /* Return true if string v1 denotes a version equal to or newer than v2. + + Example: + versionAtLeast "1.1" "1.0" + => true + versionAtLeast "1.1" "1.1" + => true + versionAtLeast "1.1" "1.2" + => false + */ + versionAtLeast = v1: v2: !versionOlder v1 v2; + + /* This function takes an argument that's either a derivation or a + derivation's "name" attribute and extracts the name part from that + argument. + + Example: + getName "youtube-dl-2016.01.01" + => "youtube-dl" + getName pkgs.youtube-dl + => "youtube-dl" + */ + getName = x: + let + parse = drv: (parseDrvName drv).name; + in if isString x + then parse x + else x.pname or (parse x.name); + + /* This function takes an argument that's either a derivation or a + derivation's "name" attribute and extracts the version part from that + argument. + + Example: + getVersion "youtube-dl-2016.01.01" + => "2016.01.01" + getVersion pkgs.youtube-dl + => "2016.01.01" + */ + getVersion = x: + let + parse = drv: (parseDrvName drv).version; + in if isString x + then parse x + else x.version or (parse x.name); + + /* Extract name with version from URL. Ask for separator which is + supposed to start extension. + + Example: + nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "-" + => "nix" + nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "_" + => "nix-1.7-x86" + */ + nameFromURL = url: sep: + let + components = splitString "/" url; + filename = lib.last components; + name = head (splitString sep filename); + in assert name != filename; name; + + /* Create a -D= string that can be passed to typical Meson + invocations. + + Type: mesonOption :: string -> string -> string + + @param feature The feature to be set + @param value The desired value + + Example: + mesonOption "engine" "opengl" + => "-Dengine=opengl" + */ + mesonOption = feature: value: + assert (lib.isString feature); + assert (lib.isString value); + "-D${feature}=${value}"; + + /* Create a -D={true,false} string that can be passed to typical + Meson invocations. + + Type: mesonBool :: string -> bool -> string + + @param condition The condition to be made true or false + @param flag The controlling flag of the condition + + Example: + mesonBool "hardened" true + => "-Dhardened=true" + mesonBool "static" false + => "-Dstatic=false" + */ + mesonBool = condition: flag: + assert (lib.isString condition); + assert (lib.isBool flag); + mesonOption condition (lib.boolToString flag); + + /* Create a -D={enabled,disabled} string that can be passed to + typical Meson invocations. + + Type: mesonEnable :: string -> bool -> string + + @param feature The feature to be enabled or disabled + @param flag The controlling flag + + Example: + mesonEnable "docs" true + => "-Ddocs=enabled" + mesonEnable "savage" false + => "-Dsavage=disabled" + */ + mesonEnable = feature: flag: + assert (lib.isString feature); + assert (lib.isBool flag); + mesonOption feature (if flag then "enabled" else "disabled"); + + /* Create an --{enable,disable}- string that can be passed to + standard GNU Autoconf scripts. + + Example: + enableFeature true "shared" + => "--enable-shared" + enableFeature false "shared" + => "--disable-shared" + */ + enableFeature = enable: feat: + assert isString feat; # e.g. passing openssl instead of "openssl" + "--${if enable then "enable" else "disable"}-${feat}"; + + /* Create an --{enable-=,disable-} string that can be passed to + standard GNU Autoconf scripts. + + Example: + enableFeatureAs true "shared" "foo" + => "--enable-shared=foo" + enableFeatureAs false "shared" (throw "ignored") + => "--disable-shared" + */ + enableFeatureAs = enable: feat: value: enableFeature enable feat + optionalString enable "=${value}"; + + /* Create an --{with,without}- string that can be passed to + standard GNU Autoconf scripts. + + Example: + withFeature true "shared" + => "--with-shared" + withFeature false "shared" + => "--without-shared" + */ + withFeature = with_: feat: + assert isString feat; # e.g. passing openssl instead of "openssl" + "--${if with_ then "with" else "without"}-${feat}"; + + /* Create an --{with-=,without-} string that can be passed to + standard GNU Autoconf scripts. + + Example: + withFeatureAs true "shared" "foo" + => "--with-shared=foo" + withFeatureAs false "shared" (throw "ignored") + => "--without-shared" + */ + withFeatureAs = with_: feat: value: withFeature with_ feat + optionalString with_ "=${value}"; + + /* Create a fixed width string with additional prefix to match + required width. + + This function will fail if the input string is longer than the + requested length. + + Type: fixedWidthString :: int -> string -> string -> string + + Example: + fixedWidthString 5 "0" (toString 15) + => "00015" + */ + fixedWidthString = width: filler: str: + let + strw = lib.stringLength str; + reqWidth = width - (lib.stringLength filler); + in + assert lib.assertMsg (strw <= width) + "fixedWidthString: requested string length (${ + toString width}) must not be shorter than actual length (${ + toString strw})"; + if strw == width then str else filler + fixedWidthString reqWidth filler str; + + /* Format a number adding leading zeroes up to fixed width. + + Example: + fixedWidthNumber 5 15 + => "00015" + */ + fixedWidthNumber = width: n: fixedWidthString width "0" (toString n); + + /* Convert a float to a string, but emit a warning when precision is lost + during the conversion + + Example: + floatToString 0.000001 + => "0.000001" + floatToString 0.0000001 + => trace: warning: Imprecise conversion from float to string 0.000000 + "0.000000" + */ + floatToString = float: let + result = toString float; + precise = float == fromJSON result; + in lib.warnIf (!precise) "Imprecise conversion from float to string ${result}" + result; + + /* Soft-deprecated function. While the original implementation is available as + isConvertibleWithToString, consider using isStringLike instead, if suitable. */ + isCoercibleToString = lib.warnIf (lib.isInOldestRelease 2305) + "lib.strings.isCoercibleToString is deprecated in favor of either isStringLike or isConvertibleWithToString. Only use the latter if it needs to return true for null, numbers, booleans and list of similarly coercibles." + isConvertibleWithToString; + + /* Check whether a list or other value can be passed to toString. + + Many types of value are coercible to string this way, including int, float, + null, bool, list of similarly coercible values. + */ + isConvertibleWithToString = x: + isStringLike x || + elem (typeOf x) [ "null" "int" "float" "bool" ] || + (isList x && lib.all isConvertibleWithToString x); + + /* Check whether a value can be coerced to a string. + The value must be a string, path, or attribute set. + + String-like values can be used without explicit conversion in + string interpolations and in most functions that expect a string. + */ + isStringLike = x: + isString x || + isPath x || + x ? outPath || + x ? __toString; + + /* Check whether a value is a store path. + + Example: + isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11/bin/python" + => false + isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11" + => true + isStorePath pkgs.python + => true + isStorePath [] || isStorePath 42 || isStorePath {} || … + => false + */ + isStorePath = x: + if isStringLike x then + let str = toString x; in + substring 0 1 str == "/" + && dirOf str == storeDir + else + false; + + /* Parse a string as an int. Does not support parsing of integers with preceding zero due to + ambiguity between zero-padded and octal numbers. See toIntBase10. + + Type: string -> int + + Example: + + toInt "1337" + => 1337 + + toInt "-4" + => -4 + + toInt " 123 " + => 123 + + toInt "00024" + => error: Ambiguity in interpretation of 00024 between octal and zero padded integer. + + toInt "3.14" + => error: floating point JSON numbers are not supported + */ + toInt = str: + let + # RegEx: Match any leading whitespace, possibly a '-', one or more digits, + # and finally match any trailing whitespace. + strippedInput = match "[[:space:]]*(-?[[:digit:]]+)[[:space:]]*" str; + + # RegEx: Match a leading '0' then one or more digits. + isLeadingZero = match "0[[:digit:]]+" (head strippedInput) == []; + + # Attempt to parse input + parsedInput = fromJSON (head strippedInput); + + generalError = "toInt: Could not convert ${escapeNixString str} to int."; + + octalAmbigError = "toInt: Ambiguity in interpretation of ${escapeNixString str}" + + " between octal and zero padded integer."; + + in + # Error on presence of non digit characters. + if strippedInput == null + then throw generalError + # Error on presence of leading zero/octal ambiguity. + else if isLeadingZero + then throw octalAmbigError + # Error if parse function fails. + else if !isInt parsedInput + then throw generalError + # Return result. + else parsedInput; + + + /* Parse a string as a base 10 int. This supports parsing of zero-padded integers. + + Type: string -> int + + Example: + toIntBase10 "1337" + => 1337 + + toIntBase10 "-4" + => -4 + + toIntBase10 " 123 " + => 123 + + toIntBase10 "00024" + => 24 + + toIntBase10 "3.14" + => error: floating point JSON numbers are not supported + */ + toIntBase10 = str: + let + # RegEx: Match any leading whitespace, then match any zero padding, + # capture possibly a '-' followed by one or more digits, + # and finally match any trailing whitespace. + strippedInput = match "[[:space:]]*0*(-?[[:digit:]]+)[[:space:]]*" str; + + # RegEx: Match at least one '0'. + isZero = match "0+" (head strippedInput) == []; + + # Attempt to parse input + parsedInput = fromJSON (head strippedInput); + + generalError = "toIntBase10: Could not convert ${escapeNixString str} to int."; + + in + # Error on presence of non digit characters. + if strippedInput == null + then throw generalError + # In the special case zero-padded zero (00000), return early. + else if isZero + then 0 + # Error if parse function fails. + else if !isInt parsedInput + then throw generalError + # Return result. + else parsedInput; + + /* Read a list of paths from `file`, relative to the `rootPath`. + Lines beginning with `#` are treated as comments and ignored. + Whitespace is significant. + + NOTE: This function is not performant and should be avoided. + + Example: + readPathsFromFile /prefix + ./pkgs/development/libraries/qt-5/5.4/qtbase/series + => [ "/prefix/dlopen-resolv.patch" "/prefix/tzdir.patch" + "/prefix/dlopen-libXcursor.patch" "/prefix/dlopen-openssl.patch" + "/prefix/dlopen-dbus.patch" "/prefix/xdg-config-dirs.patch" + "/prefix/nix-profiles-library-paths.patch" + "/prefix/compose-search-path.patch" ] + */ + readPathsFromFile = lib.warn "lib.readPathsFromFile is deprecated, use a list instead" + (rootPath: file: + let + lines = lib.splitString "\n" (readFile file); + removeComments = lib.filter (line: line != "" && !(lib.hasPrefix "#" line)); + relativePaths = removeComments lines; + absolutePaths = map (path: rootPath + "/${path}") relativePaths; + in + absolutePaths); + + /* Read the contents of a file removing the trailing \n + + Type: fileContents :: path -> string + + Example: + $ echo "1.0" > ./version + + fileContents ./version + => "1.0" + */ + fileContents = file: removeSuffix "\n" (readFile file); + + + /* Creates a valid derivation name from a potentially invalid one. + + Type: sanitizeDerivationName :: String -> String + + Example: + sanitizeDerivationName "../hello.bar # foo" + => "-hello.bar-foo" + sanitizeDerivationName "" + => "unknown" + sanitizeDerivationName pkgs.hello + => "-nix-store-2g75chlbpxlrqn15zlby2dfh8hr9qwbk-hello-2.10" + */ + sanitizeDerivationName = + let okRegex = match "[[:alnum:]+_?=-][[:alnum:]+._?=-]*"; + in + string: + # First detect the common case of already valid strings, to speed those up + if stringLength string <= 207 && okRegex string != null + then unsafeDiscardStringContext string + else lib.pipe string [ + # Get rid of string context. This is safe under the assumption that the + # resulting string is only used as a derivation name + unsafeDiscardStringContext + # Strip all leading "." + (x: elemAt (match "\\.*(.*)" x) 0) + # Split out all invalid characters + # https://github.com/NixOS/nix/blob/2.3.2/src/libstore/store-api.cc#L85-L112 + # https://github.com/NixOS/nix/blob/2242be83c61788b9c0736a92bb0b5c7bbfc40803/nix-rust/src/store/path.rs#L100-L125 + (split "[^[:alnum:]+._?=-]+") + # Replace invalid character ranges with a "-" + (concatMapStrings (s: if lib.isList s then "-" else s)) + # Limit to 211 characters (minus 4 chars for ".drv") + (x: substring (lib.max (stringLength x - 207) 0) (-1) x) + # If the result is empty, replace it with "unknown" + (x: if stringLength x == 0 then "unknown" else x) + ]; + + /* Computes the Levenshtein distance between two strings. + Complexity O(n*m) where n and m are the lengths of the strings. + Algorithm adjusted from https://stackoverflow.com/a/9750974/6605742 + + Type: levenshtein :: string -> string -> int + + Example: + levenshtein "foo" "foo" + => 0 + levenshtein "book" "hook" + => 1 + levenshtein "hello" "Heyo" + => 3 + */ + levenshtein = a: b: let + # Two dimensional array with dimensions (stringLength a + 1, stringLength b + 1) + arr = lib.genList (i: + lib.genList (j: + dist i j + ) (stringLength b + 1) + ) (stringLength a + 1); + d = x: y: lib.elemAt (lib.elemAt arr x) y; + dist = i: j: + let c = if substring (i - 1) 1 a == substring (j - 1) 1 b + then 0 else 1; + in + if j == 0 then i + else if i == 0 then j + else lib.min + ( lib.min (d (i - 1) j + 1) (d i (j - 1) + 1)) + ( d (i - 1) (j - 1) + c ); + in d (stringLength a) (stringLength b); + + /* Returns the length of the prefix common to both strings. + */ + commonPrefixLength = a: b: + let + m = lib.min (stringLength a) (stringLength b); + go = i: if i >= m then m else if substring i 1 a == substring i 1 b then go (i + 1) else i; + in go 0; + + /* Returns the length of the suffix common to both strings. + */ + commonSuffixLength = a: b: + let + m = lib.min (stringLength a) (stringLength b); + go = i: if i >= m then m else if substring (stringLength a - i - 1) 1 a == substring (stringLength b - i - 1) 1 b then go (i + 1) else i; + in go 0; + + /* Returns whether the levenshtein distance between two strings is at most some value + Complexity is O(min(n,m)) for k <= 2 and O(n*m) otherwise + + Type: levenshteinAtMost :: int -> string -> string -> bool + + Example: + levenshteinAtMost 0 "foo" "foo" + => true + levenshteinAtMost 1 "foo" "boa" + => false + levenshteinAtMost 2 "foo" "boa" + => true + levenshteinAtMost 2 "This is a sentence" "this is a sentense." + => false + levenshteinAtMost 3 "This is a sentence" "this is a sentense." + => true + + */ + levenshteinAtMost = let + infixDifferAtMost1 = x: y: stringLength x <= 1 && stringLength y <= 1; + + # This function takes two strings stripped by their common pre and suffix, + # and returns whether they differ by at most two by Levenshtein distance. + # Because of this stripping, if they do indeed differ by at most two edits, + # we know that those edits were (if at all) done at the start or the end, + # while the middle has to have stayed the same. This fact is used in the + # implementation. + infixDifferAtMost2 = x: y: + let + xlen = stringLength x; + ylen = stringLength y; + # This function is only called with |x| >= |y| and |x| - |y| <= 2, so + # diff is one of 0, 1 or 2 + diff = xlen - ylen; + + # Infix of x and y, stripped by the left and right most character + xinfix = substring 1 (xlen - 2) x; + yinfix = substring 1 (ylen - 2) y; + + # x and y but a character deleted at the left or right + xdelr = substring 0 (xlen - 1) x; + xdell = substring 1 (xlen - 1) x; + ydelr = substring 0 (ylen - 1) y; + ydell = substring 1 (ylen - 1) y; + in + # A length difference of 2 can only be gotten with 2 delete edits, + # which have to have happened at the start and end of x + # Example: "abcdef" -> "bcde" + if diff == 2 then xinfix == y + # A length difference of 1 can only be gotten with a deletion on the + # right and a replacement on the left or vice versa. + # Example: "abcdef" -> "bcdez" or "zbcde" + else if diff == 1 then xinfix == ydelr || xinfix == ydell + # No length difference can either happen through replacements on both + # sides, or a deletion on the left and an insertion on the right or + # vice versa + # Example: "abcdef" -> "zbcdez" or "bcdefz" or "zabcde" + else xinfix == yinfix || xdelr == ydell || xdell == ydelr; + + in k: if k <= 0 then a: b: a == b else + let f = a: b: + let + alen = stringLength a; + blen = stringLength b; + prelen = commonPrefixLength a b; + suflen = commonSuffixLength a b; + presuflen = prelen + suflen; + ainfix = substring prelen (alen - presuflen) a; + binfix = substring prelen (blen - presuflen) b; + in + # Make a be the bigger string + if alen < blen then f b a + # If a has over k more characters than b, even with k deletes on a, b can't be reached + else if alen - blen > k then false + else if k == 1 then infixDifferAtMost1 ainfix binfix + else if k == 2 then infixDifferAtMost2 ainfix binfix + else levenshtein ainfix binfix <= k; + in f; +} diff --git a/systems/architectures.nix b/systems/architectures.nix new file mode 100644 index 000000000..57b9184ca --- /dev/null +++ b/systems/architectures.nix @@ -0,0 +1,114 @@ +{ lib }: + +rec { + # gcc.arch to its features (as in /proc/cpuinfo) + features = { + default = [ ]; + # x86_64 Intel + westmere = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" ]; + sandybridge = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" ]; + ivybridge = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" ]; + haswell = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "fma" ]; + broadwell = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "fma" ]; + skylake = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "fma" ]; + skylake-avx512 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + cannonlake = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + icelake-client = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + icelake-server = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + cascadelake = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + cooperlake = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + tigerlake = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" "avx2" "avx512" "fma" ]; + # x86_64 AMD + btver1 = [ "sse3" "ssse3" "sse4_1" "sse4_2" ]; + btver2 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "aes" "avx" ]; + bdver1 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "fma" "fma4" ]; + bdver2 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "fma" "fma4" ]; + bdver3 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "fma" "fma4" ]; + bdver4 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "avx2" "fma" "fma4" ]; + znver1 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "avx2" "fma" ]; + znver2 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "avx2" "fma" ]; + znver3 = [ "sse3" "ssse3" "sse4_1" "sse4_2" "sse4a" "aes" "avx" "avx2" "fma" ]; + # other + armv5te = [ ]; + armv6 = [ ]; + armv7-a = [ ]; + armv8-a = [ ]; + mips32 = [ ]; + loongson2f = [ ]; + }; + + # a superior CPU has all the features of an inferior and is able to build and test code for it + inferiors = { + # x86_64 Intel + # https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html + default = [ ]; + westmere = [ ]; + sandybridge = [ "westmere" ] ++ inferiors.westmere; + ivybridge = [ "sandybridge" ] ++ inferiors.sandybridge; + haswell = [ "ivybridge" ] ++ inferiors.ivybridge; + broadwell = [ "haswell" ] ++ inferiors.haswell; + skylake = [ "broadwell" ] ++ inferiors.broadwell; + skylake-avx512 = [ "skylake" ] ++ inferiors.skylake; + cannonlake = [ "skylake-avx512" ] ++ inferiors.skylake-avx512; + icelake-client = [ "cannonlake" ] ++ inferiors.cannonlake; + icelake-server = [ "icelake-client" ] ++ inferiors.icelake-client; + cascadelake = [ "skylake-avx512" ] ++ inferiors.cannonlake; + cooperlake = [ "cascadelake" ] ++ inferiors.cascadelake; + tigerlake = [ "icelake-server" ] ++ inferiors.icelake-server; + + # x86_64 AMD + # TODO: fill this (need testing) + btver1 = [ ]; + btver2 = [ ]; + bdver1 = [ ]; + bdver2 = [ ]; + bdver3 = [ ]; + bdver4 = [ ]; + # Regarding `skylake` as inferior of `znver1`, there are reports of + # successful usage by Gentoo users and Phoronix benchmarking of different + # `-march` targets. + # + # The GCC documentation on extensions used and wikichip documentation + # regarding supperted extensions on znver1 and skylake was used to create + # this partial order. + # + # Note: + # + # - The successors of `skylake` (`cannonlake`, `icelake`, etc) use `avx512` + # which no current AMD Zen michroarch support. + # - `znver1` uses `ABM`, `CLZERO`, `CX16`, `MWAITX`, and `SSE4A` which no + # current Intel microarch support. + # + # https://www.phoronix.com/scan.php?page=article&item=amd-znver3-gcc11&num=1 + # https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html + # https://en.wikichip.org/wiki/amd/microarchitectures/zen + # https://en.wikichip.org/wiki/intel/microarchitectures/skylake + znver1 = [ "skylake" ] ++ inferiors.skylake; + znver2 = [ "znver1" ] ++ inferiors.znver1; + znver3 = [ "znver2" ] ++ inferiors.znver2; + + # other + armv5te = [ ]; + armv6 = [ ]; + armv7-a = [ ]; + armv8-a = [ ]; + mips32 = [ ]; + loongson2f = [ ]; + }; + + predicates = let + featureSupport = feature: x: builtins.elem feature features.${x} or []; + in { + sse3Support = featureSupport "sse3"; + ssse3Support = featureSupport "ssse3"; + sse4_1Support = featureSupport "sse4_1"; + sse4_2Support = featureSupport "sse4_2"; + sse4_aSupport = featureSupport "sse4a"; + avxSupport = featureSupport "avx"; + avx2Support = featureSupport "avx2"; + avx512Support = featureSupport "avx512"; + aesSupport = featureSupport "aes"; + fmaSupport = featureSupport "fma"; + fma4Support = featureSupport "fma4"; + }; +} diff --git a/systems/default.nix b/systems/default.nix new file mode 100644 index 000000000..f4784c61c --- /dev/null +++ b/systems/default.nix @@ -0,0 +1,228 @@ +{ lib }: + let inherit (lib.attrsets) mapAttrs; in + +rec { + doubles = import ./doubles.nix { inherit lib; }; + parse = import ./parse.nix { inherit lib; }; + inspect = import ./inspect.nix { inherit lib; }; + platforms = import ./platforms.nix { inherit lib; }; + examples = import ./examples.nix { inherit lib; }; + architectures = import ./architectures.nix { inherit lib; }; + + /* List of all Nix system doubles the nixpkgs flake will expose the package set + for. All systems listed here must be supported by nixpkgs as `localSystem`. + + **Warning**: This attribute is considered experimental and is subject to change. + */ + flakeExposed = import ./flake-systems.nix { }; + + # Elaborate a `localSystem` or `crossSystem` so that it contains everything + # necessary. + # + # `parsed` is inferred from args, both because there are two options with one + # clearly preferred, and to prevent cycles. A simpler fixed point where the RHS + # always just used `final.*` would fail on both counts. + elaborate = args': let + args = if lib.isString args' then { system = args'; } + else args'; + final = { + # Prefer to parse `config` as it is strictly more informative. + parsed = parse.mkSystemFromString (if args ? config then args.config else args.system); + # Either of these can be losslessly-extracted from `parsed` iff parsing succeeds. + system = parse.doubleFromSystem final.parsed; + config = parse.tripleFromSystem final.parsed; + # Determine whether we can execute binaries built for the provided platform. + canExecute = platform: + final.isAndroid == platform.isAndroid && + parse.isCompatible final.parsed.cpu platform.parsed.cpu + && final.parsed.kernel == platform.parsed.kernel; + isCompatible = _: throw "2022-05-23: isCompatible has been removed in favor of canExecute, refer to the 22.11 changelog for details"; + # Derived meta-data + libc = + /**/ if final.isDarwin then "libSystem" + else if final.isMinGW then "msvcrt" + else if final.isWasi then "wasilibc" + else if final.isRedox then "relibc" + else if final.isMusl then "musl" + else if final.isUClibc then "uclibc" + else if final.isAndroid then "bionic" + else if final.isLinux /* default */ then "glibc" + else if final.isFreeBSD then "fblibc" + else if final.isNetBSD then "nblibc" + else if final.isAvr then "avrlibc" + else if final.isGhcjs then null + else if final.isNone then "newlib" + # TODO(@Ericson2314) think more about other operating systems + else "native/impure"; + # Choose what linker we wish to use by default. Someday we might also + # choose the C compiler, runtime library, C++ standard library, etc. in + # this way, nice and orthogonally, and deprecate `useLLVM`. But due to + # the monolithic GCC build we cannot actually make those choices + # independently, so we are just doing `linker` and keeping `useLLVM` for + # now. + linker = + /**/ if final.useLLVM or false then "lld" + else if final.isDarwin then "cctools" + # "bfd" and "gold" both come from GNU binutils. The existence of Gold + # is why we use the more obscure "bfd" and not "binutils" for this + # choice. + else "bfd"; + extensions = rec { + sharedLibrary = + /**/ if final.isDarwin then ".dylib" + else if final.isWindows then ".dll" + else ".so"; + staticLibrary = + /**/ if final.isWindows then ".lib" + else ".a"; + library = + /**/ if final.isStatic then staticLibrary + else sharedLibrary; + executable = + /**/ if final.isWindows then ".exe" + else ""; + }; + # Misc boolean options + useAndroidPrebuilt = false; + useiOSPrebuilt = false; + + # Output from uname + uname = { + # uname -s + system = { + linux = "Linux"; + windows = "Windows"; + darwin = "Darwin"; + netbsd = "NetBSD"; + freebsd = "FreeBSD"; + openbsd = "OpenBSD"; + wasi = "Wasi"; + redox = "Redox"; + genode = "Genode"; + }.${final.parsed.kernel.name} or null; + + # uname -m + processor = + if final.isPower64 + then "ppc64${lib.optionalString final.isLittleEndian "le"}" + else if final.isPower + then "ppc${lib.optionalString final.isLittleEndian "le"}" + else if final.isMips64 + then "mips64" # endianness is *not* included on mips64 + else final.parsed.cpu.name; + + # uname -r + release = null; + }; + isStatic = final.isWasm || final.isRedox; + + # Just a guess, based on `system` + inherit + ({ + linux-kernel = args.linux-kernel or {}; + gcc = args.gcc or {}; + rustc = args.rustc or {}; + } // platforms.select final) + linux-kernel gcc rustc; + + linuxArch = + if final.isAarch32 then "arm" + else if final.isAarch64 then "arm64" + else if final.isx86_32 then "i386" + else if final.isx86_64 then "x86_64" + # linux kernel does not distinguish microblaze/microblazeel + else if final.isMicroBlaze then "microblaze" + else if final.isMips32 then "mips" + else if final.isMips64 then "mips" # linux kernel does not distinguish mips32/mips64 + else if final.isPower then "powerpc" + else if final.isRiscV then "riscv" + else if final.isS390 then "s390" + else if final.isLoongArch64 then "loongarch" + else final.parsed.cpu.name; + + qemuArch = + if final.isAarch32 then "arm" + else if final.isS390 && !final.isS390x then null + else if final.isx86_64 then "x86_64" + else if final.isx86 then "i386" + else if final.isMips64 then "mips64${lib.optionalString final.isLittleEndian "el"}" + else final.uname.processor; + + # Name used by UEFI for architectures. + efiArch = + if final.isx86_32 then "ia32" + else if final.isx86_64 then "x64" + else if final.isAarch32 then "arm" + else if final.isAarch64 then "aa64" + else final.parsed.cpu.name; + + darwinArch = { + armv7a = "armv7"; + aarch64 = "arm64"; + }.${final.parsed.cpu.name} or final.parsed.cpu.name; + + darwinPlatform = + if final.isMacOS then "macos" + else if final.isiOS then "ios" + else null; + # The canonical name for this attribute is darwinSdkVersion, but some + # platforms define the old name "sdkVer". + darwinSdkVersion = final.sdkVer or (if final.isAarch64 then "11.0" else "10.12"); + darwinMinVersion = final.darwinSdkVersion; + darwinMinVersionVariable = + if final.isMacOS then "MACOSX_DEPLOYMENT_TARGET" + else if final.isiOS then "IPHONEOS_DEPLOYMENT_TARGET" + else null; + } // ( + let + selectEmulator = pkgs: + let + qemu-user = pkgs.qemu.override { + smartcardSupport = false; + spiceSupport = false; + openGLSupport = false; + virglSupport = false; + vncSupport = false; + gtkSupport = false; + sdlSupport = false; + pulseSupport = false; + smbdSupport = false; + seccompSupport = false; + enableDocs = false; + hostCpuTargets = [ "${final.qemuArch}-linux-user" ]; + }; + wine = (pkgs.winePackagesFor "wine${toString final.parsed.cpu.bits}").minimal; + in + if final.parsed.kernel.name == pkgs.stdenv.hostPlatform.parsed.kernel.name && + pkgs.stdenv.hostPlatform.canExecute final + then "${pkgs.runtimeShell} -c '\"$@\"' --" + else if final.isWindows + then "${wine}/bin/wine${lib.optionalString (final.parsed.cpu.bits == 64) "64"}" + else if final.isLinux && pkgs.stdenv.hostPlatform.isLinux && final.qemuArch != null + then "${qemu-user}/bin/qemu-${final.qemuArch}" + else if final.isWasi + then "${pkgs.wasmtime}/bin/wasmtime" + else if final.isMmix + then "${pkgs.mmixware}/bin/mmix" + else null; + in { + emulatorAvailable = pkgs: (selectEmulator pkgs) != null; + + emulator = pkgs: + if (final.emulatorAvailable pkgs) + then selectEmulator pkgs + else throw "Don't know how to run ${final.config} executables."; + + }) // mapAttrs (n: v: v final.parsed) inspect.predicates + // mapAttrs (n: v: v final.gcc.arch or "default") architectures.predicates + // args; + in assert final.useAndroidPrebuilt -> final.isAndroid; + assert lib.foldl + (pass: { assertion, message }: + if assertion final + then pass + else throw message) + true + (final.parsed.abi.assertions or []); + final; +} diff --git a/systems/doubles.nix b/systems/doubles.nix new file mode 100644 index 000000000..66bf3d872 --- /dev/null +++ b/systems/doubles.nix @@ -0,0 +1,119 @@ +{ lib }: +let + inherit (lib) lists; + inherit (lib.systems) parse; + inherit (lib.systems.inspect) predicates; + inherit (lib.attrsets) matchAttrs; + + all = [ + # Cygwin + "i686-cygwin" "x86_64-cygwin" + + # Darwin + "x86_64-darwin" "i686-darwin" "aarch64-darwin" "armv7a-darwin" + + # FreeBSD + "i686-freebsd13" "x86_64-freebsd13" + + # Genode + "aarch64-genode" "i686-genode" "x86_64-genode" + + # illumos + "x86_64-solaris" + + # JS + "javascript-ghcjs" + + # Linux + "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux" + "armv7l-linux" "i686-linux" "loongarch64-linux" "m68k-linux" "microblaze-linux" + "microblazeel-linux" "mips-linux" "mips64-linux" "mips64el-linux" + "mipsel-linux" "powerpc64-linux" "powerpc64le-linux" "riscv32-linux" + "riscv64-linux" "s390-linux" "s390x-linux" "x86_64-linux" + + # MMIXware + "mmix-mmixware" + + # NetBSD + "aarch64-netbsd" "armv6l-netbsd" "armv7a-netbsd" "armv7l-netbsd" + "i686-netbsd" "m68k-netbsd" "mipsel-netbsd" "powerpc-netbsd" + "riscv32-netbsd" "riscv64-netbsd" "x86_64-netbsd" + + # none + "aarch64_be-none" "aarch64-none" "arm-none" "armv6l-none" "avr-none" "i686-none" + "microblaze-none" "microblazeel-none" "msp430-none" "or1k-none" "m68k-none" + "powerpc-none" "powerpcle-none" "riscv32-none" "riscv64-none" "rx-none" + "s390-none" "s390x-none" "vc4-none" "x86_64-none" + + # OpenBSD + "i686-openbsd" "x86_64-openbsd" + + # Redox + "x86_64-redox" + + # WASI + "wasm64-wasi" "wasm32-wasi" + + # Windows + "x86_64-windows" "i686-windows" + ]; + + allParsed = map parse.mkSystemFromString all; + + filterDoubles = f: map parse.doubleFromSystem (lists.filter f allParsed); + +in { + inherit all; + + none = []; + + arm = filterDoubles predicates.isAarch32; + armv7 = filterDoubles predicates.isArmv7; + aarch64 = filterDoubles predicates.isAarch64; + x86 = filterDoubles predicates.isx86; + i686 = filterDoubles predicates.isi686; + x86_64 = filterDoubles predicates.isx86_64; + microblaze = filterDoubles predicates.isMicroBlaze; + mips = filterDoubles predicates.isMips; + mmix = filterDoubles predicates.isMmix; + power = filterDoubles predicates.isPower; + riscv = filterDoubles predicates.isRiscV; + riscv32 = filterDoubles predicates.isRiscV32; + riscv64 = filterDoubles predicates.isRiscV64; + rx = filterDoubles predicates.isRx; + vc4 = filterDoubles predicates.isVc4; + or1k = filterDoubles predicates.isOr1k; + m68k = filterDoubles predicates.isM68k; + s390 = filterDoubles predicates.isS390; + s390x = filterDoubles predicates.isS390x; + loongarch64 = filterDoubles predicates.isLoongArch64; + js = filterDoubles predicates.isJavaScript; + + bigEndian = filterDoubles predicates.isBigEndian; + littleEndian = filterDoubles predicates.isLittleEndian; + + cygwin = filterDoubles predicates.isCygwin; + darwin = filterDoubles predicates.isDarwin; + freebsd = filterDoubles predicates.isFreeBSD; + # Should be better, but MinGW is unclear. + gnu = filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnu; }) + ++ filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnueabi; }) + ++ filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnueabihf; }) + ++ filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnuabin32; }) + ++ filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnuabi64; }) + ++ filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnuabielfv1; }) + ++ filterDoubles (matchAttrs { kernel = parse.kernels.linux; abi = parse.abis.gnuabielfv2; }); + illumos = filterDoubles predicates.isSunOS; + linux = filterDoubles predicates.isLinux; + netbsd = filterDoubles predicates.isNetBSD; + openbsd = filterDoubles predicates.isOpenBSD; + unix = filterDoubles predicates.isUnix; + wasi = filterDoubles predicates.isWasi; + redox = filterDoubles predicates.isRedox; + windows = filterDoubles predicates.isWindows; + genode = filterDoubles predicates.isGenode; + + embedded = filterDoubles predicates.isNone; + + mesaPlatforms = ["i686-linux" "x86_64-linux" "x86_64-darwin" "armv5tel-linux" "armv6l-linux" "armv7l-linux" "armv7a-linux" "aarch64-linux" "powerpc64-linux" "powerpc64le-linux" "aarch64-darwin" "riscv64-linux"]; +} diff --git a/systems/examples.nix b/systems/examples.nix new file mode 100644 index 000000000..45b95c150 --- /dev/null +++ b/systems/examples.nix @@ -0,0 +1,335 @@ +# These can be passed to nixpkgs as either the `localSystem` or +# `crossSystem`. They are put here for user convenience, but also used by cross +# tests and linux cross stdenv building, so handle with care! +{ lib }: +let + platforms = import ./platforms.nix { inherit lib; }; + + riscv = bits: { + config = "riscv${bits}-unknown-linux-gnu"; + }; +in + +rec { + # + # Linux + # + powernv = { + config = "powerpc64le-unknown-linux-gnu"; + }; + musl-power = { + config = "powerpc64le-unknown-linux-musl"; + }; + + ppc64 = { + config = "powerpc64-unknown-linux-gnuabielfv2"; + }; + ppc64-musl = { + config = "powerpc64-unknown-linux-musl"; + gcc = { abi = "elfv2"; }; + }; + + sheevaplug = { + config = "armv5tel-unknown-linux-gnueabi"; + } // platforms.sheevaplug; + + raspberryPi = { + config = "armv6l-unknown-linux-gnueabihf"; + } // platforms.raspberrypi; + + remarkable1 = { + config = "armv7l-unknown-linux-gnueabihf"; + } // platforms.zero-gravitas; + + remarkable2 = { + config = "armv7l-unknown-linux-gnueabihf"; + } // platforms.zero-sugar; + + armv7l-hf-multiplatform = { + config = "armv7l-unknown-linux-gnueabihf"; + }; + + aarch64-multiplatform = { + config = "aarch64-unknown-linux-gnu"; + }; + + armv7a-android-prebuilt = { + config = "armv7a-unknown-linux-androideabi"; + rustc.config = "armv7-linux-androideabi"; + sdkVer = "28"; + ndkVer = "24"; + useAndroidPrebuilt = true; + } // platforms.armv7a-android; + + aarch64-android-prebuilt = { + config = "aarch64-unknown-linux-android"; + rustc.config = "aarch64-linux-android"; + sdkVer = "28"; + ndkVer = "24"; + useAndroidPrebuilt = true; + }; + + aarch64-android = { + config = "aarch64-unknown-linux-android"; + sdkVer = "30"; + ndkVer = "24"; + libc = "bionic"; + useAndroidPrebuilt = false; + useLLVM = true; + }; + + pogoplug4 = { + config = "armv5tel-unknown-linux-gnueabi"; + } // platforms.pogoplug4; + + ben-nanonote = { + config = "mipsel-unknown-linux-uclibc"; + } // platforms.ben_nanonote; + + fuloongminipc = { + config = "mipsel-unknown-linux-gnu"; + } // platforms.fuloong2f_n32; + + # can execute on 32bit chip + mips-linux-gnu = { config = "mips-unknown-linux-gnu"; } // platforms.gcc_mips32r2_o32; + mipsel-linux-gnu = { config = "mipsel-unknown-linux-gnu"; } // platforms.gcc_mips32r2_o32; + + # require 64bit chip (for more registers, 64-bit floating point, 64-bit "long long") but use 32bit pointers + mips64-linux-gnuabin32 = { config = "mips64-unknown-linux-gnuabin32"; } // platforms.gcc_mips64r2_n32; + mips64el-linux-gnuabin32 = { config = "mips64el-unknown-linux-gnuabin32"; } // platforms.gcc_mips64r2_n32; + + # 64bit pointers + mips64-linux-gnuabi64 = { config = "mips64-unknown-linux-gnuabi64"; } // platforms.gcc_mips64r2_64; + mips64el-linux-gnuabi64 = { config = "mips64el-unknown-linux-gnuabi64"; } // platforms.gcc_mips64r2_64; + + muslpi = raspberryPi // { + config = "armv6l-unknown-linux-musleabihf"; + }; + + aarch64-multiplatform-musl = { + config = "aarch64-unknown-linux-musl"; + }; + + gnu64 = { config = "x86_64-unknown-linux-gnu"; }; + gnu32 = { config = "i686-unknown-linux-gnu"; }; + + musl64 = { config = "x86_64-unknown-linux-musl"; }; + musl32 = { config = "i686-unknown-linux-musl"; }; + + riscv64 = riscv "64"; + riscv32 = riscv "32"; + + riscv64-embedded = { + config = "riscv64-none-elf"; + libc = "newlib"; + }; + + riscv32-embedded = { + config = "riscv32-none-elf"; + libc = "newlib"; + }; + + loongarch64-linux = { + config = "loongarch64-unknown-linux-gnu"; + }; + + mmix = { + config = "mmix-unknown-mmixware"; + libc = "newlib"; + }; + + rx-embedded = { + config = "rx-none-elf"; + libc = "newlib"; + }; + + msp430 = { + config = "msp430-elf"; + libc = "newlib"; + }; + + avr = { + config = "avr"; + }; + + vc4 = { + config = "vc4-elf"; + libc = "newlib"; + }; + + or1k = { + config = "or1k-elf"; + libc = "newlib"; + }; + + m68k = { + config = "m68k-unknown-linux-gnu"; + }; + + s390 = { + config = "s390-unknown-linux-gnu"; + }; + + s390x = { + config = "s390x-unknown-linux-gnu"; + }; + + arm-embedded = { + config = "arm-none-eabi"; + libc = "newlib"; + }; + armhf-embedded = { + config = "arm-none-eabihf"; + libc = "newlib"; + # GCC8+ does not build without this + # (https://www.mail-archive.com/gcc-bugs@gcc.gnu.org/msg552339.html): + gcc = { + arch = "armv5t"; + fpu = "vfp"; + }; + }; + + aarch64-embedded = { + config = "aarch64-none-elf"; + libc = "newlib"; + }; + + aarch64be-embedded = { + config = "aarch64_be-none-elf"; + libc = "newlib"; + }; + + ppc-embedded = { + config = "powerpc-none-eabi"; + libc = "newlib"; + }; + + ppcle-embedded = { + config = "powerpcle-none-eabi"; + libc = "newlib"; + }; + + i686-embedded = { + config = "i686-elf"; + libc = "newlib"; + }; + + x86_64-embedded = { + config = "x86_64-elf"; + libc = "newlib"; + }; + + # + # Redox + # + + x86_64-unknown-redox = { + config = "x86_64-unknown-redox"; + libc = "relibc"; + }; + + # + # Darwin + # + + iphone64 = { + config = "aarch64-apple-ios"; + # config = "aarch64-apple-darwin14"; + sdkVer = "14.3"; + xcodeVer = "12.3"; + xcodePlatform = "iPhoneOS"; + useiOSPrebuilt = true; + }; + + iphone32 = { + config = "armv7a-apple-ios"; + # config = "arm-apple-darwin10"; + sdkVer = "14.3"; + xcodeVer = "12.3"; + xcodePlatform = "iPhoneOS"; + useiOSPrebuilt = true; + }; + + iphone64-simulator = { + config = "x86_64-apple-ios"; + # config = "x86_64-apple-darwin14"; + sdkVer = "14.3"; + xcodeVer = "12.3"; + xcodePlatform = "iPhoneSimulator"; + darwinPlatform = "ios-simulator"; + useiOSPrebuilt = true; + }; + + iphone32-simulator = { + config = "i686-apple-ios"; + # config = "i386-apple-darwin11"; + sdkVer = "14.3"; + xcodeVer = "12.3"; + xcodePlatform = "iPhoneSimulator"; + darwinPlatform = "ios-simulator"; + useiOSPrebuilt = true; + }; + + aarch64-darwin = { + config = "aarch64-apple-darwin"; + xcodePlatform = "MacOSX"; + platform = {}; + }; + + x86_64-darwin = { + config = "x86_64-apple-darwin"; + xcodePlatform = "MacOSX"; + platform = {}; + }; + + # + # Windows + # + + # 32 bit mingw-w64 + mingw32 = { + config = "i686-w64-mingw32"; + libc = "msvcrt"; # This distinguishes the mingw (non posix) toolchain + }; + + # 64 bit mingw-w64 + mingwW64 = { + # That's the triplet they use in the mingw-w64 docs. + config = "x86_64-w64-mingw32"; + libc = "msvcrt"; # This distinguishes the mingw (non posix) toolchain + }; + + # BSDs + + x86_64-freebsd = { + config = "x86_64-unknown-freebsd13"; + useLLVM = true; + }; + + x86_64-netbsd = { + config = "x86_64-unknown-netbsd"; + }; + + # this is broken and never worked fully + x86_64-netbsd-llvm = { + config = "x86_64-unknown-netbsd"; + useLLVM = true; + }; + + # + # WASM + # + + wasi32 = { + config = "wasm32-unknown-wasi"; + useLLVM = true; + }; + + # Ghcjs + ghcjs = { + # This triple is special to GHC/Cabal/GHCJS and not recognized by autotools + # See: https://gitlab.haskell.org/ghc/ghc/-/commit/6636b670233522f01d002c9b97827d00289dbf5c + # https://github.com/ghcjs/ghcjs/issues/53 + config = "javascript-unknown-ghcjs"; + }; +} diff --git a/systems/flake-systems.nix b/systems/flake-systems.nix new file mode 100644 index 000000000..b1988c6a4 --- /dev/null +++ b/systems/flake-systems.nix @@ -0,0 +1,29 @@ +# See [RFC 46] for mandated platform support and ../../pkgs/stdenv for +# implemented platform support. This list is mainly descriptive, i.e. all +# system doubles for platforms where nixpkgs can do native compilation +# reasonably well are included. +# +# [RFC 46]: https://github.com/NixOS/rfcs/blob/master/rfcs/0046-platform-support-tiers.md +{ }: + +[ + # Tier 1 + "x86_64-linux" + # Tier 2 + "aarch64-linux" + "x86_64-darwin" + # Tier 3 + "armv6l-linux" + "armv7l-linux" + "i686-linux" + "mipsel-linux" + + # Other platforms with sufficient support in stdenv which is not formally + # mandated by their platform tier. + "aarch64-darwin" + "armv5tel-linux" + "powerpc64le-linux" + "riscv64-linux" + + # "x86_64-freebsd" is excluded because it is mostly broken +] diff --git a/systems/inspect.nix b/systems/inspect.nix new file mode 100644 index 000000000..89e9f4231 --- /dev/null +++ b/systems/inspect.nix @@ -0,0 +1,117 @@ +{ lib }: +with import ./parse.nix { inherit lib; }; +with lib.attrsets; +with lib.lists; + +let abis_ = abis; in +let abis = lib.mapAttrs (_: abi: builtins.removeAttrs abi [ "assertions" ]) abis_; in + +rec { + # these patterns are to be matched against {host,build,target}Platform.parsed + patterns = rec { + # The patterns below are lists in sum-of-products form. + # + # Each attribute is list of product conditions; non-list values are treated + # as a singleton list. If *any* product condition in the list matches then + # the predicate matches. Each product condition is tested by + # `lib.attrsets.matchAttrs`, which requires a match on *all* attributes of + # the product. + + isi686 = { cpu = cpuTypes.i686; }; + isx86_32 = { cpu = { family = "x86"; bits = 32; }; }; + isx86_64 = { cpu = { family = "x86"; bits = 64; }; }; + isPower = { cpu = { family = "power"; }; }; + isPower64 = { cpu = { family = "power"; bits = 64; }; }; + # This ABI is the default in NixOS PowerPC64 BE, but not on mainline GCC, + # so it sometimes causes issues in certain packages that makes the wrong + # assumption on the used ABI. + isAbiElfv2 = [ + { abi = { abi = "elfv2"; }; } + { abi = { name = "musl"; }; cpu = { family = "power"; bits = 64; }; } + ]; + isx86 = { cpu = { family = "x86"; }; }; + isAarch32 = { cpu = { family = "arm"; bits = 32; }; }; + isArmv7 = map ({ arch, ... }: { cpu = { inherit arch; }; }) + (lib.filter (cpu: lib.hasPrefix "armv7" cpu.arch or "") + (lib.attrValues cpuTypes)); + isAarch64 = { cpu = { family = "arm"; bits = 64; }; }; + isAarch = { cpu = { family = "arm"; }; }; + isMicroBlaze = { cpu = { family = "microblaze"; }; }; + isMips = { cpu = { family = "mips"; }; }; + isMips32 = { cpu = { family = "mips"; bits = 32; }; }; + isMips64 = { cpu = { family = "mips"; bits = 64; }; }; + isMips64n32 = { cpu = { family = "mips"; bits = 64; }; abi = { abi = "n32"; }; }; + isMips64n64 = { cpu = { family = "mips"; bits = 64; }; abi = { abi = "64"; }; }; + isMmix = { cpu = { family = "mmix"; }; }; + isRiscV = { cpu = { family = "riscv"; }; }; + isRiscV32 = { cpu = { family = "riscv"; bits = 32; }; }; + isRiscV64 = { cpu = { family = "riscv"; bits = 64; }; }; + isRx = { cpu = { family = "rx"; }; }; + isSparc = { cpu = { family = "sparc"; }; }; + isWasm = { cpu = { family = "wasm"; }; }; + isMsp430 = { cpu = { family = "msp430"; }; }; + isVc4 = { cpu = { family = "vc4"; }; }; + isAvr = { cpu = { family = "avr"; }; }; + isAlpha = { cpu = { family = "alpha"; }; }; + isOr1k = { cpu = { family = "or1k"; }; }; + isM68k = { cpu = { family = "m68k"; }; }; + isS390 = { cpu = { family = "s390"; }; }; + isS390x = { cpu = { family = "s390"; bits = 64; }; }; + isLoongArch64 = { cpu = { family = "loongarch"; bits = 64; }; }; + isJavaScript = { cpu = cpuTypes.javascript; }; + + is32bit = { cpu = { bits = 32; }; }; + is64bit = { cpu = { bits = 64; }; }; + isILP32 = map (a: { abi = { abi = a; }; }) [ "n32" "ilp32" "x32" ]; + isBigEndian = { cpu = { significantByte = significantBytes.bigEndian; }; }; + isLittleEndian = { cpu = { significantByte = significantBytes.littleEndian; }; }; + + isBSD = { kernel = { families = { inherit (kernelFamilies) bsd; }; }; }; + isDarwin = { kernel = { families = { inherit (kernelFamilies) darwin; }; }; }; + isUnix = [ isBSD isDarwin isLinux isSunOS isCygwin isRedox ]; + + isMacOS = { kernel = kernels.macos; }; + isiOS = { kernel = kernels.ios; }; + isLinux = { kernel = kernels.linux; }; + isSunOS = { kernel = kernels.solaris; }; + isFreeBSD = { kernel = { name = "freebsd"; }; }; + isNetBSD = { kernel = kernels.netbsd; }; + isOpenBSD = { kernel = kernels.openbsd; }; + isWindows = { kernel = kernels.windows; }; + isCygwin = { kernel = kernels.windows; abi = abis.cygnus; }; + isMinGW = { kernel = kernels.windows; abi = abis.gnu; }; + isWasi = { kernel = kernels.wasi; }; + isRedox = { kernel = kernels.redox; }; + isGhcjs = { kernel = kernels.ghcjs; }; + isGenode = { kernel = kernels.genode; }; + isNone = { kernel = kernels.none; }; + + isAndroid = [ { abi = abis.android; } { abi = abis.androideabi; } ]; + isGnu = with abis; map (a: { abi = a; }) [ gnuabi64 gnu gnueabi gnueabihf gnuabielfv1 gnuabielfv2 ]; + isMusl = with abis; map (a: { abi = a; }) [ musl musleabi musleabihf muslabin32 muslabi64 ]; + isUClibc = with abis; map (a: { abi = a; }) [ uclibc uclibceabi uclibceabihf ]; + + isEfi = [ + { cpu = { family = "arm"; version = "6"; }; } + { cpu = { family = "arm"; version = "7"; }; } + { cpu = { family = "arm"; version = "8"; }; } + { cpu = { family = "riscv"; }; } + { cpu = { family = "x86"; }; } + ]; + }; + + matchAnyAttrs = patterns: + if builtins.isList patterns then attrs: any (pattern: matchAttrs pattern attrs) patterns + else matchAttrs patterns; + + predicates = mapAttrs (_: matchAnyAttrs) patterns; + + # these patterns are to be matched against the entire + # {host,build,target}Platform structure; they include a `parsed={}` marker so + # that `lib.meta.availableOn` can distinguish them from the patterns which + # apply only to the `parsed` field. + + platformPatterns = mapAttrs (_: p: { parsed = {}; } // p) { + isStatic = { isStatic = true; }; + }; +} diff --git a/systems/parse.nix b/systems/parse.nix new file mode 100644 index 000000000..6eb4f27cc --- /dev/null +++ b/systems/parse.nix @@ -0,0 +1,503 @@ +# Define the list of system with their properties. +# +# See https://clang.llvm.org/docs/CrossCompilation.html and +# http://llvm.org/docs/doxygen/html/Triple_8cpp_source.html especially +# Triple::normalize. Parsing should essentially act as a more conservative +# version of that last function. +# +# Most of the types below come in "open" and "closed" pairs. The open ones +# specify what information we need to know about systems in general, and the +# closed ones are sub-types representing the whitelist of systems we support in +# practice. +# +# Code in the remainder of nixpkgs shouldn't rely on the closed ones in +# e.g. exhaustive cases. Its more a sanity check to make sure nobody defines +# systems that overlap with existing ones and won't notice something amiss. +# +{ lib }: +with lib.lists; +with lib.types; +with lib.attrsets; +with lib.strings; +with (import ./inspect.nix { inherit lib; }).predicates; + +let + inherit (lib.options) mergeOneOption; + + setTypes = type: + mapAttrs (name: value: + assert type.check value; + setType type.name ({ inherit name; } // value)); + +in + +rec { + + ################################################################################ + + types.openSignificantByte = mkOptionType { + name = "significant-byte"; + description = "Endianness"; + merge = mergeOneOption; + }; + + types.significantByte = enum (attrValues significantBytes); + + significantBytes = setTypes types.openSignificantByte { + bigEndian = {}; + littleEndian = {}; + }; + + ################################################################################ + + # Reasonable power of 2 + types.bitWidth = enum [ 8 16 32 64 128 ]; + + ################################################################################ + + types.openCpuType = mkOptionType { + name = "cpu-type"; + description = "instruction set architecture name and information"; + merge = mergeOneOption; + check = x: types.bitWidth.check x.bits + && (if 8 < x.bits + then types.significantByte.check x.significantByte + else !(x ? significantByte)); + }; + + types.cpuType = enum (attrValues cpuTypes); + + cpuTypes = with significantBytes; setTypes types.openCpuType { + arm = { bits = 32; significantByte = littleEndian; family = "arm"; }; + armv5tel = { bits = 32; significantByte = littleEndian; family = "arm"; version = "5"; arch = "armv5t"; }; + armv6m = { bits = 32; significantByte = littleEndian; family = "arm"; version = "6"; arch = "armv6-m"; }; + armv6l = { bits = 32; significantByte = littleEndian; family = "arm"; version = "6"; arch = "armv6"; }; + armv7a = { bits = 32; significantByte = littleEndian; family = "arm"; version = "7"; arch = "armv7-a"; }; + armv7r = { bits = 32; significantByte = littleEndian; family = "arm"; version = "7"; arch = "armv7-r"; }; + armv7m = { bits = 32; significantByte = littleEndian; family = "arm"; version = "7"; arch = "armv7-m"; }; + armv7l = { bits = 32; significantByte = littleEndian; family = "arm"; version = "7"; arch = "armv7"; }; + armv8a = { bits = 32; significantByte = littleEndian; family = "arm"; version = "8"; arch = "armv8-a"; }; + armv8r = { bits = 32; significantByte = littleEndian; family = "arm"; version = "8"; arch = "armv8-a"; }; + armv8m = { bits = 32; significantByte = littleEndian; family = "arm"; version = "8"; arch = "armv8-m"; }; + aarch64 = { bits = 64; significantByte = littleEndian; family = "arm"; version = "8"; arch = "armv8-a"; }; + aarch64_be = { bits = 64; significantByte = bigEndian; family = "arm"; version = "8"; arch = "armv8-a"; }; + + i386 = { bits = 32; significantByte = littleEndian; family = "x86"; arch = "i386"; }; + i486 = { bits = 32; significantByte = littleEndian; family = "x86"; arch = "i486"; }; + i586 = { bits = 32; significantByte = littleEndian; family = "x86"; arch = "i586"; }; + i686 = { bits = 32; significantByte = littleEndian; family = "x86"; arch = "i686"; }; + x86_64 = { bits = 64; significantByte = littleEndian; family = "x86"; arch = "x86-64"; }; + + microblaze = { bits = 32; significantByte = bigEndian; family = "microblaze"; }; + microblazeel = { bits = 32; significantByte = littleEndian; family = "microblaze"; }; + + mips = { bits = 32; significantByte = bigEndian; family = "mips"; }; + mipsel = { bits = 32; significantByte = littleEndian; family = "mips"; }; + mips64 = { bits = 64; significantByte = bigEndian; family = "mips"; }; + mips64el = { bits = 64; significantByte = littleEndian; family = "mips"; }; + + mmix = { bits = 64; significantByte = bigEndian; family = "mmix"; }; + + m68k = { bits = 32; significantByte = bigEndian; family = "m68k"; }; + + powerpc = { bits = 32; significantByte = bigEndian; family = "power"; }; + powerpc64 = { bits = 64; significantByte = bigEndian; family = "power"; }; + powerpc64le = { bits = 64; significantByte = littleEndian; family = "power"; }; + powerpcle = { bits = 32; significantByte = littleEndian; family = "power"; }; + + riscv32 = { bits = 32; significantByte = littleEndian; family = "riscv"; }; + riscv64 = { bits = 64; significantByte = littleEndian; family = "riscv"; }; + + s390 = { bits = 32; significantByte = bigEndian; family = "s390"; }; + s390x = { bits = 64; significantByte = bigEndian; family = "s390"; }; + + sparc = { bits = 32; significantByte = bigEndian; family = "sparc"; }; + sparc64 = { bits = 64; significantByte = bigEndian; family = "sparc"; }; + + wasm32 = { bits = 32; significantByte = littleEndian; family = "wasm"; }; + wasm64 = { bits = 64; significantByte = littleEndian; family = "wasm"; }; + + alpha = { bits = 64; significantByte = littleEndian; family = "alpha"; }; + + rx = { bits = 32; significantByte = littleEndian; family = "rx"; }; + msp430 = { bits = 16; significantByte = littleEndian; family = "msp430"; }; + avr = { bits = 8; family = "avr"; }; + + vc4 = { bits = 32; significantByte = littleEndian; family = "vc4"; }; + + or1k = { bits = 32; significantByte = bigEndian; family = "or1k"; }; + + loongarch64 = { bits = 64; significantByte = littleEndian; family = "loongarch"; }; + + javascript = { bits = 32; significantByte = littleEndian; family = "javascript"; }; + }; + + # GNU build systems assume that older NetBSD architectures are using a.out. + gnuNetBSDDefaultExecFormat = cpu: + if (cpu.family == "arm" && cpu.bits == 32) || + (cpu.family == "sparc" && cpu.bits == 32) || + (cpu.family == "m68k" && cpu.bits == 32) || + (cpu.family == "x86" && cpu.bits == 32) + then execFormats.aout + else execFormats.elf; + + # Determine when two CPUs are compatible with each other. That is, + # can code built for system B run on system A? For that to happen, + # the programs that system B accepts must be a subset of the + # programs that system A accepts. + # + # We have the following properties of the compatibility relation, + # which must be preserved when adding compatibility information for + # additional CPUs. + # - (reflexivity) + # Every CPU is compatible with itself. + # - (transitivity) + # If A is compatible with B and B is compatible with C then A is compatible with C. + # + # Note: Since 22.11 the archs of a mode switching CPU are no longer considered + # pairwise compatible. Mode switching implies that binaries built for A + # and B respectively can't be executed at the same time. + isCompatible = a: b: with cpuTypes; lib.any lib.id [ + # x86 + (b == i386 && isCompatible a i486) + (b == i486 && isCompatible a i586) + (b == i586 && isCompatible a i686) + + # XXX: Not true in some cases. Like in WSL mode. + (b == i686 && isCompatible a x86_64) + + # ARMv4 + (b == arm && isCompatible a armv5tel) + + # ARMv5 + (b == armv5tel && isCompatible a armv6l) + + # ARMv6 + (b == armv6l && isCompatible a armv6m) + (b == armv6m && isCompatible a armv7l) + + # ARMv7 + (b == armv7l && isCompatible a armv7a) + (b == armv7l && isCompatible a armv7r) + (b == armv7l && isCompatible a armv7m) + + # ARMv8 + (b == aarch64 && a == armv8a) + (b == armv8a && isCompatible a aarch64) + (b == armv8r && isCompatible a armv8a) + (b == armv8m && isCompatible a armv8a) + + # PowerPC + (b == powerpc && isCompatible a powerpc64) + (b == powerpcle && isCompatible a powerpc64le) + + # MIPS + (b == mips && isCompatible a mips64) + (b == mipsel && isCompatible a mips64el) + + # RISCV + (b == riscv32 && isCompatible a riscv64) + + # SPARC + (b == sparc && isCompatible a sparc64) + + # WASM + (b == wasm32 && isCompatible a wasm64) + + # identity + (b == a) + ]; + + ################################################################################ + + types.openVendor = mkOptionType { + name = "vendor"; + description = "vendor for the platform"; + merge = mergeOneOption; + }; + + types.vendor = enum (attrValues vendors); + + vendors = setTypes types.openVendor { + apple = {}; + pc = {}; + # Actually matters, unlocking some MinGW-w64-specific options in GCC. See + # bottom of https://sourceforge.net/p/mingw-w64/wiki2/Unicode%20apps/ + w64 = {}; + + none = {}; + unknown = {}; + }; + + ################################################################################ + + types.openExecFormat = mkOptionType { + name = "exec-format"; + description = "executable container used by the kernel"; + merge = mergeOneOption; + }; + + types.execFormat = enum (attrValues execFormats); + + execFormats = setTypes types.openExecFormat { + aout = {}; # a.out + elf = {}; + macho = {}; + pe = {}; + wasm = {}; + + unknown = {}; + }; + + ################################################################################ + + types.openKernelFamily = mkOptionType { + name = "exec-format"; + description = "executable container used by the kernel"; + merge = mergeOneOption; + }; + + types.kernelFamily = enum (attrValues kernelFamilies); + + kernelFamilies = setTypes types.openKernelFamily { + bsd = {}; + darwin = {}; + }; + + ################################################################################ + + types.openKernel = mkOptionType { + name = "kernel"; + description = "kernel name and information"; + merge = mergeOneOption; + check = x: types.execFormat.check x.execFormat + && all types.kernelFamily.check (attrValues x.families); + }; + + types.kernel = enum (attrValues kernels); + + kernels = with execFormats; with kernelFamilies; setTypes types.openKernel { + # TODO(@Ericson2314): Don't want to mass-rebuild yet to keeping 'darwin' as + # the normalized name for macOS. + macos = { execFormat = macho; families = { inherit darwin; }; name = "darwin"; }; + ios = { execFormat = macho; families = { inherit darwin; }; }; + # A tricky thing about FreeBSD is that there is no stable ABI across + # versions. That means that putting in the version as part of the + # config string is paramount. + freebsd12 = { execFormat = elf; families = { inherit bsd; }; name = "freebsd"; version = 12; }; + freebsd13 = { execFormat = elf; families = { inherit bsd; }; name = "freebsd"; version = 13; }; + linux = { execFormat = elf; families = { }; }; + netbsd = { execFormat = elf; families = { inherit bsd; }; }; + none = { execFormat = unknown; families = { }; }; + openbsd = { execFormat = elf; families = { inherit bsd; }; }; + solaris = { execFormat = elf; families = { }; }; + wasi = { execFormat = wasm; families = { }; }; + redox = { execFormat = elf; families = { }; }; + windows = { execFormat = pe; families = { }; }; + ghcjs = { execFormat = unknown; families = { }; }; + genode = { execFormat = elf; families = { }; }; + mmixware = { execFormat = unknown; families = { }; }; + } // { # aliases + # 'darwin' is the kernel for all of them. We choose macOS by default. + darwin = kernels.macos; + watchos = kernels.ios; + tvos = kernels.ios; + win32 = kernels.windows; + }; + + ################################################################################ + + types.openAbi = mkOptionType { + name = "abi"; + description = "binary interface for compiled code and syscalls"; + merge = mergeOneOption; + }; + + types.abi = enum (attrValues abis); + + abis = setTypes types.openAbi { + cygnus = {}; + msvc = {}; + + # Note: eabi is specific to ARM and PowerPC. + # On PowerPC, this corresponds to PPCEABI. + # On ARM, this corresponds to ARMEABI. + eabi = { float = "soft"; }; + eabihf = { float = "hard"; }; + + # Other architectures should use ELF in embedded situations. + elf = {}; + + androideabi = {}; + android = { + assertions = [ + { assertion = platform: !platform.isAarch32; + message = '' + The "android" ABI is not for 32-bit ARM. Use "androideabi" instead. + ''; + } + ]; + }; + + gnueabi = { float = "soft"; }; + gnueabihf = { float = "hard"; }; + gnu = { + assertions = [ + { assertion = platform: !platform.isAarch32; + message = '' + The "gnu" ABI is ambiguous on 32-bit ARM. Use "gnueabi" or "gnueabihf" instead. + ''; + } + { assertion = platform: with platform; !(isPower64 && isBigEndian); + message = '' + The "gnu" ABI is ambiguous on big-endian 64-bit PowerPC. Use "gnuabielfv2" or "gnuabielfv1" instead. + ''; + } + ]; + }; + gnuabi64 = { abi = "64"; }; + muslabi64 = { abi = "64"; }; + + # NOTE: abi=n32 requires a 64-bit MIPS chip! That is not a typo. + # It is basically the 64-bit abi with 32-bit pointers. Details: + # https://www.linux-mips.org/pub/linux/mips/doc/ABI/MIPS-N32-ABI-Handbook.pdf + gnuabin32 = { abi = "n32"; }; + muslabin32 = { abi = "n32"; }; + + gnuabielfv2 = { abi = "elfv2"; }; + gnuabielfv1 = { abi = "elfv1"; }; + + musleabi = { float = "soft"; }; + musleabihf = { float = "hard"; }; + musl = {}; + + uclibceabi = { float = "soft"; }; + uclibceabihf = { float = "hard"; }; + uclibc = {}; + + unknown = {}; + }; + + ################################################################################ + + types.parsedPlatform = mkOptionType { + name = "system"; + description = "fully parsed representation of llvm- or nix-style platform tuple"; + merge = mergeOneOption; + check = { cpu, vendor, kernel, abi }: + types.cpuType.check cpu + && types.vendor.check vendor + && types.kernel.check kernel + && types.abi.check abi; + }; + + isSystem = isType "system"; + + mkSystem = components: + assert types.parsedPlatform.check components; + setType "system" components; + + mkSkeletonFromList = l: { + "1" = if elemAt l 0 == "avr" + then { cpu = elemAt l 0; kernel = "none"; abi = "unknown"; } + else throw "Target specification with 1 components is ambiguous"; + "2" = # We only do 2-part hacks for things Nix already supports + if elemAt l 1 == "cygwin" + then { cpu = elemAt l 0; kernel = "windows"; abi = "cygnus"; } + # MSVC ought to be the default ABI so this case isn't needed. But then it + # becomes difficult to handle the gnu* variants for Aarch32 correctly for + # minGW. So it's easier to make gnu* the default for the MinGW, but + # hack-in MSVC for the non-MinGW case right here. + else if elemAt l 1 == "windows" + then { cpu = elemAt l 0; kernel = "windows"; abi = "msvc"; } + else if (elemAt l 1) == "elf" + then { cpu = elemAt l 0; vendor = "unknown"; kernel = "none"; abi = elemAt l 1; } + else { cpu = elemAt l 0; kernel = elemAt l 1; }; + "3" = + # cpu-kernel-environment + if elemAt l 1 == "linux" || + elem (elemAt l 2) ["eabi" "eabihf" "elf" "gnu"] + then { + cpu = elemAt l 0; + kernel = elemAt l 1; + abi = elemAt l 2; + vendor = "unknown"; + } + # cpu-vendor-os + else if elemAt l 1 == "apple" || + elem (elemAt l 2) [ "wasi" "redox" "mmixware" "ghcjs" "mingw32" ] || + hasPrefix "freebsd" (elemAt l 2) || + hasPrefix "netbsd" (elemAt l 2) || + hasPrefix "genode" (elemAt l 2) + then { + cpu = elemAt l 0; + vendor = elemAt l 1; + kernel = if elemAt l 2 == "mingw32" + then "windows" # autotools breaks on -gnu for window + else elemAt l 2; + } + else throw "Target specification with 3 components is ambiguous"; + "4" = { cpu = elemAt l 0; vendor = elemAt l 1; kernel = elemAt l 2; abi = elemAt l 3; }; + }.${toString (length l)} + or (throw "system string has invalid number of hyphen-separated components"); + + # This should revert the job done by config.guess from the gcc compiler. + mkSystemFromSkeleton = { cpu + , # Optional, but fallback too complex for here. + # Inferred below instead. + vendor ? assert false; null + , kernel + , # Also inferred below + abi ? assert false; null + } @ args: let + getCpu = name: cpuTypes.${name} or (throw "Unknown CPU type: ${name}"); + getVendor = name: vendors.${name} or (throw "Unknown vendor: ${name}"); + getKernel = name: kernels.${name} or (throw "Unknown kernel: ${name}"); + getAbi = name: abis.${name} or (throw "Unknown ABI: ${name}"); + + parsed = { + cpu = getCpu args.cpu; + vendor = + /**/ if args ? vendor then getVendor args.vendor + else if isDarwin parsed then vendors.apple + else if isWindows parsed then vendors.pc + else vendors.unknown; + kernel = if hasPrefix "darwin" args.kernel then getKernel "darwin" + else if hasPrefix "netbsd" args.kernel then getKernel "netbsd" + else getKernel args.kernel; + abi = + /**/ if args ? abi then getAbi args.abi + else if isLinux parsed || isWindows parsed then + if isAarch32 parsed then + if lib.versionAtLeast (parsed.cpu.version or "0") "6" + then abis.gnueabihf + else abis.gnueabi + # Default ppc64 BE to ELFv2 + else if isPower64 parsed && isBigEndian parsed then abis.gnuabielfv2 + else abis.gnu + else abis.unknown; + }; + + in mkSystem parsed; + + mkSystemFromString = s: mkSystemFromSkeleton (mkSkeletonFromList (lib.splitString "-" s)); + + kernelName = kernel: + kernel.name + toString (kernel.version or ""); + + doubleFromSystem = { cpu, kernel, abi, ... }: + /**/ if abi == abis.cygnus then "${cpu.name}-cygwin" + else if kernel.families ? darwin then "${cpu.name}-darwin" + else "${cpu.name}-${kernelName kernel}"; + + tripleFromSystem = { cpu, vendor, kernel, abi, ... } @ sys: assert isSystem sys; let + optExecFormat = + lib.optionalString (kernel.name == "netbsd" && + gnuNetBSDDefaultExecFormat cpu != kernel.execFormat) + kernel.execFormat.name; + optAbi = lib.optionalString (abi != abis.unknown) "-${abi.name}"; + in "${cpu.name}-${vendor.name}-${kernelName kernel}${optExecFormat}${optAbi}"; + + ################################################################################ + +} diff --git a/systems/platforms.nix b/systems/platforms.nix new file mode 100644 index 000000000..d574943e4 --- /dev/null +++ b/systems/platforms.nix @@ -0,0 +1,565 @@ +# Note: lib/systems/default.nix takes care of producing valid, +# fully-formed "platform" values (e.g. hostPlatform, buildPlatform, +# targetPlatform, etc) containing at least the minimal set of attrs +# required (see types.parsedPlatform in lib/systems/parse.nix). This +# file takes an already-valid platform and further elaborates it with +# optional fields; currently these are: linux-kernel, gcc, and rustc. + +{ lib }: +rec { + pc = { + linux-kernel = { + name = "pc"; + + baseConfig = "defconfig"; + # Build whatever possible as a module, if not stated in the extra config. + autoModules = true; + target = "bzImage"; + }; + }; + + pc_simplekernel = lib.recursiveUpdate pc { + linux-kernel.autoModules = false; + }; + + powernv = { + linux-kernel = { + name = "PowerNV"; + + baseConfig = "powernv_defconfig"; + target = "vmlinux"; + autoModules = true; + # avoid driver/FS trouble arising from unusual page size + extraConfig = '' + PPC_64K_PAGES n + PPC_4K_PAGES y + IPV6 y + + ATA_BMDMA y + ATA_SFF y + VIRTIO_MENU y + ''; + }; + }; + + ## + ## ARM + ## + + pogoplug4 = { + linux-kernel = { + name = "pogoplug4"; + + baseConfig = "multi_v5_defconfig"; + autoModules = false; + extraConfig = '' + # Ubi for the mtd + MTD_UBI y + UBIFS_FS y + UBIFS_FS_XATTR y + UBIFS_FS_ADVANCED_COMPR y + UBIFS_FS_LZO y + UBIFS_FS_ZLIB y + UBIFS_FS_DEBUG n + ''; + makeFlags = [ "LOADADDR=0x8000" ]; + target = "uImage"; + # TODO reenable once manual-config's config actually builds a .dtb and this is checked to be working + #DTB = true; + }; + gcc = { + arch = "armv5te"; + }; + }; + + sheevaplug = { + linux-kernel = { + name = "sheevaplug"; + + baseConfig = "multi_v5_defconfig"; + autoModules = false; + extraConfig = '' + BLK_DEV_RAM y + BLK_DEV_INITRD y + BLK_DEV_CRYPTOLOOP m + BLK_DEV_DM m + DM_CRYPT m + MD y + REISERFS_FS m + BTRFS_FS m + XFS_FS m + JFS_FS m + EXT4_FS m + USB_STORAGE_CYPRESS_ATACB m + + # mv cesa requires this sw fallback, for mv-sha1 + CRYPTO_SHA1 y + # Fast crypto + CRYPTO_TWOFISH y + CRYPTO_TWOFISH_COMMON y + CRYPTO_BLOWFISH y + CRYPTO_BLOWFISH_COMMON y + + IP_PNP y + IP_PNP_DHCP y + NFS_FS y + ROOT_NFS y + TUN m + NFS_V4 y + NFS_V4_1 y + NFS_FSCACHE y + NFSD m + NFSD_V2_ACL y + NFSD_V3 y + NFSD_V3_ACL y + NFSD_V4 y + NETFILTER y + IP_NF_IPTABLES y + IP_NF_FILTER y + IP_NF_MATCH_ADDRTYPE y + IP_NF_TARGET_LOG y + IP_NF_MANGLE y + IPV6 m + VLAN_8021Q m + + CIFS y + CIFS_XATTR y + CIFS_POSIX y + CIFS_FSCACHE y + CIFS_ACL y + + WATCHDOG y + WATCHDOG_CORE y + ORION_WATCHDOG m + + ZRAM m + NETCONSOLE m + + # Disable OABI to have seccomp_filter (required for systemd) + # https://github.com/raspberrypi/firmware/issues/651 + OABI_COMPAT n + + # Fail to build + DRM n + SCSI_ADVANSYS n + USB_ISP1362_HCD n + SND_SOC n + SND_ALI5451 n + FB_SAVAGE n + SCSI_NSP32 n + ATA_SFF n + SUNGEM n + IRDA n + ATM_HE n + SCSI_ACARD n + BLK_DEV_CMD640_ENHANCED n + + FUSE_FS m + + # systemd uses cgroups + CGROUPS y + + # Latencytop + LATENCYTOP y + + # Ubi for the mtd + MTD_UBI y + UBIFS_FS y + UBIFS_FS_XATTR y + UBIFS_FS_ADVANCED_COMPR y + UBIFS_FS_LZO y + UBIFS_FS_ZLIB y + UBIFS_FS_DEBUG n + + # Kdb, for kernel troubles + KGDB y + KGDB_SERIAL_CONSOLE y + KGDB_KDB y + ''; + makeFlags = [ "LOADADDR=0x0200000" ]; + target = "uImage"; + DTB = true; # Beyond 3.10 + }; + gcc = { + arch = "armv5te"; + }; + }; + + raspberrypi = { + linux-kernel = { + name = "raspberrypi"; + + baseConfig = "bcm2835_defconfig"; + DTB = true; + autoModules = true; + preferBuiltin = true; + extraConfig = '' + # Disable OABI to have seccomp_filter (required for systemd) + # https://github.com/raspberrypi/firmware/issues/651 + OABI_COMPAT n + ''; + target = "zImage"; + }; + gcc = { + arch = "armv6"; + fpu = "vfp"; + }; + }; + + # Legacy attribute, for compatibility with existing configs only. + raspberrypi2 = armv7l-hf-multiplatform; + + zero-gravitas = { + linux-kernel = { + name = "zero-gravitas"; + + baseConfig = "zero-gravitas_defconfig"; + # Target verified by checking /boot on reMarkable 1 device + target = "zImage"; + autoModules = false; + DTB = true; + }; + gcc = { + fpu = "neon"; + cpu = "cortex-a9"; + }; + }; + + zero-sugar = { + linux-kernel = { + name = "zero-sugar"; + + baseConfig = "zero-sugar_defconfig"; + DTB = true; + autoModules = false; + preferBuiltin = true; + target = "zImage"; + }; + gcc = { + cpu = "cortex-a7"; + fpu = "neon-vfpv4"; + float-abi = "hard"; + }; + }; + + utilite = { + linux-kernel = { + name = "utilite"; + maseConfig = "multi_v7_defconfig"; + autoModules = false; + extraConfig = '' + # Ubi for the mtd + MTD_UBI y + UBIFS_FS y + UBIFS_FS_XATTR y + UBIFS_FS_ADVANCED_COMPR y + UBIFS_FS_LZO y + UBIFS_FS_ZLIB y + UBIFS_FS_DEBUG n + ''; + makeFlags = [ "LOADADDR=0x10800000" ]; + target = "uImage"; + DTB = true; + }; + gcc = { + cpu = "cortex-a9"; + fpu = "neon"; + }; + }; + + guruplug = lib.recursiveUpdate sheevaplug { + # Define `CONFIG_MACH_GURUPLUG' (see + # ) + # and other GuruPlug-specific things. Requires the `guruplug-defconfig' + # patch. + linux-kernel.baseConfig = "guruplug_defconfig"; + }; + + beaglebone = lib.recursiveUpdate armv7l-hf-multiplatform { + linux-kernel = { + name = "beaglebone"; + baseConfig = "bb.org_defconfig"; + autoModules = false; + extraConfig = ""; # TBD kernel config + target = "zImage"; + }; + }; + + # https://developer.android.com/ndk/guides/abis#v7a + armv7a-android = { + linux-kernel.name = "armeabi-v7a"; + gcc = { + arch = "armv7-a"; + float-abi = "softfp"; + fpu = "vfpv3-d16"; + }; + }; + + armv7l-hf-multiplatform = { + linux-kernel = { + name = "armv7l-hf-multiplatform"; + Major = "2.6"; # Using "2.6" enables 2.6 kernel syscalls in glibc. + baseConfig = "multi_v7_defconfig"; + DTB = true; + autoModules = true; + preferBuiltin = true; + target = "zImage"; + extraConfig = '' + # Serial port for Raspberry Pi 3. Wasn't included in ARMv7 defconfig + # until 4.17. + SERIAL_8250_BCM2835AUX y + SERIAL_8250_EXTENDED y + SERIAL_8250_SHARE_IRQ y + + # Hangs ODROID-XU4 + ARM_BIG_LITTLE_CPUIDLE n + + # Disable OABI to have seccomp_filter (required for systemd) + # https://github.com/raspberrypi/firmware/issues/651 + OABI_COMPAT n + + # >=5.12 fails with: + # drivers/net/ethernet/micrel/ks8851_common.o: in function `ks8851_probe_common': + # ks8851_common.c:(.text+0x179c): undefined reference to `__this_module' + # See: https://lore.kernel.org/netdev/20210116164828.40545-1-marex@denx.de/T/ + KS8851_MLL y + ''; + }; + gcc = { + # Some table about fpu flags: + # http://community.arm.com/servlet/JiveServlet/showImage/38-1981-3827/blogentry-103749-004812900+1365712953_thumb.png + # Cortex-A5: -mfpu=neon-fp16 + # Cortex-A7 (rpi2): -mfpu=neon-vfpv4 + # Cortex-A8 (beaglebone): -mfpu=neon + # Cortex-A9: -mfpu=neon-fp16 + # Cortex-A15: -mfpu=neon-vfpv4 + + # More about FPU: + # https://wiki.debian.org/ArmHardFloatPort/VfpComparison + + # vfpv3-d16 is what Debian uses and seems to be the best compromise: NEON is not supported in e.g. Scaleway or Tegra 2, + # and the above page suggests NEON is only an improvement with hand-written assembly. + arch = "armv7-a"; + fpu = "vfpv3-d16"; + + # For Raspberry Pi the 2 the best would be: + # cpu = "cortex-a7"; + # fpu = "neon-vfpv4"; + }; + }; + + aarch64-multiplatform = { + linux-kernel = { + name = "aarch64-multiplatform"; + baseConfig = "defconfig"; + DTB = true; + autoModules = true; + preferBuiltin = true; + extraConfig = '' + # Raspberry Pi 3 stuff. Not needed for s >= 4.10. + ARCH_BCM2835 y + BCM2835_MBOX y + BCM2835_WDT y + RASPBERRYPI_FIRMWARE y + RASPBERRYPI_POWER y + SERIAL_8250_BCM2835AUX y + SERIAL_8250_EXTENDED y + SERIAL_8250_SHARE_IRQ y + + # Cavium ThunderX stuff. + PCI_HOST_THUNDER_ECAM y + + # Nvidia Tegra stuff. + PCI_TEGRA y + + # The default (=y) forces us to have the XHCI firmware available in initrd, + # which our initrd builder can't currently do easily. + USB_XHCI_TEGRA m + ''; + target = "Image"; + }; + gcc = { + arch = "armv8-a"; + }; + }; + + apple-m1 = { + gcc = { + arch = "armv8.3-a+crypto+sha2+aes+crc+fp16+lse+simd+ras+rdm+rcpc"; + cpu = "apple-a13"; + }; + }; + + ## + ## MIPS + ## + + ben_nanonote = { + linux-kernel = { + name = "ben_nanonote"; + }; + gcc = { + arch = "mips32"; + float = "soft"; + }; + }; + + fuloong2f_n32 = { + linux-kernel = { + name = "fuloong2f_n32"; + baseConfig = "lemote2f_defconfig"; + autoModules = false; + extraConfig = '' + MIGRATION n + COMPACTION n + + # nixos mounts some cgroup + CGROUPS y + + BLK_DEV_RAM y + BLK_DEV_INITRD y + BLK_DEV_CRYPTOLOOP m + BLK_DEV_DM m + DM_CRYPT m + MD y + REISERFS_FS m + EXT4_FS m + USB_STORAGE_CYPRESS_ATACB m + + IP_PNP y + IP_PNP_DHCP y + IP_PNP_BOOTP y + NFS_FS y + ROOT_NFS y + TUN m + NFS_V4 y + NFS_V4_1 y + NFS_FSCACHE y + NFSD m + NFSD_V2_ACL y + NFSD_V3 y + NFSD_V3_ACL y + NFSD_V4 y + + # Fail to build + DRM n + SCSI_ADVANSYS n + USB_ISP1362_HCD n + SND_SOC n + SND_ALI5451 n + FB_SAVAGE n + SCSI_NSP32 n + ATA_SFF n + SUNGEM n + IRDA n + ATM_HE n + SCSI_ACARD n + BLK_DEV_CMD640_ENHANCED n + + FUSE_FS m + + # Needed for udev >= 150 + SYSFS_DEPRECATED_V2 n + + VGA_CONSOLE n + VT_HW_CONSOLE_BINDING y + SERIAL_8250_CONSOLE y + FRAMEBUFFER_CONSOLE y + EXT2_FS y + EXT3_FS y + REISERFS_FS y + MAGIC_SYSRQ y + + # The kernel doesn't boot at all, with FTRACE + FTRACE n + ''; + target = "vmlinux"; + }; + gcc = { + arch = "loongson2f"; + float = "hard"; + abi = "n32"; + }; + }; + + # can execute on 32bit chip + gcc_mips32r2_o32 = { gcc = { arch = "mips32r2"; abi = "32"; }; }; + gcc_mips32r6_o32 = { gcc = { arch = "mips32r6"; abi = "32"; }; }; + gcc_mips64r2_n32 = { gcc = { arch = "mips64r2"; abi = "n32"; }; }; + gcc_mips64r6_n32 = { gcc = { arch = "mips64r6"; abi = "n32"; }; }; + gcc_mips64r2_64 = { gcc = { arch = "mips64r2"; abi = "64"; }; }; + gcc_mips64r6_64 = { gcc = { arch = "mips64r6"; abi = "64"; }; }; + + # based on: + # https://www.mail-archive.com/qemu-discuss@nongnu.org/msg05179.html + # https://gmplib.org/~tege/qemu.html#mips64-debian + mips64el-qemu-linux-gnuabi64 = { + linux-kernel = { + name = "mips64el"; + baseConfig = "64r2el_defconfig"; + target = "vmlinuz"; + autoModules = false; + DTB = true; + # for qemu 9p passthrough filesystem + extraConfig = '' + MIPS_MALTA y + PAGE_SIZE_4KB y + CPU_LITTLE_ENDIAN y + CPU_MIPS64_R2 y + 64BIT y + CPU_MIPS64_R2 y + + NET_9P y + NET_9P_VIRTIO y + 9P_FS y + 9P_FS_POSIX_ACL y + PCI y + VIRTIO_PCI y + ''; + }; + }; + + ## + ## Other + ## + + riscv-multiplatform = { + linux-kernel = { + name = "riscv-multiplatform"; + target = "Image"; + autoModules = true; + baseConfig = "defconfig"; + DTB = true; + extraConfig = '' + SERIAL_OF_PLATFORM y + ''; + }; + }; + + # This function takes a minimally-valid "platform" and returns an + # attrset containing zero or more additional attrs which should be + # included in the platform in order to further elaborate it. + select = platform: + # x86 + /**/ if platform.isx86 then pc + + # ARM + else if platform.isAarch32 then let + version = platform.parsed.cpu.version or null; + in if version == null then pc + else if lib.versionOlder version "6" then sheevaplug + else if lib.versionOlder version "7" then raspberrypi + else armv7l-hf-multiplatform + + else if platform.isAarch64 then + if platform.isDarwin then apple-m1 + else aarch64-multiplatform + + else if platform.isRiscV then riscv-multiplatform + + else if platform.parsed.cpu == lib.systems.parse.cpuTypes.mipsel then (import ./examples.nix { inherit lib; }).mipsel-linux-gnu + + else if platform.parsed.cpu == lib.systems.parse.cpuTypes.powerpc64le then powernv + + else { }; +} diff --git a/tests/check-eval.nix b/tests/check-eval.nix new file mode 100644 index 000000000..8bd7b605a --- /dev/null +++ b/tests/check-eval.nix @@ -0,0 +1,7 @@ +# Throws an error if any of our lib tests fail. + +let tests = [ "misc" "systems" ]; + all = builtins.concatLists (map (f: import (./. + "/${f}.nix")) tests); +in if all == [] + then null + else throw (builtins.toJSON all) diff --git a/tests/filesystem.sh b/tests/filesystem.sh new file mode 100755 index 000000000..cfd333d00 --- /dev/null +++ b/tests/filesystem.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +# Tests lib/filesystem.nix +# Run: +# [nixpkgs]$ lib/tests/filesystem.sh +# or: +# [nixpkgs]$ nix-build lib/tests/release.nix + +set -euo pipefail +shopt -s inherit_errexit + +# Use +# || die +die() { + echo >&2 "test case failed: " "$@" + exit 1 +} + +if test -n "${TEST_LIB:-}"; then + NIX_PATH=nixpkgs="$(dirname "$TEST_LIB")" +else + NIX_PATH=nixpkgs="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.."; pwd)" +fi +export NIX_PATH + +work="$(mktemp -d)" +clean_up() { + rm -rf "$work" +} +trap clean_up EXIT +cd "$work" + +mkdir directory +touch regular +ln -s target symlink +mkfifo fifo + +expectSuccess() { + local expr=$1 + local expectedResultRegex=$2 + if ! result=$(nix-instantiate --eval --strict --json \ + --expr "with (import ).filesystem; $expr"); then + die "$expr failed to evaluate, but it was expected to succeed" + fi + if [[ ! "$result" =~ $expectedResultRegex ]]; then + die "$expr == $result, but $expectedResultRegex was expected" + fi +} + +expectFailure() { + local expr=$1 + local expectedErrorRegex=$2 + if result=$(nix-instantiate --eval --strict --json 2>"$work/stderr" \ + --expr "with (import ).filesystem; $expr"); then + die "$expr evaluated successfully to $result, but it was expected to fail" + fi + if [[ ! "$(<"$work/stderr")" =~ $expectedErrorRegex ]]; then + die "Error was $(<"$work/stderr"), but $expectedErrorRegex was expected" + fi +} + +expectSuccess "pathType /." '"directory"' +expectSuccess "pathType $PWD/directory" '"directory"' +expectSuccess "pathType $PWD/regular" '"regular"' +expectSuccess "pathType $PWD/symlink" '"symlink"' +expectSuccess "pathType $PWD/fifo" '"unknown"' +# Different errors depending on whether the builtins.readFilePath primop is available or not +expectFailure "pathType $PWD/non-existent" "error: (evaluation aborted with the following error message: 'lib.filesystem.pathType: Path $PWD/non-existent does not exist.'|getting status of '$PWD/non-existent': No such file or directory)" + +expectSuccess "pathIsDirectory /." "true" +expectSuccess "pathIsDirectory $PWD/directory" "true" +expectSuccess "pathIsDirectory $PWD/regular" "false" +expectSuccess "pathIsDirectory $PWD/symlink" "false" +expectSuccess "pathIsDirectory $PWD/fifo" "false" +expectSuccess "pathIsDirectory $PWD/non-existent" "false" + +expectSuccess "pathIsRegularFile /." "false" +expectSuccess "pathIsRegularFile $PWD/directory" "false" +expectSuccess "pathIsRegularFile $PWD/regular" "true" +expectSuccess "pathIsRegularFile $PWD/symlink" "false" +expectSuccess "pathIsRegularFile $PWD/fifo" "false" +expectSuccess "pathIsRegularFile $PWD/non-existent" "false" + +echo >&2 tests ok diff --git a/tests/maintainer-module.nix b/tests/maintainer-module.nix new file mode 100644 index 000000000..afa12587a --- /dev/null +++ b/tests/maintainer-module.nix @@ -0,0 +1,32 @@ +{ lib, ... }: +let + inherit (lib) types; +in { + options = { + name = lib.mkOption { + type = types.str; + }; + email = lib.mkOption { + type = types.nullOr types.str; + default = null; + }; + matrix = lib.mkOption { + type = types.nullOr types.str; + default = null; + }; + github = lib.mkOption { + type = types.nullOr types.str; + default = null; + }; + githubId = lib.mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + }; + keys = lib.mkOption { + type = types.listOf (types.submodule { + options.fingerprint = lib.mkOption { type = types.str; }; + }); + default = []; + }; + }; +} diff --git a/tests/maintainers.nix b/tests/maintainers.nix new file mode 100644 index 000000000..be1c8aaa8 --- /dev/null +++ b/tests/maintainers.nix @@ -0,0 +1,53 @@ +# to run these tests (and the others) +# nix-build nixpkgs/lib/tests/release.nix +# These tests should stay in sync with the comment in maintainers/maintainers-list.nix +{ # The pkgs used for dependencies for the testing itself + pkgs ? import ../.. {} +, lib ? pkgs.lib +}: + +let + checkMaintainer = handle: uncheckedAttrs: + let + prefix = [ "lib" "maintainers" handle ]; + checkedAttrs = (lib.modules.evalModules { + inherit prefix; + modules = [ + ./maintainer-module.nix + { + _file = toString ../../maintainers/maintainer-list.nix; + config = uncheckedAttrs; + } + ]; + }).config; + + checks = lib.optional (checkedAttrs.github != null && checkedAttrs.githubId == null) '' + echo ${lib.escapeShellArg (lib.showOption prefix)}': If `github` is specified, `githubId` must be too.' + # Calling this too often would hit non-authenticated API limits, but this + # shouldn't happen since such errors will get fixed rather quickly + info=$(curl -sS https://api.github.com/users/${checkedAttrs.github}) + id=$(jq -r '.id' <<< "$info") + echo "The GitHub ID for GitHub user ${checkedAttrs.github} is $id:" + echo -e " githubId = $id;\n" + '' ++ lib.optional (checkedAttrs.email == null && checkedAttrs.github == null && checkedAttrs.matrix == null) '' + echo ${lib.escapeShellArg (lib.showOption prefix)}': At least one of `email`, `github` or `matrix` must be specified, so that users know how to reach you.' + '' ++ lib.optional (checkedAttrs.email != null && lib.hasSuffix "noreply.github.com" checkedAttrs.email) '' + echo ${lib.escapeShellArg (lib.showOption prefix)}': If an email address is given, it should allow people to reach you. If you do not want that, you can just provide `github` or `matrix` instead.' + ''; + in lib.deepSeq checkedAttrs checks; + + missingGithubIds = lib.concatLists (lib.mapAttrsToList checkMaintainer lib.maintainers); + + success = pkgs.runCommand "checked-maintainers-success" {} ">$out"; + + failure = pkgs.runCommand "checked-maintainers-failure" { + nativeBuildInputs = [ pkgs.curl pkgs.jq ]; + outputHash = "sha256:${lib.fakeSha256}"; + outputHAlgo = "sha256"; + outputHashMode = "flat"; + SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + } '' + ${lib.concatStringsSep "\n" missingGithubIds} + exit 1 + ''; +in if missingGithubIds == [] then success else failure diff --git a/tests/misc.nix b/tests/misc.nix new file mode 100644 index 000000000..ce980436c --- /dev/null +++ b/tests/misc.nix @@ -0,0 +1,1657 @@ +# to run these tests: +# nix-instantiate --eval --strict nixpkgs/lib/tests/misc.nix +# if the resulting list is empty, all tests passed +with import ../default.nix; + +let + testingThrow = expr: { + expr = (builtins.tryEval (builtins.seq expr "didn't throw")); + expected = { success = false; value = false; }; + }; + testingDeepThrow = expr: testingThrow (builtins.deepSeq expr expr); + + testSanitizeDerivationName = { name, expected }: + let + drv = derivation { + name = strings.sanitizeDerivationName name; + builder = "x"; + system = "x"; + }; + in { + # Evaluate the derivation so an invalid name would be caught + expr = builtins.seq drv.drvPath drv.name; + inherit expected; + }; + +in + +runTests { + +# TRIVIAL + + testId = { + expr = id 1; + expected = 1; + }; + + testConst = { + expr = const 2 3; + expected = 2; + }; + + testPipe = { + expr = pipe 2 [ + (x: x + 2) # 2 + 2 = 4 + (x: x * 2) # 4 * 2 = 8 + ]; + expected = 8; + }; + + testPipeEmpty = { + expr = pipe 2 []; + expected = 2; + }; + + testPipeStrings = { + expr = pipe [ 3 4 ] [ + (map toString) + (map (s: s + "\n")) + concatStrings + ]; + expected = '' + 3 + 4 + ''; + }; + + /* + testOr = { + expr = or true false; + expected = true; + }; + */ + + testAnd = { + expr = and true false; + expected = false; + }; + + testFix = { + expr = fix (x: {a = if x ? a then "a" else "b";}); + expected = {a = "a";}; + }; + + testComposeExtensions = { + expr = let obj = makeExtensible (self: { foo = self.bar; }); + f = self: super: { bar = false; baz = true; }; + g = self: super: { bar = super.baz or false; }; + f_o_g = composeExtensions f g; + composed = obj.extend f_o_g; + in composed.foo; + expected = true; + }; + + testComposeManyExtensions0 = { + expr = let obj = makeExtensible (self: { foo = true; }); + emptyComposition = composeManyExtensions []; + composed = obj.extend emptyComposition; + in composed.foo; + expected = true; + }; + + testComposeManyExtensions = + let f = self: super: { bar = false; baz = true; }; + g = self: super: { bar = super.baz or false; }; + h = self: super: { qux = super.bar or false; }; + obj = makeExtensible (self: { foo = self.qux; }); + in { + expr = let composition = composeManyExtensions [f g h]; + composed = obj.extend composition; + in composed.foo; + expected = (obj.extend (composeExtensions f (composeExtensions g h))).foo; + }; + + testBitAnd = { + expr = (bitAnd 3 10); + expected = 2; + }; + + testBitOr = { + expr = (bitOr 3 10); + expected = 11; + }; + + testBitXor = { + expr = (bitXor 3 10); + expected = 9; + }; + + testToHexString = { + expr = toHexString 250; + expected = "FA"; + }; + + testToBaseDigits = { + expr = toBaseDigits 2 6; + expected = [ 1 1 0 ]; + }; + + testFunctionArgsFunctor = { + expr = functionArgs { __functor = self: { a, b }: null; }; + expected = { a = false; b = false; }; + }; + + testFunctionArgsSetFunctionArgs = { + expr = functionArgs (setFunctionArgs (args: args.x) { x = false; }); + expected = { x = false; }; + }; + +# STRINGS + + testConcatMapStrings = { + expr = concatMapStrings (x: x + ";") ["a" "b" "c"]; + expected = "a;b;c;"; + }; + + testConcatStringsSep = { + expr = concatStringsSep "," ["a" "b" "c"]; + expected = "a,b,c"; + }; + + testConcatLines = { + expr = concatLines ["a" "b" "c"]; + expected = "a\nb\nc\n"; + }; + + testSplitStringsSimple = { + expr = strings.splitString "." "a.b.c.d"; + expected = [ "a" "b" "c" "d" ]; + }; + + testSplitStringsEmpty = { + expr = strings.splitString "." "a..b"; + expected = [ "a" "" "b" ]; + }; + + testSplitStringsOne = { + expr = strings.splitString ":" "a.b"; + expected = [ "a.b" ]; + }; + + testSplitStringsNone = { + expr = strings.splitString "." ""; + expected = [ "" ]; + }; + + testSplitStringsFirstEmpty = { + expr = strings.splitString "/" "/a/b/c"; + expected = [ "" "a" "b" "c" ]; + }; + + testSplitStringsLastEmpty = { + expr = strings.splitString ":" "2001:db8:0:0042::8a2e:370:"; + expected = [ "2001" "db8" "0" "0042" "" "8a2e" "370" "" ]; + }; + + testSplitStringsRegex = { + expr = strings.splitString "\\[{}]()^$?*+|." "A\\[{}]()^$?*+|.B"; + expected = [ "A" "B" ]; + }; + + testSplitStringsDerivation = { + expr = take 3 (strings.splitString "/" (derivation { + name = "name"; + builder = "builder"; + system = "system"; + })); + expected = ["" "nix" "store"]; + }; + + testSplitVersionSingle = { + expr = versions.splitVersion "1"; + expected = [ "1" ]; + }; + + testSplitVersionDouble = { + expr = versions.splitVersion "1.2"; + expected = [ "1" "2" ]; + }; + + testSplitVersionTriple = { + expr = versions.splitVersion "1.2.3"; + expected = [ "1" "2" "3" ]; + }; + + testPadVersionLess = { + expr = versions.pad 3 "1.2"; + expected = "1.2.0"; + }; + + testPadVersionLessExtra = { + expr = versions.pad 3 "1.3-rc1"; + expected = "1.3.0-rc1"; + }; + + testPadVersionMore = { + expr = versions.pad 3 "1.2.3.4"; + expected = "1.2.3"; + }; + + testIsStorePath = { + expr = + let goodPath = + "${builtins.storeDir}/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11"; + in { + storePath = isStorePath goodPath; + storePathDerivation = isStorePath (import ../.. { system = "x86_64-linux"; }).hello; + storePathAppendix = isStorePath + "${goodPath}/bin/python"; + nonAbsolute = isStorePath (concatStrings (tail (stringToCharacters goodPath))); + asPath = isStorePath (/. + goodPath); + otherPath = isStorePath "/something/else"; + otherVals = { + attrset = isStorePath {}; + list = isStorePath []; + int = isStorePath 42; + }; + }; + expected = { + storePath = true; + storePathDerivation = true; + storePathAppendix = false; + nonAbsolute = false; + asPath = true; + otherPath = false; + otherVals = { + attrset = false; + list = false; + int = false; + }; + }; + }; + + testEscapeXML = { + expr = escapeXML ''"test" 'test' < & >''; + expected = ""test" 'test' < & >"; + }; + + testToShellVars = { + expr = '' + ${toShellVars { + STRing01 = "just a 'string'"; + _array_ = [ "with" "more strings" ]; + assoc."with some" = '' + strings + possibly newlines + ''; + drv = { + outPath = "/drv"; + foo = "ignored attribute"; + }; + path = /path; + stringable = { + __toString = _: "hello toString"; + bar = "ignored attribute"; + }; + }} + ''; + expected = '' + STRing01='just a '\'''string'\'''' + declare -a _array_=('with' 'more strings') + declare -A assoc=(['with some']='strings + possibly newlines + ') + drv='/drv' + path='/path' + stringable='hello toString' + ''; + }; + + testHasInfixFalse = { + expr = hasInfix "c" "abde"; + expected = false; + }; + + testHasInfixTrue = { + expr = hasInfix "c" "abcde"; + expected = true; + }; + + testHasInfixDerivation = { + expr = hasInfix "hello" (import ../.. { system = "x86_64-linux"; }).hello; + expected = true; + }; + + testHasInfixPath = { + expr = hasInfix "tests" ./.; + expected = true; + }; + + testHasInfixPathStoreDir = { + expr = hasInfix builtins.storeDir ./.; + expected = true; + }; + + testHasInfixToString = { + expr = hasInfix "a" { __toString = _: "a"; }; + expected = true; + }; + + testNormalizePath = { + expr = strings.normalizePath "//a/b//c////d/"; + expected = "/a/b/c/d/"; + }; + + testCharToInt = { + expr = strings.charToInt "A"; + expected = 65; + }; + + testEscapeC = { + expr = strings.escapeC [ " " ] "Hello World"; + expected = "Hello\\x20World"; + }; + + testEscapeURL = testAllTrue [ + ("" == strings.escapeURL "") + ("Hello" == strings.escapeURL "Hello") + ("Hello%20World" == strings.escapeURL "Hello World") + ("Hello%2FWorld" == strings.escapeURL "Hello/World") + ("42%25" == strings.escapeURL "42%") + ("%20%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%09%3A%2F%40%24%27%28%29%2A%2C%3B" == strings.escapeURL " ?&=#+%!<>#\"{}|\\^[]`\t:/@$'()*,;") + ]; + + testToInt = testAllTrue [ + # Naive + (123 == toInt "123") + (0 == toInt "0") + # Whitespace Padding + (123 == toInt " 123") + (123 == toInt "123 ") + (123 == toInt " 123 ") + (123 == toInt " 123 ") + (0 == toInt " 0") + (0 == toInt "0 ") + (0 == toInt " 0 ") + (-1 == toInt "-1") + (-1 == toInt " -1 ") + ]; + + testToIntFails = testAllTrue [ + ( builtins.tryEval (toInt "") == { success = false; value = false; } ) + ( builtins.tryEval (toInt "123 123") == { success = false; value = false; } ) + ( builtins.tryEval (toInt "0 123") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " 0d ") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " 1d ") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " d0 ") == { success = false; value = false; } ) + ( builtins.tryEval (toInt "00") == { success = false; value = false; } ) + ( builtins.tryEval (toInt "01") == { success = false; value = false; } ) + ( builtins.tryEval (toInt "002") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " 002 ") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " foo ") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " foo 123 ") == { success = false; value = false; } ) + ( builtins.tryEval (toInt " foo123 ") == { success = false; value = false; } ) + ]; + + testToIntBase10 = testAllTrue [ + # Naive + (123 == toIntBase10 "123") + (0 == toIntBase10 "0") + # Whitespace Padding + (123 == toIntBase10 " 123") + (123 == toIntBase10 "123 ") + (123 == toIntBase10 " 123 ") + (123 == toIntBase10 " 123 ") + (0 == toIntBase10 " 0") + (0 == toIntBase10 "0 ") + (0 == toIntBase10 " 0 ") + # Zero Padding + (123 == toIntBase10 "0123") + (123 == toIntBase10 "0000123") + (0 == toIntBase10 "000000") + # Whitespace and Zero Padding + (123 == toIntBase10 " 0123") + (123 == toIntBase10 "0123 ") + (123 == toIntBase10 " 0123 ") + (123 == toIntBase10 " 0000123") + (123 == toIntBase10 "0000123 ") + (123 == toIntBase10 " 0000123 ") + (0 == toIntBase10 " 000000") + (0 == toIntBase10 "000000 ") + (0 == toIntBase10 " 000000 ") + (-1 == toIntBase10 "-1") + (-1 == toIntBase10 " -1 ") + ]; + + testToIntBase10Fails = testAllTrue [ + ( builtins.tryEval (toIntBase10 "") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 "123 123") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 "0 123") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " 0d ") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " 1d ") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " d0 ") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " foo ") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " foo 123 ") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " foo 00123 ") == { success = false; value = false; } ) + ( builtins.tryEval (toIntBase10 " foo00123 ") == { success = false; value = false; } ) + ]; + +# LISTS + + testFilter = { + expr = filter (x: x != "a") ["a" "b" "c" "a"]; + expected = ["b" "c"]; + }; + + testFold = + let + f = op: fold: fold op 0 (range 0 100); + # fold with associative operator + assoc = f builtins.add; + # fold with non-associative operator + nonAssoc = f builtins.sub; + in { + expr = { + assocRight = assoc foldr; + # right fold with assoc operator is same as left fold + assocRightIsLeft = assoc foldr == assoc foldl; + nonAssocRight = nonAssoc foldr; + nonAssocLeft = nonAssoc foldl; + # with non-assoc operator the fold results are not the same + nonAssocRightIsNotLeft = nonAssoc foldl != nonAssoc foldr; + # fold is an alias for foldr + foldIsRight = nonAssoc fold == nonAssoc foldr; + }; + expected = { + assocRight = 5050; + assocRightIsLeft = true; + nonAssocRight = 50; + nonAssocLeft = (-5050); + nonAssocRightIsNotLeft = true; + foldIsRight = true; + }; + }; + + testTake = testAllTrue [ + ([] == (take 0 [ 1 2 3 ])) + ([1] == (take 1 [ 1 2 3 ])) + ([ 1 2 ] == (take 2 [ 1 2 3 ])) + ([ 1 2 3 ] == (take 3 [ 1 2 3 ])) + ([ 1 2 3 ] == (take 4 [ 1 2 3 ])) + ]; + + testFoldAttrs = { + expr = foldAttrs (n: a: [n] ++ a) [] [ + { a = 2; b = 7; } + { a = 3; c = 8; } + ]; + expected = { a = [ 2 3 ]; b = [7]; c = [8];}; + }; + + testSort = { + expr = sort builtins.lessThan [ 40 2 30 42 ]; + expected = [2 30 40 42]; + }; + + testReplicate = { + expr = replicate 3 "a"; + expected = ["a" "a" "a"]; + }; + + testToIntShouldConvertStringToInt = { + expr = toInt "27"; + expected = 27; + }; + + testToIntShouldThrowErrorIfItCouldNotConvertToInt = { + expr = builtins.tryEval (toInt "\"foo\""); + expected = { success = false; value = false; }; + }; + + testHasAttrByPathTrue = { + expr = hasAttrByPath ["a" "b"] { a = { b = "yey"; }; }; + expected = true; + }; + + testHasAttrByPathFalse = { + expr = hasAttrByPath ["a" "b"] { a = { c = "yey"; }; }; + expected = false; + }; + + testFindFirstExample1 = { + expr = findFirst (x: x > 3) 7 [ 1 6 4 ]; + expected = 6; + }; + + testFindFirstExample2 = { + expr = findFirst (x: x > 9) 7 [ 1 6 4 ]; + expected = 7; + }; + + testFindFirstEmpty = { + expr = findFirst (abort "when the list is empty, the predicate is not needed") null []; + expected = null; + }; + + testFindFirstSingleMatch = { + expr = findFirst (x: x == 5) null [ 5 ]; + expected = 5; + }; + + testFindFirstSingleDefault = { + expr = findFirst (x: false) null [ (abort "if the predicate doesn't access the value, it must not be evaluated") ]; + expected = null; + }; + + testFindFirstNone = { + expr = builtins.tryEval (findFirst (x: x == 2) null [ 1 (throw "the last element must be evaluated when there's no match") ]); + expected = { success = false; value = false; }; + }; + + # Makes sure that the implementation doesn't cause a stack overflow + testFindFirstBig = { + expr = findFirst (x: x == 1000000) null (range 0 1000000); + expected = 1000000; + }; + + testFindFirstLazy = { + expr = findFirst (x: x == 1) 7 [ 1 (abort "list elements after the match must not be evaluated") ]; + expected = 1; + }; + +# ATTRSETS + + testConcatMapAttrs = { + expr = concatMapAttrs + (name: value: { + ${name} = value; + ${name + value} = value; + }) + { + foo = "bar"; + foobar = "baz"; + }; + expected = { + foo = "bar"; + foobar = "baz"; + foobarbaz = "baz"; + }; + }; + + # code from example + testFoldlAttrs = { + expr = { + example = foldlAttrs + (acc: name: value: { + sum = acc.sum + value; + names = acc.names ++ [ name ]; + }) + { sum = 0; names = [ ]; } + { + foo = 1; + bar = 10; + }; + # should just return the initial value + emptySet = foldlAttrs (throw "function not needed") 123 { }; + # should just evaluate to the last value + accNotNeeded = foldlAttrs (_acc: _name: v: v) (throw "accumulator not needed") { z = 3; a = 2; }; + # the accumulator doesnt have to be an attrset it can be as trivial as being just a number or string + trivialAcc = foldlAttrs (acc: _name: v: acc * 10 + v) 1 { z = 1; a = 2; }; + }; + expected = { + example = { + sum = 11; + names = [ "bar" "foo" ]; + }; + emptySet = 123; + accNotNeeded = 3; + trivialAcc = 121; + }; + }; + + # code from the example + testRecursiveUpdateUntil = { + expr = recursiveUpdateUntil (path: l: r: path == ["foo"]) { + # first attribute set + foo.bar = 1; + foo.baz = 2; + bar = 3; + } { + #second attribute set + foo.bar = 1; + foo.quz = 2; + baz = 4; + }; + expected = { + foo.bar = 1; # 'foo.*' from the second set + foo.quz = 2; # + bar = 3; # 'bar' from the first set + baz = 4; # 'baz' from the second set + }; + }; + + testOverrideExistingEmpty = { + expr = overrideExisting {} { a = 1; }; + expected = {}; + }; + + testOverrideExistingDisjoint = { + expr = overrideExisting { b = 2; } { a = 1; }; + expected = { b = 2; }; + }; + + testOverrideExistingOverride = { + expr = overrideExisting { a = 3; b = 2; } { a = 1; }; + expected = { a = 1; b = 2; }; + }; + +# GENERATORS +# these tests assume attributes are converted to lists +# in alphabetical order + + testMkKeyValueDefault = { + expr = generators.mkKeyValueDefault {} ":" "f:oo" "bar"; + expected = ''f\:oo:bar''; + }; + + testMkValueString = { + expr = let + vals = { + int = 42; + string = ''fo"o''; + bool = true; + bool2 = false; + null = null; + # float = 42.23; # floats are strange + }; + in mapAttrs + (const (generators.mkValueStringDefault {})) + vals; + expected = { + int = "42"; + string = ''fo"o''; + bool = "true"; + bool2 = "false"; + null = "null"; + # float = "42.23" true false [ "bar" ] ]''; + }; + }; + + testToKeyValue = { + expr = generators.toKeyValue {} { + key = "value"; + "other=key" = "baz"; + }; + expected = '' + key=value + other\=key=baz + ''; + }; + + testToINIEmpty = { + expr = generators.toINI {} {}; + expected = ""; + }; + + testToINIEmptySection = { + expr = generators.toINI {} { foo = {}; bar = {}; }; + expected = '' + [bar] + + [foo] + ''; + }; + + testToINIDuplicateKeys = { + expr = generators.toINI { listsAsDuplicateKeys = true; } { foo.bar = true; baz.qux = [ 1 false ]; }; + expected = '' + [baz] + qux=1 + qux=false + + [foo] + bar=true + ''; + }; + + testToINIDefaultEscapes = { + expr = generators.toINI {} { + "no [ and ] allowed unescaped" = { + "and also no = in keys" = 42; + }; + }; + expected = '' + [no \[ and \] allowed unescaped] + and also no \= in keys=42 + ''; + }; + + testToINIDefaultFull = { + expr = generators.toINI {} { + "section 1" = { + attribute1 = 5; + x = "Me-se JarJar Binx"; + # booleans are converted verbatim by default + boolean = false; + }; + "foo[]" = { + "he\\h=he" = "this is okay"; + }; + }; + expected = '' + [foo\[\]] + he\h\=he=this is okay + + [section 1] + attribute1=5 + boolean=false + x=Me-se JarJar Binx + ''; + }; + + testToINIWithGlobalSectionEmpty = { + expr = generators.toINIWithGlobalSection {} { + globalSection = { + }; + sections = { + }; + }; + expected = '' + ''; + }; + + testToINIWithGlobalSectionGlobalEmptyIsTheSameAsToINI = + let + sections = { + "section 1" = { + attribute1 = 5; + x = "Me-se JarJar Binx"; + }; + "foo" = { + "he\\h=he" = "this is okay"; + }; + }; + in { + expr = + generators.toINIWithGlobalSection {} { + globalSection = {}; + sections = sections; + }; + expected = generators.toINI {} sections; + }; + + testToINIWithGlobalSectionFull = { + expr = generators.toINIWithGlobalSection {} { + globalSection = { + foo = "bar"; + test = false; + }; + sections = { + "section 1" = { + attribute1 = 5; + x = "Me-se JarJar Binx"; + }; + "foo" = { + "he\\h=he" = "this is okay"; + }; + }; + }; + expected = '' + foo=bar + test=false + + [foo] + he\h\=he=this is okay + + [section 1] + attribute1=5 + x=Me-se JarJar Binx + ''; + }; + + /* right now only invocation check */ + testToJSONSimple = + let val = { + foobar = [ "baz" 1 2 3 ]; + }; + in { + expr = generators.toJSON {} val; + # trivial implementation + expected = builtins.toJSON val; + }; + + /* right now only invocation check */ + testToYAMLSimple = + let val = { + list = [ { one = 1; } { two = 2; } ]; + all = 42; + }; + in { + expr = generators.toYAML {} val; + # trivial implementation + expected = builtins.toJSON val; + }; + + testToPretty = + let + deriv = derivation { name = "test"; builder = "/bin/sh"; system = "aarch64-linux"; }; + in { + expr = mapAttrs (const (generators.toPretty { multiline = false; })) rec { + int = 42; + float = 0.1337; + bool = true; + emptystring = ""; + string = "fn\${o}\"r\\d"; + newlinestring = "\n"; + path = /. + "/foo"; + null_ = null; + function = x: x; + functionArgs = { arg ? 4, foo }: arg; + list = [ 3 4 function [ false ] ]; + emptylist = []; + attrs = { foo = null; "foo b/ar" = "baz"; }; + emptyattrs = {}; + drv = deriv; + }; + expected = rec { + int = "42"; + float = "0.1337"; + bool = "true"; + emptystring = ''""''; + string = ''"fn\''${o}\"r\\d"''; + newlinestring = "\"\\n\""; + path = "/foo"; + null_ = "null"; + function = ""; + functionArgs = ""; + list = "[ 3 4 ${function} [ false ] ]"; + emptylist = "[ ]"; + attrs = "{ foo = null; \"foo b/ar\" = \"baz\"; }"; + emptyattrs = "{ }"; + drv = ""; + }; + }; + + testToPrettyLimit = + let + a.b = 1; + a.c = a; + in { + expr = generators.toPretty { } (generators.withRecursion { throwOnDepthLimit = false; depthLimit = 2; } a); + expected = "{\n b = 1;\n c = {\n b = \"\";\n c = {\n b = \"\";\n c = \"\";\n };\n };\n}"; + }; + + testToPrettyLimitThrow = + let + a.b = 1; + a.c = a; + in { + expr = (builtins.tryEval + (generators.toPretty { } (generators.withRecursion { depthLimit = 2; } a))).success; + expected = false; + }; + + testWithRecursionDealsWithFunctors = + let + functor = { + __functor = self: { a, b, }: null; + }; + a = { + value = "1234"; + b = functor; + c.d = functor; + }; + in { + expr = generators.toPretty { } (generators.withRecursion { depthLimit = 1; throwOnDepthLimit = false; } a); + expected = "{\n b = ;\n c = {\n d = \"\";\n };\n value = \"\";\n}"; + }; + + testToPrettyMultiline = { + expr = mapAttrs (const (generators.toPretty { })) rec { + list = [ 3 4 [ false ] ]; + attrs = { foo = null; bar.foo = "baz"; }; + newlinestring = "\n"; + multilinestring = '' + hello + ''${there} + te'''st + ''; + multilinestring' = '' + hello + there + test''; + }; + expected = rec { + list = '' + [ + 3 + 4 + [ + false + ] + ]''; + attrs = '' + { + bar = { + foo = "baz"; + }; + foo = null; + }''; + newlinestring = "''\n \n''"; + multilinestring = '' + ''' + hello + '''''${there} + te''''st + '''''; + multilinestring' = '' + ''' + hello + there + test'''''; + + }; + }; + + testToPrettyAllowPrettyValues = { + expr = generators.toPretty { allowPrettyValues = true; } + { __pretty = v: "«" + v + "»"; val = "foo"; }; + expected = "«foo»"; + }; + + testToPlist = + let + deriv = derivation { name = "test"; builder = "/bin/sh"; system = "aarch64-linux"; }; + in { + expr = mapAttrs (const (generators.toPlist { })) { + value = { + nested.values = rec { + int = 42; + float = 0.1337; + bool = true; + emptystring = ""; + string = "fn\${o}\"r\\d"; + newlinestring = "\n"; + path = /. + "/foo"; + null_ = null; + list = [ 3 4 "test" ]; + emptylist = []; + attrs = { foo = null; "foo b/ar" = "baz"; }; + emptyattrs = {}; + }; + }; + }; + expected = { value = builtins.readFile ./test-to-plist-expected.plist; }; + }; + + testToLuaEmptyAttrSet = { + expr = generators.toLua {} {}; + expected = ''{}''; + }; + + testToLuaEmptyList = { + expr = generators.toLua {} []; + expected = ''{}''; + }; + + testToLuaListOfVariousTypes = { + expr = generators.toLua {} [ null 43 3.14159 true ]; + expected = '' + { + nil, + 43, + 3.14159, + true + }''; + }; + + testToLuaString = { + expr = generators.toLua {} ''double-quote (") and single quotes (')''; + expected = ''"double-quote (\") and single quotes (')"''; + }; + + testToLuaAttrsetWithLuaInline = { + expr = generators.toLua {} { x = generators.mkLuaInline ''"abc" .. "def"''; }; + expected = '' + { + ["x"] = ("abc" .. "def") + }''; + }; + + testToLuaAttrsetWithSpaceInKey = { + expr = generators.toLua {} { "some space and double-quote (\")" = 42; }; + expected = '' + { + ["some space and double-quote (\")"] = 42 + }''; + }; + + testToLuaWithoutMultiline = { + expr = generators.toLua { multiline = false; } [ 41 43 ]; + expected = ''{ 41, 43 }''; + }; + + testToLuaEmptyBindings = { + expr = generators.toLua { asBindings = true; } {}; + expected = ""; + }; + + testToLuaBindings = { + expr = generators.toLua { asBindings = true; } { x1 = 41; _y = { a = 43; }; }; + expected = '' + _y = { + ["a"] = 43 + } + x1 = 41 + ''; + }; + + testToLuaPartialTableBindings = { + expr = generators.toLua { asBindings = true; } { "x.y" = 42; }; + expected = '' + x.y = 42 + ''; + }; + + testToLuaIndentedBindings = { + expr = generators.toLua { asBindings = true; indent = " "; } { x = { y = 42; }; }; + expected = " x = {\n [\"y\"] = 42\n }\n"; + }; + + testToLuaBindingsWithSpace = testingThrow ( + generators.toLua { asBindings = true; } { "with space" = 42; } + ); + + testToLuaBindingsWithLeadingDigit = testingThrow ( + generators.toLua { asBindings = true; } { "11eleven" = 42; } + ); + + testToLuaBasicExample = { + expr = generators.toLua {} { + cmd = [ "typescript-language-server" "--stdio" ]; + settings.workspace.library = generators.mkLuaInline ''vim.api.nvim_get_runtime_file("", true)''; + }; + expected = '' + { + ["cmd"] = { + "typescript-language-server", + "--stdio" + }, + ["settings"] = { + ["workspace"] = { + ["library"] = (vim.api.nvim_get_runtime_file("", true)) + } + } + }''; + }; + +# CLI + + testToGNUCommandLine = { + expr = cli.toGNUCommandLine {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + }; + + expected = [ + "-X" "PUT" + "--data" "{\"id\":0}" + "--retry" "3" + "--url" "https://example.com/foo" + "--url" "https://example.com/bar" + "--verbose" + ]; + }; + + testToGNUCommandLineShell = { + expr = cli.toGNUCommandLineShell {} { + data = builtins.toJSON { id = 0; }; + X = "PUT"; + retry = 3; + retry-delay = null; + url = [ "https://example.com/foo" "https://example.com/bar" ]; + silent = false; + verbose = true; + }; + + expected = "'-X' 'PUT' '--data' '{\"id\":0}' '--retry' '3' '--url' 'https://example.com/foo' '--url' 'https://example.com/bar' '--verbose'"; + }; + + testSanitizeDerivationNameLeadingDots = testSanitizeDerivationName { + name = "..foo"; + expected = "foo"; + }; + + testSanitizeDerivationNameUnicode = testSanitizeDerivationName { + name = "fö"; + expected = "f-"; + }; + + testSanitizeDerivationNameAscii = testSanitizeDerivationName { + name = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + expected = "-+--.-0123456789-=-?-ABCDEFGHIJKLMNOPQRSTUVWXYZ-_-abcdefghijklmnopqrstuvwxyz-"; + }; + + testSanitizeDerivationNameTooLong = testSanitizeDerivationName { + name = "This string is loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong"; + expected = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong"; + }; + + testSanitizeDerivationNameTooLongWithInvalid = testSanitizeDerivationName { + name = "Hello there aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &&&&&&&&"; + expected = "there-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-"; + }; + + testSanitizeDerivationNameEmpty = testSanitizeDerivationName { + name = ""; + expected = "unknown"; + }; + + testFreeformOptions = { + expr = + let + submodule = { lib, ... }: { + freeformType = lib.types.attrsOf (lib.types.submodule { + options.bar = lib.mkOption {}; + }); + options.bar = lib.mkOption {}; + }; + + module = { lib, ... }: { + options.foo = lib.mkOption { + type = lib.types.submodule submodule; + }; + }; + + options = (evalModules { + modules = [ module ]; + }).options; + + locs = filter (o: ! o.internal) (optionAttrSetToDocList options); + in map (o: o.loc) locs; + expected = [ [ "_module" "args" ] [ "foo" ] [ "foo" "" "bar" ] [ "foo" "bar" ] ]; + }; + + testCartesianProductOfEmptySet = { + expr = cartesianProductOfSets {}; + expected = [ {} ]; + }; + + testCartesianProductOfOneSet = { + expr = cartesianProductOfSets { a = [ 1 2 3 ]; }; + expected = [ { a = 1; } { a = 2; } { a = 3; } ]; + }; + + testCartesianProductOfTwoSets = { + expr = cartesianProductOfSets { a = [ 1 ]; b = [ 10 20 ]; }; + expected = [ + { a = 1; b = 10; } + { a = 1; b = 20; } + ]; + }; + + testCartesianProductOfTwoSetsWithOneEmpty = { + expr = cartesianProductOfSets { a = [ ]; b = [ 10 20 ]; }; + expected = [ ]; + }; + + testCartesianProductOfThreeSets = { + expr = cartesianProductOfSets { + a = [ 1 2 3 ]; + b = [ 10 20 30 ]; + c = [ 100 200 300 ]; + }; + expected = [ + { a = 1; b = 10; c = 100; } + { a = 1; b = 10; c = 200; } + { a = 1; b = 10; c = 300; } + + { a = 1; b = 20; c = 100; } + { a = 1; b = 20; c = 200; } + { a = 1; b = 20; c = 300; } + + { a = 1; b = 30; c = 100; } + { a = 1; b = 30; c = 200; } + { a = 1; b = 30; c = 300; } + + { a = 2; b = 10; c = 100; } + { a = 2; b = 10; c = 200; } + { a = 2; b = 10; c = 300; } + + { a = 2; b = 20; c = 100; } + { a = 2; b = 20; c = 200; } + { a = 2; b = 20; c = 300; } + + { a = 2; b = 30; c = 100; } + { a = 2; b = 30; c = 200; } + { a = 2; b = 30; c = 300; } + + { a = 3; b = 10; c = 100; } + { a = 3; b = 10; c = 200; } + { a = 3; b = 10; c = 300; } + + { a = 3; b = 20; c = 100; } + { a = 3; b = 20; c = 200; } + { a = 3; b = 20; c = 300; } + + { a = 3; b = 30; c = 100; } + { a = 3; b = 30; c = 200; } + { a = 3; b = 30; c = 300; } + ]; + }; + + # The example from the showAttrPath documentation + testShowAttrPathExample = { + expr = showAttrPath [ "foo" "10" "bar" ]; + expected = "foo.\"10\".bar"; + }; + + testShowAttrPathEmpty = { + expr = showAttrPath []; + expected = ""; + }; + + testShowAttrPathVarious = { + expr = showAttrPath [ + "." + "foo" + "2" + "a2-b" + "_bc'de" + ]; + expected = ''".".foo."2".a2-b._bc'de''; + }; + + testGroupBy = { + expr = groupBy (n: toString (mod n 5)) (range 0 16); + expected = { + "0" = [ 0 5 10 15 ]; + "1" = [ 1 6 11 16 ]; + "2" = [ 2 7 12 ]; + "3" = [ 3 8 13 ]; + "4" = [ 4 9 14 ]; + }; + }; + + testGroupBy' = { + expr = groupBy' builtins.add 0 (x: boolToString (x > 2)) [ 5 1 2 3 4 ]; + expected = { false = 3; true = 12; }; + }; + + # The example from the updateManyAttrsByPath documentation + testUpdateManyAttrsByPathExample = { + expr = updateManyAttrsByPath [ + { + path = [ "a" "b" ]; + update = old: { d = old.c; }; + } + { + path = [ "a" "b" "c" ]; + update = old: old + 1; + } + { + path = [ "x" "y" ]; + update = old: "xy"; + } + ] { a.b.c = 0; }; + expected = { a = { b = { d = 1; }; }; x = { y = "xy"; }; }; + }; + + # If there are no updates, the value is passed through + testUpdateManyAttrsByPathNone = { + expr = updateManyAttrsByPath [] "something"; + expected = "something"; + }; + + # A single update to the root path is just like applying the function directly + testUpdateManyAttrsByPathSingleIncrement = { + expr = updateManyAttrsByPath [ + { + path = [ ]; + update = old: old + 1; + } + ] 0; + expected = 1; + }; + + # Multiple updates can be applied are done in order + testUpdateManyAttrsByPathMultipleIncrements = { + expr = updateManyAttrsByPath [ + { + path = [ ]; + update = old: old + "a"; + } + { + path = [ ]; + update = old: old + "b"; + } + { + path = [ ]; + update = old: old + "c"; + } + ] ""; + expected = "abc"; + }; + + # If an update doesn't use the value, all previous updates are not evaluated + testUpdateManyAttrsByPathLazy = { + expr = updateManyAttrsByPath [ + { + path = [ ]; + update = old: old + throw "nope"; + } + { + path = [ ]; + update = old: "untainted"; + } + ] (throw "start"); + expected = "untainted"; + }; + + # Deeply nested attributes can be updated without affecting others + testUpdateManyAttrsByPathDeep = { + expr = updateManyAttrsByPath [ + { + path = [ "a" "b" "c" ]; + update = old: old + 1; + } + ] { + a.b.c = 0; + + a.b.z = 0; + a.y.z = 0; + x.y.z = 0; + }; + expected = { + a.b.c = 1; + + a.b.z = 0; + a.y.z = 0; + x.y.z = 0; + }; + }; + + # Nested attributes are updated first + testUpdateManyAttrsByPathNestedBeforehand = { + expr = updateManyAttrsByPath [ + { + path = [ "a" ]; + update = old: old // { x = old.b; }; + } + { + path = [ "a" "b" ]; + update = old: old + 1; + } + ] { + a.b = 0; + }; + expected = { + a.b = 1; + a.x = 1; + }; + }; + + ## Levenshtein distance functions and co. + testCommonPrefixLengthEmpty = { + expr = strings.commonPrefixLength "" "hello"; + expected = 0; + }; + + testCommonPrefixLengthSame = { + expr = strings.commonPrefixLength "hello" "hello"; + expected = 5; + }; + + testCommonPrefixLengthDiffering = { + expr = strings.commonPrefixLength "hello" "hey"; + expected = 2; + }; + + testCommonSuffixLengthEmpty = { + expr = strings.commonSuffixLength "" "hello"; + expected = 0; + }; + + testCommonSuffixLengthSame = { + expr = strings.commonSuffixLength "hello" "hello"; + expected = 5; + }; + + testCommonSuffixLengthDiffering = { + expr = strings.commonSuffixLength "test" "rest"; + expected = 3; + }; + + testLevenshteinEmpty = { + expr = strings.levenshtein "" ""; + expected = 0; + }; + + testLevenshteinOnlyAdd = { + expr = strings.levenshtein "" "hello there"; + expected = 11; + }; + + testLevenshteinOnlyRemove = { + expr = strings.levenshtein "hello there" ""; + expected = 11; + }; + + testLevenshteinOnlyTransform = { + expr = strings.levenshtein "abcdef" "ghijkl"; + expected = 6; + }; + + testLevenshteinMixed = { + expr = strings.levenshtein "kitchen" "sitting"; + expected = 5; + }; + + testLevenshteinAtMostZeroFalse = { + expr = strings.levenshteinAtMost 0 "foo" "boo"; + expected = false; + }; + + testLevenshteinAtMostZeroTrue = { + expr = strings.levenshteinAtMost 0 "foo" "foo"; + expected = true; + }; + + testLevenshteinAtMostOneFalse = { + expr = strings.levenshteinAtMost 1 "car" "ct"; + expected = false; + }; + + testLevenshteinAtMostOneTrue = { + expr = strings.levenshteinAtMost 1 "car" "cr"; + expected = true; + }; + + # We test levenshteinAtMost 2 particularly well because it uses a complicated + # implementation + testLevenshteinAtMostTwoIsEmpty = { + expr = strings.levenshteinAtMost 2 "" ""; + expected = true; + }; + + testLevenshteinAtMostTwoIsZero = { + expr = strings.levenshteinAtMost 2 "abcdef" "abcdef"; + expected = true; + }; + + testLevenshteinAtMostTwoIsOne = { + expr = strings.levenshteinAtMost 2 "abcdef" "abddef"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff0False = { + expr = strings.levenshteinAtMost 2 "abcdef" "aczyef"; + expected = false; + }; + + testLevenshteinAtMostTwoDiff0Outer = { + expr = strings.levenshteinAtMost 2 "abcdef" "zbcdez"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff0DelLeft = { + expr = strings.levenshteinAtMost 2 "abcdef" "bcdefz"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff0DelRight = { + expr = strings.levenshteinAtMost 2 "abcdef" "zabcde"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff1False = { + expr = strings.levenshteinAtMost 2 "abcdef" "bddez"; + expected = false; + }; + + testLevenshteinAtMostTwoDiff1DelLeft = { + expr = strings.levenshteinAtMost 2 "abcdef" "bcdez"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff1DelRight = { + expr = strings.levenshteinAtMost 2 "abcdef" "zbcde"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff2False = { + expr = strings.levenshteinAtMost 2 "hello" "hxo"; + expected = false; + }; + + testLevenshteinAtMostTwoDiff2True = { + expr = strings.levenshteinAtMost 2 "hello" "heo"; + expected = true; + }; + + testLevenshteinAtMostTwoDiff3 = { + expr = strings.levenshteinAtMost 2 "hello" "ho"; + expected = false; + }; + + testLevenshteinAtMostThreeFalse = { + expr = strings.levenshteinAtMost 3 "hello" "Holla!"; + expected = false; + }; + + testLevenshteinAtMostThreeTrue = { + expr = strings.levenshteinAtMost 3 "hello" "Holla"; + expected = true; + }; + + # lazyDerivation + + testLazyDerivationIsLazyInDerivationForAttrNames = { + expr = attrNames (lazyDerivation { + derivation = throw "not lazy enough"; + }); + # It's ok to add attribute names here when lazyDerivation is improved + # in accordance with its inline comments. + expected = [ "drvPath" "meta" "name" "out" "outPath" "outputName" "outputs" "system" "type" ]; + }; + + testLazyDerivationIsLazyInDerivationForPassthruAttr = { + expr = (lazyDerivation { + derivation = throw "not lazy enough"; + passthru.tests = "whatever is in tests"; + }).tests; + expected = "whatever is in tests"; + }; + + testLazyDerivationIsLazyInDerivationForPassthruAttr2 = { + # passthru.tests is not a special case. It works for any attr. + expr = (lazyDerivation { + derivation = throw "not lazy enough"; + passthru.foo = "whatever is in foo"; + }).foo; + expected = "whatever is in foo"; + }; + + testLazyDerivationIsLazyInDerivationForMeta = { + expr = (lazyDerivation { + derivation = throw "not lazy enough"; + meta = "whatever is in meta"; + }).meta; + expected = "whatever is in meta"; + }; + + testLazyDerivationReturnsDerivationAttrs = let + derivation = { + type = "derivation"; + outputs = ["out"]; + out = "test out"; + outPath = "test outPath"; + outputName = "out"; + drvPath = "test drvPath"; + name = "test name"; + system = "test system"; + meta = "test meta"; + }; + in { + expr = lazyDerivation { inherit derivation; }; + expected = derivation; + }; + + testTypeDescriptionInt = { + expr = (with types; int).description; + expected = "signed integer"; + }; + testTypeDescriptionListOfInt = { + expr = (with types; listOf int).description; + expected = "list of signed integer"; + }; + testTypeDescriptionListOfListOfInt = { + expr = (with types; listOf (listOf int)).description; + expected = "list of list of signed integer"; + }; + testTypeDescriptionListOfEitherStrOrBool = { + expr = (with types; listOf (either str bool)).description; + expected = "list of (string or boolean)"; + }; + testTypeDescriptionEitherListOfStrOrBool = { + expr = (with types; either (listOf bool) str).description; + expected = "(list of boolean) or string"; + }; + testTypeDescriptionEitherStrOrListOfBool = { + expr = (with types; either str (listOf bool)).description; + expected = "string or list of boolean"; + }; + testTypeDescriptionOneOfListOfStrOrBool = { + expr = (with types; oneOf [ (listOf bool) str ]).description; + expected = "(list of boolean) or string"; + }; + testTypeDescriptionOneOfListOfStrOrBoolOrNumber = { + expr = (with types; oneOf [ (listOf bool) str number ]).description; + expected = "(list of boolean) or string or signed integer or floating point number"; + }; + testTypeDescriptionEitherListOfBoolOrEitherStringOrNumber = { + expr = (with types; either (listOf bool) (either str number)).description; + expected = "(list of boolean) or string or signed integer or floating point number"; + }; + testTypeDescriptionEitherEitherListOfBoolOrStringOrNumber = { + expr = (with types; either (either (listOf bool) str) number).description; + expected = "(list of boolean) or string or signed integer or floating point number"; + }; + testTypeDescriptionEitherNullOrBoolOrString = { + expr = (with types; either (nullOr bool) str).description; + expected = "null or boolean or string"; + }; + testTypeDescriptionEitherListOfEitherBoolOrStrOrInt = { + expr = (with types; either (listOf (either bool str)) int).description; + expected = "(list of (boolean or string)) or signed integer"; + }; + testTypeDescriptionEitherIntOrListOrEitherBoolOrStr = { + expr = (with types; either int (listOf (either bool str))).description; + expected = "signed integer or list of (boolean or string)"; + }; +} diff --git a/tests/modules.sh b/tests/modules.sh new file mode 100755 index 000000000..7aebba6b5 --- /dev/null +++ b/tests/modules.sh @@ -0,0 +1,408 @@ +#!/usr/bin/env bash +# +# This script is used to test that the module system is working as expected. +# By default it test the version of nixpkgs which is defined in the NIX_PATH. + +set -o errexit -o noclobber -o nounset -o pipefail +shopt -s failglob inherit_errexit + +# https://stackoverflow.com/a/246128/6605742 +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +cd "$DIR"/modules + +pass=0 +fail=0 + +evalConfig() { + local attr=$1 + shift + local script="import ./default.nix { modules = [ $* ];}" + nix-instantiate --timeout 1 -E "$script" -A "$attr" --eval-only --show-trace --read-write-mode +} + +reportFailure() { + local attr=$1 + shift + local script="import ./default.nix { modules = [ $* ];}" + echo 2>&1 "$ nix-instantiate -E '$script' -A '$attr' --eval-only" + evalConfig "$attr" "$@" || true + ((++fail)) +} + +checkConfigOutput() { + local outputContains=$1 + shift + if evalConfig "$@" 2>/dev/null | grep --silent "$outputContains" ; then + ((++pass)) + else + echo 2>&1 "error: Expected result matching '$outputContains', while evaluating" + reportFailure "$@" + fi +} + +checkConfigError() { + local errorContains=$1 + local err="" + shift + if err="$(evalConfig "$@" 2>&1 >/dev/null)"; then + echo 2>&1 "error: Expected error code, got exit code 0, while evaluating" + reportFailure "$@" + else + if echo "$err" | grep -zP --silent "$errorContains" ; then + ((++pass)) + else + echo 2>&1 "error: Expected error matching '$errorContains', while evaluating" + reportFailure "$@" + fi + fi +} + +# Shorthand meta attribute does not duplicate the config +checkConfigOutput '^"one two"$' config.result ./shorthand-meta.nix + +# Check boolean option. +checkConfigOutput '^false$' config.enable ./declare-enable.nix +checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./define-enable.nix +checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*' config.enable ./define-enable-throw.nix +checkConfigError 'while evaluating a definition from `.*/define-enable-abort.nix' config.enable ./define-enable-abort.nix +checkConfigError 'while evaluating the error message for definitions for .enable., which is an option that does not exist' config.enable ./define-enable-abort.nix + +checkConfigOutput '^1$' config.bare-submodule.nested ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix +checkConfigOutput '^2$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix +checkConfigOutput '^42$' config.bare-submodule.nested ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix ./declare-bare-submodule-deep-option.nix ./define-bare-submodule-values.nix +checkConfigOutput '^420$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-nested-option.nix ./declare-bare-submodule-deep-option.nix ./define-bare-submodule-values.nix +checkConfigOutput '^2$' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix ./define-shorthandOnlyDefinesConfig-true.nix +checkConfigError 'The option .bare-submodule.deep. in .*/declare-bare-submodule-deep-option.nix. is already declared in .*/declare-bare-submodule-deep-option-duplicate.nix' config.bare-submodule.deep ./declare-bare-submodule.nix ./declare-bare-submodule-deep-option.nix ./declare-bare-submodule-deep-option-duplicate.nix + +# Check integer types. +# unsigned +checkConfigOutput '^42$' config.value ./declare-int-unsigned-value.nix ./define-value-int-positive.nix +checkConfigError 'A definition for option .* is not of type.*unsigned integer.*. Definition values:\n\s*- In .*: -23' config.value ./declare-int-unsigned-value.nix ./define-value-int-negative.nix +# positive +checkConfigError 'A definition for option .* is not of type.*positive integer.*. Definition values:\n\s*- In .*: 0' config.value ./declare-int-positive-value.nix ./define-value-int-zero.nix +# between +checkConfigOutput '^42$' config.value ./declare-int-between-value.nix ./define-value-int-positive.nix +checkConfigError 'A definition for option .* is not of type.*between.*-21 and 43.*inclusive.*. Definition values:\n\s*- In .*: -23' config.value ./declare-int-between-value.nix ./define-value-int-negative.nix + +# Check either types +# types.either +checkConfigOutput '^42$' config.value ./declare-either.nix ./define-value-int-positive.nix +checkConfigOutput '^"24"$' config.value ./declare-either.nix ./define-value-string.nix +# types.oneOf +checkConfigOutput '^42$' config.value ./declare-oneOf.nix ./define-value-int-positive.nix +checkConfigOutput '^\[ \]$' config.value ./declare-oneOf.nix ./define-value-list.nix +checkConfigOutput '^"24"$' config.value ./declare-oneOf.nix ./define-value-string.nix + +# Check mkForce without submodules. +set -- config.enable ./declare-enable.nix ./define-enable.nix +checkConfigOutput '^true$' "$@" +checkConfigOutput '^false$' "$@" ./define-force-enable.nix +checkConfigOutput '^false$' "$@" ./define-enable-force.nix + +# Check mkForce with option and submodules. +checkConfigError 'attribute .*foo.* .* not found' config.attrsOfSub.foo.enable ./declare-attrsOfSub-any-enable.nix +checkConfigOutput '^false$' config.attrsOfSub.foo.enable ./declare-attrsOfSub-any-enable.nix ./define-attrsOfSub-foo.nix +set -- config.attrsOfSub.foo.enable ./declare-attrsOfSub-any-enable.nix ./define-attrsOfSub-foo-enable.nix +checkConfigOutput '^true$' "$@" +checkConfigOutput '^false$' "$@" ./define-force-attrsOfSub-foo-enable.nix +checkConfigOutput '^false$' "$@" ./define-attrsOfSub-force-foo-enable.nix +checkConfigOutput '^false$' "$@" ./define-attrsOfSub-foo-force-enable.nix +checkConfigOutput '^false$' "$@" ./define-attrsOfSub-foo-enable-force.nix + +# Check overriding effect of mkForce on submodule definitions. +checkConfigError 'attribute .*bar.* .* not found' config.attrsOfSub.bar.enable ./declare-attrsOfSub-any-enable.nix ./define-attrsOfSub-foo.nix +checkConfigOutput '^false$' config.attrsOfSub.bar.enable ./declare-attrsOfSub-any-enable.nix ./define-attrsOfSub-foo.nix ./define-attrsOfSub-bar.nix +set -- config.attrsOfSub.bar.enable ./declare-attrsOfSub-any-enable.nix ./define-attrsOfSub-foo.nix ./define-attrsOfSub-bar-enable.nix +checkConfigOutput '^true$' "$@" +checkConfigError 'attribute .*bar.* .* not found' "$@" ./define-force-attrsOfSub-foo-enable.nix +checkConfigError 'attribute .*bar.* .* not found' "$@" ./define-attrsOfSub-force-foo-enable.nix +checkConfigOutput '^true$' "$@" ./define-attrsOfSub-foo-force-enable.nix +checkConfigOutput '^true$' "$@" ./define-attrsOfSub-foo-enable-force.nix + +# Check mkIf with submodules. +checkConfigError 'attribute .*foo.* .* not found' config.attrsOfSub.foo.enable ./declare-enable.nix ./declare-attrsOfSub-any-enable.nix +set -- config.attrsOfSub.foo.enable ./declare-enable.nix ./declare-attrsOfSub-any-enable.nix +checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-if-attrsOfSub-foo-enable.nix +checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-attrsOfSub-if-foo-enable.nix +checkConfigError 'attribute .*foo.* .* not found' "$@" ./define-attrsOfSub-foo-if-enable.nix +checkConfigOutput '^false$' "$@" ./define-attrsOfSub-foo-enable-if.nix +checkConfigOutput '^true$' "$@" ./define-enable.nix ./define-if-attrsOfSub-foo-enable.nix +checkConfigOutput '^true$' "$@" ./define-enable.nix ./define-attrsOfSub-if-foo-enable.nix +checkConfigOutput '^true$' "$@" ./define-enable.nix ./define-attrsOfSub-foo-if-enable.nix +checkConfigOutput '^true$' "$@" ./define-enable.nix ./define-attrsOfSub-foo-enable-if.nix + +# Check disabledModules with config definitions and option declarations. +set -- config.enable ./define-enable.nix ./declare-enable.nix +checkConfigOutput '^true$' "$@" +checkConfigOutput '^false$' "$@" ./disable-define-enable.nix +checkConfigOutput '^false$' "$@" ./disable-define-enable-string-path.nix +checkConfigError "The option .*enable.* does not exist. Definition values:\n\s*- In .*: true" "$@" ./disable-declare-enable.nix +checkConfigError "attribute .*enable.* in selection path .*config.enable.* not found" "$@" ./disable-define-enable.nix ./disable-declare-enable.nix +checkConfigError "attribute .*enable.* in selection path .*config.enable.* not found" "$@" ./disable-enable-modules.nix + +checkConfigOutput '^true$' 'config.positive.enable' ./disable-module-with-key.nix +checkConfigOutput '^false$' 'config.negative.enable' ./disable-module-with-key.nix +checkConfigError 'Module ..*disable-module-bad-key.nix. contains a disabledModules item that is an attribute set, presumably a module, that does not have a .key. attribute. .*' 'config.enable' ./disable-module-bad-key.nix + +# Not sure if we want to keep supporting module keys that aren't strings, paths or v?key, but we shouldn't remove support accidentally. +checkConfigOutput '^true$' 'config.positive.enable' ./disable-module-with-toString-key.nix +checkConfigOutput '^false$' 'config.negative.enable' ./disable-module-with-toString-key.nix + +# Check _module.args. +set -- config.enable ./declare-enable.nix ./define-enable-with-custom-arg.nix +checkConfigError 'while evaluating the module argument .*custom.* in .*define-enable-with-custom-arg.nix.*:' "$@" +checkConfigOutput '^true$' "$@" ./define-_module-args-custom.nix + +# Check that using _module.args on imports cause infinite recursions, with +# the proper error context. +set -- "$@" ./define-_module-args-custom.nix ./import-custom-arg.nix +checkConfigError 'while evaluating the module argument .*custom.* in .*import-custom-arg.nix.*:' "$@" +checkConfigError 'infinite recursion encountered' "$@" + +# Check _module.check. +set -- config.enable ./declare-enable.nix ./define-enable.nix ./define-attrsOfSub-foo.nix +checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*' "$@" +checkConfigOutput '^true$' "$@" ./define-module-check.nix + +# Check coerced value. +set -- +checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix +checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix +checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix + +# Check coerced value with unsound coercion +checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix +checkConfigError 'A definition for option .* is not of type .*. Definition values:\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix +checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix + +# Check mkAliasOptionModule. +checkConfigOutput '^true$' config.enable ./alias-with-priority.nix +checkConfigOutput '^true$' config.enableAlias ./alias-with-priority.nix +checkConfigOutput '^false$' config.enable ./alias-with-priority-can-override.nix +checkConfigOutput '^false$' config.enableAlias ./alias-with-priority-can-override.nix + +# Check mkPackageOption +checkConfigOutput '^"hello"$' config.package.pname ./declare-mkPackageOption.nix +checkConfigError 'The option .undefinedPackage. is used but not defined' config.undefinedPackage ./declare-mkPackageOption.nix +checkConfigOutput '^null$' config.nullablePackage ./declare-mkPackageOption.nix + +# submoduleWith + +## specialArgs should work +checkConfigOutput '^"foo"$' config.submodule.foo ./declare-submoduleWith-special.nix + +## shorthandOnlyDefines config behaves as expected +checkConfigOutput '^true$' config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-shorthand.nix +checkConfigError 'is not of type `boolean' config.submodule.config ./declare-submoduleWith-shorthand.nix ./define-submoduleWith-noshorthand.nix +checkConfigError "You're trying to define a value of type \`bool'\n\s*rather than an attribute set for the option" config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-shorthand.nix +checkConfigOutput '^true$' config.submodule.config ./declare-submoduleWith-noshorthand.nix ./define-submoduleWith-noshorthand.nix + +## submoduleWith should merge all modules in one swoop +checkConfigOutput '^true$' config.submodule.inner ./declare-submoduleWith-modules.nix +checkConfigOutput '^true$' config.submodule.outer ./declare-submoduleWith-modules.nix +# Should also be able to evaluate the type name (which evaluates freeformType, +# which evaluates all the modules defined by the type) +checkConfigOutput '^"submodule"$' options.submodule.type.description ./declare-submoduleWith-modules.nix + +## submodules can be declared using (evalModules {...}).type +checkConfigOutput '^true$' config.submodule.inner ./declare-submodule-via-evalModules.nix +checkConfigOutput '^true$' config.submodule.outer ./declare-submodule-via-evalModules.nix +# Should also be able to evaluate the type name (which evaluates freeformType, +# which evaluates all the modules defined by the type) +checkConfigOutput '^"submodule"$' options.submodule.type.description ./declare-submodule-via-evalModules.nix + +## Paths should be allowed as values and work as expected +checkConfigOutput '^true$' config.submodule.enable ./declare-submoduleWith-path.nix + +## deferredModule +# default module is merged into nodes.foo +checkConfigOutput '"beta"' config.nodes.foo.settingsDict.c ./deferred-module.nix +# errors from the default module are reported with accurate location +checkConfigError 'In `the-file-that-contains-the-bad-config.nix, via option default'\'': "bogus"' config.nodes.foo.bottom ./deferred-module.nix +checkConfigError '.*lib/tests/modules/deferred-module-error.nix, via option deferred [(]:anon-1:anon-1:anon-1[)] does not look like a module.' config.result ./deferred-module-error.nix + +# Check the file location information is propagated into submodules +checkConfigOutput the-file.nix config.submodule.internalFiles.0 ./submoduleFiles.nix + + +# Check that disabledModules works recursively and correctly +checkConfigOutput '^true$' config.enable ./disable-recursive/main.nix +checkConfigOutput '^true$' config.enable ./disable-recursive/{main.nix,disable-foo.nix} +checkConfigOutput '^true$' config.enable ./disable-recursive/{main.nix,disable-bar.nix} +checkConfigError 'The option .* does not exist. Definition values:\n\s*- In .*: true' config.enable ./disable-recursive/{main.nix,disable-foo.nix,disable-bar.nix} + +# Check that imports can depend on derivations +checkConfigOutput '^true$' config.enable ./import-from-store.nix + +# Check that configs can be conditional on option existence +checkConfigOutput '^true$' config.enable ./define-option-dependently.nix ./declare-enable.nix ./declare-int-positive-value.nix +checkConfigOutput '^360$' config.value ./define-option-dependently.nix ./declare-enable.nix ./declare-int-positive-value.nix +checkConfigOutput '^7$' config.value ./define-option-dependently.nix ./declare-int-positive-value.nix +checkConfigOutput '^true$' config.set.enable ./define-option-dependently-nested.nix ./declare-enable-nested.nix ./declare-int-positive-value-nested.nix +checkConfigOutput '^360$' config.set.value ./define-option-dependently-nested.nix ./declare-enable-nested.nix ./declare-int-positive-value-nested.nix +checkConfigOutput '^7$' config.set.value ./define-option-dependently-nested.nix ./declare-int-positive-value-nested.nix + +# Check attrsOf and lazyAttrsOf. Only lazyAttrsOf should be lazy, and only +# attrsOf should work with conditional definitions +# In addition, lazyAttrsOf should honor an options emptyValue +checkConfigError "is not lazy" config.isLazy ./declare-attrsOf.nix ./attrsOf-lazy-check.nix +checkConfigOutput '^true$' config.isLazy ./declare-lazyAttrsOf.nix ./attrsOf-lazy-check.nix +checkConfigOutput '^true$' config.conditionalWorks ./declare-attrsOf.nix ./attrsOf-conditional-check.nix +checkConfigOutput '^false$' config.conditionalWorks ./declare-lazyAttrsOf.nix ./attrsOf-conditional-check.nix +checkConfigOutput '^"empty"$' config.value.foo ./declare-lazyAttrsOf.nix ./attrsOf-conditional-check.nix + + +# Even with multiple assignments, a type error should be thrown if any of them aren't valid +checkConfigError 'A definition for option .* is not of type .*' \ + config.value ./declare-int-unsigned-value.nix ./define-value-list.nix ./define-value-int-positive.nix + +## Freeform modules +# Assigning without a declared option should work +checkConfigOutput '^"24"$' config.value ./freeform-attrsOf.nix ./define-value-string.nix +# Shorthand modules interpret `meta` and `class` as config items +checkConfigOutput '^true$' options._module.args.value.result ./freeform-attrsOf.nix ./define-freeform-keywords-shorthand.nix +# No freeform assignments shouldn't make it error +checkConfigOutput '^{ }$' config ./freeform-attrsOf.nix +# but only if the type matches +checkConfigError 'A definition for option .* is not of type .*' config.value ./freeform-attrsOf.nix ./define-value-list.nix +# and properties should be applied +checkConfigOutput '^"yes"$' config.value ./freeform-attrsOf.nix ./define-value-string-properties.nix +# Options should still be declarable, and be able to have a type that doesn't match the freeform type +checkConfigOutput '^false$' config.enable ./freeform-attrsOf.nix ./define-value-string.nix ./declare-enable.nix +checkConfigOutput '^"24"$' config.value ./freeform-attrsOf.nix ./define-value-string.nix ./declare-enable.nix +# and this should work too with nested values +checkConfigOutput '^false$' config.nest.foo ./freeform-attrsOf.nix ./freeform-nested.nix +checkConfigOutput '^"bar"$' config.nest.bar ./freeform-attrsOf.nix ./freeform-nested.nix +# Check whether a declared option can depend on an freeform-typed one +checkConfigOutput '^null$' config.foo ./freeform-attrsOf.nix ./freeform-str-dep-unstr.nix +checkConfigOutput '^"24"$' config.foo ./freeform-attrsOf.nix ./freeform-str-dep-unstr.nix ./define-value-string.nix +# Check whether an freeform-typed value can depend on a declared option, this can only work with lazyAttrsOf +checkConfigError 'infinite recursion encountered' config.foo ./freeform-attrsOf.nix ./freeform-unstr-dep-str.nix +checkConfigError 'The option .* is used but not defined' config.foo ./freeform-lazyAttrsOf.nix ./freeform-unstr-dep-str.nix +checkConfigOutput '^"24"$' config.foo ./freeform-lazyAttrsOf.nix ./freeform-unstr-dep-str.nix ./define-value-string.nix +# submodules in freeformTypes should have their locations annotated +checkConfigOutput '/freeform-submodules.nix"$' config.fooDeclarations.0 ./freeform-submodules.nix +# freeformTypes can get merged using `types.type`, including submodules +checkConfigOutput '^10$' config.free.xxx.foo ./freeform-submodules.nix +checkConfigOutput '^10$' config.free.yyy.bar ./freeform-submodules.nix + +## types.anything +# Check that attribute sets are merged recursively +checkConfigOutput '^null$' config.value.foo ./types-anything/nested-attrs.nix +checkConfigOutput '^null$' config.value.l1.foo ./types-anything/nested-attrs.nix +checkConfigOutput '^null$' config.value.l1.l2.foo ./types-anything/nested-attrs.nix +checkConfigOutput '^null$' config.value.l1.l2.l3.foo ./types-anything/nested-attrs.nix +# Attribute sets that are coercible to strings shouldn't be recursed into +checkConfigOutput '^"foo"$' config.value.outPath ./types-anything/attrs-coercible.nix +# Multiple lists aren't concatenated together +checkConfigError 'The option .* has conflicting definitions' config.value ./types-anything/lists.nix +# Check that all equalizable atoms can be used as long as all definitions are equal +checkConfigOutput '^0$' config.value.int ./types-anything/equal-atoms.nix +checkConfigOutput '^false$' config.value.bool ./types-anything/equal-atoms.nix +checkConfigOutput '^""$' config.value.string ./types-anything/equal-atoms.nix +checkConfigOutput '^/$' config.value.path ./types-anything/equal-atoms.nix +checkConfigOutput '^null$' config.value.null ./types-anything/equal-atoms.nix +checkConfigOutput '^0.1$' config.value.float ./types-anything/equal-atoms.nix +# Functions can't be merged together +checkConfigError "The option .value.multiple-lambdas.. has conflicting option types" config.applied.multiple-lambdas ./types-anything/functions.nix +checkConfigOutput '^$' config.value.single-lambda ./types-anything/functions.nix +checkConfigOutput '^null$' config.applied.merging-lambdas.x ./types-anything/functions.nix +checkConfigOutput '^null$' config.applied.merging-lambdas.y ./types-anything/functions.nix +# Check that all mk* modifiers are applied +checkConfigError 'attribute .* not found' config.value.mkiffalse ./types-anything/mk-mods.nix +checkConfigOutput '^{ }$' config.value.mkiftrue ./types-anything/mk-mods.nix +checkConfigOutput '^1$' config.value.mkdefault ./types-anything/mk-mods.nix +checkConfigOutput '^{ }$' config.value.mkmerge ./types-anything/mk-mods.nix +checkConfigOutput '^true$' config.value.mkbefore ./types-anything/mk-mods.nix +checkConfigOutput '^1$' config.value.nested.foo ./types-anything/mk-mods.nix +checkConfigOutput '^"baz"$' config.value.nested.bar.baz ./types-anything/mk-mods.nix + +## types.functionTo +checkConfigOutput '^"input is input"$' config.result ./functionTo/trivial.nix +checkConfigOutput '^"a b"$' config.result ./functionTo/merging-list.nix +checkConfigError 'A definition for option .fun.. is not of type .string.. Definition values:\n\s*- In .*wrong-type.nix' config.result ./functionTo/wrong-type.nix +checkConfigOutput '^"b a"$' config.result ./functionTo/list-order.nix +checkConfigOutput '^"a c"$' config.result ./functionTo/merging-attrs.nix +checkConfigOutput '^"a bee"$' config.result ./functionTo/submodule-options.nix +checkConfigOutput '^"fun..a fun..b"$' config.optionsResult ./functionTo/submodule-options.nix + +# moduleType +checkConfigOutput '^"a b"$' config.resultFoo ./declare-variants.nix ./define-variant.nix +checkConfigOutput '^"a b y z"$' config.resultFooBar ./declare-variants.nix ./define-variant.nix +checkConfigOutput '^"a b c"$' config.resultFooFoo ./declare-variants.nix ./define-variant.nix + +## emptyValue's +checkConfigOutput "[ ]" config.list.a ./emptyValues.nix +checkConfigOutput "{ }" config.attrs.a ./emptyValues.nix +checkConfigOutput "null" config.null.a ./emptyValues.nix +checkConfigOutput "{ }" config.submodule.a ./emptyValues.nix +# These types don't have empty values +checkConfigError 'The option .int.a. is used but not defined' config.int.a ./emptyValues.nix +checkConfigError 'The option .nonEmptyList.a. is used but not defined' config.nonEmptyList.a ./emptyValues.nix + +## types.raw +checkConfigOutput "{ foo = ; }" config.unprocessedNesting ./raw.nix +checkConfigOutput "10" config.processedToplevel ./raw.nix +checkConfigError "The option .multiple. is defined multiple times" config.multiple ./raw.nix +checkConfigOutput "bar" config.priorities ./raw.nix + +## Option collision +checkConfigError \ + 'The option .set. in module .*/declare-set.nix. would be a parent of the following options, but its type .attribute set of signed integer. does not support nested options.\n\s*- option[(]s[)] with prefix .set.enable. in module .*/declare-enable-nested.nix.' \ + config.set \ + ./declare-set.nix ./declare-enable-nested.nix + +# Test that types.optionType merges types correctly +checkConfigOutput '^10$' config.theOption.int ./optionTypeMerging.nix +checkConfigOutput '^"hello"$' config.theOption.str ./optionTypeMerging.nix + +# Test that types.optionType correctly annotates option locations +checkConfigError 'The option .theOption.nested. in .other.nix. is already declared in .optionTypeFile.nix.' config.theOption.nested ./optionTypeFile.nix + +# Test that types.optionType leaves types untouched as long as they don't need to be merged +checkConfigOutput 'ok' config.freeformItems.foo.bar ./adhoc-freeformType-survives-type-merge.nix + +# Anonymous submodules don't get nixed by import resolution/deduplication +# because of an `extendModules` bug, issue 168767. +checkConfigOutput '^1$' config.sub.specialisation.value ./extendModules-168767-imports.nix + +# Class checks, evalModules +checkConfigOutput '^{ }$' config.ok.config ./class-check.nix +checkConfigOutput '"nixos"' config.ok.class ./class-check.nix +checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.fail.config ./class-check.nix +checkConfigError 'The module foo.nix#darwinModules.default was imported into nixos instead of darwin.' config.fail-anon.config ./class-check.nix + +# Class checks, submoduleWith +checkConfigOutput '^{ }$' config.sub.nixosOk ./class-check.nix +checkConfigError 'The module .*/module-class-is-darwin.nix was imported into nixos instead of darwin.' config.sub.nixosFail.config ./class-check.nix + +# submoduleWith type merge with different class +checkConfigError 'A submoduleWith option is declared multiple times with conflicting class values "darwin" and "nixos".' config.sub.mergeFail.config ./class-check.nix + +# _type check +checkConfigError 'Could not load a value as a module, because it is of type "flake", in file .*/module-imports-_type-check.nix' config.ok.config ./module-imports-_type-check.nix +checkConfigOutput '^true$' "$@" config.enable ./declare-enable.nix ./define-enable-with-top-level-mkIf.nix +checkConfigError 'Could not load a value as a module, because it is of type "configuration", in file .*/import-configuration.nix.*please only import the modules that make up the configuration.*' config ./import-configuration.nix + +# doRename works when `warnings` does not exist. +checkConfigOutput '^1234$' config.c.d.e ./doRename-basic.nix +# doRename adds a warning. +checkConfigOutput '^"The option `a\.b. defined in `.*/doRename-warnings\.nix. has been renamed to `c\.d\.e.\."$' \ + config.result \ + ./doRename-warnings.nix + +# Anonymous modules get deduplicated by key +checkConfigOutput '^"pear"$' config.once.raw ./merge-module-with-key.nix +checkConfigOutput '^"pear\\npear"$' config.twice.raw ./merge-module-with-key.nix + +cat < + # - default + # where all nodes include the default + ({ config, ... }: { + _file = "generic.nix"; + options.nodes = mkOption { + type = lazyAttrsOf (submodule { imports = [ config.default ]; }); + default = {}; + }; + options.default = mkOption { + type = deferredModule; + default = { }; + description = '' + Module that is included in all nodes. + ''; + }; + }) + + { + _file = "default-1.nix"; + default = { config, ... }: { + options.settingsDict = lib.mkOption { type = lazyAttrsOf str; default = {}; }; + options.bottom = lib.mkOption { type = enum []; }; + }; + } + + { + _file = "default-a-is-b.nix"; + default = ./define-settingsDict-a-is-b.nix; + } + + { + _file = "nodes-foo.nix"; + nodes.foo.settingsDict.b = "beta"; + } + + { + _file = "the-file-that-contains-the-bad-config.nix"; + default.bottom = "bogus"; + } + + { + _file = "nodes-foo-c-is-a.nix"; + nodes.foo = { config, ... }: { + settingsDict.c = config.settingsDict.a; + }; + } + + ]; +} diff --git a/tests/modules/define-_module-args-custom.nix b/tests/modules/define-_module-args-custom.nix new file mode 100644 index 000000000..e565fd215 --- /dev/null +++ b/tests/modules/define-_module-args-custom.nix @@ -0,0 +1,7 @@ +{ lib, ... }: + +{ + config = { + _module.args.custom = true; + }; +} diff --git a/tests/modules/define-attrsOfSub-bar-enable.nix b/tests/modules/define-attrsOfSub-bar-enable.nix new file mode 100644 index 000000000..99c55d8b3 --- /dev/null +++ b/tests/modules/define-attrsOfSub-bar-enable.nix @@ -0,0 +1,3 @@ +{ + attrsOfSub.bar.enable = true; +} diff --git a/tests/modules/define-attrsOfSub-bar.nix b/tests/modules/define-attrsOfSub-bar.nix new file mode 100644 index 000000000..2a33068a5 --- /dev/null +++ b/tests/modules/define-attrsOfSub-bar.nix @@ -0,0 +1,3 @@ +{ + attrsOfSub.bar = {}; +} diff --git a/tests/modules/define-attrsOfSub-foo-enable-force.nix b/tests/modules/define-attrsOfSub-foo-enable-force.nix new file mode 100644 index 000000000..c9ee36446 --- /dev/null +++ b/tests/modules/define-attrsOfSub-foo-enable-force.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + attrsOfSub.foo.enable = lib.mkForce false; +} diff --git a/tests/modules/define-attrsOfSub-foo-enable-if.nix b/tests/modules/define-attrsOfSub-foo-enable-if.nix new file mode 100644 index 000000000..0b3baddb5 --- /dev/null +++ b/tests/modules/define-attrsOfSub-foo-enable-if.nix @@ -0,0 +1,5 @@ +{ config, lib, ... }: + +{ + attrsOfSub.foo.enable = lib.mkIf config.enable true; +} diff --git a/tests/modules/define-attrsOfSub-foo-enable.nix b/tests/modules/define-attrsOfSub-foo-enable.nix new file mode 100644 index 000000000..39cd63cef --- /dev/null +++ b/tests/modules/define-attrsOfSub-foo-enable.nix @@ -0,0 +1,3 @@ +{ + attrsOfSub.foo.enable = true; +} diff --git a/tests/modules/define-attrsOfSub-foo-force-enable.nix b/tests/modules/define-attrsOfSub-foo-force-enable.nix new file mode 100644 index 000000000..009da7c77 --- /dev/null +++ b/tests/modules/define-attrsOfSub-foo-force-enable.nix @@ -0,0 +1,7 @@ +{ lib, ... }: + +{ + attrsOfSub.foo = lib.mkForce { + enable = false; + }; +} diff --git a/tests/modules/define-attrsOfSub-foo-if-enable.nix b/tests/modules/define-attrsOfSub-foo-if-enable.nix new file mode 100644 index 000000000..93702dfa8 --- /dev/null +++ b/tests/modules/define-attrsOfSub-foo-if-enable.nix @@ -0,0 +1,7 @@ +{ config, lib, ... }: + +{ + attrsOfSub.foo = lib.mkIf config.enable { + enable = true; + }; +} diff --git a/tests/modules/define-attrsOfSub-foo.nix b/tests/modules/define-attrsOfSub-foo.nix new file mode 100644 index 000000000..e6bb531de --- /dev/null +++ b/tests/modules/define-attrsOfSub-foo.nix @@ -0,0 +1,3 @@ +{ + attrsOfSub.foo = {}; +} diff --git a/tests/modules/define-attrsOfSub-force-foo-enable.nix b/tests/modules/define-attrsOfSub-force-foo-enable.nix new file mode 100644 index 000000000..5c02dd343 --- /dev/null +++ b/tests/modules/define-attrsOfSub-force-foo-enable.nix @@ -0,0 +1,7 @@ +{ lib, ... }: + +{ + attrsOfSub = lib.mkForce { + foo.enable = false; + }; +} diff --git a/tests/modules/define-attrsOfSub-if-foo-enable.nix b/tests/modules/define-attrsOfSub-if-foo-enable.nix new file mode 100644 index 000000000..a3fe6051d --- /dev/null +++ b/tests/modules/define-attrsOfSub-if-foo-enable.nix @@ -0,0 +1,7 @@ +{ config, lib, ... }: + +{ + attrsOfSub = lib.mkIf config.enable { + foo.enable = true; + }; +} diff --git a/tests/modules/define-bare-submodule-values.nix b/tests/modules/define-bare-submodule-values.nix new file mode 100644 index 000000000..00ede929e --- /dev/null +++ b/tests/modules/define-bare-submodule-values.nix @@ -0,0 +1,4 @@ +{ + bare-submodule.nested = 42; + bare-submodule.deep = 420; +} diff --git a/tests/modules/define-enable-abort.nix b/tests/modules/define-enable-abort.nix new file mode 100644 index 000000000..85b58a567 --- /dev/null +++ b/tests/modules/define-enable-abort.nix @@ -0,0 +1,3 @@ +{ + config.enable = abort "oops"; +} diff --git a/tests/modules/define-enable-force.nix b/tests/modules/define-enable-force.nix new file mode 100644 index 000000000..f4990a328 --- /dev/null +++ b/tests/modules/define-enable-force.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + enable = lib.mkForce false; +} diff --git a/tests/modules/define-enable-throw.nix b/tests/modules/define-enable-throw.nix new file mode 100644 index 000000000..16a59b781 --- /dev/null +++ b/tests/modules/define-enable-throw.nix @@ -0,0 +1,3 @@ +{ + config.enable = throw "oops"; +} diff --git a/tests/modules/define-enable-with-custom-arg.nix b/tests/modules/define-enable-with-custom-arg.nix new file mode 100644 index 000000000..7da74671d --- /dev/null +++ b/tests/modules/define-enable-with-custom-arg.nix @@ -0,0 +1,7 @@ +{ lib, custom, ... }: + +{ + config = { + enable = custom; + }; +} diff --git a/tests/modules/define-enable-with-top-level-mkIf.nix b/tests/modules/define-enable-with-top-level-mkIf.nix new file mode 100644 index 000000000..4909c16d8 --- /dev/null +++ b/tests/modules/define-enable-with-top-level-mkIf.nix @@ -0,0 +1,5 @@ +{ lib, ... }: +# I think this might occur more realistically in a submodule +{ + imports = [ (lib.mkIf true { enable = true; }) ]; +} diff --git a/tests/modules/define-enable.nix b/tests/modules/define-enable.nix new file mode 100644 index 000000000..7dc26010a --- /dev/null +++ b/tests/modules/define-enable.nix @@ -0,0 +1,3 @@ +{ + enable = true; +} diff --git a/tests/modules/define-force-attrsOfSub-foo-enable.nix b/tests/modules/define-force-attrsOfSub-foo-enable.nix new file mode 100644 index 000000000..dafb2360e --- /dev/null +++ b/tests/modules/define-force-attrsOfSub-foo-enable.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +lib.mkForce { + attrsOfSub.foo.enable = false; +} diff --git a/tests/modules/define-force-enable.nix b/tests/modules/define-force-enable.nix new file mode 100644 index 000000000..978caa2a8 --- /dev/null +++ b/tests/modules/define-force-enable.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +lib.mkForce { + enable = false; +} diff --git a/tests/modules/define-freeform-keywords-shorthand.nix b/tests/modules/define-freeform-keywords-shorthand.nix new file mode 100644 index 000000000..8de1ec6a7 --- /dev/null +++ b/tests/modules/define-freeform-keywords-shorthand.nix @@ -0,0 +1,15 @@ +{ config, ... }: { + class = { "just" = "data"; }; + a = "one"; + b = "two"; + meta = "meta"; + + _module.args.result = + let r = builtins.removeAttrs config [ "_module" ]; + in builtins.trace (builtins.deepSeq r r) (r == { + a = "one"; + b = "two"; + class = { "just" = "data"; }; + meta = "meta"; + }); +} diff --git a/tests/modules/define-if-attrsOfSub-foo-enable.nix b/tests/modules/define-if-attrsOfSub-foo-enable.nix new file mode 100644 index 000000000..6a8e32e80 --- /dev/null +++ b/tests/modules/define-if-attrsOfSub-foo-enable.nix @@ -0,0 +1,5 @@ +{ config, lib, ... }: + +lib.mkIf config.enable { + attrsOfSub.foo.enable = true; +} diff --git a/tests/modules/define-module-check.nix b/tests/modules/define-module-check.nix new file mode 100644 index 000000000..5a0707c97 --- /dev/null +++ b/tests/modules/define-module-check.nix @@ -0,0 +1,3 @@ +{ + _module.check = false; +} diff --git a/tests/modules/define-option-dependently-nested.nix b/tests/modules/define-option-dependently-nested.nix new file mode 100644 index 000000000..69ee42555 --- /dev/null +++ b/tests/modules/define-option-dependently-nested.nix @@ -0,0 +1,16 @@ +{ lib, options, ... }: + +# Some modules may be distributed separately and need to adapt to other modules +# that are distributed and versioned separately. +{ + + # Always defined, but the value depends on the presence of an option. + config.set = { + value = if options ? set.enable then 360 else 7; + } + # Only define if possible. + // lib.optionalAttrs (options ? set.enable) { + enable = true; + }; + +} diff --git a/tests/modules/define-option-dependently.nix b/tests/modules/define-option-dependently.nix new file mode 100644 index 000000000..ad85f99a9 --- /dev/null +++ b/tests/modules/define-option-dependently.nix @@ -0,0 +1,16 @@ +{ lib, options, ... }: + +# Some modules may be distributed separately and need to adapt to other modules +# that are distributed and versioned separately. +{ + + # Always defined, but the value depends on the presence of an option. + config = { + value = if options ? enable then 360 else 7; + } + # Only define if possible. + // lib.optionalAttrs (options ? enable) { + enable = true; + }; + +} diff --git a/tests/modules/define-settingsDict-a-is-b.nix b/tests/modules/define-settingsDict-a-is-b.nix new file mode 100644 index 000000000..42363f45f --- /dev/null +++ b/tests/modules/define-settingsDict-a-is-b.nix @@ -0,0 +1,3 @@ +{ config, ... }: { + settingsDict.a = config.settingsDict.b; +} diff --git a/tests/modules/define-shorthandOnlyDefinesConfig-true.nix b/tests/modules/define-shorthandOnlyDefinesConfig-true.nix new file mode 100644 index 000000000..bd3a73dce --- /dev/null +++ b/tests/modules/define-shorthandOnlyDefinesConfig-true.nix @@ -0,0 +1 @@ +{ shorthandOnlyDefinesConfig = true; } diff --git a/tests/modules/define-submoduleWith-noshorthand.nix b/tests/modules/define-submoduleWith-noshorthand.nix new file mode 100644 index 000000000..35e1607b6 --- /dev/null +++ b/tests/modules/define-submoduleWith-noshorthand.nix @@ -0,0 +1,3 @@ +{ + submodule.config.config = true; +} diff --git a/tests/modules/define-submoduleWith-shorthand.nix b/tests/modules/define-submoduleWith-shorthand.nix new file mode 100644 index 000000000..17df248db --- /dev/null +++ b/tests/modules/define-submoduleWith-shorthand.nix @@ -0,0 +1,3 @@ +{ + submodule.config = true; +} diff --git a/tests/modules/define-value-int-negative.nix b/tests/modules/define-value-int-negative.nix new file mode 100644 index 000000000..a04122298 --- /dev/null +++ b/tests/modules/define-value-int-negative.nix @@ -0,0 +1,3 @@ +{ + value = -23; +} diff --git a/tests/modules/define-value-int-positive.nix b/tests/modules/define-value-int-positive.nix new file mode 100644 index 000000000..5803de172 --- /dev/null +++ b/tests/modules/define-value-int-positive.nix @@ -0,0 +1,3 @@ +{ + value = 42; +} diff --git a/tests/modules/define-value-int-zero.nix b/tests/modules/define-value-int-zero.nix new file mode 100644 index 000000000..68bb9f415 --- /dev/null +++ b/tests/modules/define-value-int-zero.nix @@ -0,0 +1,3 @@ +{ + value = 0; +} diff --git a/tests/modules/define-value-list.nix b/tests/modules/define-value-list.nix new file mode 100644 index 000000000..4831c1cc0 --- /dev/null +++ b/tests/modules/define-value-list.nix @@ -0,0 +1,3 @@ +{ + value = []; +} diff --git a/tests/modules/define-value-string-arbitrary.nix b/tests/modules/define-value-string-arbitrary.nix new file mode 100644 index 000000000..8e3abaf53 --- /dev/null +++ b/tests/modules/define-value-string-arbitrary.nix @@ -0,0 +1,3 @@ +{ + value = "foobar"; +} diff --git a/tests/modules/define-value-string-bigint.nix b/tests/modules/define-value-string-bigint.nix new file mode 100644 index 000000000..f27e31985 --- /dev/null +++ b/tests/modules/define-value-string-bigint.nix @@ -0,0 +1,3 @@ +{ + value = "1000"; +} diff --git a/tests/modules/define-value-string-properties.nix b/tests/modules/define-value-string-properties.nix new file mode 100644 index 000000000..972304c01 --- /dev/null +++ b/tests/modules/define-value-string-properties.nix @@ -0,0 +1,12 @@ +{ lib, ... }: { + + imports = [{ + value = lib.mkDefault "def"; + }]; + + value = lib.mkMerge [ + (lib.mkIf false "nope") + "yes" + ]; + +} diff --git a/tests/modules/define-value-string.nix b/tests/modules/define-value-string.nix new file mode 100644 index 000000000..e7a166965 --- /dev/null +++ b/tests/modules/define-value-string.nix @@ -0,0 +1,3 @@ +{ + value = "24"; +} diff --git a/tests/modules/define-variant.nix b/tests/modules/define-variant.nix new file mode 100644 index 000000000..423cb0e37 --- /dev/null +++ b/tests/modules/define-variant.nix @@ -0,0 +1,22 @@ +{ config, lib, ... }: +let inherit (lib) types mkOption attrNames; +in +{ + options = { + attrs = mkOption { type = types.attrsOf lib.types.int; }; + result = mkOption { }; + resultFoo = mkOption { }; + resultFooBar = mkOption { }; + resultFooFoo = mkOption { }; + }; + config = { + attrs.a = 1; + variants.foo.attrs.b = 1; + variants.bar.attrs.y = 1; + variants.foo.variants.bar.attrs.z = 1; + variants.foo.variants.foo.attrs.c = 3; + resultFoo = lib.concatMapStringsSep " " toString (attrNames config.variants.foo.attrs); + resultFooBar = lib.concatMapStringsSep " " toString (attrNames config.variants.foo.variants.bar.attrs); + resultFooFoo = lib.concatMapStringsSep " " toString (attrNames config.variants.foo.variants.foo.attrs); + }; +} diff --git a/tests/modules/disable-declare-enable.nix b/tests/modules/disable-declare-enable.nix new file mode 100644 index 000000000..a373ee7e5 --- /dev/null +++ b/tests/modules/disable-declare-enable.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + disabledModules = [ ./declare-enable.nix ]; +} diff --git a/tests/modules/disable-define-enable-string-path.nix b/tests/modules/disable-define-enable-string-path.nix new file mode 100644 index 000000000..6429a6d63 --- /dev/null +++ b/tests/modules/disable-define-enable-string-path.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + disabledModules = [ (toString ./define-enable.nix) ]; +} diff --git a/tests/modules/disable-define-enable.nix b/tests/modules/disable-define-enable.nix new file mode 100644 index 000000000..0d84a7c3c --- /dev/null +++ b/tests/modules/disable-define-enable.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + disabledModules = [ ./define-enable.nix ]; +} diff --git a/tests/modules/disable-enable-modules.nix b/tests/modules/disable-enable-modules.nix new file mode 100644 index 000000000..c325f4e07 --- /dev/null +++ b/tests/modules/disable-enable-modules.nix @@ -0,0 +1,5 @@ +{ lib, ... }: + +{ + disabledModules = [ "define-enable.nix" "declare-enable.nix" ]; +} diff --git a/tests/modules/disable-module-bad-key.nix b/tests/modules/disable-module-bad-key.nix new file mode 100644 index 000000000..f50d06f2f --- /dev/null +++ b/tests/modules/disable-module-bad-key.nix @@ -0,0 +1,16 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; + + moduleWithKey = { config, ... }: { + config = { + enable = true; + }; + }; +in +{ + imports = [ + ./declare-enable.nix + ]; + disabledModules = [ { } ]; +} diff --git a/tests/modules/disable-module-with-key.nix b/tests/modules/disable-module-with-key.nix new file mode 100644 index 000000000..ea2a60aa8 --- /dev/null +++ b/tests/modules/disable-module-with-key.nix @@ -0,0 +1,34 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; + + moduleWithKey = { + key = "disable-module-with-key.nix#moduleWithKey"; + config = { + enable = true; + }; + }; +in +{ + options = { + positive = mkOption { + type = types.submodule { + imports = [ + ./declare-enable.nix + moduleWithKey + ]; + }; + default = {}; + }; + negative = mkOption { + type = types.submodule { + imports = [ + ./declare-enable.nix + moduleWithKey + ]; + disabledModules = [ moduleWithKey ]; + }; + default = {}; + }; + }; +} diff --git a/tests/modules/disable-module-with-toString-key.nix b/tests/modules/disable-module-with-toString-key.nix new file mode 100644 index 000000000..3f8c81904 --- /dev/null +++ b/tests/modules/disable-module-with-toString-key.nix @@ -0,0 +1,34 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; + + moduleWithKey = { + key = 123; + config = { + enable = true; + }; + }; +in +{ + options = { + positive = mkOption { + type = types.submodule { + imports = [ + ./declare-enable.nix + moduleWithKey + ]; + }; + default = {}; + }; + negative = mkOption { + type = types.submodule { + imports = [ + ./declare-enable.nix + moduleWithKey + ]; + disabledModules = [ 123 ]; + }; + default = {}; + }; + }; +} diff --git a/tests/modules/disable-recursive/bar.nix b/tests/modules/disable-recursive/bar.nix new file mode 100644 index 000000000..4d9240a43 --- /dev/null +++ b/tests/modules/disable-recursive/bar.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ../declare-enable.nix + ]; +} diff --git a/tests/modules/disable-recursive/disable-bar.nix b/tests/modules/disable-recursive/disable-bar.nix new file mode 100644 index 000000000..987b2802a --- /dev/null +++ b/tests/modules/disable-recursive/disable-bar.nix @@ -0,0 +1,7 @@ +{ + + disabledModules = [ + ./bar.nix + ]; + +} diff --git a/tests/modules/disable-recursive/disable-foo.nix b/tests/modules/disable-recursive/disable-foo.nix new file mode 100644 index 000000000..5b68a3c46 --- /dev/null +++ b/tests/modules/disable-recursive/disable-foo.nix @@ -0,0 +1,7 @@ +{ + + disabledModules = [ + ./foo.nix + ]; + +} diff --git a/tests/modules/disable-recursive/foo.nix b/tests/modules/disable-recursive/foo.nix new file mode 100644 index 000000000..4d9240a43 --- /dev/null +++ b/tests/modules/disable-recursive/foo.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ../declare-enable.nix + ]; +} diff --git a/tests/modules/disable-recursive/main.nix b/tests/modules/disable-recursive/main.nix new file mode 100644 index 000000000..48a3c6218 --- /dev/null +++ b/tests/modules/disable-recursive/main.nix @@ -0,0 +1,8 @@ +{ + imports = [ + ./foo.nix + ./bar.nix + ]; + + enable = true; +} diff --git a/tests/modules/doRename-basic.nix b/tests/modules/doRename-basic.nix new file mode 100644 index 000000000..9d79fa4f2 --- /dev/null +++ b/tests/modules/doRename-basic.nix @@ -0,0 +1,11 @@ +{ lib, ... }: { + imports = [ + (lib.doRename { from = ["a" "b"]; to = ["c" "d" "e"]; warn = true; use = x: x; visible = true; }) + ]; + options = { + c.d.e = lib.mkOption {}; + }; + config = { + a.b = 1234; + }; +} diff --git a/tests/modules/doRename-warnings.nix b/tests/modules/doRename-warnings.nix new file mode 100644 index 000000000..6f0f1e87e --- /dev/null +++ b/tests/modules/doRename-warnings.nix @@ -0,0 +1,14 @@ +{ lib, config, ... }: { + imports = [ + (lib.doRename { from = ["a" "b"]; to = ["c" "d" "e"]; warn = true; use = x: x; visible = true; }) + ]; + options = { + warnings = lib.mkOption { type = lib.types.listOf lib.types.str; }; + c.d.e = lib.mkOption {}; + result = lib.mkOption {}; + }; + config = { + a.b = 1234; + result = lib.concatStringsSep "%" config.warnings; + }; +} diff --git a/tests/modules/emptyValues.nix b/tests/modules/emptyValues.nix new file mode 100644 index 000000000..77825de32 --- /dev/null +++ b/tests/modules/emptyValues.nix @@ -0,0 +1,36 @@ +{ lib, ... }: +let + inherit (lib) types; +in { + + options = { + int = lib.mkOption { + type = types.lazyAttrsOf types.int; + }; + list = lib.mkOption { + type = types.lazyAttrsOf (types.listOf types.int); + }; + nonEmptyList = lib.mkOption { + type = types.lazyAttrsOf (types.nonEmptyListOf types.int); + }; + attrs = lib.mkOption { + type = types.lazyAttrsOf (types.attrsOf types.int); + }; + null = lib.mkOption { + type = types.lazyAttrsOf (types.nullOr types.int); + }; + submodule = lib.mkOption { + type = types.lazyAttrsOf (types.submodule {}); + }; + }; + + config = { + int.a = lib.mkIf false null; + list.a = lib.mkIf false null; + nonEmptyList.a = lib.mkIf false null; + attrs.a = lib.mkIf false null; + null.a = lib.mkIf false null; + submodule.a = lib.mkIf false null; + }; + +} diff --git a/tests/modules/extendModules-168767-imports.nix b/tests/modules/extendModules-168767-imports.nix new file mode 100644 index 000000000..489e6b5a5 --- /dev/null +++ b/tests/modules/extendModules-168767-imports.nix @@ -0,0 +1,41 @@ +{ lib +, extendModules +, ... +}: +with lib; +{ + imports = [ + + { + options.sub = mkOption { + default = { }; + type = types.submodule ( + { config + , extendModules + , ... + }: + { + options.value = mkOption { + type = types.int; + }; + + options.specialisation = mkOption { + default = { }; + inherit + (extendModules { + modules = [{ + specialisation = mkOverride 0 { }; + }]; + }) + type; + }; + } + ); + }; + } + + { config.sub.value = 1; } + + + ]; +} diff --git a/tests/modules/freeform-attrsOf.nix b/tests/modules/freeform-attrsOf.nix new file mode 100644 index 000000000..8cc577f38 --- /dev/null +++ b/tests/modules/freeform-attrsOf.nix @@ -0,0 +1,3 @@ +{ lib, ... }: { + freeformType = with lib.types; attrsOf (either str (attrsOf str)); +} diff --git a/tests/modules/freeform-lazyAttrsOf.nix b/tests/modules/freeform-lazyAttrsOf.nix new file mode 100644 index 000000000..36d6c0b13 --- /dev/null +++ b/tests/modules/freeform-lazyAttrsOf.nix @@ -0,0 +1,3 @@ +{ lib, ... }: { + freeformType = with lib.types; lazyAttrsOf (either str (lazyAttrsOf str)); +} diff --git a/tests/modules/freeform-nested.nix b/tests/modules/freeform-nested.nix new file mode 100644 index 000000000..b81fa7f0d --- /dev/null +++ b/tests/modules/freeform-nested.nix @@ -0,0 +1,14 @@ +{ lib, ... }: +let + deathtrapArgs = lib.mapAttrs + (k: _: throw "The module system is too strict, accessing an unused option's ${k} mkOption-attribute.") + (lib.functionArgs lib.mkOption); +in +{ + options.nest.foo = lib.mkOption { + type = lib.types.bool; + default = false; + }; + options.nest.unused = lib.mkOption deathtrapArgs; + config.nest.bar = "bar"; +} diff --git a/tests/modules/freeform-str-dep-unstr.nix b/tests/modules/freeform-str-dep-unstr.nix new file mode 100644 index 000000000..a2dfbc80c --- /dev/null +++ b/tests/modules/freeform-str-dep-unstr.nix @@ -0,0 +1,8 @@ +{ lib, config, ... }: { + options.foo = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + }; + + config.foo = lib.mkIf (config ? value) config.value; +} diff --git a/tests/modules/freeform-submodules.nix b/tests/modules/freeform-submodules.nix new file mode 100644 index 000000000..3910435a7 --- /dev/null +++ b/tests/modules/freeform-submodules.nix @@ -0,0 +1,22 @@ +{ lib, options, ... }: with lib.types; { + + options.fooDeclarations = lib.mkOption { + default = (options.free.type.getSubOptions [])._freeformOptions.foo.declarations; + }; + + options.free = lib.mkOption { + type = submodule { + config._module.freeformType = lib.mkMerge [ + (attrsOf (submodule { + options.foo = lib.mkOption {}; + })) + (attrsOf (submodule { + options.bar = lib.mkOption {}; + })) + ]; + }; + }; + + config.free.xxx.foo = 10; + config.free.yyy.bar = 10; +} diff --git a/tests/modules/freeform-unstr-dep-str.nix b/tests/modules/freeform-unstr-dep-str.nix new file mode 100644 index 000000000..549d89afe --- /dev/null +++ b/tests/modules/freeform-unstr-dep-str.nix @@ -0,0 +1,8 @@ +{ lib, config, ... }: { + options.value = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + }; + + config.foo = lib.mkIf (config.value != null) config.value; +} diff --git a/tests/modules/functionTo/list-order.nix b/tests/modules/functionTo/list-order.nix new file mode 100644 index 000000000..77a1a43a8 --- /dev/null +++ b/tests/modules/functionTo/list-order.nix @@ -0,0 +1,25 @@ + +{ lib, config, ... }: +let + inherit (lib) types; +in { + options = { + fun = lib.mkOption { + type = types.functionTo (types.listOf types.str); + }; + + result = lib.mkOption { + type = types.str; + default = toString (config.fun { + a = "a"; + b = "b"; + c = "c"; + }); + }; + }; + + config.fun = lib.mkMerge [ + (input: lib.mkAfter [ input.a ]) + (input: [ input.b ]) + ]; +} diff --git a/tests/modules/functionTo/merging-attrs.nix b/tests/modules/functionTo/merging-attrs.nix new file mode 100644 index 000000000..97c015f92 --- /dev/null +++ b/tests/modules/functionTo/merging-attrs.nix @@ -0,0 +1,27 @@ +{ lib, config, ... }: +let + inherit (lib) types; +in { + options = { + fun = lib.mkOption { + type = types.functionTo (types.attrsOf types.str); + }; + + result = lib.mkOption { + type = types.str; + default = toString (lib.attrValues (config.fun { + a = "a"; + b = "b"; + c = "c"; + })); + }; + }; + + config.fun = lib.mkMerge [ + (input: { inherit (input) a; }) + (input: { inherit (input) b; }) + (input: { + b = lib.mkForce input.c; + }) + ]; +} diff --git a/tests/modules/functionTo/merging-list.nix b/tests/modules/functionTo/merging-list.nix new file mode 100644 index 000000000..15fcd2bdc --- /dev/null +++ b/tests/modules/functionTo/merging-list.nix @@ -0,0 +1,24 @@ +{ lib, config, ... }: +let + inherit (lib) types; +in { + options = { + fun = lib.mkOption { + type = types.functionTo (types.listOf types.str); + }; + + result = lib.mkOption { + type = types.str; + default = toString (config.fun { + a = "a"; + b = "b"; + c = "c"; + }); + }; + }; + + config.fun = lib.mkMerge [ + (input: [ input.a ]) + (input: [ input.b ]) + ]; +} diff --git a/tests/modules/functionTo/submodule-options.nix b/tests/modules/functionTo/submodule-options.nix new file mode 100644 index 000000000..b884892ef --- /dev/null +++ b/tests/modules/functionTo/submodule-options.nix @@ -0,0 +1,61 @@ +{ lib, config, options, ... }: +let + inherit (lib) types; +in +{ + imports = [ + + # fun..a + ({ ... }: { + options = { + fun = lib.mkOption { + type = types.functionTo (types.submodule { + options.a = lib.mkOption { default = "a"; }; + }); + }; + }; + }) + + # fun..b + ({ ... }: { + options = { + fun = lib.mkOption { + type = types.functionTo (types.submodule { + options.b = lib.mkOption { default = "b"; }; + }); + }; + }; + }) + ]; + + options = { + result = lib.mkOption + { + type = types.str; + default = lib.concatStringsSep " " (lib.attrValues (config.fun (throw "shouldn't use input param"))); + }; + + optionsResult = lib.mkOption + { + type = types.str; + default = lib.concatStringsSep " " + (lib.concatLists + (lib.mapAttrsToList + (k: v: + if k == "_module" + then [ ] + else [ (lib.showOption v.loc) ] + ) + ( + (options.fun.type.getSubOptions [ "fun" ]) + ) + ) + ); + }; + }; + + config.fun = lib.mkMerge + [ + (input: { b = "bee"; }) + ]; +} diff --git a/tests/modules/functionTo/trivial.nix b/tests/modules/functionTo/trivial.nix new file mode 100644 index 000000000..0962a0cf8 --- /dev/null +++ b/tests/modules/functionTo/trivial.nix @@ -0,0 +1,17 @@ +{ lib, config, ... }: +let + inherit (lib) types; +in { + options = { + fun = lib.mkOption { + type = types.functionTo types.str; + }; + + result = lib.mkOption { + type = types.str; + default = config.fun "input"; + }; + }; + + config.fun = input: "input is ${input}"; +} diff --git a/tests/modules/functionTo/wrong-type.nix b/tests/modules/functionTo/wrong-type.nix new file mode 100644 index 000000000..fd65b7508 --- /dev/null +++ b/tests/modules/functionTo/wrong-type.nix @@ -0,0 +1,18 @@ + +{ lib, config, ... }: +let + inherit (lib) types; +in { + options = { + fun = lib.mkOption { + type = types.functionTo types.str; + }; + + result = lib.mkOption { + type = types.str; + default = config.fun 0; + }; + }; + + config.fun = input: input + 1; +} diff --git a/tests/modules/import-configuration.nix b/tests/modules/import-configuration.nix new file mode 100644 index 000000000..a2a32bbee --- /dev/null +++ b/tests/modules/import-configuration.nix @@ -0,0 +1,12 @@ +{ lib, ... }: +let + myconf = lib.evalModules { modules = [ { } ]; }; +in +{ + imports = [ + # We can't do this. A configuration is not equal to its set of a modules. + # Equating those would lead to a mess, as specialArgs, anonymous modules + # that can't be deduplicated, and possibly more come into play. + myconf + ]; +} diff --git a/tests/modules/import-custom-arg.nix b/tests/modules/import-custom-arg.nix new file mode 100644 index 000000000..3e687b661 --- /dev/null +++ b/tests/modules/import-custom-arg.nix @@ -0,0 +1,6 @@ +{ lib, custom, ... }: + +{ + imports = [] + ++ lib.optional custom ./define-enable-force.nix; +} diff --git a/tests/modules/import-from-store.nix b/tests/modules/import-from-store.nix new file mode 100644 index 000000000..f5af22432 --- /dev/null +++ b/tests/modules/import-from-store.nix @@ -0,0 +1,11 @@ +{ lib, ... }: +{ + + imports = [ + "${builtins.toFile "drv" "{}"}" + ./declare-enable.nix + ./define-enable.nix + ]; + +} + diff --git a/tests/modules/merge-module-with-key.nix b/tests/modules/merge-module-with-key.nix new file mode 100644 index 000000000..21f00e6ef --- /dev/null +++ b/tests/modules/merge-module-with-key.nix @@ -0,0 +1,49 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; + + moduleWithoutKey = { + config = { + raw = "pear"; + }; + }; + + moduleWithKey = { + key = __curPos.file + "#moduleWithKey"; + config = { + raw = "pear"; + }; + }; + + decl = { + options = { + raw = mkOption { + type = types.lines; + }; + }; + }; +in +{ + options = { + once = mkOption { + type = types.submodule { + imports = [ + decl + moduleWithKey + moduleWithKey + ]; + }; + default = {}; + }; + twice = mkOption { + type = types.submodule { + imports = [ + decl + moduleWithoutKey + moduleWithoutKey + ]; + }; + default = {}; + }; + }; +} diff --git a/tests/modules/module-class-is-darwin.nix b/tests/modules/module-class-is-darwin.nix new file mode 100644 index 000000000..bacf45626 --- /dev/null +++ b/tests/modules/module-class-is-darwin.nix @@ -0,0 +1,4 @@ +{ + _class = "darwin"; + config = {}; +} diff --git a/tests/modules/module-class-is-nixos.nix b/tests/modules/module-class-is-nixos.nix new file mode 100644 index 000000000..6d62feeda --- /dev/null +++ b/tests/modules/module-class-is-nixos.nix @@ -0,0 +1,4 @@ +{ + _class = "nixos"; + config = {}; +} diff --git a/tests/modules/module-imports-_type-check.nix b/tests/modules/module-imports-_type-check.nix new file mode 100644 index 000000000..1e29c469d --- /dev/null +++ b/tests/modules/module-imports-_type-check.nix @@ -0,0 +1,3 @@ +{ + imports = [ { _type = "flake"; } ]; +} diff --git a/tests/modules/optionTypeFile.nix b/tests/modules/optionTypeFile.nix new file mode 100644 index 000000000..6015d59a7 --- /dev/null +++ b/tests/modules/optionTypeFile.nix @@ -0,0 +1,28 @@ +{ config, lib, ... }: { + + _file = "optionTypeFile.nix"; + + options.theType = lib.mkOption { + type = lib.types.optionType; + }; + + options.theOption = lib.mkOption { + type = config.theType; + default = {}; + }; + + config.theType = lib.mkMerge [ + (lib.types.submodule { + options.nested = lib.mkOption { + type = lib.types.int; + }; + }) + (lib.types.submodule { + _file = "other.nix"; + options.nested = lib.mkOption { + type = lib.types.str; + }; + }) + ]; + +} diff --git a/tests/modules/optionTypeMerging.nix b/tests/modules/optionTypeMerging.nix new file mode 100644 index 000000000..74a620c46 --- /dev/null +++ b/tests/modules/optionTypeMerging.nix @@ -0,0 +1,27 @@ +{ config, lib, ... }: { + + options.theType = lib.mkOption { + type = lib.types.optionType; + }; + + options.theOption = lib.mkOption { + type = config.theType; + }; + + config.theType = lib.mkMerge [ + (lib.types.submodule { + options.int = lib.mkOption { + type = lib.types.int; + default = 10; + }; + }) + (lib.types.submodule { + options.str = lib.mkOption { + type = lib.types.str; + }; + }) + ]; + + config.theOption.str = "hello"; + +} diff --git a/tests/modules/raw.nix b/tests/modules/raw.nix new file mode 100644 index 000000000..418e671ed --- /dev/null +++ b/tests/modules/raw.nix @@ -0,0 +1,30 @@ +{ lib, ... }: { + + options = { + processedToplevel = lib.mkOption { + type = lib.types.raw; + }; + unprocessedNesting = lib.mkOption { + type = lib.types.raw; + }; + multiple = lib.mkOption { + type = lib.types.raw; + }; + priorities = lib.mkOption { + type = lib.types.raw; + }; + }; + + config = { + processedToplevel = lib.mkIf true 10; + unprocessedNesting.foo = throw "foo"; + multiple = lib.mkMerge [ + "foo" + "foo" + ]; + priorities = lib.mkMerge [ + "foo" + (lib.mkForce "bar") + ]; + }; +} diff --git a/tests/modules/shorthand-meta.nix b/tests/modules/shorthand-meta.nix new file mode 100644 index 000000000..8c9619e18 --- /dev/null +++ b/tests/modules/shorthand-meta.nix @@ -0,0 +1,19 @@ +{ lib, ... }: +let + inherit (lib) types mkOption; +in +{ + imports = [ + ({ config, ... }: { + options = { + meta.foo = mkOption { + type = types.listOf types.str; + }; + result = mkOption { default = lib.concatStringsSep " " config.meta.foo; }; + }; + }) + { + meta.foo = [ "one" "two" ]; + } + ]; +} diff --git a/tests/modules/submoduleFiles.nix b/tests/modules/submoduleFiles.nix new file mode 100644 index 000000000..c0d9b2cef --- /dev/null +++ b/tests/modules/submoduleFiles.nix @@ -0,0 +1,21 @@ +{ lib, ... }: { + options.submodule = lib.mkOption { + default = {}; + type = lib.types.submoduleWith { + modules = [ ({ options, ... }: { + options.value = lib.mkOption {}; + + options.internalFiles = lib.mkOption { + default = options.value.files; + }; + })]; + }; + }; + + imports = [ + { + _file = "the-file.nix"; + submodule.value = 10; + } + ]; +} diff --git a/tests/modules/types-anything/attrs-coercible.nix b/tests/modules/types-anything/attrs-coercible.nix new file mode 100644 index 000000000..085cbd638 --- /dev/null +++ b/tests/modules/types-anything/attrs-coercible.nix @@ -0,0 +1,12 @@ +{ lib, ... }: { + + options.value = lib.mkOption { + type = lib.types.anything; + }; + + config.value = { + outPath = "foo"; + err = throw "err"; + }; + +} diff --git a/tests/modules/types-anything/equal-atoms.nix b/tests/modules/types-anything/equal-atoms.nix new file mode 100644 index 000000000..972711201 --- /dev/null +++ b/tests/modules/types-anything/equal-atoms.nix @@ -0,0 +1,26 @@ +{ lib, ... }: { + + options.value = lib.mkOption { + type = lib.types.anything; + }; + + config = lib.mkMerge [ + { + value.int = 0; + value.bool = false; + value.string = ""; + value.path = /.; + value.null = null; + value.float = 0.1; + } + { + value.int = 0; + value.bool = false; + value.string = ""; + value.path = /.; + value.null = null; + value.float = 0.1; + } + ]; + +} diff --git a/tests/modules/types-anything/functions.nix b/tests/modules/types-anything/functions.nix new file mode 100644 index 000000000..21edd4aff --- /dev/null +++ b/tests/modules/types-anything/functions.nix @@ -0,0 +1,23 @@ +{ lib, config, ... }: { + + options.value = lib.mkOption { + type = lib.types.anything; + }; + + options.applied = lib.mkOption { + default = lib.mapAttrs (name: fun: fun null) config.value; + }; + + config = lib.mkMerge [ + { + value.single-lambda = x: x; + value.multiple-lambdas = x: { inherit x; }; + value.merging-lambdas = x: { inherit x; }; + } + { + value.multiple-lambdas = x: [ x ]; + value.merging-lambdas = y: { inherit y; }; + } + ]; + +} diff --git a/tests/modules/types-anything/lists.nix b/tests/modules/types-anything/lists.nix new file mode 100644 index 000000000..bd846afd3 --- /dev/null +++ b/tests/modules/types-anything/lists.nix @@ -0,0 +1,16 @@ +{ lib, ... }: { + + options.value = lib.mkOption { + type = lib.types.anything; + }; + + config = lib.mkMerge [ + { + value = [ null ]; + } + { + value = [ null ]; + } + ]; + +} diff --git a/tests/modules/types-anything/mk-mods.nix b/tests/modules/types-anything/mk-mods.nix new file mode 100644 index 000000000..f84ad01df --- /dev/null +++ b/tests/modules/types-anything/mk-mods.nix @@ -0,0 +1,44 @@ +{ lib, ... }: { + + options.value = lib.mkOption { + type = lib.types.anything; + }; + + config = lib.mkMerge [ + { + value.mkiffalse = lib.mkIf false {}; + } + { + value.mkiftrue = lib.mkIf true {}; + } + { + value.mkdefault = lib.mkDefault 0; + } + { + value.mkdefault = 1; + } + { + value.mkmerge = lib.mkMerge [ + {} + ]; + } + { + value.mkbefore = lib.mkBefore true; + } + { + value.nested = lib.mkMerge [ + { + foo = lib.mkDefault 0; + bar = lib.mkIf false 0; + } + (lib.mkIf true { + foo = lib.mkIf true (lib.mkForce 1); + bar = { + baz = lib.mkDefault "baz"; + }; + }) + ]; + } + ]; + +} diff --git a/tests/modules/types-anything/nested-attrs.nix b/tests/modules/types-anything/nested-attrs.nix new file mode 100644 index 000000000..e57d33ef8 --- /dev/null +++ b/tests/modules/types-anything/nested-attrs.nix @@ -0,0 +1,22 @@ +{ lib, ... }: { + + options.value = lib.mkOption { + type = lib.types.anything; + }; + + config = lib.mkMerge [ + { + value.foo = null; + } + { + value.l1.foo = null; + } + { + value.l1.l2.foo = null; + } + { + value.l1.l2.l3.foo = null; + } + ]; + +} diff --git a/tests/release.nix b/tests/release.nix new file mode 100644 index 000000000..c3bf58db2 --- /dev/null +++ b/tests/release.nix @@ -0,0 +1,64 @@ +{ # The pkgs used for dependencies for the testing itself + # Don't test properties of pkgs.lib, but rather the lib in the parent directory + pkgs ? import ../.. {} // { lib = throw "pkgs.lib accessed, but the lib tests should use nixpkgs' lib path directly!"; }, + nix ? pkgs.nix, + nixVersions ? [ pkgs.nixVersions.minimum nix pkgs.nixVersions.unstable ], +}: + +let + testWithNix = nix: + pkgs.runCommand "nixpkgs-lib-tests-nix-${nix.version}" { + buildInputs = [ + (import ./check-eval.nix) + (import ./maintainers.nix { + inherit pkgs; + lib = import ../.; + }) + (import ./teams.nix { + inherit pkgs; + lib = import ../.; + }) + (import ../path/tests { + inherit pkgs; + }) + ]; + nativeBuildInputs = [ + nix + ]; + strictDeps = true; + } '' + datadir="${nix}/share" + export TEST_ROOT=$(pwd)/test-tmp + export NIX_BUILD_HOOK= + export NIX_CONF_DIR=$TEST_ROOT/etc + export NIX_LOCALSTATE_DIR=$TEST_ROOT/var + export NIX_LOG_DIR=$TEST_ROOT/var/log/nix + export NIX_STATE_DIR=$TEST_ROOT/var/nix + export NIX_STORE_DIR=$TEST_ROOT/store + export PAGER=cat + cacheDir=$TEST_ROOT/binary-cache + + mkdir -p $NIX_CONF_DIR + echo "experimental-features = nix-command" >> $NIX_CONF_DIR/nix.conf + + nix-store --init + + cp -r ${../.} lib + echo "Running lib/tests/modules.sh" + bash lib/tests/modules.sh + + echo "Running lib/tests/filesystem.sh" + TEST_LIB=$PWD/lib bash lib/tests/filesystem.sh + + echo "Running lib/tests/sources.sh" + TEST_LIB=$PWD/lib bash lib/tests/sources.sh + + mkdir $out + echo success > $out/${nix.version} + ''; + +in + pkgs.symlinkJoin { + name = "nixpkgs-lib-tests"; + paths = map testWithNix nixVersions; + } diff --git a/tests/sources.sh b/tests/sources.sh new file mode 100755 index 000000000..cda77aa96 --- /dev/null +++ b/tests/sources.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail +shopt -s inherit_errexit + +# Use +# || die +die() { + echo >&2 "test case failed: " "$@" + exit 1 +} + +if test -n "${TEST_LIB:-}"; then + NIX_PATH=nixpkgs="$(dirname "$TEST_LIB")" +else + NIX_PATH=nixpkgs="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.."; pwd)" +fi +export NIX_PATH + +work="$(mktemp -d)" +clean_up() { + rm -rf "$work" +} +trap clean_up EXIT +cd "$work" + +# Crudely unquotes a JSON string by just taking everything between the first and the second quote. +# We're only using this for resulting /nix/store paths, which can't contain " anyways, +# nor can they contain any other characters that would need to be escaped specially in JSON +# This way we don't need to add a dependency on e.g. jq +crudeUnquoteJSON() { + cut -d \" -f2 +} + +touch {README.md,module.o,foo.bar} + +dir="$(nix-instantiate --eval --strict --read-write-mode --json --expr '(with import ; "${ + cleanSource ./. +}")' | crudeUnquoteJSON)" +(cd "$dir"; find) | sort -f | diff -U10 - <(cat <&2 tests ok diff --git a/tests/systems.nix b/tests/systems.nix new file mode 100644 index 000000000..2afe128c4 --- /dev/null +++ b/tests/systems.nix @@ -0,0 +1,42 @@ +# We assert that the new algorithmic way of generating these lists matches the +# way they were hard-coded before. +# +# One might think "if we exhaustively test, what's the point of procedurally +# calculating the lists anyway?". The answer is one can mindlessly update these +# tests as new platforms become supported, and then just give the diff a quick +# sanity check before committing :). +let + lib = import ../default.nix; + mseteq = x: y: { + expr = lib.sort lib.lessThan x; + expected = lib.sort lib.lessThan y; + }; +in +with lib.systems.doubles; lib.runTests { + testall = mseteq all (linux ++ darwin ++ freebsd ++ openbsd ++ netbsd ++ illumos ++ wasi ++ windows ++ embedded ++ mmix ++ js ++ genode ++ redox); + + testarm = mseteq arm [ "armv5tel-linux" "armv6l-linux" "armv6l-netbsd" "armv6l-none" "armv7a-linux" "armv7a-netbsd" "armv7l-linux" "armv7l-netbsd" "arm-none" "armv7a-darwin" ]; + testarmv7 = mseteq armv7 [ "armv7a-darwin" "armv7a-linux" "armv7l-linux" "armv7a-netbsd" "armv7l-netbsd" ]; + testi686 = mseteq i686 [ "i686-linux" "i686-freebsd13" "i686-genode" "i686-netbsd" "i686-openbsd" "i686-cygwin" "i686-windows" "i686-none" "i686-darwin" ]; + testmips = mseteq mips [ "mips-linux" "mips64-linux" "mips64el-linux" "mipsel-linux" "mipsel-netbsd" ]; + testmmix = mseteq mmix [ "mmix-mmixware" ]; + testpower = mseteq power [ "powerpc-netbsd" "powerpc-none" "powerpc64-linux" "powerpc64le-linux" "powerpcle-none" ]; + testriscv = mseteq riscv [ "riscv32-linux" "riscv64-linux" "riscv32-netbsd" "riscv64-netbsd" "riscv32-none" "riscv64-none" ]; + testriscv32 = mseteq riscv32 [ "riscv32-linux" "riscv32-netbsd" "riscv32-none" ]; + testriscv64 = mseteq riscv64 [ "riscv64-linux" "riscv64-netbsd" "riscv64-none" ]; + tests390x = mseteq s390x [ "s390x-linux" "s390x-none" ]; + testx86_64 = mseteq x86_64 [ "x86_64-linux" "x86_64-darwin" "x86_64-freebsd13" "x86_64-genode" "x86_64-redox" "x86_64-openbsd" "x86_64-netbsd" "x86_64-cygwin" "x86_64-solaris" "x86_64-windows" "x86_64-none" ]; + + testcygwin = mseteq cygwin [ "i686-cygwin" "x86_64-cygwin" ]; + testdarwin = mseteq darwin [ "x86_64-darwin" "i686-darwin" "aarch64-darwin" "armv7a-darwin" ]; + testfreebsd = mseteq freebsd [ "i686-freebsd13" "x86_64-freebsd13" ]; + testgenode = mseteq genode [ "aarch64-genode" "i686-genode" "x86_64-genode" ]; + testredox = mseteq redox [ "x86_64-redox" ]; + testgnu = mseteq gnu (linux /* ++ kfreebsd ++ ... */); + testillumos = mseteq illumos [ "x86_64-solaris" ]; + testlinux = mseteq linux [ "aarch64-linux" "armv5tel-linux" "armv6l-linux" "armv7a-linux" "armv7l-linux" "i686-linux" "loongarch64-linux" "m68k-linux" "microblaze-linux" "microblazeel-linux" "mips-linux" "mips64-linux" "mips64el-linux" "mipsel-linux" "powerpc64-linux" "powerpc64le-linux" "riscv32-linux" "riscv64-linux" "s390-linux" "s390x-linux" "x86_64-linux" ]; + testnetbsd = mseteq netbsd [ "aarch64-netbsd" "armv6l-netbsd" "armv7a-netbsd" "armv7l-netbsd" "i686-netbsd" "m68k-netbsd" "mipsel-netbsd" "powerpc-netbsd" "riscv32-netbsd" "riscv64-netbsd" "x86_64-netbsd" ]; + testopenbsd = mseteq openbsd [ "i686-openbsd" "x86_64-openbsd" ]; + testwindows = mseteq windows [ "i686-cygwin" "x86_64-cygwin" "i686-windows" "x86_64-windows" ]; + testunix = mseteq unix (linux ++ darwin ++ freebsd ++ openbsd ++ netbsd ++ illumos ++ cygwin ++ redox); +} diff --git a/tests/teams.nix b/tests/teams.nix new file mode 100644 index 000000000..8a0a5d272 --- /dev/null +++ b/tests/teams.nix @@ -0,0 +1,50 @@ +# to run these tests: +# nix-build nixpkgs/lib/tests/teams.nix +# If it builds, all tests passed +{ pkgs ? import ../.. {}, lib ? pkgs.lib }: + +let + inherit (lib) types; + + teamModule = { config, ... }: { + options = { + shortName = lib.mkOption { + type = types.str; + }; + scope = lib.mkOption { + type = types.str; + }; + enableFeatureFreezePing = lib.mkOption { + type = types.bool; + default = false; + }; + members = lib.mkOption { + type = types.listOf (types.submodule + (import ./maintainer-module.nix { inherit lib; }) + ); + default = []; + }; + githubTeams = lib.mkOption { + type = types.listOf types.str; + default = []; + }; + }; + }; + + checkTeam = team: uncheckedAttrs: + let + prefix = [ "lib" "maintainer-team" team ]; + checkedAttrs = (lib.modules.evalModules { + inherit prefix; + modules = [ + teamModule + { + _file = toString ../../maintainers/team-list.nix; + config = uncheckedAttrs; + } + ]; + }).config; + in checkedAttrs; + + checkedTeams = lib.mapAttrs checkTeam lib.teams; +in pkgs.writeTextDir "maintainer-teams.json" (builtins.toJSON checkedTeams) diff --git a/tests/test-to-plist-expected.plist b/tests/test-to-plist-expected.plist new file mode 100644 index 000000000..df0528a60 --- /dev/null +++ b/tests/test-to-plist-expected.plist @@ -0,0 +1,46 @@ + + + + + nested + + values + + attrs + + foo b/ar + baz + + bool + + emptyattrs + + + + emptylist + + + + emptystring + + float + 0.133700 + int + 42 + list + + 3 + 4 + test + + newlinestring + + + path + /foo + string + fn${o}"r\d + + + + \ No newline at end of file diff --git a/trivial.nix b/trivial.nix new file mode 100644 index 000000000..26e4b3240 --- /dev/null +++ b/trivial.nix @@ -0,0 +1,522 @@ +{ lib }: + +rec { + + ## Simple (higher order) functions + + /* The identity function + For when you need a function that does “nothing”. + + Type: id :: a -> a + */ + id = + # The value to return + x: x; + + /* The constant function + + Ignores the second argument. If called with only one argument, + constructs a function that always returns a static value. + + Type: const :: a -> b -> a + Example: + let f = const 5; in f 10 + => 5 + */ + const = + # Value to return + x: + # Value to ignore + y: x; + + /* Pipes a value through a list of functions, left to right. + + Type: pipe :: a -> [] -> + Example: + pipe 2 [ + (x: x + 2) # 2 + 2 = 4 + (x: x * 2) # 4 * 2 = 8 + ] + => 8 + + # ideal to do text transformations + pipe [ "a/b" "a/c" ] [ + + # create the cp command + (map (file: ''cp "${src}/${file}" $out\n'')) + + # concatenate all commands into one string + lib.concatStrings + + # make that string into a nix derivation + (pkgs.runCommand "copy-to-out" {}) + + ] + => + + The output type of each function has to be the input type + of the next function, and the last function returns the + final value. + */ + pipe = val: functions: + let reverseApply = x: f: f x; + in builtins.foldl' reverseApply val functions; + + # note please don’t add a function like `compose = flip pipe`. + # This would confuse users, because the order of the functions + # in the list is not clear. With pipe, it’s obvious that it + # goes first-to-last. With `compose`, not so much. + + ## Named versions corresponding to some builtin operators. + + /* Concatenate two lists + + Type: concat :: [a] -> [a] -> [a] + + Example: + concat [ 1 2 ] [ 3 4 ] + => [ 1 2 3 4 ] + */ + concat = x: y: x ++ y; + + /* boolean “or” */ + or = x: y: x || y; + + /* boolean “and” */ + and = x: y: x && y; + + /* bitwise “and” */ + bitAnd = builtins.bitAnd + or (import ./zip-int-bits.nix + (a: b: if a==1 && b==1 then 1 else 0)); + + /* bitwise “or” */ + bitOr = builtins.bitOr + or (import ./zip-int-bits.nix + (a: b: if a==1 || b==1 then 1 else 0)); + + /* bitwise “xor” */ + bitXor = builtins.bitXor + or (import ./zip-int-bits.nix + (a: b: if a!=b then 1 else 0)); + + /* bitwise “not” */ + bitNot = builtins.sub (-1); + + /* Convert a boolean to a string. + + This function uses the strings "true" and "false" to represent + boolean values. Calling `toString` on a bool instead returns "1" + and "" (sic!). + + Type: boolToString :: bool -> string + */ + boolToString = b: if b then "true" else "false"; + + /* Merge two attribute sets shallowly, right side trumps left + + mergeAttrs :: attrs -> attrs -> attrs + + Example: + mergeAttrs { a = 1; b = 2; } { b = 3; c = 4; } + => { a = 1; b = 3; c = 4; } + */ + mergeAttrs = + # Left attribute set + x: + # Right attribute set (higher precedence for equal keys) + y: x // y; + + /* Flip the order of the arguments of a binary function. + + Type: flip :: (a -> b -> c) -> (b -> a -> c) + + Example: + flip concat [1] [2] + => [ 2 1 ] + */ + flip = f: a: b: f b a; + + /* Apply function if the supplied argument is non-null. + + Example: + mapNullable (x: x+1) null + => null + mapNullable (x: x+1) 22 + => 23 + */ + mapNullable = + # Function to call + f: + # Argument to check for null before passing it to `f` + a: if a == null then a else f a; + + # Pull in some builtins not included elsewhere. + inherit (builtins) + pathExists readFile isBool + isInt isFloat add sub lessThan + seq deepSeq genericClosure; + + + ## nixpkgs version strings + + /* Returns the current full nixpkgs version number. */ + version = release + versionSuffix; + + /* Returns the current nixpkgs release number as string. */ + release = lib.strings.fileContents ../.version; + + /* The latest release that is supported, at the time of release branch-off, + if applicable. + + Ideally, out-of-tree modules should be able to evaluate cleanly with all + supported Nixpkgs versions (master, release and old release until EOL). + So if possible, deprecation warnings should take effect only when all + out-of-tree expressions/libs/modules can upgrade to the new way without + losing support for supported Nixpkgs versions. + + This release number allows deprecation warnings to be implemented such that + they take effect as soon as the oldest release reaches end of life. */ + oldestSupportedRelease = + # Update on master only. Do not backport. + 2211; + + /* Whether a feature is supported in all supported releases (at the time of + release branch-off, if applicable). See `oldestSupportedRelease`. */ + isInOldestRelease = + /* Release number of feature introduction as an integer, e.g. 2111 for 21.11. + Set it to the upcoming release, matching the nixpkgs/.version file. + */ + release: + release <= lib.trivial.oldestSupportedRelease; + + /* Returns the current nixpkgs release code name. + + On each release the first letter is bumped and a new animal is chosen + starting with that new letter. + */ + codeName = "Tapir"; + + /* Returns the current nixpkgs version suffix as string. */ + versionSuffix = + let suffixFile = ../.version-suffix; + in if pathExists suffixFile + then lib.strings.fileContents suffixFile + else "pre-git"; + + /* Attempts to return the the current revision of nixpkgs and + returns the supplied default value otherwise. + + Type: revisionWithDefault :: string -> string + */ + revisionWithDefault = + # Default value to return if revision can not be determined + default: + let + revisionFile = "${toString ./..}/.git-revision"; + gitRepo = "${toString ./..}/.git"; + in if lib.pathIsGitRepo gitRepo + then lib.commitIdFromGitRepo gitRepo + else if lib.pathExists revisionFile then lib.fileContents revisionFile + else default; + + nixpkgsVersion = builtins.trace "`lib.nixpkgsVersion` is deprecated, use `lib.version` instead!" version; + + /* Determine whether the function is being called from inside a Nix + shell. + + Type: inNixShell :: bool + */ + inNixShell = builtins.getEnv "IN_NIX_SHELL" != ""; + + /* Determine whether the function is being called from inside pure-eval mode + by seeing whether `builtins` contains `currentSystem`. If not, we must be in + pure-eval mode. + + Type: inPureEvalMode :: bool + */ + inPureEvalMode = ! builtins ? currentSystem; + + ## Integer operations + + /* Return minimum of two numbers. */ + min = x: y: if x < y then x else y; + + /* Return maximum of two numbers. */ + max = x: y: if x > y then x else y; + + /* Integer modulus + + Example: + mod 11 10 + => 1 + mod 1 10 + => 1 + */ + mod = base: int: base - (int * (builtins.div base int)); + + + ## Comparisons + + /* C-style comparisons + + a < b, compare a b => -1 + a == b, compare a b => 0 + a > b, compare a b => 1 + */ + compare = a: b: + if a < b + then -1 + else if a > b + then 1 + else 0; + + /* Split type into two subtypes by predicate `p`, take all elements + of the first subtype to be less than all the elements of the + second subtype, compare elements of a single subtype with `yes` + and `no` respectively. + + Type: (a -> bool) -> (a -> a -> int) -> (a -> a -> int) -> (a -> a -> int) + + Example: + let cmp = splitByAndCompare (hasPrefix "foo") compare compare; in + + cmp "a" "z" => -1 + cmp "fooa" "fooz" => -1 + + cmp "f" "a" => 1 + cmp "fooa" "a" => -1 + # while + compare "fooa" "a" => 1 + */ + splitByAndCompare = + # Predicate + p: + # Comparison function if predicate holds for both values + yes: + # Comparison function if predicate holds for neither value + no: + # First value to compare + a: + # Second value to compare + b: + if p a + then if p b then yes a b else -1 + else if p b then 1 else no a b; + + + /* Reads a JSON file. + + Type :: path -> any + */ + importJSON = path: + builtins.fromJSON (builtins.readFile path); + + /* Reads a TOML file. + + Type :: path -> any + */ + importTOML = path: + builtins.fromTOML (builtins.readFile path); + + ## Warnings + + # See https://github.com/NixOS/nix/issues/749. Eventually we'd like these + # to expand to Nix builtins that carry metadata so that Nix can filter out + # the INFO messages without parsing the message string. + # + # Usage: + # { + # foo = lib.warn "foo is deprecated" oldFoo; + # bar = lib.warnIf (bar == "") "Empty bar is deprecated" bar; + # } + # + # TODO: figure out a clever way to integrate location information from + # something like __unsafeGetAttrPos. + + /* + Print a warning before returning the second argument. This function behaves + like `builtins.trace`, but requires a string message and formats it as a + warning, including the `warning: ` prefix. + + To get a call stack trace and abort evaluation, set the environment variable + `NIX_ABORT_ON_WARN=true` and set the Nix options `--option pure-eval false --show-trace` + + Type: string -> a -> a + */ + warn = + if lib.elem (builtins.getEnv "NIX_ABORT_ON_WARN") ["1" "true" "yes"] + then msg: builtins.trace "warning: ${msg}" (abort "NIX_ABORT_ON_WARN=true; warnings are treated as unrecoverable errors.") + else msg: builtins.trace "warning: ${msg}"; + + /* + Like warn, but only warn when the first argument is `true`. + + Type: bool -> string -> a -> a + */ + warnIf = cond: msg: if cond then warn msg else x: x; + + /* + Like warnIf, but negated (warn if the first argument is `false`). + + Type: bool -> string -> a -> a + */ + warnIfNot = cond: msg: if cond then x: x else warn msg; + + /* + Like the `assert b; e` expression, but with a custom error message and + without the semicolon. + + If true, return the identity function, `r: r`. + + If false, throw the error message. + + Calls can be juxtaposed using function application, as `(r: r) a = a`, so + `(r: r) (r: r) a = a`, and so forth. + + Type: bool -> string -> a -> a + + Example: + + throwIfNot (lib.isList overlays) "The overlays argument to nixpkgs must be a list." + lib.foldr (x: throwIfNot (lib.isFunction x) "All overlays passed to nixpkgs must be functions.") (r: r) overlays + pkgs + + */ + throwIfNot = cond: msg: if cond then x: x else throw msg; + + /* + Like throwIfNot, but negated (throw if the first argument is `true`). + + Type: bool -> string -> a -> a + */ + throwIf = cond: msg: if cond then throw msg else x: x; + + /* Check if the elements in a list are valid values from a enum, returning the identity function, or throwing an error message otherwise. + + Example: + let colorVariants = ["bright" "dark" "black"] + in checkListOfEnum "color variants" [ "standard" "light" "dark" ] colorVariants; + => + error: color variants: bright, black unexpected; valid ones: standard, light, dark + + Type: String -> List ComparableVal -> List ComparableVal -> a -> a + */ + checkListOfEnum = msg: valid: given: + let + unexpected = lib.subtractLists valid given; + in + lib.throwIfNot (unexpected == []) + "${msg}: ${builtins.concatStringsSep ", " (builtins.map builtins.toString unexpected)} unexpected; valid ones: ${builtins.concatStringsSep ", " (builtins.map builtins.toString valid)}"; + + info = msg: builtins.trace "INFO: ${msg}"; + + showWarnings = warnings: res: lib.foldr (w: x: warn w x) res warnings; + + ## Function annotations + + /* Add metadata about expected function arguments to a function. + The metadata should match the format given by + builtins.functionArgs, i.e. a set from expected argument to a bool + representing whether that argument has a default or not. + setFunctionArgs : (a → b) → Map String Bool → (a → b) + + This function is necessary because you can't dynamically create a + function of the { a, b ? foo, ... }: format, but some facilities + like callPackage expect to be able to query expected arguments. + */ + setFunctionArgs = f: args: + { # TODO: Should we add call-time "type" checking like built in? + __functor = self: f; + __functionArgs = args; + }; + + /* Extract the expected function arguments from a function. + This works both with nix-native { a, b ? foo, ... }: style + functions and functions with args set with 'setFunctionArgs'. It + has the same return type and semantics as builtins.functionArgs. + setFunctionArgs : (a → b) → Map String Bool. + */ + functionArgs = f: + if f ? __functor + then f.__functionArgs or (lib.functionArgs (f.__functor f)) + else builtins.functionArgs f; + + /* Check whether something is a function or something + annotated with function args. + */ + isFunction = f: builtins.isFunction f || + (f ? __functor && isFunction (f.__functor f)); + + /* + Turns any non-callable values into constant functions. + Returns callable values as is. + + Example: + + nix-repl> lib.toFunction 1 2 + 1 + + nix-repl> lib.toFunction (x: x + 1) 2 + 3 + */ + toFunction = + # Any value + v: + if isFunction v + then v + else k: v; + + /* Convert the given positive integer to a string of its hexadecimal + representation. For example: + + toHexString 0 => "0" + + toHexString 16 => "10" + + toHexString 250 => "FA" + */ + toHexString = i: + let + toHexDigit = d: + if d < 10 + then toString d + else + { + "10" = "A"; + "11" = "B"; + "12" = "C"; + "13" = "D"; + "14" = "E"; + "15" = "F"; + }.${toString d}; + in + lib.concatMapStrings toHexDigit (toBaseDigits 16 i); + + /* `toBaseDigits base i` converts the positive integer i to a list of its + digits in the given base. For example: + + toBaseDigits 10 123 => [ 1 2 3 ] + + toBaseDigits 2 6 => [ 1 1 0 ] + + toBaseDigits 16 250 => [ 15 10 ] + */ + toBaseDigits = base: i: + let + go = i: + if i < base + then [i] + else + let + r = i - ((i / base) * base); + q = (i - r) / base; + in + [r] ++ go q; + in + assert (isInt base); + assert (isInt i); + assert (base >= 2); + assert (i >= 0); + lib.reverseList (go i); +} diff --git a/types.nix b/types.nix new file mode 100644 index 000000000..9360d42f5 --- /dev/null +++ b/types.nix @@ -0,0 +1,908 @@ +# Definitions related to run-time type checking. Used in particular +# to type-check NixOS configurations. +{ lib }: + +let + inherit (lib) + elem + flip + isAttrs + isBool + isDerivation + isFloat + isFunction + isInt + isList + isString + isStorePath + toDerivation + toList + ; + inherit (lib.lists) + all + concatLists + count + elemAt + filter + foldl' + head + imap1 + last + length + tail + ; + inherit (lib.attrsets) + attrNames + filterAttrs + hasAttr + mapAttrs + optionalAttrs + zipAttrsWith + ; + inherit (lib.options) + getFiles + getValues + mergeDefaultOption + mergeEqualOption + mergeOneOption + mergeUniqueOption + showFiles + showOption + ; + inherit (lib.strings) + concatMapStringsSep + concatStringsSep + escapeNixString + hasInfix + isStringLike + ; + inherit (lib.trivial) + boolToString + ; + + inherit (lib.modules) + mergeDefinitions + fixupOptionType + mergeOptionDecls + ; + outer_types = +rec { + isType = type: x: (x._type or "") == type; + + setType = typeName: value: value // { + _type = typeName; + }; + + + # Default type merging function + # takes two type functors and return the merged type + defaultTypeMerge = f: f': + let wrapped = f.wrapped.typeMerge f'.wrapped.functor; + payload = f.binOp f.payload f'.payload; + in + # cannot merge different types + if f.name != f'.name + then null + # simple types + else if (f.wrapped == null && f'.wrapped == null) + && (f.payload == null && f'.payload == null) + then f.type + # composed types + else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null) + then f.type wrapped + # value types + else if (f.payload != null && f'.payload != null) && (payload != null) + then f.type payload + else null; + + # Default type functor + defaultFunctor = name: { + inherit name; + type = types.${name} or null; + wrapped = null; + payload = null; + binOp = a: b: null; + }; + + isOptionType = isType "option-type"; + mkOptionType = + { # Human-readable representation of the type, should be equivalent to + # the type function name. + name + , # Description of the type, defined recursively by embedding the wrapped type if any. + description ? null + # A hint for whether or not this description needs parentheses. Possible values: + # - "noun": a simple noun phrase such as "positive integer" + # - "conjunction": a phrase with a potentially ambiguous "or" connective. + # - "composite": a phrase with an "of" connective + # See the `optionDescriptionPhrase` function. + , descriptionClass ? null + , # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING! + # Function applied to each definition that must return false when a definition + # does not match the type. It should not check more than the root of the value, + # because checking nested values reduces laziness, leading to unnecessary + # infinite recursions in the module system. + # Further checks of nested values should be performed by throwing in + # the merge function. + # Strict and deep type checking can be performed by calling lib.deepSeq on + # the merged value. + # + # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change, + # https://github.com/NixOS/nixpkgs/pull/173568 and + # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this, + # https://github.com/NixOS/nixpkgs/issues/191124 and + # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore + # this disclaimer. + check ? (x: true) + , # Merge a list of definitions together into a single value. + # This function is called with two arguments: the location of + # the option in the configuration as a list of strings + # (e.g. ["boot" "loader "grub" "enable"]), and a list of + # definition values and locations (e.g. [ { file = "/foo.nix"; + # value = 1; } { file = "/bar.nix"; value = 2 } ]). + merge ? mergeDefaultOption + , # Whether this type has a value representing nothingness. If it does, + # this should be a value of the form { value = ; } + # If it doesn't, this should be {} + # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`. + emptyValue ? {} + , # Return a flat list of sub-options. Used to generate + # documentation. + getSubOptions ? prefix: {} + , # List of modules if any, or null if none. + getSubModules ? null + , # Function for building the same option type with a different list of + # modules. + substSubModules ? m: null + , # Function that merge type declarations. + # internal, takes a functor as argument and returns the merged type. + # returning null means the type is not mergeable + typeMerge ? defaultTypeMerge functor + , # The type functor. + # internal, representation of the type as an attribute set. + # name: name of the type + # type: type function. + # wrapped: the type wrapped in case of compound types. + # payload: values of the type, two payloads of the same type must be + # combinable with the binOp binary operation. + # binOp: binary operation that merge two payloads of the same type. + functor ? defaultFunctor name + , # The deprecation message to display when this type is used by an option + # If null, the type isn't deprecated + deprecationMessage ? null + , # The types that occur in the definition of this type. This is used to + # issue deprecation warnings recursively. Can also be used to reuse + # nested types + nestedTypes ? {} + }: + { _type = "option-type"; + inherit + name check merge emptyValue getSubOptions getSubModules substSubModules + typeMerge functor deprecationMessage nestedTypes descriptionClass; + description = if description == null then name else description; + }; + + # optionDescriptionPhrase :: (str -> bool) -> optionType -> str + # + # Helper function for producing unambiguous but readable natural language + # descriptions of types. + # + # Parameters + # + # optionDescriptionPhase unparenthesize optionType + # + # `unparenthesize`: A function from descriptionClass string to boolean. + # It must return true when the class of phrase will fit unambiguously into + # the description of the caller. + # + # `optionType`: The option type to parenthesize or not. + # The option whose description we're returning. + # + # Return value + # + # The description of the `optionType`, with parentheses if there may be an + # ambiguity. + optionDescriptionPhrase = unparenthesize: t: + if unparenthesize (t.descriptionClass or null) + then t.description + else "(${t.description})"; + + # When adding new types don't forget to document them in + # nixos/doc/manual/development/option-types.xml! + types = rec { + + raw = mkOptionType rec { + name = "raw"; + description = "raw value"; + descriptionClass = "noun"; + check = value: true; + merge = mergeOneOption; + }; + + anything = mkOptionType { + name = "anything"; + description = "anything"; + descriptionClass = "noun"; + check = value: true; + merge = loc: defs: + let + getType = value: + if isAttrs value && isStringLike value + then "stringCoercibleSet" + else builtins.typeOf value; + + # Returns the common type of all definitions, throws an error if they + # don't have the same type + commonType = foldl' (type: def: + if getType def.value == type + then type + else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}" + ) (getType (head defs).value) defs; + + mergeFunction = { + # Recursively merge attribute sets + set = (attrsOf anything).merge; + # Safe and deterministic behavior for lists is to only accept one definition + # listOf only used to apply mkIf and co. + list = + if length defs > 1 + then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}." + else (listOf anything).merge; + # This is the type of packages, only accept a single definition + stringCoercibleSet = mergeOneOption; + lambda = loc: defs: arg: anything.merge + (loc ++ [ "" ]) + (map (def: { + file = def.file; + value = def.value arg; + }) defs); + # Otherwise fall back to only allowing all equal definitions + }.${commonType} or mergeEqualOption; + in mergeFunction loc defs; + }; + + unspecified = mkOptionType { + name = "unspecified"; + description = "unspecified value"; + descriptionClass = "noun"; + }; + + bool = mkOptionType { + name = "bool"; + description = "boolean"; + descriptionClass = "noun"; + check = isBool; + merge = mergeEqualOption; + }; + + int = mkOptionType { + name = "int"; + description = "signed integer"; + descriptionClass = "noun"; + check = isInt; + merge = mergeEqualOption; + }; + + # Specialized subdomains of int + ints = + let + betweenDesc = lowest: highest: + "${toString lowest} and ${toString highest} (both inclusive)"; + between = lowest: highest: + assert lib.assertMsg (lowest <= highest) + "ints.between: lowest must be smaller than highest"; + addCheck int (x: x >= lowest && x <= highest) // { + name = "intBetween"; + description = "integer between ${betweenDesc lowest highest}"; + }; + ign = lowest: highest: name: docStart: + between lowest highest // { + inherit name; + description = docStart + "; between ${betweenDesc lowest highest}"; + }; + unsign = bit: range: ign 0 (range - 1) + "unsignedInt${toString bit}" "${toString bit} bit unsigned integer"; + sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1) + "signedInt${toString bit}" "${toString bit} bit signed integer"; + + in { + /* An int with a fixed range. + * + * Example: + * (ints.between 0 100).check (-1) + * => false + * (ints.between 0 100).check (101) + * => false + * (ints.between 0 0).check 0 + * => true + */ + inherit between; + + unsigned = addCheck types.int (x: x >= 0) // { + name = "unsignedInt"; + description = "unsigned integer, meaning >=0"; + }; + positive = addCheck types.int (x: x > 0) // { + name = "positiveInt"; + description = "positive integer, meaning >0"; + }; + u8 = unsign 8 256; + u16 = unsign 16 65536; + # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808) + # the smallest int Nix accepts is -2^63 (-9223372036854775807) + u32 = unsign 32 4294967296; + # u64 = unsign 64 18446744073709551616; + + s8 = sign 8 256; + s16 = sign 16 65536; + s32 = sign 32 4294967296; + }; + + # Alias of u16 for a port number + port = ints.u16; + + float = mkOptionType { + name = "float"; + description = "floating point number"; + descriptionClass = "noun"; + check = isFloat; + merge = mergeEqualOption; + }; + + number = either int float; + + numbers = let + betweenDesc = lowest: highest: + "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)"; + in { + between = lowest: highest: + assert lib.assertMsg (lowest <= highest) + "numbers.between: lowest must be smaller than highest"; + addCheck number (x: x >= lowest && x <= highest) // { + name = "numberBetween"; + description = "integer or floating point number between ${betweenDesc lowest highest}"; + }; + + nonnegative = addCheck number (x: x >= 0) // { + name = "numberNonnegative"; + description = "nonnegative integer or floating point number, meaning >=0"; + }; + positive = addCheck number (x: x > 0) // { + name = "numberPositive"; + description = "positive integer or floating point number, meaning >0"; + }; + }; + + str = mkOptionType { + name = "str"; + description = "string"; + descriptionClass = "noun"; + check = isString; + merge = mergeEqualOption; + }; + + nonEmptyStr = mkOptionType { + name = "nonEmptyStr"; + description = "non-empty string"; + descriptionClass = "noun"; + check = x: str.check x && builtins.match "[ \t\n]*" x == null; + inherit (str) merge; + }; + + # Allow a newline character at the end and trim it in the merge function. + singleLineStr = + let + inherit (strMatching "[^\n\r]*\n?") check merge; + in + mkOptionType { + name = "singleLineStr"; + description = "(optionally newline-terminated) single-line string"; + descriptionClass = "noun"; + inherit check; + merge = loc: defs: + lib.removeSuffix "\n" (merge loc defs); + }; + + strMatching = pattern: mkOptionType { + name = "strMatching ${escapeNixString pattern}"; + description = "string matching the pattern ${pattern}"; + descriptionClass = "noun"; + check = x: str.check x && builtins.match pattern x != null; + inherit (str) merge; + }; + + # Merge multiple definitions by concatenating them (with the given + # separator between the values). + separatedString = sep: mkOptionType rec { + name = "separatedString"; + description = if sep == "" + then "Concatenated string" # for types.string. + else "strings concatenated with ${builtins.toJSON sep}" + ; + descriptionClass = "noun"; + check = isString; + merge = loc: defs: concatStringsSep sep (getValues defs); + functor = (defaultFunctor name) // { + payload = sep; + binOp = sepLhs: sepRhs: + if sepLhs == sepRhs then sepLhs + else null; + }; + }; + + lines = separatedString "\n"; + commas = separatedString ","; + envVar = separatedString ":"; + + # Deprecated; should not be used because it quietly concatenates + # strings, which is usually not what you want. + string = separatedString "" // { + name = "string"; + deprecationMessage = "See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."; + }; + + passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // { + name = "passwdEntry ${entryType.name}"; + description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons"; + }; + + attrs = mkOptionType { + name = "attrs"; + description = "attribute set"; + check = isAttrs; + merge = loc: foldl' (res: def: res // def.value) {}; + emptyValue = { value = {}; }; + }; + + # A package is a top-level store path (/nix/store/hash-name). This includes: + # - derivations + # - more generally, attribute sets with an `outPath` or `__toString` attribute + # pointing to a store path, e.g. flake inputs + # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo) + # - hardcoded store path literals (/nix/store/hash-foo) or strings without context + # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath. + package = mkOptionType { + name = "package"; + descriptionClass = "noun"; + check = x: isDerivation x || isStorePath x; + merge = loc: defs: + let res = mergeOneOption loc defs; + in if builtins.isPath res || (builtins.isString res && ! builtins.hasContext res) + then toDerivation res + else res; + }; + + shellPackage = package // { + check = x: isDerivation x && hasAttr "shellPath" x; + }; + + pkgs = addCheck + (unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs // { + name = "pkgs"; + descriptionClass = "noun"; + description = "Nixpkgs package set"; + }) + (x: (x._type or null) == "pkgs"); + + path = mkOptionType { + name = "path"; + descriptionClass = "noun"; + check = x: isStringLike x && builtins.substring 0 1 (toString x) == "/"; + merge = mergeEqualOption; + }; + + listOf = elemType: mkOptionType rec { + name = "listOf"; + description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + descriptionClass = "composite"; + check = isList; + merge = loc: defs: + map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: + imap1 (m: def': + (mergeDefinitions + (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) + elemType + [{ inherit (def) file; value = def'; }] + ).optionalValue + ) def.value + ) defs))); + emptyValue = { value = []; }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); + getSubModules = elemType.getSubModules; + substSubModules = m: listOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + nonEmptyListOf = elemType: + let list = addCheck (types.listOf elemType) (l: l != []); + in list // { + description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}"; + emptyValue = { }; # no .value attr, meaning unset + }; + + attrsOf = elemType: mkOptionType rec { + name = "attrsOf"; + description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + descriptionClass = "composite"; + check = isAttrs; + merge = loc: defs: + mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: + (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue + ) + # Push down position info. + (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs))); + emptyValue = { value = {}; }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); + getSubModules = elemType.getSubModules; + substSubModules = m: attrsOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + # A version of attrsOf that's lazy in its values at the expense of + # conditional definitions not working properly. E.g. defining a value with + # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with + # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an + # error that it's not defined. Use only if conditional definitions don't make sense. + lazyAttrsOf = elemType: mkOptionType rec { + name = "lazyAttrsOf"; + description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + descriptionClass = "composite"; + check = isAttrs; + merge = loc: defs: + zipAttrsWith (name: defs: + let merged = mergeDefinitions (loc ++ [name]) elemType defs; + # mergedValue will trigger an appropriate error when accessed + in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue + ) + # Push down position info. + (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs); + emptyValue = { value = {}; }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); + getSubModules = elemType.getSubModules; + substSubModules = m: lazyAttrsOf (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + # TODO: deprecate this in the future: + loaOf = elemType: types.attrsOf elemType // { + name = "loaOf"; + deprecationMessage = "Mixing lists with attribute values is no longer" + + " possible; please use `types.attrsOf` instead. See" + + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation."; + nestedTypes.elemType = elemType; + }; + + # Value of given type but with no merging (i.e. `uniq list`s are not concatenated). + uniq = elemType: mkOptionType rec { + name = "uniq"; + inherit (elemType) description descriptionClass check; + merge = mergeOneOption; + emptyValue = elemType.emptyValue; + getSubOptions = elemType.getSubOptions; + getSubModules = elemType.getSubModules; + substSubModules = m: uniq (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + unique = { message }: type: mkOptionType rec { + name = "unique"; + inherit (type) description descriptionClass check; + merge = mergeUniqueOption { inherit message; }; + emptyValue = type.emptyValue; + getSubOptions = type.getSubOptions; + getSubModules = type.getSubModules; + substSubModules = m: uniq (type.substSubModules m); + functor = (defaultFunctor name) // { wrapped = type; }; + nestedTypes.elemType = type; + }; + + # Null or value of ... + nullOr = elemType: mkOptionType rec { + name = "nullOr"; + description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}"; + descriptionClass = "conjunction"; + check = x: x == null || elemType.check x; + merge = loc: defs: + let nrNulls = count (def: def.value == null) defs; in + if nrNulls == length defs then null + else if nrNulls != 0 then + throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}." + else elemType.merge loc defs; + emptyValue = { value = null; }; + getSubOptions = elemType.getSubOptions; + getSubModules = elemType.getSubModules; + substSubModules = m: nullOr (elemType.substSubModules m); + functor = (defaultFunctor name) // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + functionTo = elemType: mkOptionType { + name = "functionTo"; + description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + descriptionClass = "composite"; + check = isFunction; + merge = loc: defs: + fnArgs: (mergeDefinitions (loc ++ [ "" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "" ]); + getSubModules = elemType.getSubModules; + substSubModules = m: functionTo (elemType.substSubModules m); + functor = (defaultFunctor "functionTo") // { wrapped = elemType; }; + nestedTypes.elemType = elemType; + }; + + # A submodule (like typed attribute set). See NixOS manual. + submodule = modules: submoduleWith { + shorthandOnlyDefinesConfig = true; + modules = toList modules; + }; + + # A module to be imported in some other part of the configuration. + deferredModule = deferredModuleWith { }; + + # A module to be imported in some other part of the configuration. + # `staticModules`' options will be added to the documentation, unlike + # options declared via `config`. + deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType { + name = "deferredModule"; + description = "module"; + descriptionClass = "noun"; + check = x: isAttrs x || isFunction x || path.check x; + merge = loc: defs: { + imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs; + }; + inherit (submoduleWith { modules = staticModules; }) + getSubOptions + getSubModules; + substSubModules = m: deferredModuleWith (attrs // { + staticModules = m; + }); + functor = defaultFunctor "deferredModuleWith" // { + type = types.deferredModuleWith; + payload = { + inherit staticModules; + }; + binOp = lhs: rhs: { + staticModules = lhs.staticModules ++ rhs.staticModules; + }; + }; + }; + + # The type of a type! + optionType = mkOptionType { + name = "optionType"; + description = "optionType"; + descriptionClass = "noun"; + check = value: value._type or null == "option-type"; + merge = loc: defs: + if length defs == 1 + then (head defs).value + else let + # Prepares the type definitions for mergeOptionDecls, which + # annotates submodules types with file locations + optionModules = map ({ value, file }: + { + _file = file; + # There's no way to merge types directly from the module system, + # but we can cheat a bit by just declaring an option with the type + options = lib.mkOption { + type = value; + }; + } + ) defs; + # Merges all the types into a single one, including submodule merging. + # This also propagates file information to all submodules + mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules); + in mergedOption.type; + }; + + submoduleWith = + { modules + , specialArgs ? {} + , shorthandOnlyDefinesConfig ? false + , description ? null + , class ? null + }@attrs: + let + inherit (lib.modules) evalModules; + + allModules = defs: map ({ value, file }: + if isAttrs value && shorthandOnlyDefinesConfig + then { _file = file; config = value; } + else { _file = file; imports = [ value ]; } + ) defs; + + base = evalModules { + inherit class specialArgs; + modules = [{ + # This is a work-around for the fact that some sub-modules, + # such as the one included in an attribute set, expects an "args" + # attribute to be given to the sub-module. As the option + # evaluation does not have any specific attribute name yet, we + # provide a default for the documentation and the freeform type. + # + # This is necessary as some option declaration might use the + # "name" attribute given as argument of the submodule and use it + # as the default of option declarations. + # + # We use lookalike unicode single angle quotation marks because + # of the docbook transformation the options receive. In all uses + # > and < wouldn't be encoded correctly so the encoded values + # would be used, and use of `<` and `>` would break the XML document. + # It shouldn't cause an issue since this is cosmetic for the manual. + _module.args.name = lib.mkOptionDefault "‹name›"; + }] ++ modules; + }; + + freeformType = base._module.freeformType; + + name = "submodule"; + + in + mkOptionType { + inherit name; + description = + if description != null then description + else freeformType.description or name; + check = x: isAttrs x || isFunction x || path.check x; + merge = loc: defs: + (base.extendModules { + modules = [ { _module.args.name = last loc; } ] ++ allModules defs; + prefix = loc; + }).config; + emptyValue = { value = {}; }; + getSubOptions = prefix: (base.extendModules + { inherit prefix; }).options // optionalAttrs (freeformType != null) { + # Expose the sub options of the freeform type. Note that the option + # discovery doesn't care about the attribute name used here, so this + # is just to avoid conflicts with potential options from the submodule + _freeformOptions = freeformType.getSubOptions prefix; + }; + getSubModules = modules; + substSubModules = m: submoduleWith (attrs // { + modules = m; + }); + nestedTypes = lib.optionalAttrs (freeformType != null) { + freeformType = freeformType; + }; + functor = defaultFunctor name // { + type = types.submoduleWith; + payload = { + inherit modules class specialArgs shorthandOnlyDefinesConfig description; + }; + binOp = lhs: rhs: { + class = + # `or null` was added for backwards compatibility only. `class` is + # always set in the current version of the module system. + if lhs.class or null == null then rhs.class or null + else if rhs.class or null == null then lhs.class or null + else if lhs.class or null == rhs.class then lhs.class or null + else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\"."; + modules = lhs.modules ++ rhs.modules; + specialArgs = + let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs; + in if intersecting == {} + then lhs.specialArgs // rhs.specialArgs + else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\""; + shorthandOnlyDefinesConfig = + if lhs.shorthandOnlyDefinesConfig == null + then rhs.shorthandOnlyDefinesConfig + else if rhs.shorthandOnlyDefinesConfig == null + then lhs.shorthandOnlyDefinesConfig + else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig + then lhs.shorthandOnlyDefinesConfig + else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values"; + description = + if lhs.description == null + then rhs.description + else if rhs.description == null + then lhs.description + else if lhs.description == rhs.description + then lhs.description + else throw "A submoduleWith option is declared multiple times with conflicting descriptions"; + }; + }; + }; + + # A value from a set of allowed ones. + enum = values: + let + inherit (lib.lists) unique; + show = v: + if builtins.isString v then ''"${v}"'' + else if builtins.isInt v then builtins.toString v + else if builtins.isBool v then boolToString v + else ''<${builtins.typeOf v}>''; + in + mkOptionType rec { + name = "enum"; + description = + # Length 0 or 1 enums may occur in a design pattern with type merging + # where an "interface" module declares an empty enum and other modules + # provide implementations, each extending the enum with their own + # identifier. + if values == [] then + "impossible (empty enum)" + else if builtins.length values == 1 then + "value ${show (builtins.head values)} (singular enum)" + else + "one of ${concatMapStringsSep ", " show values}"; + descriptionClass = + if builtins.length values < 2 + then "noun" + else "conjunction"; + check = flip elem values; + merge = mergeEqualOption; + functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); }; + }; + + # Either value of type `t1` or `t2`. + either = t1: t2: mkOptionType rec { + name = "either"; + description = "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}"; + descriptionClass = "conjunction"; + check = x: t1.check x || t2.check x; + merge = loc: defs: + let + defList = map (d: d.value) defs; + in + if all (x: t1.check x) defList + then t1.merge loc defs + else if all (x: t2.check x) defList + then t2.merge loc defs + else mergeOneOption loc defs; + typeMerge = f': + let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor; + mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor; + in + if (name == f'.name) && (mt1 != null) && (mt2 != null) + then functor.type mt1 mt2 + else null; + functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; }; + nestedTypes.left = t1; + nestedTypes.right = t2; + }; + + # Any of the types in the given list + oneOf = ts: + let + head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts; + tail' = tail ts; + in foldl' either head' tail'; + + # Either value of type `coercedType` or `finalType`, the former is + # converted to `finalType` using `coerceFunc`. + coercedTo = coercedType: coerceFunc: finalType: + assert lib.assertMsg (coercedType.getSubModules == null) + "coercedTo: coercedType must not have submodules (it’s a ${ + coercedType.description})"; + mkOptionType rec { + name = "coercedTo"; + description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it"; + check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x; + merge = loc: defs: + let + coerceVal = val: + if coercedType.check val then coerceFunc val + else val; + in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); + emptyValue = finalType.emptyValue; + getSubOptions = finalType.getSubOptions; + getSubModules = finalType.getSubModules; + substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); + typeMerge = t1: t2: null; + functor = (defaultFunctor name) // { wrapped = finalType; }; + nestedTypes.coercedType = coercedType; + nestedTypes.finalType = finalType; + }; + + # Augment the given type with an additional type check function. + addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; + + }; +}; + +in outer_types // outer_types.types diff --git a/versions.nix b/versions.nix new file mode 100644 index 000000000..986e7e5f9 --- /dev/null +++ b/versions.nix @@ -0,0 +1,64 @@ +/* Version string functions. */ +{ lib }: + +rec { + + /* Break a version string into its component parts. + + Example: + splitVersion "1.2.3" + => ["1" "2" "3"] + */ + splitVersion = builtins.splitVersion or (lib.splitString "."); + + /* Get the major version string from a string. + + Example: + major "1.2.3" + => "1" + */ + major = v: builtins.elemAt (splitVersion v) 0; + + /* Get the minor version string from a string. + + Example: + minor "1.2.3" + => "2" + */ + minor = v: builtins.elemAt (splitVersion v) 1; + + /* Get the patch version string from a string. + + Example: + patch "1.2.3" + => "3" + */ + patch = v: builtins.elemAt (splitVersion v) 2; + + /* Get string of the first two parts (major and minor) + of a version string. + + Example: + majorMinor "1.2.3" + => "1.2" + */ + majorMinor = v: + builtins.concatStringsSep "." + (lib.take 2 (splitVersion v)); + + /* Pad a version string with zeros to match the given number of components. + + Example: + pad 3 "1.2" + => "1.2.0" + pad 3 "1.3-rc1" + => "1.3.0-rc1" + pad 3 "1.2.3.4" + => "1.2.3" + */ + pad = n: version: let + numericVersion = lib.head (lib.splitString "-" version); + versionSuffix = lib.removePrefix numericVersion version; + in lib.concatStringsSep "." (lib.take n (lib.splitVersion numericVersion ++ lib.genList (_: "0") n)) + versionSuffix; + +} diff --git a/zip-int-bits.nix b/zip-int-bits.nix new file mode 100644 index 000000000..53efd2bb0 --- /dev/null +++ b/zip-int-bits.nix @@ -0,0 +1,39 @@ +/* Helper function to implement a fallback for the bit operators + `bitAnd`, `bitOr` and `bitXor` on older nix version. + See ./trivial.nix +*/ +f: x: y: + let + # (intToBits 6) -> [ 0 1 1 ] + intToBits = x: + if x == 0 || x == -1 then + [] + else + let + headbit = if (x / 2) * 2 != x then 1 else 0; # x & 1 + tailbits = if x < 0 then ((x + 1) / 2) - 1 else x / 2; # x >> 1 + in + [headbit] ++ (intToBits tailbits); + + # (bitsToInt [ 0 1 1 ] 0) -> 6 + # (bitsToInt [ 0 1 0 ] 1) -> -6 + bitsToInt = l: signum: + if l == [] then + (if signum == 0 then 0 else -1) + else + (builtins.head l) + (2 * (bitsToInt (builtins.tail l) signum)); + + xsignum = if x < 0 then 1 else 0; + ysignum = if y < 0 then 1 else 0; + zipListsWith' = fst: snd: + if fst==[] && snd==[] then + [] + else if fst==[] then + [(f xsignum (builtins.head snd))] ++ (zipListsWith' [] (builtins.tail snd)) + else if snd==[] then + [(f (builtins.head fst) ysignum )] ++ (zipListsWith' (builtins.tail fst) [] ) + else + [(f (builtins.head fst) (builtins.head snd))] ++ (zipListsWith' (builtins.tail fst) (builtins.tail snd)); + in + assert (builtins.isInt x) && (builtins.isInt y); + bitsToInt (zipListsWith' (intToBits x) (intToBits y)) (f xsignum ysignum) From d8079ee3503b487af0769ad53398171e617869da Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 16 Aug 2023 16:16:58 +0200 Subject: [PATCH 033/299] Document jobCategory() --- src/libstore/build/goal.hh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libstore/build/goal.hh b/src/libstore/build/goal.hh index a313bf22c..d3127caea 100644 --- a/src/libstore/build/goal.hh +++ b/src/libstore/build/goal.hh @@ -162,6 +162,10 @@ public: virtual void cleanup() { } + /** + * @brief Hint for the scheduler, which concurrency limit applies. + * @see JobCategory + */ virtual JobCategory jobCategory() = 0; }; From 17ceec3a91a57c77c8df51f380f3a8a0c88460c4 Mon Sep 17 00:00:00 2001 From: Felix Uhl Date: Thu, 17 Aug 2023 13:03:43 +0200 Subject: [PATCH 034/299] Test repl formatting with and without :p --- tests/repl.sh | 58 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/tests/repl.sh b/tests/repl.sh index 2b3789521..bb8b60e50 100644 --- a/tests/repl.sh +++ b/tests/repl.sh @@ -54,11 +54,17 @@ testRepl # Same thing (kind-of), but with a remote store. testRepl --store "$TEST_ROOT/store?real=$NIX_STORE_DIR" -testReplResponse () { +# Remove ANSI escape sequences. They can prevent grep from finding a match. +stripColors () { + sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' +} + +testReplResponseGeneral () { + local grepMode="$1"; shift local commands="$1"; shift local expectedResponse="$1"; shift - local response="$(nix repl "$@" <<< "$commands")" - echo "$response" | grepQuiet -s "$expectedResponse" \ + local response="$(nix repl "$@" <<< "$commands" | stripColors)" + echo "$response" | grepQuiet "$grepMode" -s "$expectedResponse" \ || fail "repl command set: $commands @@ -69,7 +75,16 @@ $expectedResponse but with: -$response" +$response +" +} + +testReplResponse () { + testReplResponseGeneral --basic-regexp "$@" +} + +testReplResponseNoRegex () { + testReplResponseGeneral --fixed-strings "$@" } # :a uses the newest version of a symbol @@ -83,9 +98,9 @@ testReplResponse ' # note the escaped \, # \\ # because the second argument is a regex -testReplResponse ' +testReplResponseNoRegex ' "$" + "{hi}" -' '"\\${hi}"' +' '"\${hi}"' testReplResponse ' drvPath @@ -131,3 +146,34 @@ echo "changingThing" ) | nix repl ./flake --experimental-features 'flakes repl-flake') echo "$replResult" | grepQuiet -s beforeChange echo "$replResult" | grepQuiet -s afterChange + +# Test recursive printing and formatting +# Normal output should print attributes in lexicographical order non-recursively +testReplResponseNoRegex ' +{ a = { b = 2; }; l = [ 1 2 3 ]; s = "string"; n = 1234; x = rec { y = { z = { inherit y; }; }; }; } +' '{ a = { ... }; l = [ ... ]; n = 1234; s = "string"; x = { ... }; }' + +# Same for lists, but order is preserved +testReplResponseNoRegex ' +[ 42 1 "thingy" ({ a = 1; }) ([ 1 2 3 ]) ] +' '[ 42 1 "thingy" { ... } [ ... ] ]' + +# Same for let expressions +testReplResponseNoRegex ' +let x = { y = { a = 1; }; inherit x; }; in x +' '{ x = { ... }; y = { ... }; }' + +# The :p command should recursively print sets, but prevent infinite recursion +testReplResponseNoRegex ' +:p { a = { b = 2; }; s = "string"; n = 1234; x = rec { y = { z = { inherit y; }; }; }; } +' '{ a = { b = 2; }; n = 1234; s = "string"; x = { y = { z = { y = «repeated»; }; }; }; }' + +# Same for lists +testReplResponseNoRegex ' +:p [ 42 1 "thingy" (rec { a = 1; b = { inherit a; inherit b; }; }) ([ 1 2 3 ]) ] +' '[ 42 1 "thingy" { a = 1; b = { a = 1; b = «repeated»; }; } [ 1 2 3 ] ]' + +# Same for let expressions +testReplResponseNoRegex ' +:p let x = { y = { a = 1; }; inherit x; }; in x +' '{ x = { x = «repeated»; y = { a = 1; }; }; y = «repeated»; }' From 1d7a57cfd9bf40658e2f35c13146d746a1011020 Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Thu, 17 Aug 2023 13:40:57 -0700 Subject: [PATCH 035/299] libexpr/tests: test that parseFlakeRef doesn't percent-encode twice --- src/libexpr/tests/flakeref.cc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/libexpr/tests/flakeref.cc diff --git a/src/libexpr/tests/flakeref.cc b/src/libexpr/tests/flakeref.cc new file mode 100644 index 000000000..2b7809b93 --- /dev/null +++ b/src/libexpr/tests/flakeref.cc @@ -0,0 +1,22 @@ +#include + +#include "flake/flakeref.hh" + +namespace nix { + +/* ----------- tests for flake/flakeref.hh --------------------------------------------------*/ + + /* ---------------------------------------------------------------------------- + * to_string + * --------------------------------------------------------------------------*/ + + TEST(to_string, doesntReencodeUrl) { + auto s = "http://localhost:8181/test/+3d.tar.gz"; + auto flakeref = parseFlakeRef(s); + auto parsed = flakeref.to_string(); + auto expected = "http://localhost:8181/test/%2B3d.tar.gz"; + + ASSERT_EQ(parsed, expected); + } + +} From 73696ec71678433dac87863bab36b66701d4b6ed Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Thu, 17 Aug 2023 13:21:38 -0700 Subject: [PATCH 036/299] libutil: fix double-encoding of URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you have a URL that needs to be percent-encoded, such as `http://localhost:8181/test/+3d.tar.gz`, and try to lock that in a Nix flake such as the following: { inputs.test = { url = "http://localhost:8181/test/+3d.tar.gz"; flake = false; }; outputs = { test, ... }: { t = builtins.readFile test; }; } running `nix flake metadata` shows that the input URL has been incorrectly double-encoded (despite the flake.lock being correctly encoded only once): [...snip...] Inputs: └───test: http://localhost:8181/test/%252B3d.tar.gz?narHash=sha256-EFUdrtf6Rn0LWIJufrmg8q99aT3jGfLvd1//zaJEufY%3D (Notice the `%252B`? That's just `%2B` but percent-encoded again) With this patch, the double-encoding is gone; running `nix flake metadata` will show the proper URL: [...snip...] Inputs: └───test: http://localhost:8181/test/%2B3d.tar.gz?narHash=sha256-EFUdrtf6Rn0LWIJufrmg8q99aT3jGfLvd1//zaJEufY%3D --- As far as I can tell, this happens because Nix already percent-encodes the URL and stores this as the value of `inputs.asdf.url`. However, when Nix later tries to read this out of the eval state as a string (via `getStrAttr`), it has to run it through `parseURL` again to get the `ParsedURL` structure. Now, this itself isn't a problem -- the true problem arises when using `ParsedURL::to_string` later, which then _re-escapes the path_. It is at this point that what would have been `%2B` (`+`) becomes `%252B` (`%2B`). --- src/libutil/url.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/url.cc b/src/libutil/url.cc index 9e44241ac..a8f7d39fd 100644 --- a/src/libutil/url.cc +++ b/src/libutil/url.cc @@ -44,7 +44,7 @@ ParsedURL parseURL(const std::string & url) .base = base, .scheme = scheme, .authority = authority, - .path = path, + .path = percentDecode(path), .query = decodeQuery(query), .fragment = percentDecode(std::string(fragment)) }; From 75243c96937b472665b94729df81f8296b262085 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 18 Aug 2023 14:46:13 +0200 Subject: [PATCH 037/299] test/flakes/follow-paths.sh: Quote Co-authored-by: Alex Ameen --- tests/flakes/follow-paths.sh | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/flakes/follow-paths.sh b/tests/flakes/follow-paths.sh index f9d5c8c80..dc97027ac 100644 --- a/tests/flakes/follow-paths.sh +++ b/tests/flakes/follow-paths.sh @@ -146,8 +146,8 @@ EOF git -C $flakeFollowsA add flake.nix -nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid'" -nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid2'" +nix flake lock "$flakeFollowsA" 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid'" +nix flake lock "$flakeFollowsA" 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid2'" # Now test follow path overloading # This tests a lockfile checking regression https://github.com/NixOS/nix/pull/8819 @@ -169,18 +169,18 @@ nix flake lock $flakeFollowsA 2>&1 | grep "warning: input 'B' has an override fo # error: input 'B/D' follows a non-existent input 'B/C/D' # # Note that for `B` to resolve its follow for `D`, it needs `C/D`, for which it needs to resolve the follow on `C` first. -flakeFollowsOverloadA=$TEST_ROOT/follows/overload/flakeA -flakeFollowsOverloadB=$TEST_ROOT/follows/overload/flakeA/flakeB -flakeFollowsOverloadC=$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC -flakeFollowsOverloadD=$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC/flakeD +flakeFollowsOverloadA="$TEST_ROOT/follows/overload/flakeA" +flakeFollowsOverloadB="$TEST_ROOT/follows/overload/flakeA/flakeB" +flakeFollowsOverloadC="$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC" +flakeFollowsOverloadD="$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC/flakeD" # Test following path flakerefs. -createGitRepo $flakeFollowsOverloadA -mkdir -p $flakeFollowsOverloadB -mkdir -p $flakeFollowsOverloadC -mkdir -p $flakeFollowsOverloadD +createGitRepo "$flakeFollowsOverloadA" +mkdir -p "$flakeFollowsOverloadB" +mkdir -p "$flakeFollowsOverloadC" +mkdir -p "$flakeFollowsOverloadD" -cat > $flakeFollowsOverloadD/flake.nix < "$flakeFollowsOverloadD/flake.nix" < $flakeFollowsOverloadD/flake.nix < $flakeFollowsOverloadC/flake.nix < "$flakeFollowsOverloadC/flake.nix" < $flakeFollowsOverloadC/flake.nix < $flakeFollowsOverloadB/flake.nix < "$flakeFollowsOverloadB/flake.nix" < $flakeFollowsOverloadB/flake.nix < $flakeFollowsOverloadA/flake.nix < "$flakeFollowsOverloadA/flake.nix" < $flakeFollowsOverloadA/flake.nix < Date: Wed, 16 Aug 2023 12:29:23 -0400 Subject: [PATCH 038/299] Fixing #7479 Types converted: - `NixStringContextElem` - `OutputsSpec` - `ExtendedOutputsSpec` - `DerivationOutput` - `DerivationType` Existing ones mostly conforming the pattern cleaned up: - `ContentAddressMethod` - `ContentAddressWithReferences` The `DerivationGoal::derivationType` field had a bogus initialization, now caught, so I made it `std::optional`. I think #8829 can make it non-optional again because it will ensure we always have the derivation when we construct a `DerivationGoal`. See that issue (#7479) for details on the general goal. `git grep 'Raw::Raw'` indicates the two types I didn't yet convert `DerivedPath` and `BuiltPath` (and their `Single` variants) . This is because @roberth and I (can't find issue right now...) plan on reworking them somewhat, so I didn't want to churn them more just yet. Co-authored-by: Eelco Dolstra --- src/libcmd/installable-attr-path.cc | 5 +- src/libcmd/installable-derived-path.cc | 2 +- src/libcmd/installable-flake.cc | 2 +- src/libcmd/installables.cc | 6 +- src/libexpr/eval-cache.cc | 2 +- src/libexpr/eval.cc | 2 +- src/libexpr/flake/flakeref.cc | 2 +- src/libexpr/primops.cc | 14 +- src/libexpr/primops/context.cc | 6 +- src/libexpr/tests/value/context.cc | 8 +- src/libexpr/value/context.cc | 2 +- src/libexpr/value/context.hh | 82 +++--- src/libstore/build/derivation-goal.cc | 7 +- src/libstore/build/derivation-goal.hh | 2 +- src/libstore/build/local-derivation-goal.cc | 18 +- src/libstore/build/worker.cc | 5 +- src/libstore/content-address.cc | 2 +- src/libstore/content-address.hh | 12 +- src/libstore/derivations.cc | 36 +-- src/libstore/derivations.hh | 311 ++++++++++---------- src/libstore/misc.cc | 8 +- src/libstore/outputs-spec.cc | 20 +- src/libstore/outputs-spec.hh | 95 +++--- src/libstore/path-with-outputs.cc | 2 +- src/libutil/variant-wrapper.hh | 30 ++ src/nix/app.cc | 2 +- src/nix/bundle.cc | 2 +- src/nix/develop.cc | 2 +- src/nix/flake.cc | 2 +- 29 files changed, 355 insertions(+), 334 deletions(-) create mode 100644 src/libutil/variant-wrapper.hh diff --git a/src/libcmd/installable-attr-path.cc b/src/libcmd/installable-attr-path.cc index 2f89eee02..06e507872 100644 --- a/src/libcmd/installable-attr-path.cc +++ b/src/libcmd/installable-attr-path.cc @@ -80,7 +80,7 @@ DerivedPathsWithInfo InstallableAttrPath::toDerivedPaths() [&](const ExtendedOutputsSpec::Explicit & e) -> OutputsSpec { return e; }, - }, extendedOutputsSpec.raw()); + }, extendedOutputsSpec.raw); auto [iter, didInsert] = byDrvPath.emplace(*drvPath, newOutputs); @@ -96,6 +96,7 @@ DerivedPathsWithInfo InstallableAttrPath::toDerivedPaths() .outputs = outputs, }, .info = make_ref(ExtraPathInfoValue::Value { + .extendedOutputsSpec = outputs, /* FIXME: reconsider backwards compatibility above so we can fill in this info. */ }), @@ -114,7 +115,7 @@ InstallableAttrPath InstallableAttrPath::parse( return { state, cmd, v, prefix == "." ? "" : std::string { prefix }, - extendedOutputsSpec + std::move(extendedOutputsSpec), }; } diff --git a/src/libcmd/installable-derived-path.cc b/src/libcmd/installable-derived-path.cc index b45641e8a..4d1f83a1c 100644 --- a/src/libcmd/installable-derived-path.cc +++ b/src/libcmd/installable-derived-path.cc @@ -55,7 +55,7 @@ InstallableDerivedPath InstallableDerivedPath::parse( .outputs = outputSpec, }; }, - }, extendedOutputsSpec.raw()); + }, extendedOutputsSpec.raw); return InstallableDerivedPath { store, std::move(derivedPath), diff --git a/src/libcmd/installable-flake.cc b/src/libcmd/installable-flake.cc index 1b169c3bd..4074da06d 100644 --- a/src/libcmd/installable-flake.cc +++ b/src/libcmd/installable-flake.cc @@ -141,7 +141,7 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths() [&](const ExtendedOutputsSpec::Explicit & e) -> OutputsSpec { return e; }, - }, extendedOutputsSpec.raw()), + }, extendedOutputsSpec.raw), }, .info = make_ref( ExtraPathInfoValue::Value { diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 4c7d134ec..eb1903084 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -459,7 +459,7 @@ Installables SourceExprCommand::parseInstallables( result.push_back( make_ref( InstallableAttrPath::parse( - state, *this, vFile, prefix, extendedOutputsSpec))); + state, *this, vFile, std::move(prefix), std::move(extendedOutputsSpec)))); } } else { @@ -475,7 +475,7 @@ Installables SourceExprCommand::parseInstallables( if (prefix.find('/') != std::string::npos) { try { result.push_back(make_ref( - InstallableDerivedPath::parse(store, prefix, extendedOutputsSpec))); + InstallableDerivedPath::parse(store, prefix, extendedOutputsSpec.raw))); continue; } catch (BadStorePath &) { } catch (...) { @@ -491,7 +491,7 @@ Installables SourceExprCommand::parseInstallables( getEvalState(), std::move(flakeRef), fragment, - extendedOutputsSpec, + std::move(extendedOutputsSpec), getDefaultFlakeAttrPaths(), getDefaultFlakeAttrPathPrefixes(), lockFlags)); diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index 6a60ba87b..2c9aa5532 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -604,7 +604,7 @@ string_t AttrCursor::getStringWithContext() [&](const NixStringContextElem::Opaque & o) -> const StorePath & { return o.path; }, - }, c.raw()); + }, c.raw); if (!root->state.store->isValidPath(path)) { valid = false; break; diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 79e2c4727..7e839dbe7 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2364,7 +2364,7 @@ std::pair EvalState::coerceToSingleDerivedP [&](NixStringContextElem::Built && b) -> SingleDerivedPath { return std::move(b); }, - }, ((NixStringContextElem &&) *context.begin()).raw()); + }, ((NixStringContextElem &&) *context.begin()).raw); return { std::move(derivedPath), std::move(s), diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index d3fa1d557..e1bce90bc 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -246,7 +246,7 @@ std::tuple parseFlakeRefWithFragment { auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(url); auto [flakeRef, fragment] = parseFlakeRefWithFragment(std::string { prefix }, baseDir, allowMissing, isFlake); - return {std::move(flakeRef), fragment, extendedOutputsSpec}; + return {std::move(flakeRef), fragment, std::move(extendedOutputsSpec)}; } } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 283f99a48..5067da449 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -69,7 +69,7 @@ StringMap EvalState::realiseContext(const NixStringContext & context) res.insert_or_assign(ctxS, ctxS); ensureValid(d.drvPath); }, - }, c.raw()); + }, c.raw); } if (drvs.empty()) return {}; @@ -1265,7 +1265,7 @@ drvName, Bindings * attrs, Value & v) [&](const NixStringContextElem::Opaque & o) { drv.inputSrcs.insert(o.path); }, - }, c.raw()); + }, c.raw); } /* Do we have all required attributes? */ @@ -1334,13 +1334,13 @@ drvName, Bindings * attrs, Value & v) if (isImpure) drv.outputs.insert_or_assign(i, DerivationOutput::Impure { - .method = method.raw, + .method = method, .hashType = ht, }); else drv.outputs.insert_or_assign(i, DerivationOutput::CAFloating { - .method = method.raw, + .method = method, .hashType = ht, }); } @@ -1373,7 +1373,7 @@ drvName, Bindings * attrs, Value & v) drv.env[i] = state.store->printStorePath(outPath); drv.outputs.insert_or_assign( i, - DerivationOutputInputAddressed { + DerivationOutput::InputAddressed { .path = std::move(outPath), }); } @@ -1381,7 +1381,7 @@ drvName, Bindings * attrs, Value & v) ; case DrvHash::Kind::Deferred: for (auto & i : outputs) { - drv.outputs.insert_or_assign(i, DerivationOutputDeferred {}); + drv.outputs.insert_or_assign(i, DerivationOutput::Deferred {}); } } } @@ -2054,7 +2054,7 @@ static void prim_toFile(EvalState & state, const PosIdx pos, Value * * args, Val StorePathSet refs; for (auto c : context) { - if (auto p = std::get_if(&c)) + if (auto p = std::get_if(&c.raw)) refs.insert(p->path); else state.debugThrowLastTrace(EvalError({ diff --git a/src/libexpr/primops/context.cc b/src/libexpr/primops/context.cc index bfc731744..e8542503a 100644 --- a/src/libexpr/primops/context.cc +++ b/src/libexpr/primops/context.cc @@ -51,13 +51,13 @@ static void prim_unsafeDiscardOutputDependency(EvalState & state, const PosIdx p NixStringContext context2; for (auto && c : context) { - if (auto * ptr = std::get_if(&c)) { + if (auto * ptr = std::get_if(&c.raw)) { context2.emplace(NixStringContextElem::Opaque { .path = ptr->drvPath }); } else { /* Can reuse original item */ - context2.emplace(std::move(c)); + context2.emplace(std::move(c).raw); } } @@ -114,7 +114,7 @@ static void prim_getContext(EvalState & state, const PosIdx pos, Value * * args, [&](NixStringContextElem::Opaque && o) { contextInfos[std::move(o.path)].path = true; }, - }, ((NixStringContextElem &&) i).raw()); + }, ((NixStringContextElem &&) i).raw); } auto attrs = state.buildBindings(contextInfos.size()); diff --git a/src/libexpr/tests/value/context.cc b/src/libexpr/tests/value/context.cc index c56b50b59..19407d071 100644 --- a/src/libexpr/tests/value/context.cc +++ b/src/libexpr/tests/value/context.cc @@ -47,7 +47,7 @@ TEST(NixStringContextElemTest, slash_invalid) { TEST(NixStringContextElemTest, opaque) { std::string_view opaque = "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x"; auto elem = NixStringContextElem::parse(opaque); - auto * p = std::get_if(&elem); + auto * p = std::get_if(&elem.raw); ASSERT_TRUE(p); ASSERT_EQ(p->path, StorePath { opaque }); ASSERT_EQ(elem.to_string(), opaque); @@ -60,7 +60,7 @@ TEST(NixStringContextElemTest, opaque) { TEST(NixStringContextElemTest, drvDeep) { std::string_view drvDeep = "=g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; auto elem = NixStringContextElem::parse(drvDeep); - auto * p = std::get_if(&elem); + auto * p = std::get_if(&elem.raw); ASSERT_TRUE(p); ASSERT_EQ(p->drvPath, StorePath { drvDeep.substr(1) }); ASSERT_EQ(elem.to_string(), drvDeep); @@ -73,7 +73,7 @@ TEST(NixStringContextElemTest, drvDeep) { TEST(NixStringContextElemTest, built_opaque) { std::string_view built = "!foo!g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; auto elem = NixStringContextElem::parse(built); - auto * p = std::get_if(&elem); + auto * p = std::get_if(&elem.raw); ASSERT_TRUE(p); ASSERT_EQ(p->output, "foo"); ASSERT_EQ(*p->drvPath, ((SingleDerivedPath) SingleDerivedPath::Opaque { @@ -96,7 +96,7 @@ TEST(NixStringContextElemTest, built_built) { std::string_view built = "!foo!bar!g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x.drv"; auto elem = NixStringContextElem::parse(built, mockXpSettings); - auto * p = std::get_if(&elem); + auto * p = std::get_if(&elem.raw); ASSERT_TRUE(p); ASSERT_EQ(p->output, "foo"); auto * drvPath = std::get_if(&*p->drvPath); diff --git a/src/libexpr/value/context.cc b/src/libexpr/value/context.cc index d8116011e..22361d8fa 100644 --- a/src/libexpr/value/context.cc +++ b/src/libexpr/value/context.cc @@ -99,7 +99,7 @@ std::string NixStringContextElem::to_string() const res += '='; res += d.drvPath.to_string(); }, - }, raw()); + }, raw); return res; } diff --git a/src/libexpr/value/context.hh b/src/libexpr/value/context.hh index a1b71695b..9f1d59317 100644 --- a/src/libexpr/value/context.hh +++ b/src/libexpr/value/context.hh @@ -4,8 +4,7 @@ #include "util.hh" #include "comparator.hh" #include "derived-path.hh" - -#include +#include "variant-wrapper.hh" #include @@ -26,58 +25,47 @@ public: } }; -/** - * Plain opaque path to some store object. - * - * Encoded as just the path: ‘’. - */ -typedef SingleDerivedPath::Opaque NixStringContextElem_Opaque; +struct NixStringContextElem { + /** + * Plain opaque path to some store object. + * + * Encoded as just the path: ‘’. + */ + using Opaque = SingleDerivedPath::Opaque; -/** - * Path to a derivation and its entire build closure. - * - * The path doesn't just refer to derivation itself and its closure, but - * also all outputs of all derivations in that closure (including the - * root derivation). - * - * Encoded in the form ‘=’. - */ -struct NixStringContextElem_DrvDeep { - StorePath drvPath; + /** + * Path to a derivation and its entire build closure. + * + * The path doesn't just refer to derivation itself and its closure, but + * also all outputs of all derivations in that closure (including the + * root derivation). + * + * Encoded in the form ‘=’. + */ + struct DrvDeep { + StorePath drvPath; - GENERATE_CMP(NixStringContextElem_DrvDeep, me->drvPath); -}; + GENERATE_CMP(DrvDeep, me->drvPath); + }; -/** - * Derivation output. - * - * Encoded in the form ‘!!’. - */ -typedef SingleDerivedPath::Built NixStringContextElem_Built; + /** + * Derivation output. + * + * Encoded in the form ‘!!’. + */ + using Built = SingleDerivedPath::Built; -using _NixStringContextElem_Raw = std::variant< - NixStringContextElem_Opaque, - NixStringContextElem_DrvDeep, - NixStringContextElem_Built ->; + using Raw = std::variant< + Opaque, + DrvDeep, + Built + >; -struct NixStringContextElem : _NixStringContextElem_Raw { - using Raw = _NixStringContextElem_Raw; - using Raw::Raw; + Raw raw; - using Opaque = NixStringContextElem_Opaque; - using DrvDeep = NixStringContextElem_DrvDeep; - using Built = NixStringContextElem_Built; + GENERATE_CMP(NixStringContextElem, me->raw); - inline const Raw & raw() const & { - return static_cast(*this); - } - inline Raw & raw() & { - return static_cast(*this); - } - inline Raw && raw() && { - return static_cast(*this); - } + MAKE_WRAPPER_CONSTRUCTOR(NixStringContextElem); /** * Decode a context string, one of: diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 8bdf2f367..84da7f2e1 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -521,7 +521,7 @@ void DerivationGoal::inputsRealised() [&](const DerivationType::Impure &) { return true; } - }, drvType.raw()); + }, drvType.raw); if (resolveDrv && !fullDrv.inputDrvs.empty()) { experimentalFeatureSettings.require(Xp::CaDerivations); @@ -996,10 +996,11 @@ void DerivationGoal::buildDone() } else { + assert(derivationType); st = dynamic_cast(&e) ? BuildResult::NotDeterministic : statusOk(status) ? BuildResult::OutputRejected : - !derivationType.isSandboxed() || diskFull ? BuildResult::TransientFailure : + !derivationType->isSandboxed() || diskFull ? BuildResult::TransientFailure : BuildResult::PermanentFailure; } @@ -1358,7 +1359,7 @@ std::pair DerivationGoal::checkPathValidity() [&](const OutputsSpec::Names & names) { return static_cast(names); }, - }, wantedOutputs.raw()); + }, wantedOutputs.raw); SingleDrvOutputs validOutputs; for (auto & i : queryPartialDerivationOutputMap()) { diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index ee8f06f25..9d6fe1c0f 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -184,7 +184,7 @@ struct DerivationGoal : public Goal /** * The sort of derivation we are building. */ - DerivationType derivationType; + std::optional derivationType; typedef void (DerivationGoal::*GoalState)(); GoalState state; diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 920097680..78f943d1f 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -178,6 +178,8 @@ void LocalDerivationGoal::tryLocalBuild() return; } + assert(derivationType); + /* Are we doing a chroot build? */ { auto noChroot = parsedDrv->getBoolAttr("__noChroot"); @@ -195,7 +197,7 @@ void LocalDerivationGoal::tryLocalBuild() else if (settings.sandboxMode == smDisabled) useChroot = false; else if (settings.sandboxMode == smRelaxed) - useChroot = derivationType.isSandboxed() && !noChroot; + useChroot = derivationType->isSandboxed() && !noChroot; } auto & localStore = getLocalStore(); @@ -689,7 +691,7 @@ void LocalDerivationGoal::startBuilder() "nogroup:x:65534:\n", sandboxGid())); /* Create /etc/hosts with localhost entry. */ - if (derivationType.isSandboxed()) + if (derivationType->isSandboxed()) writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n::1 localhost\n"); /* Make the closure of the inputs available in the chroot, @@ -893,7 +895,7 @@ void LocalDerivationGoal::startBuilder() us. */ - if (derivationType.isSandboxed()) + if (derivationType->isSandboxed()) privateNetwork = true; userNamespaceSync.create(); @@ -1121,7 +1123,7 @@ void LocalDerivationGoal::initEnv() derivation, tell the builder, so that for instance `fetchurl' can skip checking the output. On older Nixes, this environment variable won't be set, so `fetchurl' will do the check. */ - if (derivationType.isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; + if (derivationType->isFixed()) env["NIX_OUTPUT_CHECKED"] = "1"; /* *Only* if this is a fixed-output derivation, propagate the values of the environment variables specified in the @@ -1132,7 +1134,7 @@ void LocalDerivationGoal::initEnv() to the builder is generally impure, but the output of fixed-output derivations is by definition pure (since we already know the cryptographic hash of the output). */ - if (!derivationType.isSandboxed()) { + if (!derivationType->isSandboxed()) { for (auto & i : parsedDrv->getStringsAttr("impureEnvVars").value_or(Strings())) env[i] = getEnv(i).value_or(""); } @@ -1797,7 +1799,7 @@ void LocalDerivationGoal::runChild() /* Fixed-output derivations typically need to access the network, so give them access to /etc/resolv.conf and so on. */ - if (!derivationType.isSandboxed()) { + if (!derivationType->isSandboxed()) { // Only use nss functions to resolve hosts and // services. Don’t use it for anything else that may // be configured for this system. This limits the @@ -2048,7 +2050,7 @@ void LocalDerivationGoal::runChild() #include "sandbox-defaults.sb" ; - if (!derivationType.isSandboxed()) + if (!derivationType->isSandboxed()) sandboxProfile += #include "sandbox-network.sb" ; @@ -2599,7 +2601,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() }); }, - }, output->raw()); + }, output->raw); /* FIXME: set proper permissions in restorePath() so we don't have to do another traversal. */ diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index 6779dbcf3..b58fc5c1c 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -268,7 +268,10 @@ void Worker::run(const Goals & _topGoals) for (auto & i : _topGoals) { topGoals.insert(i); if (auto goal = dynamic_cast(i.get())) { - topPaths.push_back(DerivedPath::Built{makeConstantStorePathRef(goal->drvPath), goal->wantedOutputs}); + topPaths.push_back(DerivedPath::Built { + .drvPath = makeConstantStorePathRef(goal->drvPath), + .outputs = goal->wantedOutputs, + }); } else if (auto goal = dynamic_cast(i.get())) { topPaths.push_back(DerivedPath::Opaque{goal->storePath}); } diff --git a/src/libstore/content-address.cc b/src/libstore/content-address.cc index 080456e18..e290a8d38 100644 --- a/src/libstore/content-address.cc +++ b/src/libstore/content-address.cc @@ -115,7 +115,7 @@ ContentAddress ContentAddress::parse(std::string_view rawCa) auto [caMethod, hashType] = parseContentAddressMethodPrefix(rest); return ContentAddress { - .method = std::move(caMethod).raw, + .method = std::move(caMethod), .hash = Hash::parseNonSRIUnprefixed(rest, hashType), }; } diff --git a/src/libstore/content-address.hh b/src/libstore/content-address.hh index 01b771e52..c4d619bdc 100644 --- a/src/libstore/content-address.hh +++ b/src/libstore/content-address.hh @@ -5,6 +5,7 @@ #include "hash.hh" #include "path.hh" #include "comparator.hh" +#include "variant-wrapper.hh" namespace nix { @@ -71,11 +72,7 @@ struct ContentAddressMethod GENERATE_CMP(ContentAddressMethod, me->raw); - /* The moral equivalent of `using Raw::Raw;` */ - ContentAddressMethod(auto &&... arg) - : raw(std::forward(arg)...) - { } - + MAKE_WRAPPER_CONSTRUCTOR(ContentAddressMethod); /** * Parse the prefix tag which indicates how the files @@ -252,10 +249,7 @@ struct ContentAddressWithReferences GENERATE_CMP(ContentAddressWithReferences, me->raw); - /* The moral equivalent of `using Raw::Raw;` */ - ContentAddressWithReferences(auto &&... arg) - : raw(std::forward(arg)...) - { } + MAKE_WRAPPER_CONSTRUCTOR(ContentAddressWithReferences); /** * Create a `ContentAddressWithReferences` from a mere diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index f4e4980c2..0b8bdaf1c 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -32,7 +32,7 @@ std::optional DerivationOutput::path(const Store & store, std::string [](const DerivationOutput::Impure &) -> std::optional { return std::nullopt; }, - }, raw()); + }, raw); } @@ -60,7 +60,7 @@ bool DerivationType::isCA() const [](const Impure &) { return true; }, - }, raw()); + }, raw); } bool DerivationType::isFixed() const @@ -75,7 +75,7 @@ bool DerivationType::isFixed() const [](const Impure &) { return false; }, - }, raw()); + }, raw); } bool DerivationType::hasKnownOutputPaths() const @@ -90,7 +90,7 @@ bool DerivationType::hasKnownOutputPaths() const [](const Impure &) { return false; }, - }, raw()); + }, raw); } @@ -106,7 +106,7 @@ bool DerivationType::isSandboxed() const [](const Impure &) { return false; }, - }, raw()); + }, raw); } @@ -122,7 +122,7 @@ bool DerivationType::isPure() const [](const Impure &) { return false; }, - }, raw()); + }, raw); } @@ -408,13 +408,13 @@ std::string Derivation::unparse(const Store & store, bool maskOutputs, s += ','; printUnquotedString(s, ""); s += ','; printUnquotedString(s, ""); }, - [&](const DerivationOutputImpure & doi) { + [&](const DerivationOutput::Impure & doi) { // FIXME s += ','; printUnquotedString(s, ""); s += ','; printUnquotedString(s, doi.method.renderPrefix() + printHashType(doi.hashType)); s += ','; printUnquotedString(s, "impure"); } - }, i.second.raw()); + }, i.second.raw); s += ')'; } @@ -509,7 +509,7 @@ DerivationType BasicDerivation::type() const [&](const DerivationOutput::Impure &) { impureOutputs.insert(i.first); }, - }, i.second.raw()); + }, i.second.raw); } if (inputAddressedOutputs.empty() @@ -626,7 +626,7 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut if (type.isFixed()) { std::map outputHashes; for (const auto & i : drv.outputs) { - auto & dof = std::get(i.second.raw()); + auto & dof = std::get(i.second.raw); auto hash = hashString(htSHA256, "fixed:out:" + dof.ca.printMethodAlgo() + ":" + dof.ca.hash.to_string(Base16, false) + ":" @@ -663,7 +663,7 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut [](const DerivationType::Impure &) -> DrvHash::Kind { assert(false); } - }, drv.type().raw()); + }, drv.type().raw); std::map inputs2; for (auto & [drvPath, inputOutputs0] : drv.inputDrvs) { @@ -720,10 +720,10 @@ StringSet BasicDerivation::outputNames() const DerivationOutputsAndOptPaths BasicDerivation::outputsAndOptPaths(const Store & store) const { DerivationOutputsAndOptPaths outsAndOptPaths; - for (auto output : outputs) + for (auto & [outputName, output] : outputs) outsAndOptPaths.insert(std::make_pair( - output.first, - std::make_pair(output.second, output.second.path(store, name, output.first)) + outputName, + std::make_pair(output, output.path(store, name, outputName)) ) ); return outsAndOptPaths; @@ -798,7 +798,7 @@ void writeDerivation(Sink & out, const Store & store, const BasicDerivation & dr << (doi.method.renderPrefix() + printHashType(doi.hashType)) << "impure"; }, - }, i.second.raw()); + }, i.second.raw); } WorkerProto::write(store, WorkerProto::WriteConn { .to = out }, @@ -840,7 +840,7 @@ static void rewriteDerivation(Store & store, BasicDerivation & drv, const String auto hashModulo = hashDerivationModulo(store, Derivation(drv), true); for (auto & [outputName, output] : drv.outputs) { - if (std::holds_alternative(output.raw())) { + if (std::holds_alternative(output.raw)) { auto h = get(hashModulo.hashes, outputName); if (!h) throw Error("derivation '%s' output '%s' has no hash (derivations.cc/rewriteDerivation)", @@ -955,7 +955,7 @@ void Derivation::checkInvariants(Store & store, const StorePath & drvPath) const [&](const DerivationOutput::Impure &) { /* Nothing to check */ }, - }, i.second.raw()); + }, i.second.raw); } } @@ -984,7 +984,7 @@ nlohmann::json DerivationOutput::toJSON( res["hashAlgo"] = doi.method.renderPrefix() + printHashType(doi.hashType); res["impure"] = true; }, - }, raw()); + }, raw); return res; } diff --git a/src/libstore/derivations.hh b/src/libstore/derivations.hh index fa79f77fd..a92082089 100644 --- a/src/libstore/derivations.hh +++ b/src/libstore/derivations.hh @@ -9,6 +9,7 @@ #include "derived-path.hh" #include "sync.hh" #include "comparator.hh" +#include "variant-wrapper.hh" #include #include @@ -20,108 +21,110 @@ class Store; /* Abstract syntax of derivations. */ -/** - * The traditional non-fixed-output derivation type. - */ -struct DerivationOutputInputAddressed -{ - StorePath path; - - GENERATE_CMP(DerivationOutputInputAddressed, me->path); -}; - -/** - * Fixed-output derivations, whose output paths are content - * addressed according to that fixed output. - */ -struct DerivationOutputCAFixed -{ - /** - * Method and hash used for expected hash computation. - * - * References are not allowed by fiat. - */ - ContentAddress ca; - - /** - * Return the \ref StorePath "store path" corresponding to this output - * - * @param drvName The name of the derivation this is an output of, without the `.drv`. - * @param outputName The name of this output. - */ - StorePath path(const Store & store, std::string_view drvName, std::string_view outputName) const; - - GENERATE_CMP(DerivationOutputCAFixed, me->ca); -}; - -/** - * Floating-output derivations, whose output paths are content - * addressed, but not fixed, and so are dynamically calculated from - * whatever the output ends up being. - * */ -struct DerivationOutputCAFloating -{ - /** - * How the file system objects will be serialized for hashing - */ - ContentAddressMethod method; - - /** - * How the serialization will be hashed - */ - HashType hashType; - - GENERATE_CMP(DerivationOutputCAFloating, me->method, me->hashType); -}; - -/** - * Input-addressed output which depends on a (CA) derivation whose hash - * isn't known yet. - */ -struct DerivationOutputDeferred { - GENERATE_CMP(DerivationOutputDeferred); -}; - -/** - * Impure output which is moved to a content-addressed location (like - * CAFloating) but isn't registered as a realization. - */ -struct DerivationOutputImpure -{ - /** - * How the file system objects will be serialized for hashing - */ - ContentAddressMethod method; - - /** - * How the serialization will be hashed - */ - HashType hashType; - - GENERATE_CMP(DerivationOutputImpure, me->method, me->hashType); -}; - -typedef std::variant< - DerivationOutputInputAddressed, - DerivationOutputCAFixed, - DerivationOutputCAFloating, - DerivationOutputDeferred, - DerivationOutputImpure -> _DerivationOutputRaw; - /** * A single output of a BasicDerivation (and Derivation). */ -struct DerivationOutput : _DerivationOutputRaw +struct DerivationOutput { - using Raw = _DerivationOutputRaw; - using Raw::Raw; + /** + * The traditional non-fixed-output derivation type. + */ + struct InputAddressed + { + StorePath path; - using InputAddressed = DerivationOutputInputAddressed; - using CAFixed = DerivationOutputCAFixed; - using CAFloating = DerivationOutputCAFloating; - using Deferred = DerivationOutputDeferred; - using Impure = DerivationOutputImpure; + GENERATE_CMP(InputAddressed, me->path); + }; + + /** + * Fixed-output derivations, whose output paths are content + * addressed according to that fixed output. + */ + struct CAFixed + { + /** + * Method and hash used for expected hash computation. + * + * References are not allowed by fiat. + */ + ContentAddress ca; + + /** + * Return the \ref StorePath "store path" corresponding to this output + * + * @param drvName The name of the derivation this is an output of, without the `.drv`. + * @param outputName The name of this output. + */ + StorePath path(const Store & store, std::string_view drvName, std::string_view outputName) const; + + GENERATE_CMP(CAFixed, me->ca); + }; + + /** + * Floating-output derivations, whose output paths are content + * addressed, but not fixed, and so are dynamically calculated from + * whatever the output ends up being. + * */ + struct CAFloating + { + /** + * How the file system objects will be serialized for hashing + */ + ContentAddressMethod method; + + /** + * How the serialization will be hashed + */ + HashType hashType; + + GENERATE_CMP(CAFloating, me->method, me->hashType); + }; + + /** + * Input-addressed output which depends on a (CA) derivation whose hash + * isn't known yet. + */ + struct Deferred { + GENERATE_CMP(Deferred); + }; + + /** + * Impure output which is moved to a content-addressed location (like + * CAFloating) but isn't registered as a realization. + */ + struct Impure + { + /** + * How the file system objects will be serialized for hashing + */ + ContentAddressMethod method; + + /** + * How the serialization will be hashed + */ + HashType hashType; + + GENERATE_CMP(Impure, me->method, me->hashType); + }; + + typedef std::variant< + InputAddressed, + CAFixed, + CAFloating, + Deferred, + Impure + > Raw; + + Raw raw; + + GENERATE_CMP(DerivationOutput, me->raw); + + MAKE_WRAPPER_CONSTRUCTOR(DerivationOutput); + + /** + * Force choosing a variant + */ + DerivationOutput() = delete; /** * \note when you use this function you should make sure that you're @@ -131,10 +134,6 @@ struct DerivationOutput : _DerivationOutputRaw */ std::optional path(const Store & store, std::string_view drvName, std::string_view outputName) const; - inline const Raw & raw() const { - return static_cast(*this); - } - nlohmann::json toJSON( const Store & store, std::string_view drvName, @@ -167,61 +166,71 @@ typedef std::map DerivationInputs; -/** - * Input-addressed derivation types - */ -struct DerivationType_InputAddressed { +struct DerivationType { /** - * True iff the derivation type can't be determined statically, - * for instance because it (transitively) depends on a content-addressed - * derivation. - */ - bool deferred; -}; - -/** - * Content-addressed derivation types - */ -struct DerivationType_ContentAddressed { - /** - * Whether the derivation should be built safely inside a sandbox. + * Input-addressed derivation types */ - bool sandboxed; + struct InputAddressed { + /** + * True iff the derivation type can't be determined statically, + * for instance because it (transitively) depends on a content-addressed + * derivation. + */ + bool deferred; + + GENERATE_CMP(InputAddressed, me->deferred); + }; + /** - * Whether the derivation's outputs' content-addresses are "fixed" - * or "floating. - * - * - Fixed: content-addresses are written down as part of the - * derivation itself. If the outputs don't end up matching the - * build fails. - * - * - Floating: content-addresses are not written down, we do not - * know them until we perform the build. + * Content-addressed derivation types */ - bool fixed; -}; + struct ContentAddressed { + /** + * Whether the derivation should be built safely inside a sandbox. + */ + bool sandboxed; + /** + * Whether the derivation's outputs' content-addresses are "fixed" + * or "floating". + * + * - Fixed: content-addresses are written down as part of the + * derivation itself. If the outputs don't end up matching the + * build fails. + * + * - Floating: content-addresses are not written down, we do not + * know them until we perform the build. + */ + bool fixed; -/** - * Impure derivation type - * - * This is similar at buil-time to the content addressed, not standboxed, not fixed - * type, but has some restrictions on its usage. - */ -struct DerivationType_Impure { -}; + GENERATE_CMP(ContentAddressed, me->sandboxed, me->fixed); + }; -typedef std::variant< - DerivationType_InputAddressed, - DerivationType_ContentAddressed, - DerivationType_Impure -> _DerivationTypeRaw; + /** + * Impure derivation type + * + * This is similar at buil-time to the content addressed, not standboxed, not fixed + * type, but has some restrictions on its usage. + */ + struct Impure { + GENERATE_CMP(Impure); + }; -struct DerivationType : _DerivationTypeRaw { - using Raw = _DerivationTypeRaw; - using Raw::Raw; - using InputAddressed = DerivationType_InputAddressed; - using ContentAddressed = DerivationType_ContentAddressed; - using Impure = DerivationType_Impure; + typedef std::variant< + InputAddressed, + ContentAddressed, + Impure + > Raw; + + Raw raw; + + GENERATE_CMP(DerivationType, me->raw); + + MAKE_WRAPPER_CONSTRUCTOR(DerivationType); + + /** + * Force choosing a variant + */ + DerivationType() = delete; /** * Do the outputs of the derivation have paths calculated from their @@ -257,10 +266,6 @@ struct DerivationType : _DerivationTypeRaw { * closure, or if fixed output. */ bool hasKnownOutputPaths() const; - - inline const Raw & raw() const { - return static_cast(*this); - } }; struct BasicDerivation diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index 185d61c15..631213306 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -88,7 +88,7 @@ const ContentAddress * getDerivationCA(const BasicDerivation & drv) auto out = drv.outputs.find("out"); if (out == drv.outputs.end()) return nullptr; - if (auto dof = std::get_if(&out->second)) { + if (auto dof = std::get_if(&out->second.raw)) { return &dof->ca; } return nullptr; @@ -370,7 +370,7 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, } return outputsOpt; }, - }, bfd.outputs.raw()); + }, bfd.outputs.raw); OutputPathMap outputs; for (auto & [outputName, outputPathOpt] : outputsOpt) { @@ -418,7 +418,7 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd) [&](const OutputsSpec::Names & names) { return static_cast(names); }, - }, bfd.outputs.raw()); + }, bfd.outputs.raw); for (auto iter = outputMap.begin(); iter != outputMap.end();) { auto & outputName = iter->first; if (bfd.outputs.contains(outputName)) { @@ -431,7 +431,7 @@ OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd) if (!outputsLeft.empty()) throw Error("derivation '%s' does not have an outputs %s", store.printStorePath(drvPath), - concatStringsSep(", ", quoteStrings(std::get(bfd.outputs)))); + concatStringsSep(", ", quoteStrings(std::get(bfd.outputs.raw)))); return outputMap; } diff --git a/src/libstore/outputs-spec.cc b/src/libstore/outputs-spec.cc index e26c38138..d943bc111 100644 --- a/src/libstore/outputs-spec.cc +++ b/src/libstore/outputs-spec.cc @@ -17,7 +17,7 @@ bool OutputsSpec::contains(const std::string & outputName) const [&](const OutputsSpec::Names & outputNames) { return outputNames.count(outputName) > 0; }, - }, raw()); + }, raw); } static std::string outputSpecRegexStr = @@ -49,7 +49,7 @@ OutputsSpec OutputsSpec::parse(std::string_view s) std::optional spec = parseOpt(s); if (!spec) throw Error("invalid outputs specifier '%s'", s); - return *spec; + return std::move(*spec); } @@ -85,7 +85,7 @@ std::string OutputsSpec::to_string() const [&](const OutputsSpec::Names & outputNames) -> std::string { return concatStringsSep(",", outputNames); }, - }, raw()); + }, raw); } @@ -98,7 +98,7 @@ std::string ExtendedOutputsSpec::to_string() const [&](const ExtendedOutputsSpec::Explicit & outputSpec) -> std::string { return "^" + outputSpec.to_string(); }, - }, raw()); + }, raw); } @@ -118,9 +118,9 @@ OutputsSpec OutputsSpec::union_(const OutputsSpec & that) const ret.insert(thoseNames.begin(), thoseNames.end()); return ret; }, - }, that.raw()); + }, that.raw); }, - }, raw()); + }, raw); } @@ -142,9 +142,9 @@ bool OutputsSpec::isSubsetOf(const OutputsSpec & that) const ret = false; return ret; }, - }, raw()); + }, raw); }, - }, that.raw()); + }, that.raw); } } @@ -169,7 +169,7 @@ void adl_serializer::to_json(json & json, OutputsSpec t) { [&](const OutputsSpec::Names & names) { json = names; }, - }, t.raw()); + }, t.raw); } @@ -189,7 +189,7 @@ void adl_serializer::to_json(json & json, ExtendedOutputsSp [&](const ExtendedOutputsSpec::Explicit & e) { adl_serializer::to_json(json, e); }, - }, t.raw()); + }, t.raw); } } diff --git a/src/libstore/outputs-spec.hh b/src/libstore/outputs-spec.hh index 5a726fe90..ae19f1040 100644 --- a/src/libstore/outputs-spec.hh +++ b/src/libstore/outputs-spec.hh @@ -6,62 +6,57 @@ #include #include +#include "comparator.hh" #include "json-impls.hh" +#include "comparator.hh" +#include "variant-wrapper.hh" namespace nix { -/** - * A non-empty set of outputs, specified by name - */ -struct OutputNames : std::set { - using std::set::set; +struct OutputsSpec { + /** + * A non-empty set of outputs, specified by name + */ + struct Names : std::set { + using std::set::set; - /* These need to be "inherited manually" */ + /* These need to be "inherited manually" */ - OutputNames(const std::set & s) - : std::set(s) - { assert(!empty()); } + Names(const std::set & s) + : std::set(s) + { assert(!empty()); } + + /** + * Needs to be "inherited manually" + */ + Names(std::set && s) + : std::set(s) + { assert(!empty()); } + + /* This set should always be non-empty, so we delete this + constructor in order make creating empty ones by mistake harder. + */ + Names() = delete; + }; /** - * Needs to be "inherited manually" + * The set of all outputs, without needing to name them explicitly */ - OutputNames(std::set && s) - : std::set(s) - { assert(!empty()); } + struct All : std::monostate { }; - /* This set should always be non-empty, so we delete this - constructor in order make creating empty ones by mistake harder. - */ - OutputNames() = delete; -}; + typedef std::variant Raw; -/** - * The set of all outputs, without needing to name them explicitly - */ -struct AllOutputs : std::monostate { }; + Raw raw; -typedef std::variant _OutputsSpecRaw; + GENERATE_CMP(OutputsSpec, me->raw); -struct OutputsSpec : _OutputsSpecRaw { - using Raw = _OutputsSpecRaw; - using Raw::Raw; + MAKE_WRAPPER_CONSTRUCTOR(OutputsSpec); /** * Force choosing a variant */ OutputsSpec() = delete; - using Names = OutputNames; - using All = AllOutputs; - - inline const Raw & raw() const { - return static_cast(*this); - } - - inline Raw & raw() { - return static_cast(*this); - } - bool contains(const std::string & output) const; /** @@ -84,20 +79,22 @@ struct OutputsSpec : _OutputsSpecRaw { std::string to_string() const; }; -struct DefaultOutputs : std::monostate { }; - -typedef std::variant _ExtendedOutputsSpecRaw; - -struct ExtendedOutputsSpec : _ExtendedOutputsSpecRaw { - using Raw = _ExtendedOutputsSpecRaw; - using Raw::Raw; - - using Default = DefaultOutputs; +struct ExtendedOutputsSpec { + struct Default : std::monostate { }; using Explicit = OutputsSpec; - inline const Raw & raw() const { - return static_cast(*this); - } + typedef std::variant Raw; + + Raw raw; + + GENERATE_CMP(ExtendedOutputsSpec, me->raw); + + MAKE_WRAPPER_CONSTRUCTOR(ExtendedOutputsSpec); + + /** + * Force choosing a variant + */ + ExtendedOutputsSpec() = delete; /** * Parse a string of the form 'prefix^output1,...outputN' or diff --git a/src/libstore/path-with-outputs.cc b/src/libstore/path-with-outputs.cc index 81f360f3a..af6837370 100644 --- a/src/libstore/path-with-outputs.cc +++ b/src/libstore/path-with-outputs.cc @@ -63,7 +63,7 @@ StorePathWithOutputs::ParseResult StorePathWithOutputs::tryFromDerivedPath(const [&](const OutputsSpec::Names & outputs) { return static_cast(outputs); }, - }, bfd.outputs.raw()), + }, bfd.outputs.raw), }; }, [&](const SingleDerivedPath::Built &) -> StorePathWithOutputs::ParseResult { diff --git a/src/libutil/variant-wrapper.hh b/src/libutil/variant-wrapper.hh new file mode 100644 index 000000000..cedcb999c --- /dev/null +++ b/src/libutil/variant-wrapper.hh @@ -0,0 +1,30 @@ +#pragma once +///@file + +// not used, but will be used by callers +#include + +/** + * Force the default versions of all constructors (copy, move, copy + * assignment). + */ +#define FORCE_DEFAULT_CONSTRUCTORS(CLASS_NAME) \ + CLASS_NAME(const CLASS_NAME &) = default; \ + CLASS_NAME(CLASS_NAME &) = default; \ + CLASS_NAME(CLASS_NAME &&) = default; \ + \ + CLASS_NAME & operator =(const CLASS_NAME &) = default; \ + CLASS_NAME & operator =(CLASS_NAME &) = default; + +/** + * Make a wrapper constructor. All args are forwarded to the + * construction of the "raw" field. (Which we assume is the only one.) + * + * The moral equivalent of `using Raw::Raw;` + */ +#define MAKE_WRAPPER_CONSTRUCTOR(CLASS_NAME) \ + FORCE_DEFAULT_CONSTRUCTORS(CLASS_NAME) \ + \ + CLASS_NAME(auto &&... arg) \ + : raw(std::forward(arg)...) \ + { } diff --git a/src/nix/app.cc b/src/nix/app.cc index 16a921194..67c5ef344 100644 --- a/src/nix/app.cc +++ b/src/nix/app.cc @@ -81,7 +81,7 @@ UnresolvedApp InstallableValue::toApp(EvalState & state) .path = o.path, }; }, - }, c.raw())); + }, c.raw)); } return UnresolvedApp{App { diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc index 5a80f0308..fbc83b08e 100644 --- a/src/nix/bundle.cc +++ b/src/nix/bundle.cc @@ -80,7 +80,7 @@ struct CmdBundle : InstallableValueCommand auto [bundlerFlakeRef, bundlerName, extendedOutputsSpec] = parseFlakeRefWithFragmentAndExtendedOutputsSpec(bundler, absPath(".")); const flake::LockFlags lockFlags{ .writeLockFile = false }; InstallableFlake bundler{this, - evalState, std::move(bundlerFlakeRef), bundlerName, extendedOutputsSpec, + evalState, std::move(bundlerFlakeRef), bundlerName, std::move(extendedOutputsSpec), {"bundlers." + settings.thisSystem.get() + ".default", "defaultBundler." + settings.thisSystem.get() }, diff --git a/src/nix/develop.cc b/src/nix/develop.cc index c033804e4..c01ef1a2a 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -547,7 +547,7 @@ struct CmdDevelop : Common, MixEnvironment state, std::move(nixpkgs), "bashInteractive", - DefaultOutputs(), + ExtendedOutputsSpec::Default(), Strings{}, Strings{"legacyPackages." + settings.thisSystem.get() + "."}, nixpkgsLockFlags); diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 83b74c8ca..87dd4da1b 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -778,7 +778,7 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand auto [templateFlakeRef, templateName] = parseFlakeRefWithFragment(templateUrl, absPath(".")); auto installable = InstallableFlake(nullptr, - evalState, std::move(templateFlakeRef), templateName, DefaultOutputs(), + evalState, std::move(templateFlakeRef), templateName, ExtendedOutputsSpec::Default(), defaultTemplateAttrPaths, defaultTemplateAttrPathsPrefixes, lockFlags); From fe71faa920ef34b67f56232decc22cf5706a00dd Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 16 Aug 2023 12:04:15 -0400 Subject: [PATCH 039/299] Delete `EvalState::addToSearchPath` This function is now trivial enough that it doesn't need to exist. `EvalState` can still be initialized with a custom search path, but we don't have a need to mutate the search path after it has been constructed, and I don't see why we would need to in the future. Fixes #8229 --- src/libexpr/eval.cc | 4 ++-- src/libexpr/eval.hh | 2 -- src/libexpr/parser.y | 6 ------ 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 7e839dbe7..6d445fd96 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -527,9 +527,9 @@ EvalState::EvalState( /* Initialise the Nix expression search path. */ if (!evalSettings.pureEval) { for (auto & i : _searchPath.elements) - addToSearchPath(SearchPath::Elem {i}); + searchPath.elements.emplace_back(SearchPath::Elem {i}); for (auto & i : evalSettings.nixPath.get()) - addToSearchPath(SearchPath::Elem::parse(i)); + searchPath.elements.emplace_back(SearchPath::Elem::parse(i)); } if (evalSettings.restrictEval || evalSettings.pureEval) { diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 0c3eb6505..fa8fa462b 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -341,8 +341,6 @@ public: std::shared_ptr buildStore = nullptr); ~EvalState(); - void addToSearchPath(SearchPath::Elem && elem); - SearchPath getSearchPath() { return searchPath; } /** diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 201370b90..792f51fde 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -736,12 +736,6 @@ Expr * EvalState::parseStdin() } -void EvalState::addToSearchPath(SearchPath::Elem && elem) -{ - searchPath.elements.emplace_back(std::move(elem)); -} - - SourcePath EvalState::findFile(const std::string_view path) { return findFile(searchPath, path); From 52248b1c2786f7aa98109d47ee12f527bd202b12 Mon Sep 17 00:00:00 2001 From: Tom Bereknyei Date: Sat, 19 Aug 2023 17:03:31 -0400 Subject: [PATCH 040/299] feat: notation to refer to no attribute search prefix An attrPath prefix of "." indicates no need to try default attrPath prefixes. For example 1nixpkgs#legacyPackages.x86_64-linux.ERROR` searches through ``` trying flake output attribute 'packages.x86_64-linux.legacyPackages.x86_64-linux.ERROR' using cached attrset attribute '' trying flake output attribute 'legacyPackages.x86_64-linux.legacyPackages.x86_64-linux.ERROR' using cached attrset attribute 'legacyPackages.x86_64-linux' trying flake output attribute 'legacyPackages.x86_64-linux.ERROR' using cached attrset attribute 'legacyPackages.x86_64-linux' ``` And there is no way to specify that one does not want the automatic search behavior. Now one can specify `nixpkgs#.legacyPackages.x86_64-linux.ERROR` to only refer to the rooted attribute path without any default injection of attribute search path or system. --- src/libcmd/installable-flake.cc | 5 +++++ src/libcmd/installables.cc | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/libcmd/installable-flake.cc b/src/libcmd/installable-flake.cc index 4074da06d..2f428cb7e 100644 --- a/src/libcmd/installable-flake.cc +++ b/src/libcmd/installable-flake.cc @@ -28,6 +28,11 @@ namespace nix { std::vector InstallableFlake::getActualAttrPaths() { std::vector res; + if (attrPaths.size() == 1 && attrPaths.front().starts_with(".")){ + attrPaths.front().erase(0,1); + res.push_back(attrPaths.front()); + return res; + } for (auto & prefix : prefixes) res.push_back(prefix + *attrPaths.begin()); diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index eb1903084..01c01441c 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -301,6 +301,11 @@ void completeFlakeRefWithFragment( completionType = ctAttrs; auto fragment = prefix.substr(hash + 1); + std::string prefixRoot = ""; + if (fragment.starts_with(".")){ + fragment = fragment.substr(1); + prefixRoot = "."; + } auto flakeRefS = std::string(prefix.substr(0, hash)); auto flakeRef = parseFlakeRef(expandTilde(flakeRefS), absPath(".")); @@ -309,6 +314,9 @@ void completeFlakeRefWithFragment( auto root = evalCache->getRoot(); + if (prefixRoot == "."){ + attrPathPrefixes.clear(); + } /* Complete 'fragment' relative to all the attrpath prefixes as well as the root of the flake. */ @@ -333,7 +341,7 @@ void completeFlakeRefWithFragment( auto attrPath2 = (*attr)->getAttrPath(attr2); /* Strip the attrpath prefix. */ attrPath2.erase(attrPath2.begin(), attrPath2.begin() + attrPathPrefix.size()); - completions->add(flakeRefS + "#" + concatStringsSep(".", evalState->symbols.resolve(attrPath2))); + completions->add(flakeRefS + "#" + prefixRoot + concatStringsSep(".", evalState->symbols.resolve(attrPath2))); } } } @@ -344,7 +352,7 @@ void completeFlakeRefWithFragment( for (auto & attrPath : defaultFlakeAttrPaths) { auto attr = root->findAlongAttrPath(parseAttrPath(*evalState, attrPath)); if (!attr) continue; - completions->add(flakeRefS + "#"); + completions->add(flakeRefS + "#" + prefixRoot); } } } From c609be4072b03dc3eabcb2ac9916593847fd3b19 Mon Sep 17 00:00:00 2001 From: Tom Bereknyei Date: Sat, 19 Aug 2023 17:19:52 -0400 Subject: [PATCH 041/299] doc: explain the . attrPath prefix notation --- doc/manual/src/release-notes/rl-next.md | 2 ++ src/nix/nix.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 7ddb5ca00..56222a21b 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -22,3 +22,5 @@ - Introduce a new [`outputOf`](@docroot@/language/builtins.md#builtins-outputOf) builtin. It is part of the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. + +- Introduce new flake installable syntax `flakeref#.attrPath` where the "." prefix denotes no searching of default attribute prefixes like `packages.` or `legacyPackages.`. diff --git a/src/nix/nix.md b/src/nix/nix.md index e0f459d6b..6e7e8a649 100644 --- a/src/nix/nix.md +++ b/src/nix/nix.md @@ -132,6 +132,8 @@ subcommands, these are `packages.`*system*, attributes `packages.x86_64-linux.hello`, `legacyPackages.x86_64-linux.hello` and `hello`. +If *attrpath* begins with `.` then no prefixes or defaults are attempted. This allows the form *flakeref*[`#.`*attrpath*], such as `github:NixOS/nixpkgs#.lib.fakeSha256` to avoid a search of `packages.*system*.lib.fakeSha256` + ### Store path Example: `/nix/store/v5sv61sszx301i0x6xysaqzla09nksnd-hello-2.10` From 8130373be9a10681a94b0546242843db158f30d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 22:22:21 +0000 Subject: [PATCH 042/299] Bump zeebe-io/backport-action from 1.3.1 to 1.4.0 Bumps [zeebe-io/backport-action](https://github.com/zeebe-io/backport-action) from 1.3.1 to 1.4.0. - [Release notes](https://github.com/zeebe-io/backport-action/releases) - [Commits](https://github.com/zeebe-io/backport-action/compare/v1.3.1...v1.4.0) --- updated-dependencies: - dependency-name: zeebe-io/backport-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 816474ed5..62bef6322 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - name: Create backport PRs # should be kept in sync with `version` - uses: zeebe-io/backport-action@v1.3.1 + uses: zeebe-io/backport-action@v1.4.0 with: # Config README: https://github.com/zeebe-io/backport-action#backport-action github_token: ${{ secrets.GITHUB_TOKEN }} From 81045f243f91adcd44da0080c9ac83d2c4176125 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 22 Aug 2023 19:29:25 +0200 Subject: [PATCH 043/299] Tarball trees: Propagate lastModified This makes them behave consistently with GitHub/GitLab flakes. --- src/libfetchers/tarball.cc | 5 ++++- tests/tarball.sh | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index 107d38e92..e3a41e75e 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -232,7 +232,7 @@ struct CurlInputScheme : InputScheme if (type != inputType()) return {}; // FIXME: some of these only apply to TarballInputScheme. - std::set allowedNames = {"type", "url", "narHash", "name", "unpack", "rev", "revCount"}; + std::set allowedNames = {"type", "url", "narHash", "name", "unpack", "rev", "revCount", "lastModified"}; for (auto & [name, value] : attrs) if (!allowedNames.count(name)) throw Error("unsupported %s input attribute '%s'", *type, name); @@ -310,6 +310,9 @@ struct TarballInputScheme : CurlInputScheme input = immutableInput; } + if (result.lastModified && !input.attrs.contains("lastModified")) + input.attrs.insert_or_assign("lastModified", uint64_t(result.lastModified)); + return {result.tree.storePath, std::move(input)}; } }; diff --git a/tests/tarball.sh b/tests/tarball.sh index 5f39658c9..6e621a28c 100644 --- a/tests/tarball.sh +++ b/tests/tarball.sh @@ -9,6 +9,7 @@ rm -rf $tarroot mkdir -p $tarroot cp dependencies.nix $tarroot/default.nix cp config.nix dependencies.builder*.sh $tarroot/ +touch -d '@1000000000' $tarroot $tarroot/* hash=$(nix hash path $tarroot) @@ -36,6 +37,8 @@ test_tarball() { nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file:///does-not-exist/must-remain-unused/$tarball; narHash = \"$hash\"; })" expectStderr 102 nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc=\"; })" | grep 'NAR hash mismatch in input' + [[ $(nix eval --impure --expr "(fetchTree file://$tarball).lastModified") = 1000000000 ]] + nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })) ? submodules)" >&2 nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = file://$tarball; narHash = \"$hash\"; })) ? submodules)" 2>&1 | grep 'true' From 4a435ad2288bffd5c28d90758e4e26160b53fb8c Mon Sep 17 00:00:00 2001 From: Uri Zafrir Date: Wed, 23 Aug 2023 18:18:25 +0300 Subject: [PATCH 044/299] Add introductory sentence to advanced topics (#8861) --- doc/manual/src/advanced-topics/advanced-topics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/advanced-topics/advanced-topics.md b/doc/manual/src/advanced-topics/advanced-topics.md index 8b1378917..9a4d12a33 100644 --- a/doc/manual/src/advanced-topics/advanced-topics.md +++ b/doc/manual/src/advanced-topics/advanced-topics.md @@ -1 +1 @@ - +This section lists advanced topics related to builds and builds performance From 7d82341633d81f50f3e5e0b0896cfac5ea805d57 Mon Sep 17 00:00:00 2001 From: p01arst0rm Date: Wed, 23 Aug 2023 19:28:24 +0100 Subject: [PATCH 045/299] update system definitions --- flake.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 6e1f48cd7..cf7c1fa12 100644 --- a/flake.nix +++ b/flake.nix @@ -19,10 +19,12 @@ then "" else "pre${builtins.substring 0 8 (self.lastModifiedDate or self.lastModified or "19700101")}_${self.shortRev or "dirty"}"; + linux32BitSystems = [ "i686-linux" ]; linux64BitSystems = [ "x86_64-linux" "aarch64-linux" ]; - linuxSystems = linux64BitSystems ++ [ "i686-linux" ]; - systems = linuxSystems ++ [ "x86_64-darwin" "aarch64-darwin" ]; - + linuxSystems = linux32BitSystems ++ linux64BitSystems; + darwinSystems = [ "x86_64-darwin" "aarch64-darwin" ]; + systems = linuxSystems ++ darwinSystems; + crossSystems = [ "armv6l-linux" "armv7l-linux" ]; stdenvs = [ "gccStdenv" "clangStdenv" "clang11Stdenv" "stdenv" "libcxxStdenv" "ccacheStdenv" ]; From d5b130ef13bd9a77d803950ca814ac9a612f77b8 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 24 Aug 2023 10:00:17 +0200 Subject: [PATCH 046/299] glossary: dedent list and do not use forced line breaks this makes it slightly easier to work with and consistent with all the other markdown lists in use --- doc/manual/src/glossary.md | 367 ++++++++++++++++++++----------------- 1 file changed, 200 insertions(+), 167 deletions(-) diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index ac0bb3c2f..d950a4ca8 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -1,236 +1,269 @@ # Glossary - - [derivation]{#gloss-derivation}\ - A description of a build task. The result of a derivation is a - store object. Derivations are typically specified in Nix expressions - using the [`derivation` primitive](./language/derivations.md). These are - translated into low-level *store derivations* (implicitly by - `nix-env` and `nix-build`, or explicitly by `nix-instantiate`). +- [derivation]{#gloss-derivation} - [derivation]: #gloss-derivation + A description of a build task. The result of a derivation is a + store object. Derivations are typically specified in Nix expressions + using the [`derivation` primitive](./language/derivations.md). These are + translated into low-level *store derivations* (implicitly by + `nix-env` and `nix-build`, or explicitly by `nix-instantiate`). - - [store derivation]{#gloss-store-derivation}\ - A [derivation] represented as a `.drv` file in the [store]. - It has a [store path], like any [store object]. + [derivation]: #gloss-derivation - Example: `/nix/store/g946hcz4c8mdvq2g8vxx42z51qb71rvp-git-2.38.1.drv` +- [store derivation]{#gloss-store-derivation} - See [`nix derivation show`](./command-ref/new-cli/nix3-derivation-show.md) (experimental) for displaying the contents of store derivations. + A [derivation] represented as a `.drv` file in the [store]. + It has a [store path], like any [store object]. - [store derivation]: #gloss-store-derivation + Example: `/nix/store/g946hcz4c8mdvq2g8vxx42z51qb71rvp-git-2.38.1.drv` - - [instantiate]{#gloss-instantiate}, instantiation\ - Translate a [derivation] into a [store derivation]. + See [`nix derivation show`](./command-ref/new-cli/nix3-derivation-show.md) (experimental) for displaying the contents of store derivations. - See [`nix-instantiate`](./command-ref/nix-instantiate.md). + [store derivation]: #gloss-store-derivation - [instantiate]: #gloss-instantiate +- [instantiate]{#gloss-instantiate}, instantiation - - [realise]{#gloss-realise}, realisation\ - Ensure a [store path] is [valid][validity]. + Translate a [derivation] into a [store derivation]. - This means either running the `builder` executable as specified in the corresponding [derivation] or fetching a pre-built [store object] from a [substituter]. + See [`nix-instantiate`](./command-ref/nix-instantiate.md). - See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md). + [instantiate]: #gloss-instantiate - See [`nix build`](./command-ref/new-cli/nix3-build.md) (experimental). +- [realise]{#gloss-realise}, realisation - [realise]: #gloss-realise + Ensure a [store path] is [valid][validity]. - - [content-addressed derivation]{#gloss-content-addressed-derivation}\ - A derivation which has the - [`__contentAddressed`](./language/advanced-attributes.md#adv-attr-__contentAddressed) - attribute set to `true`. + This means either running the `builder` executable as specified in the corresponding [derivation] or fetching a pre-built [store object] from a [substituter]. - - [fixed-output derivation]{#gloss-fixed-output-derivation}\ - A derivation which includes the - [`outputHash`](./language/advanced-attributes.md#adv-attr-outputHash) attribute. + See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md). - - [store]{#gloss-store}\ - The location in the file system where store objects live. Typically - `/nix/store`. + See [`nix build`](./command-ref/new-cli/nix3-build.md) (experimental). - From the perspective of the location where Nix is - invoked, the Nix store can be referred to - as a "_local_" or a "_remote_" one: + [realise]: #gloss-realise - + A [local store]{#gloss-local-store} exists on the filesystem of - the machine where Nix is invoked. You can use other - local stores by passing the `--store` flag to the - `nix` command. Local stores can be used for building derivations. +- [content-addressed derivation]{#gloss-content-addressed-derivation} - + A *remote store* exists anywhere other than the - local filesystem. One example is the `/nix/store` - directory on another machine, accessed via `ssh` or - served by the `nix-serve` Perl script. + A derivation which has the + [`__contentAddressed`](./language/advanced-attributes.md#adv-attr-__contentAddressed) + attribute set to `true`. - [store]: #gloss-store - [local store]: #gloss-local-store +- [fixed-output derivation]{#gloss-fixed-output-derivation} - - [chroot store]{#gloss-chroot-store}\ - A [local store] whose canonical path is anything other than `/nix/store`. + A derivation which includes the + [`outputHash`](./language/advanced-attributes.md#adv-attr-outputHash) attribute. - - [binary cache]{#gloss-binary-cache}\ - A *binary cache* is a Nix store which uses a different format: its - metadata and signatures are kept in `.narinfo` files rather than in a - [Nix database]. This different format simplifies serving store objects - over the network, but cannot host builds. Examples of binary caches - include S3 buckets and the [NixOS binary cache](https://cache.nixos.org). +- [store]{#gloss-store} - - [store path]{#gloss-store-path}\ - The location of a [store object] in the file system, i.e., an - immediate child of the Nix store directory. + The location in the file system where store objects live. Typically + `/nix/store`. - Example: `/nix/store/a040m110amc4h71lds2jmr8qrkj2jhxd-git-2.38.1` + From the perspective of the location where Nix is + invoked, the Nix store can be referred to + as a "_local_" or a "_remote_" one: - [store path]: #gloss-store-path + + A [local store]{#gloss-local-store} exists on the filesystem of + the machine where Nix is invoked. You can use other + local stores by passing the `--store` flag to the + `nix` command. Local stores can be used for building derivations. - - [file system object]{#gloss-store-object}\ - The Nix data model for representing simplified file system data. + + A *remote store* exists anywhere other than the + local filesystem. One example is the `/nix/store` + directory on another machine, accessed via `ssh` or + served by the `nix-serve` Perl script. - See [File System Object](@docroot@/architecture/file-system-object.md) for details. + [store]: #gloss-store + [local store]: #gloss-local-store - [file system object]: #gloss-file-system-object +- [chroot store]{#gloss-chroot-store} - - [store object]{#gloss-store-object}\ + A [local store] whose canonical path is anything other than `/nix/store`. - A store object consists of a [file system object], [reference]s to other store objects, and other metadata. - It can be referred to by a [store path]. +- [binary cache]{#gloss-binary-cache} - [store object]: #gloss-store-object + A *binary cache* is a Nix store which uses a different format: its + metadata and signatures are kept in `.narinfo` files rather than in a + [Nix database]. This different format simplifies serving store objects + over the network, but cannot host builds. Examples of binary caches + include S3 buckets and the [NixOS binary cache](https://cache.nixos.org). - - [input-addressed store object]{#gloss-input-addressed-store-object}\ - A store object produced by building a - non-[content-addressed](#gloss-content-addressed-derivation), - non-[fixed-output](#gloss-fixed-output-derivation) - derivation. +- [store path]{#gloss-store-path} - - [output-addressed store object]{#gloss-output-addressed-store-object}\ - A [store object] whose [store path] is determined by its contents. - This includes derivations, the outputs of [content-addressed derivations](#gloss-content-addressed-derivation), and the outputs of [fixed-output derivations](#gloss-fixed-output-derivation). + The location of a [store object] in the file system, i.e., an + immediate child of the Nix store directory. - - [substitute]{#gloss-substitute}\ - A substitute is a command invocation stored in the [Nix database] that - describes how to build a store object, bypassing the normal build - mechanism (i.e., derivations). Typically, the substitute builds the - store object by downloading a pre-built version of the store object - from some server. + Example: `/nix/store/a040m110amc4h71lds2jmr8qrkj2jhxd-git-2.38.1` - - [substituter]{#gloss-substituter}\ - An additional [store]{#gloss-store} from which Nix can obtain store objects instead of building them. - Often the substituter is a [binary cache](#gloss-binary-cache), but any store can serve as substituter. + [store path]: #gloss-store-path - See the [`substituters` configuration option](./command-ref/conf-file.md#conf-substituters) for details. +- [file system object]{#gloss-store-object} - [substituter]: #gloss-substituter + The Nix data model for representing simplified file system data. - - [purity]{#gloss-purity}\ - The assumption that equal Nix derivations when run always produce - the same output. This cannot be guaranteed in general (e.g., a - builder can rely on external inputs such as the network or the - system time) but the Nix model assumes it. + See [File System Object](@docroot@/architecture/file-system-object.md) for details. - - [Nix database]{#gloss-nix-database}\ - An SQlite database to track [reference]s between [store object]s. - This is an implementation detail of the [local store]. + [file system object]: #gloss-file-system-object - Default location: `/nix/var/nix/db`. +- [store object]{#gloss-store-object} - [Nix database]: #gloss-nix-database - - [Nix expression]{#gloss-nix-expression}\ - A high-level description of software packages and compositions - thereof. Deploying software using Nix entails writing Nix - expressions for your packages. Nix expressions are translated to - derivations that are stored in the Nix store. These derivations can - then be built. + A store object consists of a [file system object], [reference]s to other store objects, and other metadata. + It can be referred to by a [store path]. - - [reference]{#gloss-reference}\ - A [store object] `O` is said to have a *reference* to a store object `P` if a [store path] to `P` appears in the contents of `O`. + [store object]: #gloss-store-object - Store objects can refer to both other store objects and themselves. - References from a store object to itself are called *self-references*. - References other than a self-reference must not form a cycle. +- [input-addressed store object]{#gloss-input-addressed-store-object} - [reference]: #gloss-reference + A store object produced by building a + non-[content-addressed](#gloss-content-addressed-derivation), + non-[fixed-output](#gloss-fixed-output-derivation) + derivation. - - [reachable]{#gloss-reachable}\ - A store path `Q` is reachable from another store path `P` if `Q` - is in the *closure* of the *references* relation. +- [output-addressed store object]{#gloss-output-addressed-store-object} - - [closure]{#gloss-closure}\ - The closure of a store path is the set of store paths that are - directly or indirectly “reachable” from that store path; that is, - it’s the closure of the path under the *references* relation. For - a package, the closure of its derivation is equivalent to the - build-time dependencies, while the closure of its output path is - equivalent to its runtime dependencies. For correct deployment it - is necessary to deploy whole closures, since otherwise at runtime - files could be missing. The command `nix-store --query --requisites ` prints out - closures of store paths. + A [store object] whose [store path] is determined by its contents. + This includes derivations, the outputs of [content-addressed derivations](#gloss-content-addressed-derivation), and the outputs of [fixed-output derivations](#gloss-fixed-output-derivation). - As an example, if the [store object] at path `P` contains a [reference] - to a store object at path `Q`, then `Q` is in the closure of `P`. Further, if `Q` - references `R` then `R` is also in the closure of `P`. +- [substitute]{#gloss-substitute} - [closure]: #gloss-closure + A substitute is a command invocation stored in the [Nix database] that + describes how to build a store object, bypassing the normal build + mechanism (i.e., derivations). Typically, the substitute builds the + store object by downloading a pre-built version of the store object + from some server. - - [output path]{#gloss-output-path}\ - A [store path] produced by a [derivation]. +- [substituter]{#gloss-substituter} - [output path]: #gloss-output-path + An additional [store]{#gloss-store} from which Nix can obtain store objects instead of building them. + Often the substituter is a [binary cache](#gloss-binary-cache), but any store can serve as substituter. - - [deriver]{#gloss-deriver}\ - The [store derivation] that produced an [output path]. + See the [`substituters` configuration option](./command-ref/conf-file.md#conf-substituters) for details. - - [validity]{#gloss-validity}\ - A store path is valid if all [store object]s in its [closure] can be read from the [store]. + [substituter]: #gloss-substituter - For a [local store], this means: - - The store path leads to an existing [store object] in that [store]. - - The store path is listed in the [Nix database] as being valid. - - All paths in the store path's [closure] are valid. +- [purity]{#gloss-purity} - [validity]: #gloss-validity + The assumption that equal Nix derivations when run always produce + the same output. This cannot be guaranteed in general (e.g., a + builder can rely on external inputs such as the network or the + system time) but the Nix model assumes it. - - [user environment]{#gloss-user-env}\ - An automatically generated store object that consists of a set of - symlinks to “active” applications, i.e., other store paths. These - are generated automatically by - [`nix-env`](./command-ref/nix-env.md). See *profiles*. +- [Nix database]{#gloss-nix-database} - - [profile]{#gloss-profile}\ - A symlink to the current *user environment* of a user, e.g., - `/nix/var/nix/profiles/default`. + An SQlite database to track [reference]s between [store object]s. + This is an implementation detail of the [local store]. - - [installable]{#gloss-installable}\ - Something that can be realised in the Nix store. + Default location: `/nix/var/nix/db`. - See [installables](./command-ref/new-cli/nix.md#installables) for [`nix` commands](./command-ref/new-cli/nix.md) (experimental) for details. + [Nix database]: #gloss-nix-database - - [NAR]{#gloss-nar}\ - A *N*ix *AR*chive. This is a serialisation of a path in the Nix - store. It can contain regular files, directories and symbolic - links. NARs are generated and unpacked using `nix-store --dump` - and `nix-store --restore`. +- [Nix expression]{#gloss-nix-expression} - - [`∅`]{#gloss-emtpy-set}\ - The empty set symbol. In the context of profile history, this denotes a package is not present in a particular version of the profile. + A high-level description of software packages and compositions + thereof. Deploying software using Nix entails writing Nix + expressions for your packages. Nix expressions are translated to + derivations that are stored in the Nix store. These derivations can + then be built. - - [`ε`]{#gloss-epsilon}\ - The epsilon symbol. In the context of a package, this means the version is empty. More precisely, the derivation does not have a version attribute. +- [reference]{#gloss-reference} - - [string interpolation]{#gloss-string-interpolation}\ - Expanding expressions enclosed in `${ }` within a [string], [path], or [attribute name]. + A [store object] `O` is said to have a *reference* to a store object `P` if a [store path] to `P` appears in the contents of `O`. - See [String interpolation](./language/string-interpolation.md) for details. + Store objects can refer to both other store objects and themselves. + References from a store object to itself are called *self-references*. + References other than a self-reference must not form a cycle. - [string]: ./language/values.md#type-string - [path]: ./language/values.md#type-path - [attribute name]: ./language/values.md#attribute-set + [reference]: #gloss-reference - - [experimental feature]{#gloss-experimental-feature}\ - Not yet stabilized functionality guarded by named experimental feature flags. - These flags are enabled or disabled with the [`experimental-features`](./command-ref/conf-file.html#conf-experimental-features) setting. +- [reachable]{#gloss-reachable} - See the contribution guide on the [purpose and lifecycle of experimental feaures](@docroot@/contributing/experimental-features.md). + A store path `Q` is reachable from another store path `P` if `Q` + is in the *closure* of the *references* relation. + +- [closure]{#gloss-closure} + + The closure of a store path is the set of store paths that are + directly or indirectly “reachable” from that store path; that is, + it’s the closure of the path under the *references* relation. For + a package, the closure of its derivation is equivalent to the + build-time dependencies, while the closure of its output path is + equivalent to its runtime dependencies. For correct deployment it + is necessary to deploy whole closures, since otherwise at runtime + files could be missing. The command `nix-store --query --requisites ` prints out + closures of store paths. + + As an example, if the [store object] at path `P` contains a [reference] + to a store object at path `Q`, then `Q` is in the closure of `P`. Further, if `Q` + references `R` then `R` is also in the closure of `P`. + + [closure]: #gloss-closure + +- [output path]{#gloss-output-path} + + A [store path] produced by a [derivation]. + + [output path]: #gloss-output-path + +- [deriver]{#gloss-deriver} + + The [store derivation] that produced an [output path]. + +- [validity]{#gloss-validity} + + A store path is valid if all [store object]s in its [closure] can be read from the [store]. + + For a [local store], this means: + - The store path leads to an existing [store object] in that [store]. + - The store path is listed in the [Nix database] as being valid. + - All paths in the store path's [closure] are valid. + + [validity]: #gloss-validity + +- [user environment]{#gloss-user-env} + + An automatically generated store object that consists of a set of + symlinks to “active” applications, i.e., other store paths. These + are generated automatically by + [`nix-env`](./command-ref/nix-env.md). See *profiles*. + +- [profile]{#gloss-profile} + + A symlink to the current *user environment* of a user, e.g., + `/nix/var/nix/profiles/default`. + +- [installable]{#gloss-installable} + + Something that can be realised in the Nix store. + + See [installables](./command-ref/new-cli/nix.md#installables) for [`nix` commands](./command-ref/new-cli/nix.md) (experimental) for details. + +- [NAR]{#gloss-nar} + + A *N*ix *AR*chive. This is a serialisation of a path in the Nix + store. It can contain regular files, directories and symbolic + links. NARs are generated and unpacked using `nix-store --dump` + and `nix-store --restore`. + +- [`∅`]{#gloss-emtpy-set} + + The empty set symbol. In the context of profile history, this denotes a package is not present in a particular version of the profile. + +- [`ε`]{#gloss-epsilon} + + The epsilon symbol. In the context of a package, this means the version is empty. More precisely, the derivation does not have a version attribute. + +- [string interpolation]{#gloss-string-interpolation} + + Expanding expressions enclosed in `${ }` within a [string], [path], or [attribute name]. + + See [String interpolation](./language/string-interpolation.md) for details. + + [string]: ./language/values.md#type-string + [path]: ./language/values.md#type-path + [attribute name]: ./language/values.md#attribute-set + +- [experimental feature]{#gloss-experimental-feature} + + Not yet stabilized functionality guarded by named experimental feature flags. + These flags are enabled or disabled with the [`experimental-features`](./command-ref/conf-file.html#conf-experimental-features) setting. + + See the contribution guide on the [purpose and lifecycle of experimental feaures](@docroot@/contributing/experimental-features.md). From 925a444b925590df90e19d3c0071936f87d2b43d Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Wed, 23 Aug 2023 12:00:00 +0000 Subject: [PATCH 047/299] add nix-store --query --valid-derivers command notably useful when nix-store --query --deriver returns a non-existing path. Co-authored-by: Felix Uhl --- doc/manual/src/command-ref/nix-store/query.md | 14 +++++++++++--- doc/manual/src/release-notes/rl-next.md | 4 ++++ src/nix-store/nix-store.cc | 18 +++++++++++++++++- tests/check-refs.nix | 2 +- tests/dependencies.nix | 13 +++++++++++++ tests/dependencies.sh | 17 +++++++++++++++++ tests/dyn-drv/eval-outputOf.sh | 4 ++-- tests/export-graph.nix | 4 ++-- tests/remote-store.sh | 2 +- 9 files changed, 68 insertions(+), 10 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/query.md b/doc/manual/src/command-ref/nix-store/query.md index cd45a4932..a158c76aa 100644 --- a/doc/manual/src/command-ref/nix-store/query.md +++ b/doc/manual/src/command-ref/nix-store/query.md @@ -5,8 +5,8 @@ # Synopsis `nix-store` {`--query` | `-q`} - {`--outputs` | `--requisites` | `-R` | `--references` | - `--referrers` | `--referrers-closure` | `--deriver` | `-d` | + {`--outputs` | `--requisites` | `-R` | `--references` | `--referrers` | + `--referrers-closure` | `--deriver` | `-d` | `--valid-derivers` | `--graph` | `--tree` | `--binding` *name* | `-b` *name* | `--hash` | `--size` | `--roots`} [`--use-output`] [`-u`] [`--force-realise`] [`-f`] @@ -82,13 +82,21 @@ symlink. in the Nix store that are dependent on *paths*. - `--deriver`; `-d`\ - Prints the [deriver] of the store paths *paths*. If + Prints the [deriver] that was used to build the store paths *paths*. If the path has no deriver (e.g., if it is a source file), or if the deriver is not known (e.g., in the case of a binary-only deployment), the string `unknown-deriver` is printed. + The returned deriver is not guaranteed to exist in the local store, for + example when *paths* were substituted from a binary cache. + Use `--valid-derivers` instead to obtain valid paths only. [deriver]: ../../glossary.md#gloss-deriver + - `--valid-derivers`\ + Prints a set of derivation files (`.drv`) which are supposed produce + said paths when realized. Might print nothing, for example for source paths + or paths subsituted from a binary cache. + - `--graph`\ Prints the references graph of the store paths *paths* in the format of the `dot` tool of AT\&T's [Graphviz diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 7ddb5ca00..66fe0c427 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -22,3 +22,7 @@ - Introduce a new [`outputOf`](@docroot@/language/builtins.md#builtins-outputOf) builtin. It is part of the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. + +- [`nix-store --query`](@docroot@/command-ref/nix-store/query.md) gained a new type of query: `--valid-derivers`. It returns all `.drv` files in the local store that *can be* used to build the output passed in argument. +This is in contrast to `--deriver`, which returns the single `.drv` file that *was actually* used to build the output passed in argument. In case the output was substituted from a binary cache, +this `.drv` file may only exist on said binary cache and not locally. diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 94956df66..96c3f7d7e 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -283,7 +283,7 @@ static void opQuery(Strings opFlags, Strings opArgs) { enum QueryType { qOutputs, qRequisites, qReferences, qReferrers - , qReferrersClosure, qDeriver, qBinding, qHash, qSize + , qReferrersClosure, qDeriver, qValidDerivers, qBinding, qHash, qSize , qTree, qGraph, qGraphML, qResolve, qRoots }; std::optional query; bool useOutput = false; @@ -299,6 +299,7 @@ static void opQuery(Strings opFlags, Strings opArgs) else if (i == "--referrers" || i == "--referers") query = qReferrers; else if (i == "--referrers-closure" || i == "--referers-closure") query = qReferrersClosure; else if (i == "--deriver" || i == "-d") query = qDeriver; + else if (i == "--valid-derivers") query = qValidDerivers; else if (i == "--binding" || i == "-b") { if (opArgs.size() == 0) throw UsageError("expected binding name"); @@ -372,6 +373,21 @@ static void opQuery(Strings opFlags, Strings opArgs) } break; + case qValidDerivers: { + StorePathSet result; + for (auto & i : opArgs) { + auto derivers = store->queryValidDerivers(store->followLinksToStorePath(i)); + for (const auto &i: derivers) { + result.insert(i); + } + } + auto sorted = store->topoSortPaths(result); + for (StorePaths::reverse_iterator i = sorted.rbegin(); + i != sorted.rend(); ++i) + cout << fmt("%s\n", store->printStorePath(*i)); + break; + } + case qBinding: for (auto & i : opArgs) { auto path = useDeriver(store->followLinksToStorePath(i)); diff --git a/tests/check-refs.nix b/tests/check-refs.nix index 99d69a226..89690e456 100644 --- a/tests/check-refs.nix +++ b/tests/check-refs.nix @@ -2,7 +2,7 @@ with import ./config.nix; rec { - dep = import ./dependencies.nix; + dep = import ./dependencies.nix {}; makeTest = nr: args: mkDerivation ({ name = "check-refs-" + toString nr; diff --git a/tests/dependencies.nix b/tests/dependencies.nix index 45aca1793..be1a7ae9a 100644 --- a/tests/dependencies.nix +++ b/tests/dependencies.nix @@ -1,3 +1,4 @@ +{ hashInvalidator ? "" }: with import ./config.nix; let { @@ -21,6 +22,17 @@ let { ''; }; + fod_input = mkDerivation { + name = "fod-input"; + buildCommand = '' + echo ${hashInvalidator} + echo FOD > $out + ''; + outputHashMode = "flat"; + outputHashAlgo = "sha256"; + outputHash = "1dq9p0hnm1y75q2x40fws5887bq1r840hzdxak0a9djbwvx0b16d"; + }; + body = mkDerivation { name = "dependencies-top"; builder = ./dependencies.builder0.sh + "/FOOBAR/../."; @@ -29,6 +41,7 @@ let { input1_drv = input1; input2_drv = input2; input0_drv = input0; + fod_input_drv = fod_input; meta.description = "Random test package"; }; diff --git a/tests/dependencies.sh b/tests/dependencies.sh index d5cd30396..b93dacac0 100644 --- a/tests/dependencies.sh +++ b/tests/dependencies.sh @@ -53,3 +53,20 @@ nix-store -q --referrers-closure "$input2OutPath" | grep "$outPath" # Check that the derivers are set properly. test $(nix-store -q --deriver "$outPath") = "$drvPath" nix-store -q --deriver "$input2OutPath" | grepQuiet -- "-input-2.drv" + +# --valid-derivers returns the currently single valid .drv file +test "$(nix-store -q --valid-derivers "$outPath")" = "$drvPath" + +# instantiate a different drv with the same output +drvPath2=$(nix-instantiate dependencies.nix --argstr hashInvalidator yay) + +# now --valid-derivers returns both +test "$(nix-store -q --valid-derivers "$outPath" | sort)" = "$(sort <<< "$drvPath"$'\n'"$drvPath2")" + +# check that nix-store --valid-derivers only returns existing drv +nix-store --delete "$drvPath" +test "$(nix-store -q --valid-derivers "$outPath")" = "$drvPath2" + +# check that --valid-derivers returns nothing when there are no valid derivers +nix-store --delete "$drvPath2" +test -z "$(nix-store -q --valid-derivers "$outPath")" diff --git a/tests/dyn-drv/eval-outputOf.sh b/tests/dyn-drv/eval-outputOf.sh index 99d917c06..9467feb8d 100644 --- a/tests/dyn-drv/eval-outputOf.sh +++ b/tests/dyn-drv/eval-outputOf.sh @@ -13,14 +13,14 @@ nix --experimental-features 'nix-command' eval --impure --expr \ # the future so it does work, but there are some design questions to # resolve first. Adding a test so we don't liberalise it by accident. expectStderr 1 nix --experimental-features 'nix-command dynamic-derivations' eval --impure --expr \ - 'builtins.outputOf (import ../dependencies.nix) "out"' \ + 'builtins.outputOf (import ../dependencies.nix {}) "out"' \ | grepQuiet "value is a set while a string was expected" # Test that "DrvDeep" string contexts are not supported at this time # # Like the above, this is a restriction we could relax later. expectStderr 1 nix --experimental-features 'nix-command dynamic-derivations' eval --impure --expr \ - 'builtins.outputOf (import ../dependencies.nix).drvPath "out"' \ + 'builtins.outputOf (import ../dependencies.nix {}).drvPath "out"' \ | grepQuiet "has a context which refers to a complete source and binary closure. This is not supported at this time" # Test using `builtins.outputOf` with static derivations diff --git a/tests/export-graph.nix b/tests/export-graph.nix index fdac9583d..64fe36bd1 100644 --- a/tests/export-graph.nix +++ b/tests/export-graph.nix @@ -17,13 +17,13 @@ rec { foo."bar.runtimeGraph" = mkDerivation { name = "dependencies"; builder = builtins.toFile "build-graph-builder" "${printRefs}"; - exportReferencesGraph = ["refs" (import ./dependencies.nix)]; + exportReferencesGraph = ["refs" (import ./dependencies.nix {})]; }; foo."bar.buildGraph" = mkDerivation { name = "dependencies"; builder = builtins.toFile "build-graph-builder" "${printRefs}"; - exportReferencesGraph = ["refs" (import ./dependencies.nix).drvPath]; + exportReferencesGraph = ["refs" (import ./dependencies.nix {}).drvPath]; }; } diff --git a/tests/remote-store.sh b/tests/remote-store.sh index ea32a20d3..50e6f24b9 100644 --- a/tests/remote-store.sh +++ b/tests/remote-store.sh @@ -24,7 +24,7 @@ fi import ( mkDerivation { name = "foo"; - bla = import ./dependencies.nix; + bla = import ./dependencies.nix {}; buildCommand = " echo \\\"hi\\\" > $out "; From 2f5d3da8062ae58242a8de2bad470a66478edea4 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 25 Aug 2023 09:53:12 -0400 Subject: [PATCH 048/299] Introduce `OutputName` and `OutputNameView` type aliases Hopefully they make the code easier to understand! --- src/libexpr/primops.cc | 2 +- src/libstore/build/local-derivation-goal.cc | 2 +- src/libstore/build/local-derivation-goal.hh | 2 +- src/libstore/derivations.cc | 12 +++++----- src/libstore/derivations.hh | 12 +++++----- src/libstore/derived-path.cc | 4 ++-- src/libstore/derived-path.hh | 4 ++-- src/libstore/downstream-placeholder.cc | 4 ++-- src/libstore/downstream-placeholder.hh | 4 ++-- src/libstore/outputs-spec.hh | 26 +++++++++++++++------ src/libstore/realisation.hh | 6 ++--- 11 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 5067da449..6b99b91e4 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1843,7 +1843,7 @@ static void prim_outputOf(EvalState & state, const PosIdx pos, Value * * args, V { SingleDerivedPath drvPath = state.coerceToSingleDerivedPath(pos, *args[0], "while evaluating the first argument to builtins.outputOf"); - std::string_view outputName = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument to builtins.outputOf"); + OutputNameView outputName = state.forceStringNoCtx(*args[1], pos, "while evaluating the second argument to builtins.outputOf"); state.mkSingleDerivedPathString( SingleDerivedPath::Built { diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 78f943d1f..64b55ca6a 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -2955,7 +2955,7 @@ bool LocalDerivationGoal::isReadDesc(int fd) } -StorePath LocalDerivationGoal::makeFallbackPath(std::string_view outputName) +StorePath LocalDerivationGoal::makeFallbackPath(OutputNameView outputName) { return worker.store.makeStorePath( "rewrite:" + std::string(drvPath.to_string()) + ":name:" + std::string(outputName), diff --git a/src/libstore/build/local-derivation-goal.hh b/src/libstore/build/local-derivation-goal.hh index 8827bfca3..0a05081c7 100644 --- a/src/libstore/build/local-derivation-goal.hh +++ b/src/libstore/build/local-derivation-goal.hh @@ -297,7 +297,7 @@ struct LocalDerivationGoal : public DerivationGoal * @todo Add option to randomize, so we can audit whether our * rewrites caught everything */ - StorePath makeFallbackPath(std::string_view outputName); + StorePath makeFallbackPath(OutputNameView outputName); }; } diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index 0b8bdaf1c..dc32c3847 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -12,7 +12,7 @@ namespace nix { -std::optional DerivationOutput::path(const Store & store, std::string_view drvName, std::string_view outputName) const +std::optional DerivationOutput::path(const Store & store, std::string_view drvName, OutputNameView outputName) const { return std::visit(overloaded { [](const DerivationOutput::InputAddressed & doi) -> std::optional { @@ -36,7 +36,7 @@ std::optional DerivationOutput::path(const Store & store, std::string } -StorePath DerivationOutput::CAFixed::path(const Store & store, std::string_view drvName, std::string_view outputName) const +StorePath DerivationOutput::CAFixed::path(const Store & store, std::string_view drvName, OutputNameView outputName) const { return store.makeFixedOutputPathFromCA( outputPathName(drvName, outputName), @@ -466,7 +466,7 @@ bool isDerivation(std::string_view fileName) } -std::string outputPathName(std::string_view drvName, std::string_view outputName) { +std::string outputPathName(std::string_view drvName, OutputNameView outputName) { std::string res { drvName }; if (outputName != "out") { res += "-"; @@ -810,7 +810,7 @@ void writeDerivation(Sink & out, const Store & store, const BasicDerivation & dr } -std::string hashPlaceholder(const std::string_view outputName) +std::string hashPlaceholder(const OutputNameView outputName) { // FIXME: memoize? return "/" + hashString(htSHA256, concatStrings("nix-output:", outputName)).to_string(Base32, false); @@ -963,7 +963,7 @@ void Derivation::checkInvariants(Store & store, const StorePath & drvPath) const const Hash impureOutputHash = hashString(htSHA256, "impure"); nlohmann::json DerivationOutput::toJSON( - const Store & store, std::string_view drvName, std::string_view outputName) const + const Store & store, std::string_view drvName, OutputNameView outputName) const { nlohmann::json res = nlohmann::json::object(); std::visit(overloaded { @@ -990,7 +990,7 @@ nlohmann::json DerivationOutput::toJSON( DerivationOutput DerivationOutput::fromJSON( - const Store & store, std::string_view drvName, std::string_view outputName, + const Store & store, std::string_view drvName, OutputNameView outputName, const nlohmann::json & _json, const ExperimentalFeatureSettings & xpSettings) { diff --git a/src/libstore/derivations.hh b/src/libstore/derivations.hh index a92082089..106056f2d 100644 --- a/src/libstore/derivations.hh +++ b/src/libstore/derivations.hh @@ -55,7 +55,7 @@ struct DerivationOutput * @param drvName The name of the derivation this is an output of, without the `.drv`. * @param outputName The name of this output. */ - StorePath path(const Store & store, std::string_view drvName, std::string_view outputName) const; + StorePath path(const Store & store, std::string_view drvName, OutputNameView outputName) const; GENERATE_CMP(CAFixed, me->ca); }; @@ -132,19 +132,19 @@ struct DerivationOutput * the safer interface provided by * BasicDerivation::outputsAndOptPaths */ - std::optional path(const Store & store, std::string_view drvName, std::string_view outputName) const; + std::optional path(const Store & store, std::string_view drvName, OutputNameView outputName) const; nlohmann::json toJSON( const Store & store, std::string_view drvName, - std::string_view outputName) const; + OutputNameView outputName) const; /** * @param xpSettings Stop-gap to avoid globals during unit tests. */ static DerivationOutput fromJSON( const Store & store, std::string_view drvName, - std::string_view outputName, + OutputNameView outputName, const nlohmann::json & json, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); }; @@ -405,7 +405,7 @@ bool isDerivation(std::string_view fileName); * This is usually -, but is just when * the output name is "out". */ -std::string outputPathName(std::string_view drvName, std::string_view outputName); +std::string outputPathName(std::string_view drvName, OutputNameView outputName); /** @@ -499,7 +499,7 @@ void writeDerivation(Sink & out, const Store & store, const BasicDerivation & dr * own outputs without needing to use the hash of a derivation in * itself, making the hash near-impossible to calculate. */ -std::string hashPlaceholder(const std::string_view outputName); +std::string hashPlaceholder(const OutputNameView outputName); extern const Hash impureOutputHash; diff --git a/src/libstore/derived-path.cc b/src/libstore/derived-path.cc index 3594b7570..47d784deb 100644 --- a/src/libstore/derived-path.cc +++ b/src/libstore/derived-path.cc @@ -167,7 +167,7 @@ void drvRequireExperiment( SingleDerivedPath::Built SingleDerivedPath::Built::parse( const Store & store, ref drv, - std::string_view output, + OutputNameView output, const ExperimentalFeatureSettings & xpSettings) { drvRequireExperiment(*drv, xpSettings); @@ -179,7 +179,7 @@ SingleDerivedPath::Built SingleDerivedPath::Built::parse( DerivedPath::Built DerivedPath::Built::parse( const Store & store, ref drv, - std::string_view outputsS, + OutputNameView outputsS, const ExperimentalFeatureSettings & xpSettings) { drvRequireExperiment(*drv, xpSettings); diff --git a/src/libstore/derived-path.hh b/src/libstore/derived-path.hh index ec30dac61..4d7033df2 100644 --- a/src/libstore/derived-path.hh +++ b/src/libstore/derived-path.hh @@ -42,7 +42,7 @@ struct SingleDerivedPath; */ struct SingleDerivedPathBuilt { ref drvPath; - std::string output; + OutputName output; /** * Get the store path this is ultimately derived from (by realising @@ -71,7 +71,7 @@ struct SingleDerivedPathBuilt { */ static SingleDerivedPathBuilt parse( const Store & store, ref drvPath, - std::string_view outputs, + OutputNameView outputs, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); nlohmann::json toJSON(Store & store) const; diff --git a/src/libstore/downstream-placeholder.cc b/src/libstore/downstream-placeholder.cc index d951b7b7d..7e3f7548d 100644 --- a/src/libstore/downstream-placeholder.cc +++ b/src/libstore/downstream-placeholder.cc @@ -11,7 +11,7 @@ std::string DownstreamPlaceholder::render() const DownstreamPlaceholder DownstreamPlaceholder::unknownCaOutput( const StorePath & drvPath, - std::string_view outputName, + OutputNameView outputName, const ExperimentalFeatureSettings & xpSettings) { xpSettings.require(Xp::CaDerivations); @@ -25,7 +25,7 @@ DownstreamPlaceholder DownstreamPlaceholder::unknownCaOutput( DownstreamPlaceholder DownstreamPlaceholder::unknownDerivation( const DownstreamPlaceholder & placeholder, - std::string_view outputName, + OutputNameView outputName, const ExperimentalFeatureSettings & xpSettings) { xpSettings.require(Xp::DynamicDerivations); diff --git a/src/libstore/downstream-placeholder.hh b/src/libstore/downstream-placeholder.hh index d58a2ac14..c911ecea2 100644 --- a/src/libstore/downstream-placeholder.hh +++ b/src/libstore/downstream-placeholder.hh @@ -58,7 +58,7 @@ public: */ static DownstreamPlaceholder unknownCaOutput( const StorePath & drvPath, - std::string_view outputName, + OutputNameView outputName, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** @@ -72,7 +72,7 @@ public: */ static DownstreamPlaceholder unknownDerivation( const DownstreamPlaceholder & drvPlaceholder, - std::string_view outputName, + OutputNameView outputName, const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** diff --git a/src/libstore/outputs-spec.hh b/src/libstore/outputs-spec.hh index ae19f1040..1ef99a5fc 100644 --- a/src/libstore/outputs-spec.hh +++ b/src/libstore/outputs-spec.hh @@ -13,24 +13,36 @@ namespace nix { +/** + * An (owned) output name. Just a type alias used to make code more + * readible. + */ +typedef std::string OutputName; + +/** + * A borrowed output name. Just a type alias used to make code more + * readible. + */ +typedef std::string_view OutputNameView; + struct OutputsSpec { /** * A non-empty set of outputs, specified by name */ - struct Names : std::set { - using std::set::set; + struct Names : std::set { + using std::set::set; /* These need to be "inherited manually" */ - Names(const std::set & s) - : std::set(s) + Names(const std::set & s) + : std::set(s) { assert(!empty()); } /** * Needs to be "inherited manually" */ - Names(std::set && s) - : std::set(s) + Names(std::set && s) + : std::set(s) { assert(!empty()); } /* This set should always be non-empty, so we delete this @@ -57,7 +69,7 @@ struct OutputsSpec { */ OutputsSpec() = delete; - bool contains(const std::string & output) const; + bool contains(const OutputName & output) const; /** * Create a new OutputsSpec which is the union of this and that. diff --git a/src/libstore/realisation.hh b/src/libstore/realisation.hh index 0548b30c1..559483ce3 100644 --- a/src/libstore/realisation.hh +++ b/src/libstore/realisation.hh @@ -34,7 +34,7 @@ struct DrvOutput { /** * The name of the output. */ - std::string outputName; + OutputName outputName; std::string to_string() const; @@ -84,7 +84,7 @@ struct Realisation { * Since these are the outputs of a single derivation, we know the * output names are unique so we can use them as the map key. */ -typedef std::map SingleDrvOutputs; +typedef std::map SingleDrvOutputs; /** * Collection type for multiple derivations' outputs' `Realisation`s. @@ -146,7 +146,7 @@ public: MissingRealisation(DrvOutput & outputId) : MissingRealisation(outputId.outputName, outputId.strHash()) {} - MissingRealisation(std::string_view drv, std::string outputName) + MissingRealisation(std::string_view drv, OutputName outputName) : Error( "cannot operate on output '%s' of the " "unbuilt derivation '%s'", outputName, From 1c4caef14b51dbb4f749c45311c8e2c9acb75a60 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 16 Aug 2023 00:05:35 -0400 Subject: [PATCH 049/299] Throw `MissingRealisation` not plain `Error` in both `resolveDerivedPath` Now we are consistent with the other `resolveDerivedPath`, and other such functions. --- src/libstore/misc.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index 631213306..c043b9b93 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -399,8 +399,7 @@ StorePath resolveDerivedPath(Store & store, const SingleDerivedPath & req, Store store.printStorePath(drvPath), bfd.output); auto & optPath = outputPaths.at(bfd.output); if (!optPath) - throw Error("'%s' does not yet map to a known concrete store path", - bfd.to_string(store)); + throw MissingRealisation(bfd.drvPath->to_string(store), bfd.output); return *optPath; }, }, req.raw()); From 692074f7142fcf8ede1266b6d8cbbd5feaf3221f Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 15 Jan 2023 17:47:24 -0500 Subject: [PATCH 050/299] Use `Worker::makeDerivationGoal` less We're about to split up `DerivationGoal` a bit. At that point `makeDerivationGoal` will mean something more specific than it does today. (Perhaps a future rename will make this clearer.) On the other hand, the more public `Worker::makeGoal` function will continue to work exactly as before. So by moving some call sites to use that instead, we preemptively avoid issues in the next step. --- src/libstore/build/derivation-goal.cc | 14 ++++++++++++-- src/libstore/build/entry-points.cc | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 84da7f2e1..ea56c0288 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -380,7 +380,12 @@ void DerivationGoal::gaveUpOnSubstitution() worker.store.printStorePath(i.first)); } - addWaitee(worker.makeDerivationGoal(i.first, i.second, buildMode == bmRepair ? bmRepair : bmNormal)); + addWaitee(worker.makeGoal( + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(i.first), + .outputs = i.second, + }, + buildMode == bmRepair ? bmRepair : bmNormal)); } /* Copy the input sources from the eval store to the build @@ -452,7 +457,12 @@ void DerivationGoal::repairClosure() if (drvPath2 == outputsToDrv.end()) addWaitee(upcast_goal(worker.makePathSubstitutionGoal(i, Repair))); else - addWaitee(worker.makeDerivationGoal(drvPath2->second, OutputsSpec::All(), bmRepair)); + addWaitee(worker.makeGoal( + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(drvPath2->second), + .outputs = OutputsSpec::All { }, + }, + bmRepair)); } if (waitees.empty()) { diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index e941b4e65..f71fb35a6 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -124,8 +124,11 @@ void Store::repairPath(const StorePath & path) auto info = queryPathInfo(path); if (info->deriver && isValidPath(*info->deriver)) { goals.clear(); - // FIXME: Should just build the specific output we need. - goals.insert(worker.makeDerivationGoal(*info->deriver, OutputsSpec::All { }, bmRepair)); + goals.insert(worker.makeGoal(DerivedPath::Built { + .drvPath = makeConstantStorePathRef(*info->deriver), + // FIXME: Should just build the specific output we need. + .outputs = OutputsSpec::All { }, + }, bmRepair)); worker.run(goals); } else throw Error(worker.failingExitStatus(), "cannot repair path '%s'", printStorePath(path)); From 5e3986f59cb58f48186a49dcec7aa317b4787522 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 8 Mar 2021 16:24:49 -0500 Subject: [PATCH 051/299] Adapt scheduler to work with dynamic derivations To avoid dealing with an optional `drvPath` (because we might not know it yet) everywhere, make an `CreateDerivationAndRealiseGoal`. This goal just builds/substitutes the derivation file, and then kicks of a build for that obtained derivation; in other words it does the chaining of goals when the drv file is missing (as can already be the case) or computed (new case). This also means the `getDerivation` state can be removed from `DerivationGoal`, which makes the `BasicDerivation` / in memory case and `Derivation` / drv file file case closer together. The map type is factored out for clarity, and because we will soon hvae a second use for it (`Derivation` itself). Co-authored-by: Robert Hensing --- .../create-derivation-and-realise-goal.cc | 157 ++++++++++++++++++ .../create-derivation-and-realise-goal.hh | 96 +++++++++++ src/libstore/build/derivation-goal.cc | 22 +-- src/libstore/build/derivation-goal.hh | 15 +- .../build/drv-output-substitution-goal.hh | 4 +- src/libstore/build/entry-points.cc | 11 +- src/libstore/build/goal.cc | 2 +- src/libstore/build/goal.hh | 22 ++- src/libstore/build/substitution-goal.hh | 4 +- src/libstore/build/worker.cc | 118 ++++++++++--- src/libstore/build/worker.hh | 22 +++ src/libstore/derived-path-map.cc | 33 ++++ src/libstore/derived-path-map.hh | 73 ++++++++ tests/dyn-drv/build-built-drv.sh | 4 +- 14 files changed, 525 insertions(+), 58 deletions(-) create mode 100644 src/libstore/build/create-derivation-and-realise-goal.cc create mode 100644 src/libstore/build/create-derivation-and-realise-goal.hh create mode 100644 src/libstore/derived-path-map.cc create mode 100644 src/libstore/derived-path-map.hh diff --git a/src/libstore/build/create-derivation-and-realise-goal.cc b/src/libstore/build/create-derivation-and-realise-goal.cc new file mode 100644 index 000000000..b01042f00 --- /dev/null +++ b/src/libstore/build/create-derivation-and-realise-goal.cc @@ -0,0 +1,157 @@ +#include "create-derivation-and-realise-goal.hh" +#include "worker.hh" + +namespace nix { + +CreateDerivationAndRealiseGoal::CreateDerivationAndRealiseGoal(ref drvReq, + const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) + : Goal(worker, DerivedPath::Built { .drvPath = drvReq, .outputs = wantedOutputs }) + , drvReq(drvReq) + , wantedOutputs(wantedOutputs) + , buildMode(buildMode) +{ + state = &CreateDerivationAndRealiseGoal::getDerivation; + name = fmt( + "outer obtaining drv from '%s' and then building outputs %s", + drvReq->to_string(worker.store), + std::visit(overloaded { + [&](const OutputsSpec::All) -> std::string { + return "* (all of them)"; + }, + [&](const OutputsSpec::Names os) { + return concatStringsSep(", ", quoteStrings(os)); + }, + }, wantedOutputs.raw)); + trace("created outer"); + + worker.updateProgress(); +} + + +CreateDerivationAndRealiseGoal::~CreateDerivationAndRealiseGoal() +{ +} + + +static StorePath pathPartOfReq(const SingleDerivedPath & req) +{ + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & bo) { + return bo.path; + }, + [&](const SingleDerivedPath::Built & bfd) { + return pathPartOfReq(*bfd.drvPath); + }, + }, req.raw()); +} + + +std::string CreateDerivationAndRealiseGoal::key() +{ + /* Ensure that derivations get built in order of their name, + i.e. a derivation named "aardvark" always comes before "baboon". And + substitution goals and inner derivation goals always happen before + derivation goals (due to "b$"). */ + return "c$" + std::string(pathPartOfReq(*drvReq).name()) + "$" + drvReq->to_string(worker.store); +} + + +void CreateDerivationAndRealiseGoal::timedOut(Error && ex) +{ +} + + +void CreateDerivationAndRealiseGoal::work() +{ + (this->*state)(); +} + + +void CreateDerivationAndRealiseGoal::addWantedOutputs(const OutputsSpec & outputs) +{ + /* If we already want all outputs, there is nothing to do. */ + auto newWanted = wantedOutputs.union_(outputs); + bool needRestart = !newWanted.isSubsetOf(wantedOutputs); + wantedOutputs = newWanted; + + if (!needRestart) return; + + if (!optDrvPath) + // haven't started steps where the outputs matter yet + return; + worker.makeDerivationGoal(*optDrvPath, outputs, buildMode); +} + + +void CreateDerivationAndRealiseGoal::getDerivation() +{ + trace("outer init"); + + /* The first thing to do is to make sure that the derivation + exists. If it doesn't, it may be created through a + substitute. */ + if (auto optDrvPath = [this]() -> std::optional { + if (buildMode != bmNormal) return std::nullopt; + + auto drvPath = StorePath::dummy; + try { + drvPath = resolveDerivedPath(worker.store, *drvReq); + } catch (MissingRealisation) { + return std::nullopt; + } + return worker.evalStore.isValidPath(drvPath) || worker.store.isValidPath(drvPath) + ? std::optional { drvPath } + : std::nullopt; + }()) { + trace(fmt("already have drv '%s' for '%s', can go straight to building", + worker.store.printStorePath(*optDrvPath), + drvReq->to_string(worker.store))); + + loadAndBuildDerivation(); + } else { + trace("need to obtain drv we want to build"); + + addWaitee(worker.makeGoal(DerivedPath::fromSingle(*drvReq))); + + state = &CreateDerivationAndRealiseGoal::loadAndBuildDerivation; + if (waitees.empty()) work(); + } +} + + +void CreateDerivationAndRealiseGoal::loadAndBuildDerivation() +{ + trace("outer load and build derivation"); + + if (nrFailed != 0) { + amDone(ecFailed, Error("cannot build missing derivation '%s'", drvReq->to_string(worker.store))); + return; + } + + StorePath drvPath = resolveDerivedPath(worker.store, *drvReq); + /* Build this step! */ + concreteDrvGoal = worker.makeDerivationGoal(drvPath, wantedOutputs, buildMode); + addWaitee(upcast_goal(concreteDrvGoal)); + state = &CreateDerivationAndRealiseGoal::buildDone; + optDrvPath = std::move(drvPath); + if (waitees.empty()) work(); +} + + +void CreateDerivationAndRealiseGoal::buildDone() +{ + trace("outer build done"); + + buildResult = upcast_goal(concreteDrvGoal)->getBuildResult(DerivedPath::Built { + .drvPath = drvReq, + .outputs = wantedOutputs, + }); + + if (buildResult.success()) + amDone(ecSuccess); + else + amDone(ecFailed, Error("building '%s' failed", drvReq->to_string(worker.store))); +} + + +} diff --git a/src/libstore/build/create-derivation-and-realise-goal.hh b/src/libstore/build/create-derivation-and-realise-goal.hh new file mode 100644 index 000000000..ca936fc95 --- /dev/null +++ b/src/libstore/build/create-derivation-and-realise-goal.hh @@ -0,0 +1,96 @@ +#pragma once + +#include "parsed-derivations.hh" +#include "lock.hh" +#include "store-api.hh" +#include "pathlocks.hh" +#include "goal.hh" + +namespace nix { + +struct DerivationGoal; + +/** + * This goal type is essentially the serial composition (like function + * composition) of a goal for getting a derivation, and then a + * `DerivationGoal` using the newly-obtained derivation. + * + * In the (currently experimental) general inductive case of derivations + * that are themselves build outputs, that first goal will be *another* + * `CreateDerivationAndRealiseGoal`. In the (much more common) base-case + * where the derivation has no provence and is just referred to by + * (content-addressed) store path, that first goal is a + * `SubstitutionGoal`. + * + * If we already have the derivation (e.g. if the evalutator has created + * the derivation locally and then instructured the store to build it), + * we can skip the first goal entirely as a small optimization. + */ +struct CreateDerivationAndRealiseGoal : public Goal +{ + /** + * How to obtain a store path of the derivation to build. + */ + ref drvReq; + + /** + * The path of the derivation, once obtained. + **/ + std::optional optDrvPath; + + /** + * The goal for the corresponding concrete derivation. + **/ + std::shared_ptr concreteDrvGoal; + + /** + * The specific outputs that we need to build. + */ + OutputsSpec wantedOutputs; + + typedef void (CreateDerivationAndRealiseGoal::*GoalState)(); + GoalState state; + + /** + * The final output paths of the build. + * + * - For input-addressed derivations, always the precomputed paths + * + * - For content-addressed derivations, calcuated from whatever the + * hash ends up being. (Note that fixed outputs derivations that + * produce the "wrong" output still install that data under its + * true content-address.) + */ + OutputPathMap finalOutputs; + + BuildMode buildMode; + + CreateDerivationAndRealiseGoal(ref drvReq, + const OutputsSpec & wantedOutputs, Worker & worker, + BuildMode buildMode = bmNormal); + virtual ~CreateDerivationAndRealiseGoal(); + + void timedOut(Error && ex) override; + + std::string key() override; + + void work() override; + + /** + * Add wanted outputs to an already existing derivation goal. + */ + void addWantedOutputs(const OutputsSpec & outputs); + + /** + * The states. + */ + void getDerivation(); + void loadAndBuildDerivation(); + void buildDone(); + + JobCategory jobCategory() const override { + return JobCategory::Administration; + }; +}; + +} diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index ea56c0288..bec0bc538 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -71,7 +71,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, , wantedOutputs(wantedOutputs) , buildMode(buildMode) { - state = &DerivationGoal::getDerivation; + state = &DerivationGoal::loadDerivation; name = fmt( "building of '%s' from .drv file", DerivedPath::Built { makeConstantStorePathRef(drvPath), wantedOutputs }.to_string(worker.store)); @@ -164,24 +164,6 @@ void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs) } -void DerivationGoal::getDerivation() -{ - trace("init"); - - /* The first thing to do is to make sure that the derivation - exists. If it doesn't, it may be created through a - substitute. */ - if (buildMode == bmNormal && worker.evalStore.isValidPath(drvPath)) { - loadDerivation(); - return; - } - - addWaitee(upcast_goal(worker.makePathSubstitutionGoal(drvPath))); - - state = &DerivationGoal::loadDerivation; -} - - void DerivationGoal::loadDerivation() { trace("loading derivation"); @@ -1493,7 +1475,7 @@ void DerivationGoal::waiteeDone(GoalPtr waitee, ExitCode result) if (!useDerivation) return; auto & fullDrv = *dynamic_cast(drv.get()); - auto * dg = dynamic_cast(&*waitee); + auto * dg = tryGetConcreteDrvGoal(waitee); if (!dg) return; auto outputs = fullDrv.inputDrvs.find(dg->drvPath); diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index 9d6fe1c0f..62b122c27 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -50,6 +50,13 @@ struct InitialOutput { std::optional known; }; +/** + * A goal for building some or all of the outputs of a derivation. + * + * The derivation must already be present, either in the store in a drv + * or in memory. If the derivation itself needs to be gotten first, a + * `CreateDerivationAndRealiseGoal` goal must be used instead. + */ struct DerivationGoal : public Goal { /** @@ -66,8 +73,7 @@ struct DerivationGoal : public Goal std::shared_ptr resolvedDrvGoal; /** - * The specific outputs that we need to build. Empty means all of - * them. + * The specific outputs that we need to build. */ OutputsSpec wantedOutputs; @@ -229,7 +235,6 @@ struct DerivationGoal : public Goal /** * The states. */ - void getDerivation(); void loadDerivation(); void haveDerivation(); void outputsSubstitutionTried(); @@ -334,7 +339,9 @@ struct DerivationGoal : public Goal StorePathSet exportReferences(const StorePathSet & storePaths); - JobCategory jobCategory() override { return JobCategory::Build; }; + JobCategory jobCategory() const override { + return JobCategory::Build; + }; }; MakeError(NotDeterministic, BuildError); diff --git a/src/libstore/build/drv-output-substitution-goal.hh b/src/libstore/build/drv-output-substitution-goal.hh index 5d1253a71..da2426e5e 100644 --- a/src/libstore/build/drv-output-substitution-goal.hh +++ b/src/libstore/build/drv-output-substitution-goal.hh @@ -73,7 +73,9 @@ public: void work() override; void handleEOF(int fd) override; - JobCategory jobCategory() override { return JobCategory::Substitution; }; + JobCategory jobCategory() const override { + return JobCategory::Substitution; + }; }; } diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index f71fb35a6..f0f0e5519 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -1,5 +1,6 @@ #include "worker.hh" #include "substitution-goal.hh" +#include "create-derivation-and-realise-goal.hh" #include "derivation-goal.hh" #include "local-store.hh" @@ -15,7 +16,7 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod worker.run(goals); - StorePathSet failed; + StringSet failed; std::optional ex; for (auto & i : goals) { if (i->ex) { @@ -25,8 +26,10 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod ex = std::move(i->ex); } if (i->exitCode != Goal::ecSuccess) { - if (auto i2 = dynamic_cast(i.get())) failed.insert(i2->drvPath); - else if (auto i2 = dynamic_cast(i.get())) failed.insert(i2->storePath); + if (auto i2 = dynamic_cast(i.get())) + failed.insert(i2->drvReq->to_string(*this)); + else if (auto i2 = dynamic_cast(i.get())) + failed.insert(printStorePath(i2->storePath)); } } @@ -35,7 +38,7 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod throw std::move(*ex); } else if (!failed.empty()) { if (ex) logError(ex->info()); - throw Error(worker.failingExitStatus(), "build of %s failed", showPaths(failed)); + throw Error(worker.failingExitStatus(), "build of %s failed", concatStringsSep(", ", quoteStrings(failed))); } } diff --git a/src/libstore/build/goal.cc b/src/libstore/build/goal.cc index ca7097a68..f8db98280 100644 --- a/src/libstore/build/goal.cc +++ b/src/libstore/build/goal.cc @@ -11,7 +11,7 @@ bool CompareGoalPtrs::operator() (const GoalPtr & a, const GoalPtr & b) const { } -BuildResult Goal::getBuildResult(const DerivedPath & req) { +BuildResult Goal::getBuildResult(const DerivedPath & req) const { BuildResult res { buildResult }; if (auto pbp = std::get_if(&req)) { diff --git a/src/libstore/build/goal.hh b/src/libstore/build/goal.hh index d3127caea..01d3c3491 100644 --- a/src/libstore/build/goal.hh +++ b/src/libstore/build/goal.hh @@ -41,8 +41,24 @@ typedef std::map WeakGoalMap; * of each category in parallel. */ enum struct JobCategory { + /** + * A build of a derivation; it will use CPU and disk resources. + */ Build, + /** + * A substitution an arbitrary store object; it will use network resources. + */ Substitution, + /** + * A goal that does no "real" work by itself, and just exists to depend on + * other goals which *do* do real work. These goals therefore are not + * limited. + * + * These goals cannot infinitely create themselves, so there is no risk of + * a "fork bomb" type situation (which would be a problem even though the + * goal do no real work) either. + */ + Administration, }; struct Goal : public std::enable_shared_from_this @@ -110,7 +126,7 @@ public: * sake of both privacy and determinism, and this "safe accessor" * ensures we don't. */ - BuildResult getBuildResult(const DerivedPath &); + BuildResult getBuildResult(const DerivedPath &) const; /** * Exception containing an error message, if any. @@ -144,7 +160,7 @@ public: void trace(std::string_view s); - std::string getName() + std::string getName() const { return name; } @@ -166,7 +182,7 @@ public: * @brief Hint for the scheduler, which concurrency limit applies. * @see JobCategory */ - virtual JobCategory jobCategory() = 0; + virtual JobCategory jobCategory() const = 0; }; void addToWeakGoals(WeakGoals & goals, GoalPtr p); diff --git a/src/libstore/build/substitution-goal.hh b/src/libstore/build/substitution-goal.hh index 1b693baa1..1d389d328 100644 --- a/src/libstore/build/substitution-goal.hh +++ b/src/libstore/build/substitution-goal.hh @@ -117,7 +117,9 @@ public: /* Called by destructor, can't be overridden */ void cleanup() override final; - JobCategory jobCategory() override { return JobCategory::Substitution; }; + JobCategory jobCategory() const override { + return JobCategory::Substitution; + }; }; } diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index b58fc5c1c..f65f63b99 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -2,6 +2,7 @@ #include "worker.hh" #include "substitution-goal.hh" #include "drv-output-substitution-goal.hh" +#include "create-derivation-and-realise-goal.hh" #include "local-derivation-goal.hh" #include "hook-instance.hh" @@ -41,6 +42,24 @@ Worker::~Worker() } +std::shared_ptr Worker::makeCreateDerivationAndRealiseGoal( + ref drvReq, + const OutputsSpec & wantedOutputs, + BuildMode buildMode) +{ + std::weak_ptr & goal_weak = outerDerivationGoals.ensureSlot(*drvReq).value; + std::shared_ptr goal = goal_weak.lock(); + if (!goal) { + goal = std::make_shared(drvReq, wantedOutputs, *this, buildMode); + goal_weak = goal; + wakeUp(goal); + } else { + goal->addWantedOutputs(wantedOutputs); + } + return goal; +} + + std::shared_ptr Worker::makeDerivationGoalCommon( const StorePath & drvPath, const OutputsSpec & wantedOutputs, @@ -111,10 +130,7 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) { return std::visit(overloaded { [&](const DerivedPath::Built & bfd) -> GoalPtr { - if (auto bop = std::get_if(&*bfd.drvPath)) - return makeDerivationGoal(bop->path, bfd.outputs, buildMode); - else - throw UnimplementedError("Building dynamic derivations in one shot is not yet implemented."); + return makeCreateDerivationAndRealiseGoal(bfd.drvPath, bfd.outputs, buildMode); }, [&](const DerivedPath::Opaque & bo) -> GoalPtr { return makePathSubstitutionGoal(bo.path, buildMode == bmRepair ? Repair : NoRepair); @@ -123,24 +139,46 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) } +template +static void cullMap(std::map & goalMap, F f) +{ + for (auto i = goalMap.begin(); i != goalMap.end();) + if (!f(i->second)) + i = goalMap.erase(i); + else ++i; +} + + template static void removeGoal(std::shared_ptr goal, std::map> & goalMap) { /* !!! inefficient */ - for (auto i = goalMap.begin(); - i != goalMap.end(); ) - if (i->second.lock() == goal) { - auto j = i; ++j; - goalMap.erase(i); - i = j; - } - else ++i; + cullMap(goalMap, [&](const std::weak_ptr & gp) -> bool { + return gp.lock() != goal; + }); +} + +template +static void removeGoal(std::shared_ptr goal, std::map>::ChildNode> & goalMap); + +template +static void removeGoal(std::shared_ptr goal, std::map>::ChildNode> & goalMap) +{ + /* !!! inefficient */ + cullMap(goalMap, [&](DerivedPathMap>::ChildNode & node) -> bool { + if (node.value.lock() == goal) + node.value.reset(); + removeGoal(goal, node.childMap); + return !node.value.expired() || !node.childMap.empty(); + }); } void Worker::removeGoal(GoalPtr goal) { - if (auto drvGoal = std::dynamic_pointer_cast(goal)) + if (auto drvGoal = std::dynamic_pointer_cast(goal)) + nix::removeGoal(drvGoal, outerDerivationGoals.map); + else if (auto drvGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(drvGoal, derivationGoals); else if (auto subGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(subGoal, substitutionGoals); @@ -198,8 +236,19 @@ void Worker::childStarted(GoalPtr goal, const std::set & fds, child.respectTimeouts = respectTimeouts; children.emplace_back(child); if (inBuildSlot) { - if (goal->jobCategory() == JobCategory::Substitution) nrSubstitutions++; - else nrLocalBuilds++; + switch (goal->jobCategory()) { + case JobCategory::Substitution: + nrSubstitutions++; + break; + case JobCategory::Build: + nrLocalBuilds++; + break; + case JobCategory::Administration: + /* Intentionally not limited, see docs */ + break; + default: + abort(); + } } } @@ -211,12 +260,20 @@ void Worker::childTerminated(Goal * goal, bool wakeSleepers) if (i == children.end()) return; if (i->inBuildSlot) { - if (goal->jobCategory() == JobCategory::Substitution) { + switch (goal->jobCategory()) { + case JobCategory::Substitution: assert(nrSubstitutions > 0); nrSubstitutions--; - } else { + break; + case JobCategory::Build: assert(nrLocalBuilds > 0); nrLocalBuilds--; + break; + case JobCategory::Administration: + /* Intentionally not limited, see docs */ + break; + default: + abort(); } } @@ -267,9 +324,9 @@ void Worker::run(const Goals & _topGoals) for (auto & i : _topGoals) { topGoals.insert(i); - if (auto goal = dynamic_cast(i.get())) { + if (auto goal = dynamic_cast(i.get())) { topPaths.push_back(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(goal->drvPath), + .drvPath = goal->drvReq, .outputs = goal->wantedOutputs, }); } else if (auto goal = dynamic_cast(i.get())) { @@ -522,11 +579,26 @@ void Worker::markContentsGood(const StorePath & path) } -GoalPtr upcast_goal(std::shared_ptr subGoal) { - return subGoal; -} -GoalPtr upcast_goal(std::shared_ptr subGoal) { +GoalPtr upcast_goal(std::shared_ptr subGoal) +{ return subGoal; } +GoalPtr upcast_goal(std::shared_ptr subGoal) +{ + return subGoal; +} + +GoalPtr upcast_goal(std::shared_ptr subGoal) +{ + return subGoal; +} + +const DerivationGoal * tryGetConcreteDrvGoal(GoalPtr waitee) +{ + auto * odg = dynamic_cast(&*waitee); + if (!odg) return nullptr; + return &*odg->concreteDrvGoal; +} + } diff --git a/src/libstore/build/worker.hh b/src/libstore/build/worker.hh index 5abceca0d..a778e311c 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/build/worker.hh @@ -4,6 +4,7 @@ #include "types.hh" #include "lock.hh" #include "store-api.hh" +#include "derived-path-map.hh" #include "goal.hh" #include "realisation.hh" @@ -13,6 +14,7 @@ namespace nix { /* Forward definition. */ +struct CreateDerivationAndRealiseGoal; struct DerivationGoal; struct PathSubstitutionGoal; class DrvOutputSubstitutionGoal; @@ -31,9 +33,23 @@ class DrvOutputSubstitutionGoal; */ GoalPtr upcast_goal(std::shared_ptr subGoal); GoalPtr upcast_goal(std::shared_ptr subGoal); +GoalPtr upcast_goal(std::shared_ptr subGoal); typedef std::chrono::time_point steady_time_point; +/** + * The current implementation of impure derivations has + * `DerivationGoal`s accumulate realisations from their waitees. + * Unfortunately, `DerivationGoal`s don't directly depend on other + * goals, but instead depend on `CreateDerivationAndRealiseGoal`s. + * + * We try not to share any of the details of any goal type with any + * other, for sake of modularity and quicker rebuilds. This means we + * cannot "just" downcast and fish out the field. So as an escape hatch, + * we have made the function, written in `worker.cc` where all the goal + * types are visible, and use it instead. + */ +const DerivationGoal * tryGetConcreteDrvGoal(GoalPtr waitee); /** * A mapping used to remember for each child process to what goal it @@ -102,6 +118,9 @@ private: * Maps used to prevent multiple instantiations of a goal for the * same derivation / path. */ + + DerivedPathMap> outerDerivationGoals; + std::map> derivationGoals; std::map> substitutionGoals; std::map> drvOutputSubstitutionGoals; @@ -189,6 +208,9 @@ public: * @ref DerivationGoal "derivation goal" */ private: + std::shared_ptr makeCreateDerivationAndRealiseGoal( + ref drvPath, + const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); std::shared_ptr makeDerivationGoalCommon( const StorePath & drvPath, const OutputsSpec & wantedOutputs, std::function()> mkDrvGoal); diff --git a/src/libstore/derived-path-map.cc b/src/libstore/derived-path-map.cc new file mode 100644 index 000000000..5c8c7a4f2 --- /dev/null +++ b/src/libstore/derived-path-map.cc @@ -0,0 +1,33 @@ +#include "derived-path-map.hh" + +namespace nix { + +template +typename DerivedPathMap::ChildNode & DerivedPathMap::ensureSlot(const SingleDerivedPath & k) +{ + std::function initIter; + initIter = [&](const auto & k) -> auto & { + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & bo) -> auto & { + // will not overwrite if already there + return map[bo.path]; + }, + [&](const SingleDerivedPath::Built & bfd) -> auto & { + auto & n = initIter(*bfd.drvPath); + return n.childMap[bfd.output]; + }, + }, k.raw()); + }; + return initIter(k); +} + +} + +// instantiations + +#include "create-derivation-and-realise-goal.hh" +namespace nix { + +template struct DerivedPathMap>; + +} diff --git a/src/libstore/derived-path-map.hh b/src/libstore/derived-path-map.hh new file mode 100644 index 000000000..9ce58206e --- /dev/null +++ b/src/libstore/derived-path-map.hh @@ -0,0 +1,73 @@ +#pragma once + +#include "types.hh" +#include "derived-path.hh" + +namespace nix { + +/** + * A simple Trie, of sorts. Conceptually a map of `SingleDerivedPath` to + * values. + * + * Concretely, an n-ary tree, as described below. A + * `SingleDerivedPath::Opaque` maps to the value of an immediate child + * of the root node. A `SingleDerivedPath::Built` maps to a deeper child + * node: the `SingleDerivedPath::Built::drvPath` is first mapped to a a + * child node (inductively), and then the + * `SingleDerivedPath::Built::output` is used to look up that child's + * child via its map. In this manner, every `SingleDerivedPath` is + * mapped to a child node. + * + * @param V A type to instantiate for each output. It should probably + * should be an "optional" type so not every interior node has to have a + * value. For example, the scheduler uses + * `DerivedPathMap>` to + * remember which goals correspond to which outputs. `* const Something` + * or `std::optional` would also be good choices for + * "optional" types. + */ +template +struct DerivedPathMap { + /** + * A child node (non-root node). + */ + struct ChildNode { + /** + * Value of this child node. + * + * @see DerivedPathMap for what `V` should be. + */ + V value; + + /** + * The map type for the root node. + */ + using Map = std::map; + + /** + * The map of the root node. + */ + Map childMap; + }; + + /** + * The map type for the root node. + */ + using Map = std::map; + + /** + * The map of root node. + */ + Map map; + + /** + * Find the node for `k`, creating it if needed. + * + * The node is referred to as a "slot" on the assumption that `V` is + * some sort of optional type, so the given key can be set or unset + * by changing this node. + */ + ChildNode & ensureSlot(const SingleDerivedPath & k); +}; + +} diff --git a/tests/dyn-drv/build-built-drv.sh b/tests/dyn-drv/build-built-drv.sh index 647be9457..94f3550bd 100644 --- a/tests/dyn-drv/build-built-drv.sh +++ b/tests/dyn-drv/build-built-drv.sh @@ -18,4 +18,6 @@ clearStore drvDep=$(nix-instantiate ./text-hashed-output.nix -A producingDrv) -expectStderr 1 nix build "${drvDep}^out^out" --no-link | grepQuiet "Building dynamic derivations in one shot is not yet implemented" +out2=$(nix build "${drvDep}^out^out" --no-link) + +test $out1 == $out2 From d2e6cfa0750cb38ceef7dc0b8bb31cf3b0387e9c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 25 Aug 2023 17:17:33 +0200 Subject: [PATCH 052/299] tests/lang/eval-okay-pathexists: Add cases --- tests/lang/eval-okay-pathexists.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/lang/eval-okay-pathexists.nix b/tests/lang/eval-okay-pathexists.nix index 50c28ee0c..8eae37e70 100644 --- a/tests/lang/eval-okay-pathexists.nix +++ b/tests/lang/eval-okay-pathexists.nix @@ -1,4 +1,6 @@ -builtins.pathExists (builtins.toPath ./lib.nix) +builtins.pathExists (./lib.nix) +&& builtins.pathExists (builtins.toPath ./lib.nix) +&& builtins.pathExists (builtins.toString ./lib.nix) && builtins.pathExists (builtins.toPath (builtins.toString ./lib.nix)) && !builtins.pathExists (builtins.toPath (builtins.toString ./bla.nix)) && builtins.pathExists ./lib.nix From 1e08e12d8138b09e6872cb498b723ade9ad71d68 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 25 Aug 2023 17:18:37 +0200 Subject: [PATCH 053/299] pathExists: isDir when endswith / Fixes https://github.com/NixOS/nix/issues/8838 --- src/libexpr/primops.cc | 16 +++++++++++++--- tests/lang/eval-okay-pathexists.nix | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 283f99a48..915b872c8 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1520,15 +1520,25 @@ static RegisterPrimOp primop_storePath({ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args, Value & v) { + auto & arg = *args[0]; + /* We don’t check the path right now, because we don’t want to throw if the path isn’t allowed, but just return false (and we can’t just catch the exception here because we still want to - throw if something in the evaluation of `*args[0]` tries to + throw if something in the evaluation of `arg` tries to access an unauthorized path). */ - auto path = realisePath(state, pos, *args[0], { .checkForPureEval = false }); + auto path = realisePath(state, pos, arg, { .checkForPureEval = false }); + + /* SourcePath doesn't know about trailing slash. */ + auto mustBeDir = arg.type() == nString && arg.str().ends_with("/"); try { - v.mkBool(state.checkSourcePath(path).pathExists()); + auto checked = state.checkSourcePath(path); + auto exists = checked.pathExists(); + if (exists && mustBeDir) { + exists = checked.lstat().type == InputAccessor::tDirectory; + } + v.mkBool(exists); } catch (SysError & e) { /* Don't give away info from errors while canonicalising ‘path’ in restricted mode. */ diff --git a/tests/lang/eval-okay-pathexists.nix b/tests/lang/eval-okay-pathexists.nix index 8eae37e70..e1246e370 100644 --- a/tests/lang/eval-okay-pathexists.nix +++ b/tests/lang/eval-okay-pathexists.nix @@ -1,6 +1,7 @@ builtins.pathExists (./lib.nix) && builtins.pathExists (builtins.toPath ./lib.nix) && builtins.pathExists (builtins.toString ./lib.nix) +&& !builtins.pathExists (builtins.toString ./lib.nix + "/") && builtins.pathExists (builtins.toPath (builtins.toString ./lib.nix)) && !builtins.pathExists (builtins.toPath (builtins.toString ./bla.nix)) && builtins.pathExists ./lib.nix From 696eb79b150011629d86addd10f79c943b2f16b2 Mon Sep 17 00:00:00 2001 From: Tom Bereknyei Date: Sun, 27 Aug 2023 04:42:52 -0400 Subject: [PATCH 054/299] test: test behavior of .-prefixed attrPaths --- tests/flakes/absolute-attr-paths.sh | 17 +++++++++++++++++ tests/local.mk | 1 + 2 files changed, 18 insertions(+) create mode 100644 tests/flakes/absolute-attr-paths.sh diff --git a/tests/flakes/absolute-attr-paths.sh b/tests/flakes/absolute-attr-paths.sh new file mode 100644 index 000000000..491adceb7 --- /dev/null +++ b/tests/flakes/absolute-attr-paths.sh @@ -0,0 +1,17 @@ +source ./common.sh + +flake1Dir=$TEST_ROOT/flake1 + +mkdir -p $flake1Dir +cat > $flake1Dir/flake.nix < Date: Mon, 28 Aug 2023 15:43:34 +0200 Subject: [PATCH 055/299] Port the flags of nix-daemon to nix daemon (#8788) The new `nix daemon` command didn't accept the same flags that `nix-daemon` did. * docs(daemon): clarify the daemon trust override flags * fix: change declaration order * docs: add examples of nix daemon usage * Apply suggestions from code review --------- Co-authored-by: Eelco Dolstra Co-authored-by: John Ericson Co-authored-by: tomberek --- src/nix/daemon.cc | 41 ++++++++++++++++++++++++++++++++++++++++- src/nix/daemon.md | 30 +++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/nix/daemon.cc b/src/nix/daemon.cc index 1511f9e6e..af428018a 100644 --- a/src/nix/daemon.cc +++ b/src/nix/daemon.cc @@ -500,6 +500,45 @@ static RegisterLegacyCommand r_nix_daemon("nix-daemon", main_nix_daemon); struct CmdDaemon : StoreCommand { + bool stdio = false; + std::optional isTrustedOpt = std::nullopt; + + CmdDaemon() + { + addFlag({ + .longName = "stdio", + .description = "Attach to standard I/O, instead of trying to bind to a UNIX socket.", + .handler = {&stdio, true}, + }); + + addFlag({ + .longName = "force-trusted", + .description = "Force the daemon to trust connecting clients.", + .handler = {[&]() { + isTrustedOpt = Trusted; + }}, + .experimentalFeature = Xp::DaemonTrustOverride, + }); + + addFlag({ + .longName = "force-untrusted", + .description = "Force the daemon to not trust connecting clients. The connection will be processed by the receiving daemon before forwarding commands.", + .handler = {[&]() { + isTrustedOpt = NotTrusted; + }}, + .experimentalFeature = Xp::DaemonTrustOverride, + }); + + addFlag({ + .longName = "default-trust", + .description = "Use Nix's default trust.", + .handler = {[&]() { + isTrustedOpt = std::nullopt; + }}, + .experimentalFeature = Xp::DaemonTrustOverride, + }); + } + std::string description() override { return "daemon to perform store operations on behalf of non-root clients"; @@ -516,7 +555,7 @@ struct CmdDaemon : StoreCommand void run(ref store) override { - runDaemon(false, std::nullopt); + runDaemon(stdio, isTrustedOpt); } }; diff --git a/src/nix/daemon.md b/src/nix/daemon.md index d5cdadf08..b1ea850ed 100644 --- a/src/nix/daemon.md +++ b/src/nix/daemon.md @@ -1,20 +1,44 @@ R""( -# Example +# Examples -* Run the daemon in the foreground: +* Run the daemon: ```console # nix daemon ``` +* Run the daemon and listen on standard I/O instead of binding to a UNIX socket: + + ```console + # nix daemon --stdio + ``` + +* Run the daemon and force all connections to be trusted: + + ```console + # nix daemon --force-trusted + ``` + +* Run the daemon and force all connections to be untrusted: + + ```console + # nix daemon --force-untrusted + ``` + +* Run the daemon, listen on standard I/O, and force all connections to use Nix's default trust: + + ```console + # nix daemon --stdio --default-trust + ``` + # Description This command runs the Nix daemon, which is a required component in multi-user Nix installations. It runs build tasks and other operations on the Nix store on behalf of non-root users. Usually you don't run the daemon directly; instead it's managed by a service -management framework such as `systemd`. +management framework such as `systemd` on Linux, or `launchctl` on Darwin. Note that this daemon does not fork into the background. From 880fef9cdf3b732fa76ec47710edaf572bf91e92 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 28 Aug 2023 20:51:44 +0200 Subject: [PATCH 056/299] do not change existing release notes --- doc/manual/src/release-notes/rl-2.12.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/release-notes/rl-2.12.md b/doc/manual/src/release-notes/rl-2.12.md index 57d092e01..e1e3efe1a 100644 --- a/doc/manual/src/release-notes/rl-2.12.md +++ b/doc/manual/src/release-notes/rl-2.12.md @@ -2,8 +2,19 @@ * On Linux, Nix can now run builds in a user namespace where they run as root (UID 0) and have 65,536 UIDs available. + This is primarily useful for running containers such as `systemd-nspawn` + inside a Nix build. For an example, see [`tests/systemd-nspawn/nix`][nspawn]. - This can be used by requiring `uid-range` [system feature] in derivations. + [nspawn]: https://github.com/NixOS/nix/blob/67bcb99700a0da1395fa063d7c6586740b304598/tests/systemd-nspawn.nix. + + A build can enable this by setting the derivation attribute: + + ``` + requiredSystemFeatures = [ "uid-range" ]; + ``` + + The `uid-range` [system feature] requires the [`auto-allocate-uids`] + setting to be enabled. [system feature]: ../command-ref/conf-file.md#conf-system-features From 151120a1ae082c6460f9a54cf795c57d154f6c27 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Mon, 28 Aug 2023 22:14:01 +0200 Subject: [PATCH 057/299] Document nix-prefetch-url defaults (#8878) --- doc/manual/src/command-ref/nix-prefetch-url.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/manual/src/command-ref/nix-prefetch-url.md b/doc/manual/src/command-ref/nix-prefetch-url.md index 3bcd209e2..45ef01e02 100644 --- a/doc/manual/src/command-ref/nix-prefetch-url.md +++ b/doc/manual/src/command-ref/nix-prefetch-url.md @@ -31,15 +31,18 @@ store already contains a file with the same hash and base name. Otherwise, the file is downloaded, and an error is signaled if the actual hash of the file does not match the specified hash. -This command prints the hash on standard output. Additionally, if the -option `--print-path` is used, the path of the downloaded file in the -Nix store is also printed. +This command prints the hash on standard output. +The hash is printed using base-32 unless `--type md5` is specified, +in which case it's printed using base-16. +Additionally, if the option `--print-path` is used, +the path of the downloaded file in the Nix store is also printed. # Options - `--type` *hashAlgo*\ - Use the specified cryptographic hash algorithm, which can be one of - `md5`, `sha1`, `sha256`, and `sha512`. + Use the specified cryptographic hash algorithm, + which can be one of `md5`, `sha1`, `sha256`, and `sha512`. + The default is `sha256`. - `--print-path`\ Print the store path of the downloaded file on standard output. From 56763ff918eb308db23080e560ed2ea3e00c80a7 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 29 Aug 2023 15:04:06 +0200 Subject: [PATCH 058/299] Document that redirected tarball flakerefs can specify lastModified --- doc/manual/src/protocols/tarball-fetcher.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/protocols/tarball-fetcher.md b/doc/manual/src/protocols/tarball-fetcher.md index 0d3212303..274fa6d63 100644 --- a/doc/manual/src/protocols/tarball-fetcher.md +++ b/doc/manual/src/protocols/tarball-fetcher.md @@ -20,8 +20,8 @@ Link: ; rel="immutable" (Note the required `<` and `>` characters around *flakeref*.) -*flakeref* must be a tarball flakeref. It can contain flake attributes -such as `narHash`, `rev` and `revCount`. If `narHash` is included, its +*flakeref* must be a tarball flakeref. It can contain the tarball flake attributes +`narHash`, `rev`, `revCount` and `lastModified`. If `narHash` is included, its value must be the NAR hash of the unpacked tarball (as computed via `nix hash path`). Nix checks the contents of the returned tarball against the `narHash` attribute. The `rev` and `revCount` attributes From 46478b44ffb477387235b3c597c24178845fb2a3 Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Tue, 29 Aug 2023 15:13:35 -0700 Subject: [PATCH 059/299] docs/testing: point out the existence of `GTEST_FILTER` (#8883) --- doc/manual/src/contributing/testing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/contributing/testing.md b/doc/manual/src/contributing/testing.md index c3c82e3c0..cd94d5cfb 100644 --- a/doc/manual/src/contributing/testing.md +++ b/doc/manual/src/contributing/testing.md @@ -7,7 +7,8 @@ under `src/{library_name}/tests` using the [googletest](https://google.github.io/googletest/) and [rapidcheck](https://github.com/emil-e/rapidcheck) frameworks. -You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`. Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option. +You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`. +Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option, or the `GTEST_FILTER` environment variable. ## Functional tests From 3384f70a3d5ee7bd1d1bf5fa48809c4eac5c8041 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 11 Aug 2023 15:58:43 +0200 Subject: [PATCH 060/299] nixpkgsLibTests: Only test our Nix Interface has changed upstream. It *should* be fine to test 23.05's other Nix versions as those *should* succeed, but that's not the case and it's obfuscating our terrible CI setup's log. --- flake.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index cf7c1fa12..36ba61484 100644 --- a/flake.nix +++ b/flake.nix @@ -656,7 +656,9 @@ tests.nixpkgsLibTests = forAllSystems (system: import (nixpkgs + "/lib/tests/release.nix") - { pkgs = nixpkgsFor.${system}.native; } + { pkgs = nixpkgsFor.${system}.native; + nixVersions = [ self.packages.${system}.nix ]; + } ); metrics.nixpkgs = import "${nixpkgs-regression}/pkgs/top-level/metrics.nix" { From be3362e74782e47b9546de94be97f53540ddfe9e Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 14 Aug 2023 14:28:02 +0200 Subject: [PATCH 061/299] Fix nix-copy test --- tests/nixos/nix-copy.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/nixos/nix-copy.nix b/tests/nixos/nix-copy.nix index ef053de03..2981cc2b8 100644 --- a/tests/nixos/nix-copy.nix +++ b/tests/nixos/nix-copy.nix @@ -1,4 +1,6 @@ # Test that ‘nix copy’ works over ssh. +# Run interactively with: +# rm key key.pub; nix run .#hydraJobs.tests.nix-copy.driverInteractive { lib, config, nixpkgs, hostPkgs, ... }: @@ -55,7 +57,9 @@ in { server.wait_for_unit("sshd") client.wait_for_unit("network.target") client.wait_for_unit("getty@tty1.service") - client.wait_for_text("]#") + # Either the prompt: ]# + # or an OCR misreading of it: 1# + client.wait_for_text("[]1]#") # Copy the closure of package A from the client to the server using password authentication, # and check that all prompts are visible From 1bc9257d7c5980fb048b519eeaa2a2b2e8925099 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 3 May 2023 15:36:13 +0200 Subject: [PATCH 062/299] reword description of how realisation works --- .../src/command-ref/nix-store/opt-common.md | 4 +- .../src/command-ref/nix-store/realise.md | 50 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/opt-common.md b/doc/manual/src/command-ref/nix-store/opt-common.md index dd9a6bf21..104b20076 100644 --- a/doc/manual/src/command-ref/nix-store/opt-common.md +++ b/doc/manual/src/command-ref/nix-store/opt-common.md @@ -1,6 +1,6 @@ -# Options +# Command options -The following options are allowed for all `nix-store` operations, but may not always have an effect. +The following options are allowed for all operations in the `nix-store` command, but may not always have an effect. - [`--add-root`](#opt-add-root) *path* diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index c19aea75e..2881a69b3 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -8,33 +8,37 @@ # Description -The operation `--realise` essentially “builds” the specified store -paths. Realisation is a somewhat overloaded term: +Ensure that the given [store paths] are [valid]. - - If the store path is a *derivation*, realisation ensures that the - output paths of the derivation are [valid] (i.e., - the output path and its closure exist in the file system). This - can be done in several ways. First, it is possible that the - outputs are already valid, in which case we are done - immediately. Otherwise, there may be [substitutes] - that produce the outputs (e.g., by downloading them). Finally, the - outputs can be produced by running the build task described - by the derivation. +Realisation of a store path works as follows: - - If the store path is not a derivation, realisation ensures that the - specified path is valid (i.e., it and its closure exist in the file - system). If the path is already valid, we are done immediately. - Otherwise, the path and any missing paths in its closure may be - produced through substitutes. If there are no (successful) - substitutes, realisation fails. +- If the path is already valid, do nothing. +- If the path leads to a [store derivation]: + 1. Realise the store derivation file itself, as a regular store path. + 2. Realise its [output paths]: + - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. + - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying build certificates in the [Nix database]. + - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's build instructions. +- Otherwise: Try to fetch the associated [store objects] in the paths' [closure] from the [substituters]. +If no substitutes are available and no store derivation is given, realisation fails. + +[store paths]: @docroot@/glossary.md#gloss-store-path [valid]: @docroot@/glossary.md#gloss-validity -[substitutes]: @docroot@/glossary.md#gloss-substitute +[store derivation]: @docroot@/glossary.md#gloss-store-derivation +[output paths]: @docroot@/glossary.md#gloss-output-path +[store objects]: @docroot@/glossary.md#gloss-store-object +[closure]: @docroot@/glossary.md#gloss-closure +[substituters]: @docroot@/command-ref/conf-file.md#conf-substituters +[content-addressed derivations]: @docroot@/contributing/experimental-features.md#xp-feature-ca-derivations +[Nix database]: @docroot@/glossary.md#gloss-nix-database -The output path of each derivation is printed on standard output. (For -non-derivations argument, the argument itself is printed.) +The resulting paths are printed on standard output. +For non-derivation arguments, the argument itself is printed. -The following flags are available: +{{#include ../status-build-failure.md}} + +# Options - `--dry-run`\ Print on standard error a description of what packages would be @@ -54,8 +58,6 @@ The following flags are available: previous build, the new output path is left in `/nix/store/name.check.` -{{#include ../status-build-failure.md}} - {{#include ./opt-common.md}} {{#include ../opt-common.md}} @@ -67,8 +69,6 @@ The following flags are available: This operation is typically used to build [store derivation]s produced by [`nix-instantiate`](@docroot@/command-ref/nix-instantiate.md): -[store derivation]: @docroot@/glossary.md#gloss-store-derivation - ```console $ nix-store --realise $(nix-instantiate ./test.nix) /nix/store/31axcgrlbfsxzmfff1gyj1bf62hvkby2-aterm-2.3.1 From 315a11bcc9ce97cb25a41dcdfb4a859c7384f74b Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 21 Jun 2023 10:54:46 +0200 Subject: [PATCH 063/299] remove superfluous word --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 2881a69b3..f304dde2e 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -19,7 +19,7 @@ Realisation of a store path works as follows: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying build certificates in the [Nix database]. - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's build instructions. -- Otherwise: Try to fetch the associated [store objects] in the paths' [closure] from the [substituters]. +- Otherwise: Try to fetch the associated [store objects] in the paths' [closure] from [substituters]. If no substitutes are available and no store derivation is given, realisation fails. From a57e0e8c5c1be81e9352499ac31dcfce87ca44be Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 27 Jun 2023 09:16:36 +0200 Subject: [PATCH 064/299] reword introductory sentence --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index f304dde2e..00acee99c 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -10,7 +10,7 @@ Ensure that the given [store paths] are [valid]. -Realisation of a store path works as follows: +Each of *paths* is processed as follows: - If the path is already valid, do nothing. - If the path leads to a [store derivation]: From b7e9e2960566b8e21f4f69b6f222b945fababce7 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 27 Jun 2023 09:16:56 +0200 Subject: [PATCH 065/299] remove abstract description --- doc/manual/src/command-ref/nix-store/realise.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 00acee99c..72dfa618c 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -8,7 +8,6 @@ # Description -Ensure that the given [store paths] are [valid]. Each of *paths* is processed as follows: From d50f116421f7c999748887955b2d3c2e8b934b28 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 27 Jun 2023 09:17:12 +0200 Subject: [PATCH 066/299] add reference link --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 72dfa618c..2028631d0 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -11,7 +11,7 @@ Each of *paths* is processed as follows: -- If the path is already valid, do nothing. +- If the path is already [valid], do nothing. - If the path leads to a [store derivation]: 1. Realise the store derivation file itself, as a regular store path. 2. Realise its [output paths]: From 0cd8f36644042b41516e86e527ebfb19faebcbc9 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 13 Jul 2023 22:17:51 +0200 Subject: [PATCH 067/299] add anchor to `builder` --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- doc/manual/src/glossary.md | 2 +- doc/manual/src/language/derivations.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 2028631d0..9dfe74277 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -17,7 +17,7 @@ Each of *paths* is processed as follows: 2. Realise its [output paths]: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying build certificates in the [Nix database]. - - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's build instructions. + - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable. - Otherwise: Try to fetch the associated [store objects] in the paths' [closure] from [substituters]. If no substitutes are available and no store derivation is given, realisation fails. diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index d950a4ca8..2fbb23311 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -33,7 +33,7 @@ Ensure a [store path] is [valid][validity]. - This means either running the `builder` executable as specified in the corresponding [derivation] or fetching a pre-built [store object] from a [substituter]. + This means either running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation] or fetching a pre-built [store object] from a [substituter]. See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md). diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 043a38191..79a09122a 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -17,7 +17,7 @@ the attributes of which specify the inputs of the build. string. This is used as a symbolic name for the package by `nix-env`, and it is appended to the output paths of the derivation. - - There must be an attribute named `builder` that identifies the + - There must be an attribute named [`builder`]{#attr-builder} that identifies the program that is executed to perform the build. It can be either a derivation or a source (a local file reference, e.g., `./builder.sh`). From 6b3320ab05cefe73c21a469d077b277cff47733b Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 19 Jul 2023 09:27:36 +0200 Subject: [PATCH 068/299] mention remote builders Co-authored-by: Robert Hensing --- doc/manual/src/glossary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index 2fbb23311..8f0b25c89 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -33,7 +33,7 @@ Ensure a [store path] is [valid][validity]. - This means either running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation] or fetching a pre-built [store object] from a [substituter]. + This means either running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation], or fetching a pre-built [store object] from a [substituter], or delegating to a [remote builder](@docroot@/advanced-topics/distributed-builds.html) and retrieving the outputs. See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md). From d460dbdd30e3b518de962314bcf183961caf7f53 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 19 Jul 2023 09:31:03 +0200 Subject: [PATCH 069/299] be more precise about substituting store derivations Co-authored-by: Robert Hensing --- doc/manual/src/command-ref/nix-store/realise.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 9dfe74277..750cbeaff 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -11,9 +11,8 @@ Each of *paths* is processed as follows: -- If the path is already [valid], do nothing. - If the path leads to a [store derivation]: - 1. Realise the store derivation file itself, as a regular store path. + 1. If it is not [valid], substitute the store derivation file itself. 2. Realise its [output paths]: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying build certificates in the [Nix database]. From cf4e14d58da04465c2afd05af6975d568f58ce1d Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 19 Jul 2023 13:28:44 +0200 Subject: [PATCH 070/299] accommodate "do nothing" branch --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 750cbeaff..6dc908c19 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -17,7 +17,7 @@ Each of *paths* is processed as follows: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying build certificates in the [Nix database]. - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable. -- Otherwise: Try to fetch the associated [store objects] in the paths' [closure] from [substituters]. +- Otherwise, and if the path is not already valid: Try to fetch the associated [store objects] in the path's [closure] from [substituters]. If no substitutes are available and no store derivation is given, realisation fails. From b951e862d0bdb93a64e2c85af8762a9294d575c9 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 19 Jul 2023 13:30:40 +0200 Subject: [PATCH 071/299] more meaningful tagline --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 6dc908c19..680e54ffa 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -1,6 +1,6 @@ # Name -`nix-store --realise` - realise specified store paths +`nix-store --realise` - build or fetch store objects # Synopsis From 894cbe43bc5caa339dab7f8af31bf065020c9f8d Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 31 Aug 2023 20:50:22 +0200 Subject: [PATCH 072/299] don't invent terms yet --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index 680e54ffa..b97c1f735 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -15,7 +15,7 @@ Each of *paths* is processed as follows: 1. If it is not [valid], substitute the store derivation file itself. 2. Realise its [output paths]: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying build certificates in the [Nix database]. + - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying content-addressed realisation entries in the [Nix database]. - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable. - Otherwise, and if the path is not already valid: Try to fetch the associated [store objects] in the path's [closure] from [substituters]. From d38a539437943f4ae6b1f72321c7dc29559547c5 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 31 Aug 2023 20:50:50 +0200 Subject: [PATCH 073/299] make description open-ended, add TODO Co-authored-by: Robert Hensing --- doc/manual/src/command-ref/nix-store/realise.md | 2 +- doc/manual/src/glossary.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index b97c1f735..a421a4220 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -16,7 +16,7 @@ Each of *paths* is processed as follows: 2. Realise its [output paths]: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying content-addressed realisation entries in the [Nix database]. - - For any store paths that cannot be substituted, produce the required store objects by first realising all outputs of the derivation's dependencies and then running the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable. + - For any store paths that cannot be substituted, produce the required store objects. This involves first realising all outputs of the derivation's dependencies and then running the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable. - Otherwise, and if the path is not already valid: Try to fetch the associated [store objects] in the path's [closure] from [substituters]. If no substitutes are available and no store derivation is given, realisation fails. diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index 8f0b25c89..57a3334dd 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -33,7 +33,7 @@ Ensure a [store path] is [valid][validity]. - This means either running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation], or fetching a pre-built [store object] from a [substituter], or delegating to a [remote builder](@docroot@/advanced-topics/distributed-builds.html) and retrieving the outputs. + This means either running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation], or fetching a pre-built [store object] from a [substituter], or delegating to a [remote builder](@docroot@/advanced-topics/distributed-builds.html) and retrieving the outputs. See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md). From 1ac181759d975e4faeaf9083259287562009b4ec Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 31 Aug 2023 21:25:33 +0200 Subject: [PATCH 074/299] revert some random change --- doc/manual/src/command-ref/nix-store/opt-common.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/opt-common.md b/doc/manual/src/command-ref/nix-store/opt-common.md index 104b20076..dd9a6bf21 100644 --- a/doc/manual/src/command-ref/nix-store/opt-common.md +++ b/doc/manual/src/command-ref/nix-store/opt-common.md @@ -1,6 +1,6 @@ -# Command options +# Options -The following options are allowed for all operations in the `nix-store` command, but may not always have an effect. +The following options are allowed for all `nix-store` operations, but may not always have an effect. - [`--add-root`](#opt-add-root) *path* From 539cc5e5f00c0d524dec6e73b08ab8cb0f5a9630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20=C4=8Cun=C3=A1t?= Date: Thu, 22 Jun 2023 11:08:56 +0200 Subject: [PATCH 075/299] flake: update nixpkgs: 22.11 -> 23.05 The lowdown input can't be updated; `nix build` would fail to find it. Co-authored-by: Robert Hensing --- flake.lock | 8 ++++---- flake.nix | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 1d2aab5ed..2bc503258 100644 --- a/flake.lock +++ b/flake.lock @@ -34,16 +34,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1670461440, - "narHash": "sha256-jy1LB8HOMKGJEGXgzFRLDU1CBGL0/LlkolgnqIsF0D8=", + "lastModified": 1693494129, + "narHash": "sha256-YrHlSbniFmhcz0ORe8MMFttifKR4hTRzyX2OQUO9VxA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "04a75b2eecc0acf6239acf9dd04485ff8d14f425", + "rev": "22a584b861ab31a2dca52121999079832f0e0f73", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-22.11-small", + "ref": "nixos-23.05-small", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 36ba61484..49aa9c4ce 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "The purely functional package manager"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11-small"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05-small"; inputs.nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2"; inputs.lowdown-src = { url = "github:kristapsdz/lowdown"; flake = false; }; inputs.flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; From 5c95b32c461da645826ff6bf248183f066c57b20 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 1 Sep 2023 14:49:49 +0200 Subject: [PATCH 076/299] Fix warning 'catching polymorphic type by value' --- src/libstore/build/create-derivation-and-realise-goal.cc | 2 +- src/libutil/util.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libstore/build/create-derivation-and-realise-goal.cc b/src/libstore/build/create-derivation-and-realise-goal.cc index b01042f00..60f67956d 100644 --- a/src/libstore/build/create-derivation-and-realise-goal.cc +++ b/src/libstore/build/create-derivation-and-realise-goal.cc @@ -96,7 +96,7 @@ void CreateDerivationAndRealiseGoal::getDerivation() auto drvPath = StorePath::dummy; try { drvPath = resolveDerivedPath(worker.store, *drvReq); - } catch (MissingRealisation) { + } catch (MissingRealisation &) { return std::nullopt; } return worker.evalStore.isValidPath(drvPath) || worker.store.isValidPath(drvPath) diff --git a/src/libutil/util.cc b/src/libutil/util.cc index f24a6e165..5a10c69e2 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -60,7 +60,7 @@ void initLibUtil() { bool caught = false; try { throwExceptionSelfCheck(); - } catch (nix::Error _e) { + } catch (const nix::Error & _e) { caught = true; } // This is not actually the main point of this check, but let's make sure anyway: From 3ae1489847e3bde531833571d28a6e0915b4b766 Mon Sep 17 00:00:00 2001 From: Walter Franzini <5097668+wfranzini@users.noreply.github.com> Date: Tue, 31 Jan 2023 23:33:05 +0100 Subject: [PATCH 077/299] nix flakes metadata: Show lastModified timestamp for each input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, the output of `nix flake metadata` in nix repo looked like this: ... Last modified: 2023-07-09 16:00:16 Inputs: ├───flake-compat: github:edolstra/flake-compat/35bb57c0c8d8b62bbfd284272c928ceb64ddbde9 ├───lowdown-src: github:kristapsdz/lowdown/d2c2b44ff6c27b936ec27358a2653caaef8f73b8 ├───nixpkgs: github:NixOS/nixpkgs/04a75b2eecc0acf6239acf9dd04485ff8d14f425 └───nixpkgs-regression: github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2 This commit changes that to: ... Last modified: 2023-07-09 16:00:16 Inputs: ├───flake-compat: github:edolstra/flake-compat/35bb57c0c8d8b62bbfd284272c928ceb64ddbde9 (2023-01-17 11:47:33) ├───lowdown-src: github:kristapsdz/lowdown/d2c2b44ff6c27b936ec27358a2653caaef8f73b8 (2021-10-06 10:00:07) ├───nixpkgs: github:NixOS/nixpkgs/04a75b2eecc0acf6239acf9dd04485ff8d14f425 (2022-12-08 01:04:00) └───nixpkgs-regression: github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2 (2022-01-24 19:20:45) --- src/nix/flake.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 87dd4da1b..b62c45d7e 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -233,9 +233,13 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON bool last = i + 1 == node.inputs.size(); if (auto lockedNode = std::get_if<0>(&input.second)) { - logger->cout("%s" ANSI_BOLD "%s" ANSI_NORMAL ": %s", + std::string lastModifiedStr = ""; + if (auto lastModified = (*lockedNode)->lockedRef.input.getLastModified()) + lastModifiedStr = fmt(" (%s)", std::put_time(std::gmtime(&*lastModified), "%F %T")); + logger->cout("%s" ANSI_BOLD "%s" ANSI_NORMAL ": %s%s", prefix + (last ? treeLast : treeConn), input.first, - (*lockedNode)->lockedRef); + (*lockedNode)->lockedRef, + lastModifiedStr); bool firstVisit = visited.insert(*lockedNode).second; From 4c50f5d130fc3adc3b048923e267d936df5b6250 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 3 Sep 2023 13:44:32 +0200 Subject: [PATCH 078/299] traces: Do not print unknown location Solves 1/3 of the infinite recursion at unknown location meme. See #8879 for ensuring we always have a trace (for stack overflows) We might want to re-add this for finding missing location info *while hacking on that problem only*. --- src/libutil/error.cc | 12 ++++-------- src/libutil/error.hh | 7 +++++++ tests/lang/eval-fail-fromTOML-timestamps.err.exp | 2 -- tests/lang/eval-fail-hashfile-missing.err.exp | 4 ---- tests/lang/eval-fail-set-override.err.exp | 2 -- tests/lang/eval-fail-substring.err.exp | 2 -- tests/lang/eval-fail-to-path.err.exp | 2 -- 7 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/libutil/error.cc b/src/libutil/error.cc index 4a1b346ef..3917fffc3 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -203,8 +203,6 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s std::ostringstream oss; - auto noSource = ANSI_ITALIC " (source not available)" ANSI_NORMAL "\n"; - /* * Traces * ------ @@ -320,7 +318,7 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s oss << "\n" << "… " << trace.hint.str() << "\n"; - if (trace.pos) { + if (trace.pos && *trace.pos) { count++; oss << "\n" << ellipsisIndent << ANSI_BLUE << "at " ANSI_WARNING << *trace.pos << ANSI_NORMAL << ":"; @@ -329,8 +327,7 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s oss << "\n"; printCodeLines(oss, "", *trace.pos, *loc); oss << "\n"; - } else - oss << noSource; + } } } oss << "\n" << prefix; @@ -338,15 +335,14 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s oss << einfo.msg << "\n"; - if (einfo.errPos) { + if (einfo.errPos && *einfo.errPos) { oss << "\n" << ANSI_BLUE << "at " ANSI_WARNING << *einfo.errPos << ANSI_NORMAL << ":"; if (auto loc = einfo.errPos->getCodeLines()) { oss << "\n"; printCodeLines(oss, "", *einfo.errPos, *loc); oss << "\n"; - } else - oss << noSource; + } } auto suggestions = einfo.suggestions.trim(); diff --git a/src/libutil/error.hh b/src/libutil/error.hh index be5c5e252..c04dcbd77 100644 --- a/src/libutil/error.hh +++ b/src/libutil/error.hh @@ -70,6 +70,13 @@ struct AbstractPos uint32_t line = 0; uint32_t column = 0; + /** + * An AbstractPos may be a "null object", representing an unknown position. + * + * Return true if this position is known. + */ + inline operator bool() const { return line != 0; }; + /** * Return the contents of the source file. */ diff --git a/tests/lang/eval-fail-fromTOML-timestamps.err.exp b/tests/lang/eval-fail-fromTOML-timestamps.err.exp index f6bd19f5a..5b60d253d 100644 --- a/tests/lang/eval-fail-fromTOML-timestamps.err.exp +++ b/tests/lang/eval-fail-fromTOML-timestamps.err.exp @@ -8,5 +8,3 @@ error: 2| key = "value" error: while parsing a TOML string: Dates and times are not supported - - at «none»:0: (source not available) diff --git a/tests/lang/eval-fail-hashfile-missing.err.exp b/tests/lang/eval-fail-hashfile-missing.err.exp index 8e77dec1e..6d38608c0 100644 --- a/tests/lang/eval-fail-hashfile-missing.err.exp +++ b/tests/lang/eval-fail-hashfile-missing.err.exp @@ -10,10 +10,6 @@ error: … while evaluating the first argument passed to builtins.toString - at «none»:0: (source not available) - … while calling the 'hashFile' builtin - at «none»:0: (source not available) - error: opening file '/pwd/lang/this-file-is-definitely-not-there-7392097': No such file or directory diff --git a/tests/lang/eval-fail-set-override.err.exp b/tests/lang/eval-fail-set-override.err.exp index beb29d678..71481683d 100644 --- a/tests/lang/eval-fail-set-override.err.exp +++ b/tests/lang/eval-fail-set-override.err.exp @@ -1,6 +1,4 @@ error: … while evaluating the `__overrides` attribute - at «none»:0: (source not available) - error: value is an integer while a set was expected diff --git a/tests/lang/eval-fail-substring.err.exp b/tests/lang/eval-fail-substring.err.exp index dc26a00bd..5c58be29a 100644 --- a/tests/lang/eval-fail-substring.err.exp +++ b/tests/lang/eval-fail-substring.err.exp @@ -8,5 +8,3 @@ error: 2| error: negative start position in 'substring' - - at «none»:0: (source not available) diff --git a/tests/lang/eval-fail-to-path.err.exp b/tests/lang/eval-fail-to-path.err.exp index 43ed2bdfc..4ffa2cf6d 100644 --- a/tests/lang/eval-fail-to-path.err.exp +++ b/tests/lang/eval-fail-to-path.err.exp @@ -9,6 +9,4 @@ error: … while evaluating the first argument passed to builtins.toPath - at «none»:0: (source not available) - error: string 'foo/bar' doesn't represent an absolute path From f1aeeea32b113ac5ae9518221015a945625fa112 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 3 Sep 2023 14:00:55 +0200 Subject: [PATCH 079/299] traces: DRY printPosMaybe --- src/libutil/error.cc | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/libutil/error.cc b/src/libutil/error.cc index 3917fffc3..5b604c328 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -155,6 +155,25 @@ static std::string indent(std::string_view indentFirst, std::string_view indentR return res; } +/** + * Print a position, if it is known. + * + * @return true if a position was printed. + */ +static bool printPosMaybe(std::ostream & oss, std::string_view indent, const std::shared_ptr & pos) { + bool hasPos = pos && *pos; + if (hasPos) { + oss << "\n" << indent << ANSI_BLUE << "at " ANSI_WARNING << *pos << ANSI_NORMAL << ":"; + + if (auto loc = pos->getCodeLines()) { + oss << "\n"; + printCodeLines(oss, "", *pos, *loc); + oss << "\n"; + } + } + return hasPos; +} + std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool showTrace) { std::string prefix; @@ -318,32 +337,15 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s oss << "\n" << "… " << trace.hint.str() << "\n"; - if (trace.pos && *trace.pos) { + if (printPosMaybe(oss, ellipsisIndent, trace.pos)) count++; - - oss << "\n" << ellipsisIndent << ANSI_BLUE << "at " ANSI_WARNING << *trace.pos << ANSI_NORMAL << ":"; - - if (auto loc = trace.pos->getCodeLines()) { - oss << "\n"; - printCodeLines(oss, "", *trace.pos, *loc); - oss << "\n"; - } - } } oss << "\n" << prefix; } oss << einfo.msg << "\n"; - if (einfo.errPos && *einfo.errPos) { - oss << "\n" << ANSI_BLUE << "at " ANSI_WARNING << *einfo.errPos << ANSI_NORMAL << ":"; - - if (auto loc = einfo.errPos->getCodeLines()) { - oss << "\n"; - printCodeLines(oss, "", *einfo.errPos, *loc); - oss << "\n"; - } - } + printPosMaybe(oss, "", einfo.errPos); auto suggestions = einfo.suggestions.trim(); if (!suggestions.suggestions.empty()) { From 477bc617bba5617d1d1da3ce039b5bd296a3e56e Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 3 Sep 2023 14:08:03 +0200 Subject: [PATCH 080/299] traces: Add _NIX_DEVELOPER_SHOW_UNKNOWN_LOCATIONS --- src/libutil/error.cc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libutil/error.cc b/src/libutil/error.cc index 5b604c328..dd9612471 100644 --- a/src/libutil/error.cc +++ b/src/libutil/error.cc @@ -155,6 +155,15 @@ static std::string indent(std::string_view indentFirst, std::string_view indentR return res; } +/** + * A development aid for finding missing positions, to improve error messages. Example use: + * + * NIX_DEVELOPER_SHOW_UNKNOWN_LOCATIONS=1 _NIX_TEST_ACCEPT=1 make tests/lang.sh.test + * git diff -U20 tests + * + */ +static bool printUnknownLocations = getEnv("_NIX_DEVELOPER_SHOW_UNKNOWN_LOCATIONS").has_value(); + /** * Print a position, if it is known. * @@ -170,6 +179,8 @@ static bool printPosMaybe(std::ostream & oss, std::string_view indent, const std printCodeLines(oss, "", *pos, *loc); oss << "\n"; } + } else if (printUnknownLocations) { + oss << "\n" << indent << ANSI_BLUE << "at " ANSI_RED << "UNKNOWN LOCATION" << ANSI_NORMAL << "\n"; } return hasPos; } From 73f6407eeaf0018519be5d4f9dcd87537450d277 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 22:52:37 +0000 Subject: [PATCH 081/299] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/backport.yml | 2 +- .github/workflows/ci.yml | 8 ++++---- .github/workflows/hydra_status.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 62bef6322..12c60c649 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -14,7 +14,7 @@ jobs: if: github.repository_owner == 'NixOS' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # required to find all branches diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3a17d106..bcda8a77a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: cachix/install-nix-action@v22 @@ -58,7 +58,7 @@ jobs: outputs: installerURL: ${{ steps.prepare-installer.outputs.installerURL }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV @@ -82,7 +82,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV - uses: cachix/install-nix-action@v22 with: @@ -108,7 +108,7 @@ jobs: needs.check_secrets.outputs.docker == 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: cachix/install-nix-action@v22 diff --git a/.github/workflows/hydra_status.yml b/.github/workflows/hydra_status.yml index 38a9c0877..2a7574747 100644 --- a/.github/workflows/hydra_status.yml +++ b/.github/workflows/hydra_status.yml @@ -13,7 +13,7 @@ jobs: if: github.repository_owner == 'NixOS' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - run: bash scripts/check-hydra-status.sh From 87508b1065c38d77870f7894a64f4edac162f3bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 22:52:40 +0000 Subject: [PATCH 082/299] Bump cachix/install-nix-action from 22 to 23 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 22 to 23. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v22...v23) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3a17d106..e024a851e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: cachix/install-nix-action@v22 + - uses: cachix/install-nix-action@v23 with: # The sandbox would otherwise be disabled by default on Darwin extra_nix_config: "sandbox = true" @@ -62,7 +62,7 @@ jobs: with: fetch-depth: 0 - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV - - uses: cachix/install-nix-action@v22 + - uses: cachix/install-nix-action@v23 with: install_url: https://releases.nixos.org/nix/nix-2.13.3/install - uses: cachix/cachix-action@v12 @@ -84,7 +84,7 @@ jobs: steps: - uses: actions/checkout@v3 - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV - - uses: cachix/install-nix-action@v22 + - uses: cachix/install-nix-action@v23 with: install_url: '${{needs.installer.outputs.installerURL}}' install_options: "--tarball-url-prefix https://${{ env.CACHIX_NAME }}.cachix.org/serve" @@ -111,7 +111,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: cachix/install-nix-action@v22 + - uses: cachix/install-nix-action@v23 with: install_url: https://releases.nixos.org/nix/nix-2.13.3/install - run: echo CACHIX_NAME="$(echo $GITHUB_REPOSITORY-install-tests | tr "[A-Z]/" "[a-z]-")" >> $GITHUB_ENV From cc388fbc3a084a1e400c7c65df08b8dc6281056d Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 30 May 2023 23:00:16 +0200 Subject: [PATCH 083/299] remove maintainers checklist in PR template maintainers are not really using it, and it produces a lot of noise when opening PRs. --- .github/PULL_REQUEST_TEMPLATE.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4488c7b7d..217b19108 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,24 +10,6 @@ -# Checklist for maintainers - - - -Maintainers: tick if completed or explain if not relevant - - - [ ] agreed on idea - - [ ] agreed on implementation strategy - - [ ] tests, as appropriate - - functional tests - `tests/**.sh` - - unit tests - `src/*/tests` - - integration tests - `tests/nixos/*` - - [ ] documentation in the manual - - [ ] documentation in the internal API docs - - [ ] code and comments are self-explanatory - - [ ] commit message explains why the change was made - - [ ] new feature or incompatible change: updated release notes - # Priorities Add :+1: to [pull requests you find important](https://github.com/NixOS/nix/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc). From 4f2b949ba808e2161f652cd914cba2517f1faea5 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 13 Jul 2023 21:57:32 +0200 Subject: [PATCH 084/299] reorder list items --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a72a8eac..6c9d97c04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,8 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). 2. Search for related issues that cover what you're going to work on. It could help to mention there that you will work on the issue. - Issues labeled ["good first issue"](https://github.com/NixOS/nix/labels/good-first-issue) should be relatively easy to fix and are likely to get merged quickly. - Pull requests addressing issues labeled ["idea approved"](https://github.com/NixOS/nix/labels/idea%20approved) are especially welcomed by maintainers and will receive prioritised review. + Issues labeled [good first issue](https://github.com/NixOS/nix/labels/good-first-issue) should be relatively easy to fix and are likely to get merged quickly. + Pull requests addressing issues labeled [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) are especially welcomed by maintainers and will receive prioritised review. 3. Check the [Nix reference manual](https://nixos.org/manual/nix/unstable/contributing/hacking.html) for information on building Nix and running its tests. @@ -40,10 +40,10 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). 4. Make your changes! 5. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) for your changes. - * [Mark the pull request as draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request) if you're not done with the changes. - * Make sure to have [a clean history of commits on your branch by using rebase](https://www.digitalocean.com/community/tutorials/how-to-rebase-and-update-a-pull-request). * Link related issues in your pull request to inform interested parties and future contributors about your change. + * Make sure to have [a clean history of commits on your branch by using rebase](https://www.digitalocean.com/community/tutorials/how-to-rebase-and-update-a-pull-request). If your pull request closes one or multiple issues, note that in the description using `Closes: #`, as it will then happen automatically when your change is merged. + * [Mark the pull request as draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request) if you're not done with the changes. 6. Do not expect your pull request to be reviewed immediately. Nix maintainers follow a [structured process for reviews and design decisions](https://github.com/NixOS/nix/tree/master/maintainers#project-board-protocol), which may or may not prioritise your work. From 3a9c1dc8a3b872adaf30562fb28697b58be4be2b Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 13 Jul 2023 21:57:41 +0200 Subject: [PATCH 085/299] add checklist to contribution guide --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c9d97c04..facbf0eb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,19 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). 6. Do not expect your pull request to be reviewed immediately. Nix maintainers follow a [structured process for reviews and design decisions](https://github.com/NixOS/nix/tree/master/maintainers#project-board-protocol), which may or may not prioritise your work. + Following this checklist will make the process smoother for everyone: + + - [ ] Fixes an [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) issue + - [ ] Tests, as appropriate: + - Functional tests – [`tests/**.sh`](./tests) + - Unit tests – [`src/*/tests`](./src/) + - Integration tests – [`tests/nixos/*`](./tests/nixos) + - [ ] User documentation in the [manual](..doc/manual/src) + - [ ] API documentation in header files + - [ ] Code and comments are self-explanatory + - [ ] Commit message explains **why** the change was made + - [ ] New feature or incompatible change: updated [release notes](./doc/manual/src/release-notes/rl-next.md) + 7. If you need additional feedback or help to getting pull request into shape, ask other contributors using [@mentions](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#mentioning-people-and-teams). ## Making changes to the Nix manual From 7ff43435f9b23c9e0d445427cc37b147c84b056f Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 4 Sep 2023 18:15:32 -0400 Subject: [PATCH 086/299] Unit test some worker protocol serializers Continue with the characterization testing idioms begun in c70484454f52a3bba994ac0c155b4a6f35a5013c, but this time for unit tests. Co-authored-by: Andreas Rammhold --- Makefile | 1 + doc/manual/src/contributing/testing.md | 73 ++++++++- flake.nix | 1 + mk/programs.mk | 2 +- src/libstore/tests/worker-protocol.cc | 139 ++++++++++++++++++ src/libstore/worker-protocol-impl.hh | 16 ++ src/libstore/worker-protocol.hh | 4 + .../worker-protocol/content-address.bin | Bin 0 -> 208 bytes .../libstore/worker-protocol/derived-path.bin | Bin 0 -> 120 bytes .../libstore/worker-protocol/store-path.bin | Bin 0 -> 120 bytes .../libstore/worker-protocol/string.bin | Bin 0 -> 88 bytes 11 files changed, 228 insertions(+), 8 deletions(-) create mode 100644 src/libstore/tests/worker-protocol.cc create mode 100644 unit-test-data/libstore/worker-protocol/content-address.bin create mode 100644 unit-test-data/libstore/worker-protocol/derived-path.bin create mode 100644 unit-test-data/libstore/worker-protocol/store-path.bin create mode 100644 unit-test-data/libstore/worker-protocol/string.bin diff --git a/Makefile b/Makefile index 31b54b93d..2eabeb077 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ makefiles = \ -include Makefile.config ifeq ($(tests), yes) +UNIT_TEST_ENV = _NIX_TEST_UNIT_DATA=unit-test-data makefiles += \ src/libutil/tests/local.mk \ src/libstore/tests/local.mk \ diff --git a/doc/manual/src/contributing/testing.md b/doc/manual/src/contributing/testing.md index cd94d5cfb..329b34575 100644 --- a/doc/manual/src/contributing/testing.md +++ b/doc/manual/src/contributing/testing.md @@ -2,14 +2,70 @@ ## Unit-tests -The unit-tests for each Nix library (`libexpr`, `libstore`, etc..) are defined -under `src/{library_name}/tests` using the -[googletest](https://google.github.io/googletest/) and -[rapidcheck](https://github.com/emil-e/rapidcheck) frameworks. +The unit tests are defined using the [googletest] and [rapidcheck] frameworks. + +[googletest]: https://google.github.io/googletest/ +[rapidcheck]: https://github.com/emil-e/rapidcheck + +### Source and header layout + +> An example of some files, demonstrating much of what is described below +> +> ``` +> src +> ├── libexpr +> │ ├── value/context.hh +> │ ├── value/context.cc +> │ │ +> │ … +> └── tests +> │ ├── value/context.hh +> │ ├── value/context.cc +> │ │ +> │ … +> │ +> ├── unit-test-data +> │ ├── libstore +> │ │ ├── worker-protocol/content-address.bin +> │ │ … +> │ … +> … +> ``` + +The unit tests for each Nix library (`libnixexpr`, `libnixstore`, etc..) live inside a directory `src/${library_shortname}/tests` within the directory for the library (`src/${library_shortname}`). + +The data is in `unit-test-data`, with one subdir per library, with the same name as where the code goes. +For example, `libnixstore` code is in `src/libstore`, and its test data is in `unit-test-data/libstore`. +The path to the `unit-test-data` directory is passed to the unit test executable with the environment variable `_NIX_TEST_UNIT_DATA`. + +> **Note** +> Due to the way googletest works, downstream unit test executables will actually include and re-run upstream library tests. +> Therefore it is important that the same value for `_NIX_TEST_UNIT_DATA` be used with the tests for each library. +> That is why we have the test data nested within a single `unit-test-data` directory. + +### Running tests You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`. Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option, or the `GTEST_FILTER` environment variable. +### Characterization testing + +See [below](#characterization-testing-1) for a broader discussion of characterization testing. + +Like with the functional characterization, `_NIX_TEST_ACCEPT=1` is also used. +For example: +```shell-session +$ _NIX_TEST_ACCEPT=1 make libstore-tests-exe_RUN +... +[ SKIPPED ] WorkerProtoTest.string_read +[ SKIPPED ] WorkerProtoTest.string_write +[ SKIPPED ] WorkerProtoTest.storePath_read +[ SKIPPED ] WorkerProtoTest.storePath_write +... +``` +will regenerate the "golden master" expected result for the `libnixstore` characterization tests. +The characterization tests will mark themselves "skipped" since they regenerated the expected result instead of actually testing anything. + ## Functional tests The functional tests reside under the `tests` directory and are listed in `tests/local.mk`. @@ -124,9 +180,12 @@ This technique is to include the exact output/behavior of a former version of Ni For example, this technique is used for the language tests, to check both the printed final value if evaluation was successful, and any errors and warnings encountered. It is frequently useful to regenerate the expected output. -To do that, rerun the failed test with `_NIX_TEST_ACCEPT=1`. -(At least, this is the convention we've used for `tests/lang.sh`. -If we add more characterization testing we should always strive to be consistent.) +To do that, rerun the failed test(s) with `_NIX_TEST_ACCEPT=1`. +For example: +```bash +_NIX_TEST_ACCEPT=1 make tests/lang.sh.test +``` +This convention is shared with the [characterization unit tests](#characterization-testing-1) too. An interesting situation to document is the case when these tests are "overfitted". The language tests are, again, an example of this. diff --git a/flake.nix b/flake.nix index cf7c1fa12..ec6ab48bd 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,7 @@ ./precompiled-headers.h ./src ./tests + ./unit-test-data ./COPYING ./scripts/local.mk (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) diff --git a/mk/programs.mk b/mk/programs.mk index 1ee1d3fa5..a88d9d949 100644 --- a/mk/programs.mk +++ b/mk/programs.mk @@ -87,6 +87,6 @@ define build-program # Phony target to run this program (typically as a dependency of 'check'). .PHONY: $(1)_RUN $(1)_RUN: $$($(1)_PATH) - $(trace-test) $$($(1)_PATH) + $(trace-test) $$(UNIT_TEST_ENV) $$($(1)_PATH) endef diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc new file mode 100644 index 000000000..4a6ccf7c0 --- /dev/null +++ b/src/libstore/tests/worker-protocol.cc @@ -0,0 +1,139 @@ +#include + +#include +#include + +#include "worker-protocol.hh" +#include "worker-protocol-impl.hh" +#include "derived-path.hh" +#include "tests/libstore.hh" + +namespace nix { + +class WorkerProtoTest : public LibStoreTest +{ +public: + Path unitTestData = getEnv("_NIX_TEST_UNIT_DATA").value() + "/libstore/worker-protocol"; + + bool testAccept() { + return getEnv("_NIX_TEST_ACCEPT") == "1"; + } + + Path goldenMaster(std::string_view testStem) { + return unitTestData + "/" + testStem + ".bin"; + } + + /** + * Golden test for `T` reading + */ + template + void readTest(PathView testStem, T value) + { + if (testAccept()) + { + GTEST_SKIP() << "Cannot read golden master because another test is also updating it"; + } + else + { + auto expected = readFile(goldenMaster(testStem)); + + T got = ({ + StringSource from { expected }; + WorkerProto::Serialise::read( + *store, + WorkerProto::ReadConn { .from = from }); + }); + + ASSERT_EQ(got, value); + } + } + + /** + * Golden test for `T` write + */ + template + void writeTest(PathView testStem, const T & value) + { + auto file = goldenMaster(testStem); + + StringSink to; + WorkerProto::write( + *store, + WorkerProto::WriteConn { .to = to }, + value); + + if (testAccept()) + { + createDirs(dirOf(file)); + writeFile(file, to.s); + GTEST_SKIP() << "Updating golden master"; + } + else + { + auto expected = readFile(file); + ASSERT_EQ(to.s, expected); + } + } +}; + +#define CHARACTERIZATION_TEST(NAME, STEM, VALUE) \ + TEST_F(WorkerProtoTest, NAME ## _read) { \ + readTest(STEM, VALUE); \ + } \ + TEST_F(WorkerProtoTest, NAME ## _write) { \ + writeTest(STEM, VALUE); \ + } + +CHARACTERIZATION_TEST( + string, + "string", + (std::tuple { + "", + "hi", + "white rabbit", + "大白兔", + "oh no \0\0\0 what was that!", + })) + +CHARACTERIZATION_TEST( + storePath, + "store-path", + (std::tuple { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, + })) + +CHARACTERIZATION_TEST( + contentAddress, + "content-address", + (std::tuple { + ContentAddress { + .method = TextIngestionMethod {}, + .hash = hashString(HashType::htSHA256, "Derive(...)"), + }, + ContentAddress { + .method = FileIngestionMethod::Flat, + .hash = hashString(HashType::htSHA1, "blob blob..."), + }, + ContentAddress { + .method = FileIngestionMethod::Recursive, + .hash = hashString(HashType::htSHA256, "(...)"), + }, + })) + +CHARACTERIZATION_TEST( + derivedPath, + "derived-path", + (std::tuple { + DerivedPath::Opaque { + .path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }), + .outputs = OutputsSpec::Names { "x", "y" }, + }, + })) + +} diff --git a/src/libstore/worker-protocol-impl.hh b/src/libstore/worker-protocol-impl.hh index d3d2792ff..4f797f95a 100644 --- a/src/libstore/worker-protocol-impl.hh +++ b/src/libstore/worker-protocol-impl.hh @@ -75,4 +75,20 @@ void WorkerProto::Serialise>::write(const Store & store, WorkerPr } } +template +std::tuple WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) +{ + return std::tuple { + WorkerProto::Serialise::read(store, conn)..., + }; +} + +template +void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::tuple & res) +{ + std::apply([&](const Us &... args) { + (WorkerProto::Serialise::write(store, conn, args), ...); + }, res); +} + } diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index ff762c924..70a5bddb9 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -28,6 +28,8 @@ class Store; struct Source; // items being serialised +class StorePath; +struct ContentAddress; struct DerivedPath; struct DrvOutput; struct Realisation; @@ -220,6 +222,8 @@ template MAKE_WORKER_PROTO(std::vector); template MAKE_WORKER_PROTO(std::set); +template +MAKE_WORKER_PROTO(std::tuple); template #define X_ std::map diff --git a/unit-test-data/libstore/worker-protocol/content-address.bin b/unit-test-data/libstore/worker-protocol/content-address.bin new file mode 100644 index 0000000000000000000000000000000000000000..8f14bcdb3e50cc72d87106ce159a6fd7b9f75713 GIT binary patch literal 208 zcmY+;K@P$o5QSmy;|7WYrYjRqQfMm$+LWPzDW_MfG4bucKks(>Y#V56lkFOiEzi24 zUMdC5VUCa86OJ&gL0BF^B{HBQEY%f|I<6v7#q+l_PBirI5N~-md%37WE|g33QTE2k zc~-WF&R}7Oxc@o)T?ojpD!+*5=xpO;%IoCKSV5FMh={>xePK|8<${>E)*huNHZ(pX literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/derived-path.bin b/unit-test-data/libstore/worker-protocol/derived-path.bin new file mode 100644 index 0000000000000000000000000000000000000000..bb1a81ac6d096cddf510a457e7506e6a81ee66a3 GIT binary patch literal 120 zcmdOAfB^lx%nJSDlKi4n{dB`}^NdR4LR_?NT7Eu*IT`wr5{vXwipmr#bSfDDBbg*z literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/store-path.bin b/unit-test-data/libstore/worker-protocol/store-path.bin new file mode 100644 index 0000000000000000000000000000000000000000..3fc05f2981d3398ab30ec34125c903cdcefa8729 GIT binary patch literal 120 tcmdOAfB^lx%nJSDlKi4n{dB`}^NdR4LR_?NT7Eu*F?srQlM;)-IsqP{BM|@q literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/string.bin b/unit-test-data/libstore/worker-protocol/string.bin new file mode 100644 index 0000000000000000000000000000000000000000..aa7b5a604745b735473ec34283177e69320776e4 GIT binary patch literal 88 zcmZQzfB+^aoskJ)@Id+H8JQ)i3Pp)YNtq=eAx^0H( Date: Tue, 5 Sep 2023 11:16:39 -0400 Subject: [PATCH 087/299] Test and begin documentation of the ATerm format for derivations Wanted to do this before the last dynamic derivations PR when I introduce a variation, to make sure I wasn't changing the old version by mistake. --- doc/manual/src/SUMMARY.md.in | 1 + doc/manual/src/protocols/derivation-aterm.md | 9 ++ src/libstore/tests/derivation.cc | 119 ++++++++++++------- 3 files changed, 83 insertions(+), 46 deletions(-) create mode 100644 doc/manual/src/protocols/derivation-aterm.md diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index 6c599abcf..3de053a17 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -100,6 +100,7 @@ - [File System Object](architecture/file-system-object.md) - [Protocols](protocols/protocols.md) - [Serving Tarball Flakes](protocols/tarball-fetcher.md) + - [Derivation on-disk "ATerm" format](protocols/derivation-aterm.md) - [Glossary](glossary.md) - [Contributing](contributing/contributing.md) - [Hacking](contributing/hacking.md) diff --git a/doc/manual/src/protocols/derivation-aterm.md b/doc/manual/src/protocols/derivation-aterm.md new file mode 100644 index 000000000..2377e51ae --- /dev/null +++ b/doc/manual/src/protocols/derivation-aterm.md @@ -0,0 +1,9 @@ +# Derivation on-disk "ATerm" format + +For historical reasons, [derivations](@docroot@/glossary.md#gloss-store-derivation) are stored on-disk in [ATerm](https://homepages.cwi.nl/~daybuild/daily-books/technology/aterm-guide/aterm-guide.html) format. + +Derivations are serialised in the following format: + +``` +Derive(...) +``` diff --git a/src/libstore/tests/derivation.cc b/src/libstore/tests/derivation.cc index 0e28c1f08..7fd7f9278 100644 --- a/src/libstore/tests/derivation.cc +++ b/src/libstore/tests/derivation.cc @@ -143,26 +143,76 @@ TEST_JSON(ImpureDerivationTest, impure, #undef TEST_JSON -#define TEST_JSON(NAME, STR, VAL, DRV_NAME) \ - TEST_F(DerivationTest, Derivation_ ## NAME ## _to_json) { \ - using nlohmann::literals::operator "" _json; \ - ASSERT_EQ( \ - STR ## _json, \ - (Derivation { VAL }).toJSON(*store)); \ - } \ - \ - TEST_F(DerivationTest, Derivation_ ## NAME ## _from_json) { \ - using nlohmann::literals::operator "" _json; \ - ASSERT_EQ( \ - Derivation { VAL }, \ - Derivation::fromJSON( \ - *store, \ - STR ## _json)); \ +#define TEST_JSON(NAME, STR, VAL) \ + TEST_F(DerivationTest, Derivation_ ## NAME ## _to_json) { \ + using nlohmann::literals::operator "" _json; \ + ASSERT_EQ( \ + STR ## _json, \ + (VAL).toJSON(*store)); \ + } \ + \ + TEST_F(DerivationTest, Derivation_ ## NAME ## _from_json) { \ + using nlohmann::literals::operator "" _json; \ + ASSERT_EQ( \ + (VAL), \ + Derivation::fromJSON( \ + *store, \ + STR ## _json)); \ } +#define TEST_ATERM(NAME, STR, VAL, DRV_NAME) \ + TEST_F(DerivationTest, Derivation_ ## NAME ## _to_aterm) { \ + ASSERT_EQ( \ + STR, \ + (VAL).unparse(*store, false)); \ + } \ + \ + TEST_F(DerivationTest, Derivation_ ## NAME ## _from_aterm) { \ + auto parsed = parseDerivation( \ + *store, \ + STR, \ + DRV_NAME); \ + ASSERT_EQ( \ + (VAL).toJSON(*store), \ + parsed.toJSON(*store)); \ + ASSERT_EQ( \ + (VAL), \ + parsed); \ + } + +Derivation makeSimpleDrv(const Store & store) { + Derivation drv; + drv.name = "simple-derivation"; + drv.inputSrcs = { + store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"), + }; + drv.inputDrvs = { + { + store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv"), + { + "cat", + "dog", + }, + }, + }; + drv.platform = "wasm-sel4"; + drv.builder = "foo"; + drv.args = { + "bar", + "baz", + }; + drv.env = { + { + "BIG_BAD", + "WOLF", + }, + }; + return drv; +} + TEST_JSON(simple, R"({ - "name": "my-derivation", + "name": "simple-derivation", "inputSrcs": [ "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1" ], @@ -183,37 +233,14 @@ TEST_JSON(simple, }, "outputs": {} })", - ({ - Derivation drv; - drv.name = "my-derivation"; - drv.inputSrcs = { - store->parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"), - }; - drv.inputDrvs = { - { - store->parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv"), - { - "cat", - "dog", - }, - } - }; - drv.platform = "wasm-sel4"; - drv.builder = "foo"; - drv.args = { - "bar", - "baz", - }; - drv.env = { - { - "BIG_BAD", - "WOLF", - }, - }; - drv; - }), - "drv-name") + makeSimpleDrv(*store)) + +TEST_ATERM(simple, + R"(Derive([],[("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv",["cat","dog"])],["/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"],"wasm-sel4","foo",["bar","baz"],[("BIG_BAD","WOLF")]))", + makeSimpleDrv(*store), + "simple-derivation") #undef TEST_JSON +#undef TEST_ATERM } From 2b3a17820f202ea2108184a399f3c573ca11c1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Wed, 6 Sep 2023 04:19:40 +0000 Subject: [PATCH 088/299] Fix globals.hh typo --- src/libstore/globals.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 7009f6bb8..cf10edebd 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -554,7 +554,7 @@ public: R"( This option determines the maximum size of the `tmpfs` filesystem mounted on `/dev/shm` in Linux sandboxes. For the format, see the - description of the `size` option of `tmpfs` in mount8. The default + description of the `size` option of `tmpfs` in mount(8). The default is `50%`. )"}; From 5c23d3a90cb1a76a0ea464be380bb16dc9f999c7 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:09:02 +0200 Subject: [PATCH 089/299] disambiguate output from output path --- doc/manual/src/glossary.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index 57a3334dd..b7c340d36 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -197,9 +197,15 @@ [closure]: #gloss-closure +- [output]{#gloss-output} + + A [store object] produced by a [derivation]. + + [output]: #gloss-output + - [output path]{#gloss-output-path} - A [store path] produced by a [derivation]. + The [store path] to the [output] of a [derivation]. [output path]: #gloss-output-path From 02c2679f0e94013b5cae6305e0b15fce8d971eb8 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:49:00 +0200 Subject: [PATCH 090/299] dedent common options listing; one sentence per line this is a pure reformatting, contents were not changed one sentence per line makes reviewing diffs and making suggestions much more convenient. the indentation was an artifat of the DocBook migration. --- doc/manual/src/command-ref/opt-common.md | 335 +++++++++++------------ 1 file changed, 161 insertions(+), 174 deletions(-) diff --git a/doc/manual/src/command-ref/opt-common.md b/doc/manual/src/command-ref/opt-common.md index 54c0a1d0d..0647228c2 100644 --- a/doc/manual/src/command-ref/opt-common.md +++ b/doc/manual/src/command-ref/opt-common.md @@ -2,217 +2,204 @@ Most Nix commands accept the following command-line options: - - [`--help`](#opt-help)\ - Prints out a summary of the command syntax and exits. +- [`--help`](#opt-help) - - [`--version`](#opt-version)\ - Prints out the Nix version number on standard output and exits. + Prints out a summary of the command syntax and exits. - - [`--verbose`](#opt-verbose) / `-v`\ - Increases the level of verbosity of diagnostic messages printed on - standard error. For each Nix operation, the information printed on - standard output is well-defined; any diagnostic information is - printed on standard error, never on standard output. +- [`--version`](#opt-version) - This option may be specified repeatedly. Currently, the following - verbosity levels exist: + Prints out the Nix version number on standard output and exits. - - 0\ - “Errors only”: only print messages explaining why the Nix - invocation failed. +- [`--verbose`](#opt-verbose) / `-v` - - 1\ - “Informational”: print *useful* messages about what Nix is - doing. This is the default. + Increases the level of verbosity of diagnostic messages printed on standard error. + For each Nix operation, the information printed on standard output is well-defined; + any diagnostic information is printed on standard error, never on standard output. - - 2\ - “Talkative”: print more informational messages. + This option may be specified repeatedly. + Currently, the following verbosity levels exist: - - 3\ - “Chatty”: print even more informational messages. + - `0` “Errors only” - - 4\ - “Debug”: print debug information. + Only print messages explaining why the Nix invocation failed. - - 5\ - “Vomit”: print vast amounts of debug information. + - `1` “Informational” - - [`--quiet`](#opt-quiet)\ - Decreases the level of verbosity of diagnostic messages printed on - standard error. This is the inverse option to `-v` / `--verbose`. + Print *useful* messages about what Nix is doing. + This is the default. - This option may be specified repeatedly. See the previous verbosity - levels list. + - `2` “Talkative” - - [`--log-format`](#opt-log-format) *format*\ - This option can be used to change the output of the log format, with - *format* being one of: + Print more informational messages. - - raw\ - This is the raw format, as outputted by nix-build. + - `3` “Chatty” - - internal-json\ - Outputs the logs in a structured manner. + Print even more informational messages. - > **Warning** - > - > While the schema itself is relatively stable, the format of - > the error-messages (namely of the `msg`-field) can change - > between releases. + - `4` “Debug” + + Print debug information. - - bar\ - Only display a progress bar during the builds. + - `5` “Vomit” - - bar-with-logs\ - Display the raw logs, with the progress bar at the bottom. + Print vast amounts of debug information. - - [`--no-build-output`](#opt-no-build-output) / `-Q`\ - By default, output written by builders to standard output and - standard error is echoed to the Nix command's standard error. This - option suppresses this behaviour. Note that the builder's standard - output and error are always written to a log file in - `prefix/nix/var/log/nix`. +- [`--quiet`](#opt-quiet) - - [`--max-jobs`](#opt-max-jobs) / `-j` *number*\ - Sets the maximum number of build jobs that Nix will perform in - parallel to the specified number. Specify `auto` to use the number - of CPUs in the system. The default is specified by the `max-jobs` - configuration setting, which itself defaults to `1`. A higher - value is useful on SMP systems or to exploit I/O latency. + Decreases the level of verbosity of diagnostic messages printed on standard error. + This is the inverse option to `-v` / `--verbose`. - Setting it to `0` disallows building on the local machine, which is - useful when you want builds to happen only on remote builders. + This option may be specified repeatedly. + See the previous verbosity levels list. - - [`--cores`](#opt-cores)\ - Sets the value of the `NIX_BUILD_CORES` environment variable in - the invocation of builders. Builders can use this variable at - their discretion to control the maximum amount of parallelism. For - instance, in Nixpkgs, if the derivation attribute - `enableParallelBuilding` is set to `true`, the builder passes the - `-jN` flag to GNU Make. It defaults to the value of the `cores` - configuration setting, if set, or `1` otherwise. The value `0` - means that the builder should use all available CPU cores in the - system. +- [`--log-format`](#opt-log-format) *format* - - [`--max-silent-time`](#opt-max-silent-time)\ - Sets the maximum number of seconds that a builder can go without - producing any data on standard output or standard error. The - default is specified by the `max-silent-time` configuration - setting. `0` means no time-out. + This option can be used to change the output of the log format, with *format* being one of: - - [`--timeout`](#opt-timeout)\ - Sets the maximum number of seconds that a builder can run. The - default is specified by the `timeout` configuration setting. `0` - means no timeout. + - `raw` - - [`--keep-going`](#opt-keep-going) / `-k`\ - Keep going in case of failed builds, to the greatest extent - possible. That is, if building an input of some derivation fails, - Nix will still build the other inputs, but not the derivation - itself. Without this option, Nix stops if any build fails (except - for builds of substitutes), possibly killing builds in progress (in - case of parallel or distributed builds). + This is the raw format, as outputted by nix-build. - - [`--keep-failed`](#opt-keep-failed) / `-K`\ - Specifies that in case of a build failure, the temporary directory - (usually in `/tmp`) in which the build takes place should not be - deleted. The path of the build directory is printed as an - informational message. + - `internal-json` - - [`--fallback`](#opt-fallback)\ - Whenever Nix attempts to build a derivation for which substitutes - are known for each output path, but realising the output paths - through the substitutes fails, fall back on building the derivation. + Outputs the logs in a structured manner. - The most common scenario in which this is useful is when we have - registered substitutes in order to perform binary distribution from, - say, a network repository. If the repository is down, the - realisation of the derivation will fail. When this option is - specified, Nix will build the derivation instead. Thus, installation - from binaries falls back on installation from source. This option is - not the default since it is generally not desirable for a transient - failure in obtaining the substitutes to lead to a full build from - source (with the related consumption of resources). + > **Warning** + > + > While the schema itself is relatively stable, the format of + > the error-messages (namely of the `msg`-field) can change + > between releases. - - [`--readonly-mode`](#opt-readonly-mode)\ - When this option is used, no attempt is made to open the Nix - database. Most Nix operations do need database access, so those - operations will fail. + - `bar` - - [`--arg`](#opt-arg) *name* *value*\ - This option is accepted by `nix-env`, `nix-instantiate`, - `nix-shell` and `nix-build`. When evaluating Nix expressions, the - expression evaluator will automatically try to call functions that - it encounters. It can automatically call functions for which every - argument has a [default - value](@docroot@/language/constructs.md#functions) (e.g., - `{ argName ? defaultValue }: ...`). With `--arg`, you can also - call functions that have arguments without a default value (or - override a default value). That is, if the evaluator encounters a - function with an argument named *name*, it will call it with value - *value*. + Only display a progress bar during the builds. - For instance, the top-level `default.nix` in Nixpkgs is actually a - function: + - `bar-with-logs` - ```nix - { # The system (e.g., `i686-linux') for which to build the packages. - system ? builtins.currentSystem - ... - }: ... - ``` + Display the raw logs, with the progress bar at the bottom. - So if you call this Nix expression (e.g., when you do `nix-env --install --attr - pkgname`), the function will be called automatically using the - value [`builtins.currentSystem`](@docroot@/language/builtins.md) for - the `system` argument. You can override this using `--arg`, e.g., - `nix-env --install --attr pkgname --arg system \"i686-freebsd\"`. (Note that - since the argument is a Nix string literal, you have to escape the - quotes.) +- [`--no-build-output`](#opt-no-build-output) / `-Q` - - [`--argstr`](#opt-argstr) *name* *value*\ - This option is like `--arg`, only the value is not a Nix - expression but a string. So instead of `--arg system - \"i686-linux\"` (the outer quotes are to keep the shell happy) you - can say `--argstr system i686-linux`. + By default, output written by builders to standard output and standard error is echoed to the Nix command's standard error. + This option suppresses this behaviour. + Note that the builder's standard output and error are always written to a log file in `prefix/nix/var/log/nix`. - - [`--attr`](#opt-attr) / `-A` *attrPath*\ - Select an attribute from the top-level Nix expression being - evaluated. (`nix-env`, `nix-instantiate`, `nix-build` and - `nix-shell` only.) The *attribute path* *attrPath* is a sequence - of attribute names separated by dots. For instance, given a - top-level Nix expression *e*, the attribute path `xorg.xorgserver` - would cause the expression `e.xorg.xorgserver` to be used. See - [`nix-env --install`](@docroot@/command-ref/nix-env/install.md) for some - concrete examples. +- [`--max-jobs`](#opt-max-jobs) / `-j` *number* - In addition to attribute names, you can also specify array indices. - For instance, the attribute path `foo.3.bar` selects the `bar` - attribute of the fourth element of the array in the `foo` attribute - of the top-level expression. + Sets the maximum number of build jobs that Nix will perform in parallel to the specified number. + Specify `auto` to use the number of CPUs in the system. + The default is specified by the `max-jobs` configuration setting, which itself defaults to `1`. + A higher value is useful on SMP systems or to exploit I/O latency. - - [`--expr`](#opt-expr) / `-E`\ - Interpret the command line arguments as a list of Nix expressions to - be parsed and evaluated, rather than as a list of file names of Nix - expressions. (`nix-instantiate`, `nix-build` and `nix-shell` only.) + Setting it to `0` disallows building on the local machine, which is useful when you want builds to happen only on remote builders. - For `nix-shell`, this option is commonly used to give you a shell in - which you can build the packages returned by the expression. If you - want to get a shell which contain the *built* packages ready for - use, give your expression to the `nix-shell --packages ` convenience flag - instead. +- [`--cores`](#opt-cores) - - [`-I`](#opt-I) *path*\ - Add an entry to the [Nix expression search path](@docroot@/command-ref/conf-file.md#conf-nix-path). - This option may be given multiple times. - Paths added through `-I` take precedence over [`NIX_PATH`](@docroot@/command-ref/env-common.md#env-NIX_PATH). + Sets the value of the `NIX_BUILD_CORES` environment variable in the invocation of builders. + Builders can use this variable at their discretion to control the maximum amount of parallelism. + For instance, in Nixpkgs, if the derivation attribute `enableParallelBuilding` is set to `true`, the builder passes the `-jN` flag to GNU Make. + It defaults to the value of the `cores` configuration setting, if set, or `1` otherwise. + The value `0` means that the builder should use all available CPU cores in the system. - - [`--option`](#opt-option) *name* *value*\ - Set the Nix configuration option *name* to *value*. This overrides - settings in the Nix configuration file (see nix.conf5). +- [`--max-silent-time`](#opt-max-silent-time) - - [`--repair`](#opt-repair)\ - Fix corrupted or missing store paths by redownloading or rebuilding - them. Note that this is slow because it requires computing a - cryptographic hash of the contents of every path in the closure of - the build. Also note the warning under `nix-store --repair-path`. + Sets the maximum number of seconds that a builder can go without producing any data on standard output or standard error. + The default is specified by the `max-silent-time` configuration setting. + `0` means no time-out. + +- [`--timeout`](#opt-timeout) + + Sets the maximum number of seconds that a builder can run. + The default is specified by the `timeout` configuration setting. + `0` means no timeout. + +- [`--keep-going`](#opt-keep-going) / `-k` + + Keep going in case of failed builds, to the greatest extent possible. + That is, if building an input of some derivation fails, Nix will still build the other inputs, but not the derivation itself. + Without this option, Nix stops if any build fails (except for builds of substitutes), possibly killing builds in progress (in case of parallel or distributed builds). + +- [`--keep-failed`](#opt-keep-failed) / `-K` + + Specifies that in case of a build failure, the temporary directory (usually in `/tmp`) in which the build takes place should not be deleted. + The path of the build directory is printed as an informational message. + +- [`--fallback`](#opt-fallback) + + Whenever Nix attempts to build a derivation for which substitutes are known for each output path, but realising the output paths through the substitutes fails, fall back on building the derivation. + + The most common scenario in which this is useful is when we have registered substitutes in order to perform binary distribution from, say, a network repository. + If the repository is down, the realisation of the derivation will fail. + When this option is specified, Nix will build the derivation instead. + Thus, installation from binaries falls back on installation from source. + This option is not the default since it is generally not desirable for a transient failure in obtaining the substitutes to lead to a full build from source (with the related consumption of resources). + +- [`--readonly-mode`](#opt-readonly-mode) + + When this option is used, no attempt is made to open the Nix database. + Most Nix operations do need database access, so those operations will fail. + +- [`--arg`](#opt-arg) *name* *value* + + This option is accepted by `nix-env`, `nix-instantiate`, `nix-shell` and `nix-build`. + When evaluating Nix expressions, the expression evaluator will automatically try to call functions that it encounters. + It can automatically call functions for which every argument has a [default value](@docroot@/language/constructs.md#functions) (e.g., `{ argName ? defaultValue }: ...`). + + With `--arg`, you can also call functions that have arguments without a default value (or override a default value). + That is, if the evaluator encounters a function with an argument named *name*, it will call it with value *value*. + + For instance, the top-level `default.nix` in Nixpkgs is actually a function: + + ```nix + { # The system (e.g., `i686-linux') for which to build the packages. + system ? builtins.currentSystem + ... + }: ... + ``` + + So if you call this Nix expression (e.g., when you do `nix-env --install --attr pkgname`), the function will be called automatically using the value [`builtins.currentSystem`](@docroot@/language/builtins.md) for the `system` argument. + You can override this using `--arg`, e.g., `nix-env --install --attr pkgname --arg system \"i686-freebsd\"`. + (Note that since the argument is a Nix string literal, you have to escape the quotes.) + +- [`--argstr`](#opt-argstr) *name* *value* + + This option is like `--arg`, only the value is not a Nix expression but a string. + So instead of `--arg system \"i686-linux\"` (the outer quotes are to keep the shell happy) you can say `--argstr system i686-linux`. + +- [`--attr`](#opt-attr) / `-A` *attrPath* + + Select an attribute from the top-level Nix expression being evaluated. + (`nix-env`, `nix-instantiate`, `nix-build` and `nix-shell` only.) + The *attribute path* *attrPath* is a sequence of attribute names separated by dots. + For instance, given a top-level Nix expression *e*, the attribute path `xorg.xorgserver` would cause the expression `e.xorg.xorgserver` to be used. + See [`nix-env --install`](@docroot@/command-ref/nix-env/install.md) for some concrete examples. + + In addition to attribute names, you can also specify array indices. + For instance, the attribute path `foo.3.bar` selects the `bar` + attribute of the fourth element of the array in the `foo` attribute + of the top-level expression. + +- [`--expr`](#opt-expr) / `-E` + + Interpret the command line arguments as a list of Nix expressions to be parsed and evaluated, rather than as a list of file names of Nix expressions. + (`nix-instantiate`, `nix-build` and `nix-shell` only.) + + For `nix-shell`, this option is commonly used to give you a shell in which you can build the packages returned by the expression. + If you want to get a shell which contain the *built* packages ready for use, give your expression to the `nix-shell --packages ` convenience flag instead. + +- [`-I`](#opt-I) *path* + + Add an entry to the [Nix expression search path](@docroot@/command-ref/conf-file.md#conf-nix-path). + This option may be given multiple times. + Paths added through `-I` take precedence over [`NIX_PATH`](@docroot@/command-ref/env-common.md#env-NIX_PATH). + +- [`--option`](#opt-option) *name* *value* + + Set the Nix configuration option *name* to *value*. + This overrides settings in the Nix configuration file (see nix.conf5). + +- [`--repair`](#opt-repair) + + Fix corrupted or missing store paths by redownloading or rebuilding them. + Note that this is slow because it requires computing a cryptographic hash of the contents of every path in the closure of the build. + Also note the warning under `nix-store --repair-path`. From 391f18063c0279e2de189176335a1b2863b9533a Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:52:56 +0200 Subject: [PATCH 091/299] add anchors to option listings --- doc/manual/generate-manpage.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 65eec42d0..bc4d2c5bc 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -98,7 +98,7 @@ let (option ? labels) (concatStringsSep " " (map (s: "*${s}*") option.labels)); in trim '' - - `--${name}` ${shortName} ${labels} + - [`--${name}`](#opt-${name}) ${shortName} ${labels} ${option.description} ''; From d568877eab59a490694e0bc3edec99d13049552c Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 6 Sep 2023 08:43:16 -0400 Subject: [PATCH 092/299] Retitle section as Robert suggests Co-authored-by: Robert Hensing --- doc/manual/src/SUMMARY.md.in | 2 +- doc/manual/src/protocols/derivation-aterm.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index 3de053a17..701a6ea69 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -100,7 +100,7 @@ - [File System Object](architecture/file-system-object.md) - [Protocols](protocols/protocols.md) - [Serving Tarball Flakes](protocols/tarball-fetcher.md) - - [Derivation on-disk "ATerm" format](protocols/derivation-aterm.md) + - [Derivation "ATerm" file format](protocols/derivation-aterm.md) - [Glossary](glossary.md) - [Contributing](contributing/contributing.md) - [Hacking](contributing/hacking.md) diff --git a/doc/manual/src/protocols/derivation-aterm.md b/doc/manual/src/protocols/derivation-aterm.md index 2377e51ae..c9dc00ef7 100644 --- a/doc/manual/src/protocols/derivation-aterm.md +++ b/doc/manual/src/protocols/derivation-aterm.md @@ -1,4 +1,4 @@ -# Derivation on-disk "ATerm" format +# Derivation "ATerm" file format For historical reasons, [derivations](@docroot@/glossary.md#gloss-store-derivation) are stored on-disk in [ATerm](https://homepages.cwi.nl/~daybuild/daily-books/technology/aterm-guide/aterm-guide.html) format. From b7edc2099fffd7d54638122d2d9950e3d3a751f6 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 6 Sep 2023 11:35:01 -0400 Subject: [PATCH 093/299] Improve derivation parsing - Don't assert: Derivation ATerms are not necessarily produced by Nix, and parsers should always throw graceful errors - Improve error message from `static void except(..)`, shows both what we expected and what we actually got. The intention is that we backport it, and then hopefully a few people might get slightly better errors if they try out new experimental drv files (for RFC 92) with an old version of Nix. --- src/libstore/derivations.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index dc32c3847..43d3dc4a2 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -154,8 +154,9 @@ static void expect(std::istream & str, std::string_view s) { char s2[s.size()]; str.read(s2, s.size()); - if (std::string(s2, s.size()) != s) - throw FormatError("expected string '%1%'", s); + std::string_view s2View { s2, s.size() }; + if (s2View != s) + throw FormatError("expected string '%s', got '%s'", s, s2View); } @@ -223,7 +224,8 @@ static DerivationOutput parseDerivationOutput(const Store & store, const auto hashType = parseHashType(hashAlgo); if (hashS == "impure") { experimentalFeatureSettings.require(Xp::ImpureDerivations); - assert(pathS == ""); + if (pathS != "") + throw FormatError("impure derivation output should not specify output path"); return DerivationOutput::Impure { .method = std::move(method), .hashType = std::move(hashType), @@ -239,7 +241,8 @@ static DerivationOutput parseDerivationOutput(const Store & store, }; } else { experimentalFeatureSettings.require(Xp::CaDerivations); - assert(pathS == ""); + if (pathS != "") + throw FormatError("content-addressed derivation output should not specify output path"); return DerivationOutput::CAFloating { .method = std::move(method), .hashType = std::move(hashType), From 589fd897fb8414561b7b328e864e998876ff822b Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:14:56 +0200 Subject: [PATCH 094/299] add todo on store docs --- doc/manual/generate-manpage.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index bc4d2c5bc..0f91a9742 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -70,6 +70,7 @@ let * [`${command} ${name}`](./${appendName filename name}.md) - ${subcmd.description} ''; + # TODO: move this confusing special case out of here when implementing #8496 maybeDocumentation = optionalString (details ? doc) (replaceStrings ["@stores@"] [storeDocs] details.doc); From b0fe7f560ddb31fea55546a79a89c44a63cd56ce Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:15:03 +0200 Subject: [PATCH 095/299] add missing link --- doc/manual/generate-manpage.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 0f91a9742..705fc0b69 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -148,7 +148,7 @@ let To use this store, you need to make sure the corresponding experimental feature, [`${experimentalFeature}`](@docroot@/contributing/experimental-features.md#xp-feature-${experimentalFeature}), is enabled. - For example, include the following in [`nix.conf`](#): + For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md): ``` extra-experimental-features = ${experimentalFeature} From 09eb7f1ef653822616e7371c906ef1ae34b3ba09 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:30:32 +0200 Subject: [PATCH 096/299] do not show configuration override flags for each command this removes a lot of noise from the web search, which precludes finding the actual documentation. some configuration settings have enough documentation to warrant individual pages, so the alternative of including full setting documentation in each command page doesn't make much sense here. this change technically means that the command line flags to override settings are "invisible", and not exported as JSON. this may or may not be desirable. a more explicit approach would be adding a `hidden` field to the flag's JSON output, but would also require adjusting post-processing of that JSON for manual rendering. --- doc/manual/generate-manpage.nix | 4 ++++ src/libutil/args.cc | 1 + 2 files changed, 5 insertions(+) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 705fc0b69..87891fa7a 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -79,6 +79,10 @@ let # Options ${showOptions details.flags toplevel.flags} + + > **Note** + > + > See [`man nix.conf`](@docroot@/command-ref/conf-file.md#command-line-flags) for overriding configuration settings with command line flags. ''; showOptions = options: commonOptions: diff --git a/src/libutil/args.cc b/src/libutil/args.cc index 00281ce6f..8db293762 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -236,6 +236,7 @@ nlohmann::json Args::toJSON() for (auto & [name, flag] : longFlags) { auto j = nlohmann::json::object(); + if (hiddenCategories.count(flag->category)) continue; if (flag->aliases.count(name)) continue; if (flag->shortName) j["shortName"] = std::string(1, flag->shortName); From aa46f536e80ca20e0fc45664af867b47597a87f3 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 6 Sep 2023 09:40:51 +0200 Subject: [PATCH 097/299] add note on overriding settings for stable commands --- doc/manual/src/command-ref/opt-common.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/manual/src/command-ref/opt-common.md b/doc/manual/src/command-ref/opt-common.md index 0647228c2..114b292f9 100644 --- a/doc/manual/src/command-ref/opt-common.md +++ b/doc/manual/src/command-ref/opt-common.md @@ -203,3 +203,7 @@ Most Nix commands accept the following command-line options: Fix corrupted or missing store paths by redownloading or rebuilding them. Note that this is slow because it requires computing a cryptographic hash of the contents of every path in the closure of the build. Also note the warning under `nix-store --repair-path`. + +> **Note** +> +> See [`man nix.conf`](@docroot@/command-ref/conf-file.md#command-line-flags) for overriding configuration settings with command line flags. From 829d4d3e03e0f7935c1fd3e1ef760980c8cf6046 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 7 Sep 2023 15:13:22 +0200 Subject: [PATCH 098/299] fix invalid anchor link --- src/libexpr/eval-settings.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/eval-settings.hh b/src/libexpr/eval-settings.hh index e6666061a..19122bc31 100644 --- a/src/libexpr/eval-settings.hh +++ b/src/libexpr/eval-settings.hh @@ -21,7 +21,7 @@ struct EvalSettings : Config R"( List of directories to be searched for `<...>` file references - In particular, outside of [pure evaluation mode](#conf-pure-evaluation), this determines the value of + In particular, outside of [pure evaluation mode](#conf-pure-eval), this determines the value of [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath). )"}; From 7ad66cb3ef7615b51a35a099e232640d528c006c Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 1 Oct 2021 18:05:53 -0400 Subject: [PATCH 099/299] Allow dynamic derivation deps in `inputDrvs` We use the same nested map representation we used for goals, again in order to save space. We might someday want to combine with `inputDrvs`, by doing `V = bool` instead of `V = std::set`, but we are not doing that yet for sake of a smaller diff. The ATerm format for Derivations also needs to be extended, in addition to the in-memory format. To accomodate this, we added a new basic versioning scheme, so old versions of Nix will get nice errors. (And going forward, if the ATerm format changes again the errors will be even better.) `parsedStrings`, an internal function used as part of parsing derivations in A-Term format, used to consume the final `]` but expect the initial `[` to already be consumed. This made for what looked like unbalanced brackets at callsites, which was confusing. Now it consumes both which is hopefully less confusing. As part of testing, we also created a unit test for the A-Term format for regular non-experimental derivations too. Co-authored-by: Robert Hensing Co-authored-by: Valentin Gagarin Apply suggestions from code review Co-authored-by: Robert Hensing --- doc/manual/src/protocols/derivation-aterm.md | 18 +- perl/lib/Nix/Store.xs | 2 +- src/build-remote/build-remote.cc | 2 +- src/libcmd/built-path.cc | 22 ++ src/libcmd/built-path.hh | 4 + src/libexpr/primops.cc | 10 +- src/libstore/build/derivation-goal.cc | 83 +++-- src/libstore/build/worker.cc | 9 +- src/libstore/build/worker.hh | 3 +- src/libstore/derivations.cc | 331 +++++++++++++++---- src/libstore/derivations.hh | 15 +- src/libstore/derived-path-map.cc | 41 ++- src/libstore/derived-path-map.hh | 25 ++ src/libstore/misc.cc | 67 ++-- src/libstore/tests/derivation.cc | 163 +++++++-- src/libutil/comparator.hh | 77 +++-- src/nix-build/nix-build.cc | 37 ++- src/nix/app.cc | 19 +- tests/dyn-drv/dep-built-drv.sh | 4 +- 19 files changed, 741 insertions(+), 191 deletions(-) diff --git a/doc/manual/src/protocols/derivation-aterm.md b/doc/manual/src/protocols/derivation-aterm.md index c9dc00ef7..e58b602a3 100644 --- a/doc/manual/src/protocols/derivation-aterm.md +++ b/doc/manual/src/protocols/derivation-aterm.md @@ -2,8 +2,18 @@ For historical reasons, [derivations](@docroot@/glossary.md#gloss-store-derivation) are stored on-disk in [ATerm](https://homepages.cwi.nl/~daybuild/daily-books/technology/aterm-guide/aterm-guide.html) format. -Derivations are serialised in the following format: +Derivations are serialised in one of the following formats: -``` -Derive(...) -``` +- ``` + Derive(...) + ``` + + For all stable derivations. + +- ``` + DrvWithVersion(, ...) + ``` + + The only `version-string`s that are in use today are for [experimental features](@docroot@/contributing/experimental-features.md): + + - `"xp-dyn-drv"` for the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. diff --git a/perl/lib/Nix/Store.xs b/perl/lib/Nix/Store.xs index c38ea2d2b..e96885e4c 100644 --- a/perl/lib/Nix/Store.xs +++ b/perl/lib/Nix/Store.xs @@ -324,7 +324,7 @@ SV * derivationFromPath(char * drvPath) hv_stores(hash, "outputs", newRV((SV *) outputs)); AV * inputDrvs = newAV(); - for (auto & i : drv.inputDrvs) + for (auto & i : drv.inputDrvs.map) av_push(inputDrvs, newSVpv(store()->printStorePath(i.first).c_str(), 0)); // !!! ignores i->second hv_stores(hash, "inputDrvs", newRV((SV *) inputDrvs)); diff --git a/src/build-remote/build-remote.cc b/src/build-remote/build-remote.cc index c1f03a8ef..d69d3a0c2 100644 --- a/src/build-remote/build-remote.cc +++ b/src/build-remote/build-remote.cc @@ -314,7 +314,7 @@ connected: // // 2. Changing the `inputSrcs` set changes the associated // output ids, which break CA derivations - if (!drv.inputDrvs.empty()) + if (!drv.inputDrvs.map.empty()) drv.inputSrcs = store->parseStorePathSet(inputs); optResult = sshStore->buildDerivation(*drvPath, (const BasicDerivation &) drv); auto & result = *optResult; diff --git a/src/libcmd/built-path.cc b/src/libcmd/built-path.cc index c6cc7fa9c..9a2dce806 100644 --- a/src/libcmd/built-path.cc +++ b/src/libcmd/built-path.cc @@ -58,6 +58,28 @@ StorePathSet BuiltPath::outPaths() const ); } +SingleDerivedPath::Built SingleBuiltPath::Built::discardOutputPath() const +{ + return SingleDerivedPath::Built { + .drvPath = make_ref(drvPath->discardOutputPath()), + .output = output.first, + }; +} + +SingleDerivedPath SingleBuiltPath::discardOutputPath() const +{ + return std::visit( + overloaded{ + [](const SingleBuiltPath::Opaque & p) -> SingleDerivedPath { + return p; + }, + [](const SingleBuiltPath::Built & b) -> SingleDerivedPath { + return b.discardOutputPath(); + }, + }, raw() + ); +} + nlohmann::json BuiltPath::Built::toJSON(const Store & store) const { nlohmann::json res; diff --git a/src/libcmd/built-path.hh b/src/libcmd/built-path.hh index 747bcc440..e677bc810 100644 --- a/src/libcmd/built-path.hh +++ b/src/libcmd/built-path.hh @@ -9,6 +9,8 @@ struct SingleBuiltPathBuilt { ref drvPath; std::pair output; + SingleDerivedPathBuilt discardOutputPath() const; + std::string to_string(const Store & store) const; static SingleBuiltPathBuilt parse(const Store & store, std::string_view, std::string_view); nlohmann::json toJSON(const Store & store) const; @@ -34,6 +36,8 @@ struct SingleBuiltPath : _SingleBuiltPathRaw { StorePath outPath() const; + SingleDerivedPath discardOutputPath() const; + static SingleBuiltPath parse(const Store & store, std::string_view); nlohmann::json toJSON(const Store & store) const; }; diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index e2b1ac4f6..cd9a05bb2 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1252,15 +1252,13 @@ drvName, Bindings * attrs, Value & v) state.store->computeFSClosure(d.drvPath, refs); for (auto & j : refs) { drv.inputSrcs.insert(j); - if (j.isDerivation()) - drv.inputDrvs[j] = state.store->readDerivation(j).outputNames(); + if (j.isDerivation()) { + drv.inputDrvs.map[j].value = state.store->readDerivation(j).outputNames(); + } } }, [&](const NixStringContextElem::Built & b) { - if (auto * p = std::get_if(&*b.drvPath)) - drv.inputDrvs[p->path].insert(b.output); - else - throw UnimplementedError("Dependencies on the outputs of dynamic derivations are not yet supported"); + drv.inputDrvs.ensureSlot(*b.drvPath).value.insert(b.output); }, [&](const NixStringContextElem::Opaque & o) { drv.inputSrcs.insert(o.path); diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index bec0bc538..6472ecd99 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -350,25 +350,37 @@ void DerivationGoal::gaveUpOnSubstitution() /* The inputs must be built before we can build this goal. */ inputDrvOutputs.clear(); - if (useDerivation) - for (auto & i : dynamic_cast(drv.get())->inputDrvs) { + if (useDerivation) { + std::function, const DerivedPathMap::ChildNode &)> addWaiteeDerivedPath; + + addWaiteeDerivedPath = [&](ref inputDrv, const DerivedPathMap::ChildNode & inputNode) { + if (!inputNode.value.empty()) + addWaitee(worker.makeGoal( + DerivedPath::Built { + .drvPath = inputDrv, + .outputs = inputNode.value, + }, + buildMode == bmRepair ? bmRepair : bmNormal)); + for (const auto & [outputName, childNode] : inputNode.childMap) + addWaiteeDerivedPath( + make_ref(SingleDerivedPath::Built { inputDrv, outputName }), + childNode); + }; + + for (const auto & [inputDrvPath, inputNode] : dynamic_cast(drv.get())->inputDrvs.map) { /* Ensure that pure, non-fixed-output derivations don't depend on impure derivations. */ if (experimentalFeatureSettings.isEnabled(Xp::ImpureDerivations) && drv->type().isPure() && !drv->type().isFixed()) { - auto inputDrv = worker.evalStore.readDerivation(i.first); + auto inputDrv = worker.evalStore.readDerivation(inputDrvPath); if (!inputDrv.type().isPure()) throw Error("pure derivation '%s' depends on impure derivation '%s'", worker.store.printStorePath(drvPath), - worker.store.printStorePath(i.first)); + worker.store.printStorePath(inputDrvPath)); } - addWaitee(worker.makeGoal( - DerivedPath::Built { - .drvPath = makeConstantStorePathRef(i.first), - .outputs = i.second, - }, - buildMode == bmRepair ? bmRepair : bmNormal)); + addWaiteeDerivedPath(makeConstantStorePathRef(inputDrvPath), inputNode); } + } /* Copy the input sources from the eval store to the build store. */ @@ -501,7 +513,7 @@ void DerivationGoal::inputsRealised() return ia.deferred; }, [&](const DerivationType::ContentAddressed & ca) { - return !fullDrv.inputDrvs.empty() && ( + return !fullDrv.inputDrvs.map.empty() && ( ca.fixed /* Can optionally resolve if fixed, which is good for avoiding unnecessary rebuilds. */ @@ -515,7 +527,7 @@ void DerivationGoal::inputsRealised() } }, drvType.raw); - if (resolveDrv && !fullDrv.inputDrvs.empty()) { + if (resolveDrv && !fullDrv.inputDrvs.map.empty()) { experimentalFeatureSettings.require(Xp::CaDerivations); /* We are be able to resolve this derivation based on the @@ -552,11 +564,13 @@ void DerivationGoal::inputsRealised() return; } - for (auto & [depDrvPath, wantedDepOutputs] : fullDrv.inputDrvs) { + std::function::ChildNode &)> accumInputPaths; + + accumInputPaths = [&](const StorePath & depDrvPath, const DerivedPathMap::ChildNode & inputNode) { /* Add the relevant output closures of the input derivation `i' as input paths. Only add the closures of output paths that are specified as inputs. */ - for (auto & j : wantedDepOutputs) { + auto getOutput = [&](const std::string & outputName) { /* TODO (impure derivations-induced tech debt): Tracking input derivation outputs statefully through the goals is error prone and has led to bugs. @@ -568,21 +582,30 @@ void DerivationGoal::inputsRealised() a representation in the store, which is a usability problem in itself. When implementing this logic entirely with lookups make sure that they're cached. */ - if (auto outPath = get(inputDrvOutputs, { depDrvPath, j })) { - worker.store.computeFSClosure(*outPath, inputPaths); + if (auto outPath = get(inputDrvOutputs, { depDrvPath, outputName })) { + return *outPath; } else { auto outMap = worker.evalStore.queryDerivationOutputMap(depDrvPath); - auto outMapPath = outMap.find(j); + auto outMapPath = outMap.find(outputName); if (outMapPath == outMap.end()) { throw Error( "derivation '%s' requires non-existent output '%s' from input derivation '%s'", - worker.store.printStorePath(drvPath), j, worker.store.printStorePath(depDrvPath)); + worker.store.printStorePath(drvPath), outputName, worker.store.printStorePath(depDrvPath)); } - worker.store.computeFSClosure(outMapPath->second, inputPaths); + return outMapPath->second; } - } - } + }; + + for (auto & outputName : inputNode.value) + worker.store.computeFSClosure(getOutput(outputName), inputPaths); + + for (auto & [outputName, childNode] : inputNode.childMap) + accumInputPaths(getOutput(outputName), childNode); + }; + + for (auto & [depDrvPath, depNode] : fullDrv.inputDrvs.map) + accumInputPaths(depDrvPath, depNode); } /* Second, the input sources. */ @@ -1475,22 +1498,24 @@ void DerivationGoal::waiteeDone(GoalPtr waitee, ExitCode result) if (!useDerivation) return; auto & fullDrv = *dynamic_cast(drv.get()); - auto * dg = tryGetConcreteDrvGoal(waitee); - if (!dg) return; + std::optional info = tryGetConcreteDrvGoal(waitee); + if (!info) return; + const auto & [dg, drvReq] = *info; - auto outputs = fullDrv.inputDrvs.find(dg->drvPath); - if (outputs == fullDrv.inputDrvs.end()) return; + auto * nodeP = fullDrv.inputDrvs.findSlot(drvReq.get()); + if (!nodeP) return; + auto & outputs = nodeP->value; - for (auto & outputName : outputs->second) { - auto buildResult = dg->getBuildResult(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(dg->drvPath), + for (auto & outputName : outputs) { + auto buildResult = dg.get().getBuildResult(DerivedPath::Built { + .drvPath = makeConstantStorePathRef(dg.get().drvPath), .outputs = OutputsSpec::Names { outputName }, }); if (buildResult.success()) { auto i = buildResult.builtOutputs.find(outputName); if (i != buildResult.builtOutputs.end()) inputDrvOutputs.insert_or_assign( - { dg->drvPath, outputName }, + { dg.get().drvPath, outputName }, i->second.outPath); } } diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index f65f63b99..b4a634e7b 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -594,11 +594,14 @@ GoalPtr upcast_goal(std::shared_ptr subGoal) return subGoal; } -const DerivationGoal * tryGetConcreteDrvGoal(GoalPtr waitee) +std::optional, std::reference_wrapper>> tryGetConcreteDrvGoal(GoalPtr waitee) { auto * odg = dynamic_cast(&*waitee); - if (!odg) return nullptr; - return &*odg->concreteDrvGoal; + if (!odg) return std::nullopt; + return {{ + std::cref(*odg->concreteDrvGoal), + std::cref(*odg->drvReq), + }}; } } diff --git a/src/libstore/build/worker.hh b/src/libstore/build/worker.hh index a778e311c..6f6d25d7d 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/build/worker.hh @@ -49,7 +49,8 @@ typedef std::chrono::time_point steady_time_point; * we have made the function, written in `worker.cc` where all the goal * types are visible, and use it instead. */ -const DerivationGoal * tryGetConcreteDrvGoal(GoalPtr waitee); + +std::optional, std::reference_wrapper>> tryGetConcreteDrvGoal(GoalPtr waitee); /** * A mapping used to remember for each child process to what goal it diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index 43d3dc4a2..67069c3c9 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -136,7 +136,7 @@ StorePath writeDerivation(Store & store, const Derivation & drv, RepairFlag repair, bool readOnly) { auto references = drv.inputSrcs; - for (auto & i : drv.inputDrvs) + for (auto & i : drv.inputDrvs.map) references.insert(i.first); /* Note that the outputs of a derivation are *not* references (that can be missing (of course) and should not necessarily be @@ -208,22 +208,25 @@ static bool endOfList(std::istream & str) static StringSet parseStrings(std::istream & str, bool arePaths) { StringSet res; + expect(str, "["); while (!endOfList(str)) res.insert(arePaths ? parsePath(str) : parseString(str)); return res; } -static DerivationOutput parseDerivationOutput(const Store & store, - std::string_view pathS, std::string_view hashAlgo, std::string_view hashS) +static DerivationOutput parseDerivationOutput( + const Store & store, + std::string_view pathS, std::string_view hashAlgo, std::string_view hashS, + const ExperimentalFeatureSettings & xpSettings) { if (hashAlgo != "") { ContentAddressMethod method = ContentAddressMethod::parsePrefix(hashAlgo); if (method == TextIngestionMethod {}) - experimentalFeatureSettings.require(Xp::DynamicDerivations); + xpSettings.require(Xp::DynamicDerivations); const auto hashType = parseHashType(hashAlgo); if (hashS == "impure") { - experimentalFeatureSettings.require(Xp::ImpureDerivations); + xpSettings.require(Xp::ImpureDerivations); if (pathS != "") throw FormatError("impure derivation output should not specify output path"); return DerivationOutput::Impure { @@ -240,7 +243,7 @@ static DerivationOutput parseDerivationOutput(const Store & store, }, }; } else { - experimentalFeatureSettings.require(Xp::CaDerivations); + xpSettings.require(Xp::CaDerivations); if (pathS != "") throw FormatError("content-addressed derivation output should not specify output path"); return DerivationOutput::CAFloating { @@ -259,29 +262,116 @@ static DerivationOutput parseDerivationOutput(const Store & store, } } -static DerivationOutput parseDerivationOutput(const Store & store, std::istringstream & str) +static DerivationOutput parseDerivationOutput( + const Store & store, std::istringstream & str, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings) { expect(str, ","); const auto pathS = parseString(str); expect(str, ","); const auto hashAlgo = parseString(str); expect(str, ","); const auto hash = parseString(str); expect(str, ")"); - return parseDerivationOutput(store, pathS, hashAlgo, hash); + return parseDerivationOutput(store, pathS, hashAlgo, hash, xpSettings); +} + +/** + * All ATerm Derivation format versions currently known. + * + * Unknown versions are rejected at the parsing stage. + */ +enum struct DerivationATermVersion { + /** + * Older unversioned form + */ + Traditional, + + /** + * Newer versioned form; only this version so far. + */ + DynamicDerivations, +}; + +static DerivedPathMap::ChildNode parseDerivedPathMapNode( + const Store & store, + std::istringstream & str, + DerivationATermVersion version) +{ + DerivedPathMap::ChildNode node; + + auto parseNonDynamic = [&]() { + node.value = parseStrings(str, false); + }; + + // Older derivation should never use new form, but newer + // derivaiton can use old form. + switch (version) { + case DerivationATermVersion::Traditional: + parseNonDynamic(); + break; + case DerivationATermVersion::DynamicDerivations: + switch (str.peek()) { + case '[': + parseNonDynamic(); + break; + case '(': + expect(str, "("); + node.value = parseStrings(str, false); + expect(str, ",["); + while (!endOfList(str)) { + expect(str, "("); + auto outputName = parseString(str); + expect(str, ","); + node.childMap.insert_or_assign(outputName, parseDerivedPathMapNode(store, str, version)); + expect(str, ")"); + } + expect(str, ")"); + break; + default: + throw FormatError("invalid inputDrvs entry in derivation"); + } + break; + default: + // invalid format, not a parse error but internal error + assert(false); + } + return node; } -Derivation parseDerivation(const Store & store, std::string && s, std::string_view name) +Derivation parseDerivation( + const Store & store, std::string && s, std::string_view name, + const ExperimentalFeatureSettings & xpSettings) { Derivation drv; drv.name = name; std::istringstream str(std::move(s)); - expect(str, "Derive(["); + expect(str, "D"); + DerivationATermVersion version; + switch (str.peek()) { + case 'e': + expect(str, "erive("); + version = DerivationATermVersion::Traditional; + break; + case 'r': + expect(str, "rvWithVersion("); + auto versionS = parseString(str); + if (versionS == "xp-dyn-drv") { + // Only verison we have so far + version = DerivationATermVersion::DynamicDerivations; + xpSettings.require(Xp::DynamicDerivations); + } else { + throw FormatError("Unknown derivation ATerm format version '%s'", versionS); + } + expect(str, ","); + break; + } /* Parse the list of outputs. */ + expect(str, "["); while (!endOfList(str)) { expect(str, "("); std::string id = parseString(str); - auto output = parseDerivationOutput(store, str); + auto output = parseDerivationOutput(store, str, xpSettings); drv.outputs.emplace(std::move(id), std::move(output)); } @@ -290,12 +380,12 @@ Derivation parseDerivation(const Store & store, std::string && s, std::string_vi while (!endOfList(str)) { expect(str, "("); Path drvPath = parsePath(str); - expect(str, ",["); - drv.inputDrvs.insert_or_assign(store.parseStorePath(drvPath), parseStrings(str, false)); + expect(str, ","); + drv.inputDrvs.map.insert_or_assign(store.parseStorePath(drvPath), parseDerivedPathMapNode(store, str, version)); expect(str, ")"); } - expect(str, ",["); drv.inputSrcs = store.parseStorePathSet(parseStrings(str, true)); + expect(str, ","); drv.inputSrcs = store.parseStorePathSet(parseStrings(str, true)); expect(str, ","); drv.platform = parseString(str); expect(str, ","); drv.builder = parseString(str); @@ -379,14 +469,67 @@ static void printUnquotedStrings(std::string & res, ForwardIterator i, ForwardIt } +static void unparseDerivedPathMapNode(const Store & store, std::string & s, const DerivedPathMap::ChildNode & node) +{ + s += ','; + if (node.childMap.empty()) { + printUnquotedStrings(s, node.value.begin(), node.value.end()); + } else { + s += "("; + printUnquotedStrings(s, node.value.begin(), node.value.end()); + s += ",["; + bool first = true; + for (auto & [outputName, childNode] : node.childMap) { + if (first) first = false; else s += ','; + s += '('; printUnquotedString(s, outputName); + unparseDerivedPathMapNode(store, s, childNode); + s += ')'; + } + s += "])"; + } +} + + +/** + * Does the derivation have a dependency on the output of a dynamic + * derivation? + * + * In other words, does it on the output of derivation that is itself an + * ouput of a derivation? This corresponds to a dependency that is an + * inductive derived path with more than one layer of + * `DerivedPath::Built`. + */ +static bool hasDynamicDrvDep(const Derivation & drv) +{ + return + std::find_if( + drv.inputDrvs.map.begin(), + drv.inputDrvs.map.end(), + [](auto & kv) { return !kv.second.childMap.empty(); }) + != drv.inputDrvs.map.end(); +} + + std::string Derivation::unparse(const Store & store, bool maskOutputs, - std::map * actualInputs) const + DerivedPathMap::ChildNode::Map * actualInputs) const { std::string s; s.reserve(65536); - s += "Derive(["; + + /* Use older unversioned form if possible, for wider compat. Use + newer form only if we need it, which we do for + `Xp::DynamicDerivations`. */ + if (hasDynamicDrvDep(*this)) { + s += "DrvWithVersion("; + // Only version we have so far + printUnquotedString(s, "xp-dyn-drv"); + s += ","; + } else { + s += "Derive("; + } bool first = true; + s += "["; for (auto & i : outputs) { if (first) first = false; else s += ','; s += '('; printUnquotedString(s, i.first); @@ -424,17 +567,17 @@ std::string Derivation::unparse(const Store & store, bool maskOutputs, s += "],["; first = true; if (actualInputs) { - for (auto & i : *actualInputs) { + for (auto & [drvHashModulo, childMap] : *actualInputs) { if (first) first = false; else s += ','; - s += '('; printUnquotedString(s, i.first); - s += ','; printUnquotedStrings(s, i.second.begin(), i.second.end()); + s += '('; printUnquotedString(s, drvHashModulo); + unparseDerivedPathMapNode(store, s, childMap); s += ')'; } } else { - for (auto & i : inputDrvs) { + for (auto & [drvPath, childMap] : inputDrvs.map) { if (first) first = false; else s += ','; - s += '('; printUnquotedString(s, store.printStorePath(i.first)); - s += ','; printUnquotedStrings(s, i.second.begin(), i.second.end()); + s += '('; printUnquotedString(s, store.printStorePath(drvPath)); + unparseDerivedPathMapNode(store, s, childMap); s += ')'; } } @@ -668,18 +811,16 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut } }, drv.type().raw); - std::map inputs2; - for (auto & [drvPath, inputOutputs0] : drv.inputDrvs) { - // Avoid lambda capture restriction with standard / Clang - auto & inputOutputs = inputOutputs0; + DerivedPathMap::ChildNode::Map inputs2; + for (auto & [drvPath, node] : drv.inputDrvs.map) { const auto & res = pathDerivationModulo(store, drvPath); if (res.kind == DrvHash::Kind::Deferred) kind = DrvHash::Kind::Deferred; - for (auto & outputName : inputOutputs) { + for (auto & outputName : node.value) { const auto h = get(res.hashes, outputName); if (!h) throw Error("no hash for output '%s' of derivation '%s'", outputName, drv.name); - inputs2[h->to_string(Base16, false)].insert(outputName); + inputs2[h->to_string(Base16, false)].value.insert(outputName); } } @@ -709,7 +850,7 @@ static DerivationOutput readDerivationOutput(Source & in, const Store & store) const auto hashAlgo = readString(in); const auto hash = readString(in); - return parseDerivationOutput(store, pathS, hashAlgo, hash); + return parseDerivationOutput(store, pathS, hashAlgo, hash, experimentalFeatureSettings); } StringSet BasicDerivation::outputNames() const @@ -824,6 +965,8 @@ std::string hashPlaceholder(const OutputNameView outputName) static void rewriteDerivation(Store & store, BasicDerivation & drv, const StringMap & rewrites) { + debug("Rewriting the derivation"); + for (auto & rewrite : rewrites) { debug("rewriting %s as %s", rewrite.first, rewrite.second); } @@ -862,14 +1005,70 @@ std::optional Derivation::tryResolve(Store & store) const { std::map, StorePath> inputDrvOutputs; - for (auto & input : inputDrvs) - for (auto & [outputName, outputPath] : store.queryPartialDerivationOutputMap(input.first)) - if (outputPath) - inputDrvOutputs.insert_or_assign({input.first, outputName}, *outputPath); + std::function::ChildNode &)> accum; + accum = [&](auto & inputDrv, auto & node) { + for (auto & [outputName, outputPath] : store.queryPartialDerivationOutputMap(inputDrv)) { + if (outputPath) { + inputDrvOutputs.insert_or_assign({inputDrv, outputName}, *outputPath); + if (auto p = get(node.childMap, outputName)) + accum(*outputPath, *p); + } + } + }; + + for (auto & [inputDrv, node] : inputDrvs.map) + accum(inputDrv, node); return tryResolve(store, inputDrvOutputs); } +static bool tryResolveInput( + Store & store, StorePathSet & inputSrcs, StringMap & inputRewrites, + const DownstreamPlaceholder * placeholderOpt, + const StorePath & inputDrv, const DerivedPathMap::ChildNode & inputNode, + const std::map, StorePath> & inputDrvOutputs) +{ + auto getOutput = [&](const std::string & outputName) { + auto * actualPathOpt = get(inputDrvOutputs, { inputDrv, outputName }); + if (!actualPathOpt) + warn("output %s of input %s missing, aborting the resolving", + outputName, + store.printStorePath(inputDrv) + ); + return actualPathOpt; + }; + + auto getPlaceholder = [&](const std::string & outputName) { + return placeholderOpt + ? DownstreamPlaceholder::unknownDerivation(*placeholderOpt, outputName) + : DownstreamPlaceholder::unknownCaOutput(inputDrv, outputName); + }; + + for (auto & outputName : inputNode.value) { + auto actualPathOpt = getOutput(outputName); + if (!actualPathOpt) return false; + auto actualPath = *actualPathOpt; + if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { + inputRewrites.emplace( + getPlaceholder(outputName).render(), + store.printStorePath(actualPath)); + } + inputSrcs.insert(std::move(actualPath)); + } + + for (auto & [outputName, childNode] : inputNode.childMap) { + auto actualPathOpt = getOutput(outputName); + if (!actualPathOpt) return false; + auto actualPath = *actualPathOpt; + auto nextPlaceholder = getPlaceholder(outputName); + if (!tryResolveInput(store, inputSrcs, inputRewrites, + &nextPlaceholder, actualPath, childNode, + inputDrvOutputs)) + return false; + } + return true; +} + std::optional Derivation::tryResolve( Store & store, const std::map, StorePath> & inputDrvOutputs) const @@ -879,23 +1078,10 @@ std::optional Derivation::tryResolve( // Input paths that we'll want to rewrite in the derivation StringMap inputRewrites; - for (auto & [inputDrv, inputOutputs] : inputDrvs) { - for (auto & outputName : inputOutputs) { - if (auto actualPath = get(inputDrvOutputs, { inputDrv, outputName })) { - if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { - inputRewrites.emplace( - DownstreamPlaceholder::unknownCaOutput(inputDrv, outputName).render(), - store.printStorePath(*actualPath)); - } - resolved.inputSrcs.insert(*actualPath); - } else { - warn("output '%s' of input '%s' missing, aborting the resolving", - outputName, - store.printStorePath(inputDrv)); - return {}; - } - } - } + for (auto & [inputDrv, inputNode] : inputDrvs.map) + if (!tryResolveInput(store, resolved.inputSrcs, inputRewrites, + nullptr, inputDrv, inputNode, inputDrvOutputs)) + return std::nullopt; rewriteDerivation(store, resolved, inputRewrites); @@ -1084,10 +1270,25 @@ nlohmann::json Derivation::toJSON(const Store & store) const } { - auto& inputDrvsObj = res["inputDrvs"]; - inputDrvsObj = nlohmann::json ::object(); - for (auto & input : inputDrvs) - inputDrvsObj[store.printStorePath(input.first)] = input.second; + std::function::ChildNode &)> doInput; + doInput = [&](const auto & inputNode) { + auto value = nlohmann::json::object(); + value["outputs"] = inputNode.value; + { + auto next = nlohmann::json::object(); + for (auto & [outputId, childNode] : inputNode.childMap) + next[outputId] = doInput(childNode); + value["dynamicOutputs"] = std::move(next); + } + return value; + }; + { + auto& inputDrvsObj = res["inputDrvs"]; + inputDrvsObj = nlohmann::json::object(); + for (auto & [inputDrv, inputNode] : inputDrvs.map) { + inputDrvsObj[store.printStorePath(inputDrv)] = doInput(inputNode); + } + } } res["system"] = platform; @@ -1101,7 +1302,8 @@ nlohmann::json Derivation::toJSON(const Store & store) const Derivation Derivation::fromJSON( const Store & store, - const nlohmann::json & json) + const nlohmann::json & json, + const ExperimentalFeatureSettings & xpSettings) { using nlohmann::detail::value_t; @@ -1133,12 +1335,21 @@ Derivation Derivation::fromJSON( } try { + std::function::ChildNode(const nlohmann::json &)> doInput; + doInput = [&](const auto & json) { + DerivedPathMap::ChildNode node; + node.value = static_cast( + ensureType(valueAt(json, "outputs"), value_t::array)); + for (auto & [outputId, childNode] : ensureType(valueAt(json, "dynamicOutputs"), value_t::object).items()) { + xpSettings.require(Xp::DynamicDerivations); + node.childMap[outputId] = doInput(childNode); + } + return node; + }; auto & inputDrvsObj = ensureType(valueAt(json, "inputDrvs"), value_t::object); - for (auto & [inputDrvPath, inputOutputs] : inputDrvsObj.items()) { - ensureType(inputOutputs, value_t::array); - res.inputDrvs[store.parseStorePath(inputDrvPath)] = - static_cast(inputOutputs); - } + for (auto & [inputDrvPath, inputOutputs] : inputDrvsObj.items()) + res.inputDrvs.map[store.parseStorePath(inputDrvPath)] = + doInput(inputOutputs); } catch (Error & e) { e.addTrace({}, "while reading key 'inputDrvs'"); throw; diff --git a/src/libstore/derivations.hh b/src/libstore/derivations.hh index 106056f2d..fa14e7536 100644 --- a/src/libstore/derivations.hh +++ b/src/libstore/derivations.hh @@ -6,7 +6,7 @@ #include "hash.hh" #include "content-address.hh" #include "repair-flag.hh" -#include "derived-path.hh" +#include "derived-path-map.hh" #include "sync.hh" #include "comparator.hh" #include "variant-wrapper.hh" @@ -323,13 +323,13 @@ struct Derivation : BasicDerivation /** * inputs that are sub-derivations */ - DerivationInputs inputDrvs; + DerivedPathMap> inputDrvs; /** * Print a derivation. */ std::string unparse(const Store & store, bool maskOutputs, - std::map * actualInputs = nullptr) const; + DerivedPathMap::ChildNode::Map * actualInputs = nullptr) const; /** * Return the underlying basic derivation but with these changes: @@ -368,7 +368,8 @@ struct Derivation : BasicDerivation nlohmann::json toJSON(const Store & store) const; static Derivation fromJSON( const Store & store, - const nlohmann::json & json); + const nlohmann::json & json, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); GENERATE_CMP(Derivation, static_cast(*me), @@ -389,7 +390,11 @@ StorePath writeDerivation(Store & store, /** * Read a derivation from a file. */ -Derivation parseDerivation(const Store & store, std::string && s, std::string_view name); +Derivation parseDerivation( + const Store & store, + std::string && s, + std::string_view name, + const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings); /** * \todo Remove. diff --git a/src/libstore/derived-path-map.cc b/src/libstore/derived-path-map.cc index 5c8c7a4f2..437b6a71a 100644 --- a/src/libstore/derived-path-map.cc +++ b/src/libstore/derived-path-map.cc @@ -21,6 +21,32 @@ typename DerivedPathMap::ChildNode & DerivedPathMap::ensureSlot(const Sing return initIter(k); } +template +typename DerivedPathMap::ChildNode * DerivedPathMap::findSlot(const SingleDerivedPath & k) +{ + std::function initIter; + initIter = [&](const auto & k) { + return std::visit(overloaded { + [&](const SingleDerivedPath::Opaque & bo) { + auto it = map.find(bo.path); + return it != map.end() + ? &it->second + : nullptr; + }, + [&](const SingleDerivedPath::Built & bfd) { + auto * n = initIter(*bfd.drvPath); + if (!n) return (ChildNode *)nullptr; + + auto it = n->childMap.find(bfd.output); + return it != n->childMap.end() + ? &it->second + : nullptr; + }, + }, k.raw()); + }; + return initIter(k); +} + } // instantiations @@ -30,4 +56,17 @@ namespace nix { template struct DerivedPathMap>; -} +GENERATE_CMP_EXT( + template<>, + DerivedPathMap>::ChildNode, + me->value, + me->childMap); + +GENERATE_CMP_EXT( + template<>, + DerivedPathMap>, + me->map); + +template struct DerivedPathMap>; + +}; diff --git a/src/libstore/derived-path-map.hh b/src/libstore/derived-path-map.hh index 9ce58206e..4a2c90733 100644 --- a/src/libstore/derived-path-map.hh +++ b/src/libstore/derived-path-map.hh @@ -48,6 +48,8 @@ struct DerivedPathMap { * The map of the root node. */ Map childMap; + + DECLARE_CMP(ChildNode); }; /** @@ -60,6 +62,8 @@ struct DerivedPathMap { */ Map map; + DECLARE_CMP(DerivedPathMap); + /** * Find the node for `k`, creating it if needed. * @@ -68,6 +72,27 @@ struct DerivedPathMap { * by changing this node. */ ChildNode & ensureSlot(const SingleDerivedPath & k); + + /** + * Like `ensureSlot` but does not create the slot if it doesn't exist. + * + * Read the entire description of `ensureSlot` to understand an + * important caveat here that "have slot" does *not* imply "key is + * set in map". To ensure a key is set one would need to get the + * child node (with `findSlot` or `ensureSlot`) *and* check the + * `ChildNode::value`. + */ + ChildNode * findSlot(const SingleDerivedPath & k); }; + +DECLARE_CMP_EXT( + template<>, + DerivedPathMap>::, + DerivedPathMap>); +DECLARE_CMP_EXT( + template<>, + DerivedPathMap>::ChildNode::, + DerivedPathMap>::ChildNode); + } diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index c043b9b93..1035691c7 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -125,14 +125,26 @@ void Store::queryMissing(const std::vector & targets, std::function doPath; + std::function, const DerivedPathMap::ChildNode &)> enqueueDerivedPaths; + + enqueueDerivedPaths = [&](ref inputDrv, const DerivedPathMap::ChildNode & inputNode) { + if (!inputNode.value.empty()) + pool.enqueue(std::bind(doPath, DerivedPath::Built { inputDrv, inputNode.value })); + for (const auto & [outputName, childNode] : inputNode.childMap) + enqueueDerivedPaths( + make_ref(SingleDerivedPath::Built { inputDrv, outputName }), + childNode); + }; + auto mustBuildDrv = [&](const StorePath & drvPath, const Derivation & drv) { { auto state(state_.lock()); state->willBuild.insert(drvPath); } - for (auto & i : drv.inputDrvs) - pool.enqueue(std::bind(doPath, DerivedPath::Built { makeConstantStorePathRef(i.first), i.second })); + for (const auto & [inputDrv, inputNode] : drv.inputDrvs.map) { + enqueueDerivedPaths(makeConstantStorePathRef(inputDrv), inputNode); + } }; auto checkOutput = [&]( @@ -322,24 +334,41 @@ std::map drvOutputReferences( { std::set inputRealisations; - for (const auto & [inputDrv, outputNames] : drv.inputDrvs) { - const auto outputHashes = - staticOutputHashes(store, store.readDerivation(inputDrv)); - for (const auto & outputName : outputNames) { - auto outputHash = get(outputHashes, outputName); - if (!outputHash) - throw Error( - "output '%s' of derivation '%s' isn't realised", outputName, - store.printStorePath(inputDrv)); - auto thisRealisation = store.queryRealisation( - DrvOutput{*outputHash, outputName}); - if (!thisRealisation) - throw Error( - "output '%s' of derivation '%s' isn't built", outputName, - store.printStorePath(inputDrv)); - inputRealisations.insert(*thisRealisation); + std::function::ChildNode &)> accumRealisations; + + accumRealisations = [&](const StorePath & inputDrv, const DerivedPathMap::ChildNode & inputNode) { + if (!inputNode.value.empty()) { + auto outputHashes = + staticOutputHashes(store, store.readDerivation(inputDrv)); + for (const auto & outputName : inputNode.value) { + auto outputHash = get(outputHashes, outputName); + if (!outputHash) + throw Error( + "output '%s' of derivation '%s' isn't realised", outputName, + store.printStorePath(inputDrv)); + auto thisRealisation = store.queryRealisation( + DrvOutput{*outputHash, outputName}); + if (!thisRealisation) + throw Error( + "output '%s' of derivation '%s' isn’t built", outputName, + store.printStorePath(inputDrv)); + inputRealisations.insert(*thisRealisation); + } } - } + if (!inputNode.value.empty()) { + auto d = makeConstantStorePathRef(inputDrv); + for (const auto & [outputName, childNode] : inputNode.childMap) { + SingleDerivedPath next = SingleDerivedPath::Built { d, outputName }; + accumRealisations( + // TODO deep resolutions for dynamic derivations, issue #8947, would go here. + resolveDerivedPath(store, next), + childNode); + } + } + }; + + for (const auto & [inputDrv, inputNode] : drv.inputDrvs.map) + accumRealisations(inputDrv, inputNode); auto info = store.queryPathInfo(outputPath); diff --git a/src/libstore/tests/derivation.cc b/src/libstore/tests/derivation.cc index 7fd7f9278..c360c9707 100644 --- a/src/libstore/tests/derivation.cc +++ b/src/libstore/tests/derivation.cc @@ -42,6 +42,26 @@ class ImpureDerivationTest : public DerivationTest } }; +TEST_F(DerivationTest, BadATerm_version) { + ASSERT_THROW( + parseDerivation( + *store, + R"(DrvWithVersion("invalid-version",[],[("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv",["cat","dog"])],["/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"],"wasm-sel4","foo",["bar","baz"],[("BIG_BAD","WOLF")]))", + "whatever", + mockXpSettings), + FormatError); +} + +TEST_F(DynDerivationTest, BadATerm_oldVersionDynDeps) { + ASSERT_THROW( + parseDerivation( + *store, + R"(Derive([],[("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv",(["cat","dog"],[("cat",["kitten"]),("goose",["gosling"])]))],["/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"],"wasm-sel4","foo",["bar","baz"],[("BIG_BAD","WOLF")]))", + "dyn-dep-derivation", + mockXpSettings), + FormatError); +} + #define TEST_JSON(FIXTURE, NAME, STR, VAL, DRV_NAME, OUTPUT_NAME) \ TEST_F(FIXTURE, DerivationOutput_ ## NAME ## _to_json) { \ using nlohmann::literals::operator "" _json; \ @@ -143,35 +163,37 @@ TEST_JSON(ImpureDerivationTest, impure, #undef TEST_JSON -#define TEST_JSON(NAME, STR, VAL) \ - TEST_F(DerivationTest, Derivation_ ## NAME ## _to_json) { \ +#define TEST_JSON(FIXTURE, NAME, STR, VAL) \ + TEST_F(FIXTURE, Derivation_ ## NAME ## _to_json) { \ using nlohmann::literals::operator "" _json; \ ASSERT_EQ( \ STR ## _json, \ (VAL).toJSON(*store)); \ } \ \ - TEST_F(DerivationTest, Derivation_ ## NAME ## _from_json) { \ + TEST_F(FIXTURE, Derivation_ ## NAME ## _from_json) { \ using nlohmann::literals::operator "" _json; \ ASSERT_EQ( \ (VAL), \ Derivation::fromJSON( \ *store, \ - STR ## _json)); \ + STR ## _json, \ + mockXpSettings)); \ } -#define TEST_ATERM(NAME, STR, VAL, DRV_NAME) \ - TEST_F(DerivationTest, Derivation_ ## NAME ## _to_aterm) { \ +#define TEST_ATERM(FIXTURE, NAME, STR, VAL, DRV_NAME) \ + TEST_F(FIXTURE, Derivation_ ## NAME ## _to_aterm) { \ ASSERT_EQ( \ STR, \ (VAL).unparse(*store, false)); \ } \ \ - TEST_F(DerivationTest, Derivation_ ## NAME ## _from_aterm) { \ + TEST_F(FIXTURE, Derivation_ ## NAME ## _from_aterm) { \ auto parsed = parseDerivation( \ - *store, \ - STR, \ - DRV_NAME); \ + *store, \ + STR, \ + DRV_NAME, \ + mockXpSettings); \ ASSERT_EQ( \ (VAL).toJSON(*store), \ parsed.toJSON(*store)); \ @@ -187,11 +209,15 @@ Derivation makeSimpleDrv(const Store & store) { store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"), }; drv.inputDrvs = { - { - store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv"), + .map = { { - "cat", - "dog", + store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv"), + { + .value = { + "cat", + "dog", + }, + }, }, }, }; @@ -210,17 +236,20 @@ Derivation makeSimpleDrv(const Store & store) { return drv; } -TEST_JSON(simple, +TEST_JSON(DerivationTest, simple, R"({ "name": "simple-derivation", "inputSrcs": [ "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1" ], "inputDrvs": { - "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv": [ - "cat", - "dog" - ] + "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv": { + "dynamicOutputs": {}, + "outputs": [ + "cat", + "dog" + ] + } }, "system": "wasm-sel4", "builder": "foo", @@ -235,11 +264,105 @@ TEST_JSON(simple, })", makeSimpleDrv(*store)) -TEST_ATERM(simple, +TEST_ATERM(DerivationTest, simple, R"(Derive([],[("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv",["cat","dog"])],["/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"],"wasm-sel4","foo",["bar","baz"],[("BIG_BAD","WOLF")]))", makeSimpleDrv(*store), "simple-derivation") +Derivation makeDynDepDerivation(const Store & store) { + Derivation drv; + drv.name = "dyn-dep-derivation"; + drv.inputSrcs = { + store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"), + }; + drv.inputDrvs = { + .map = { + { + store.parseStorePath("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv"), + DerivedPathMap::ChildNode { + .value = { + "cat", + "dog", + }, + .childMap = { + { + "cat", + DerivedPathMap::ChildNode { + .value = { + "kitten", + }, + }, + }, + { + "goose", + DerivedPathMap::ChildNode { + .value = { + "gosling", + }, + }, + }, + }, + }, + }, + }, + }; + drv.platform = "wasm-sel4"; + drv.builder = "foo"; + drv.args = { + "bar", + "baz", + }; + drv.env = { + { + "BIG_BAD", + "WOLF", + }, + }; + return drv; +} + +TEST_JSON(DynDerivationTest, dynDerivationDeps, + R"({ + "name": "dyn-dep-derivation", + "inputSrcs": [ + "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1" + ], + "inputDrvs": { + "/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv": { + "dynamicOutputs": { + "cat": { + "dynamicOutputs": {}, + "outputs": ["kitten"] + }, + "goose": { + "dynamicOutputs": {}, + "outputs": ["gosling"] + } + }, + "outputs": [ + "cat", + "dog" + ] + } + }, + "system": "wasm-sel4", + "builder": "foo", + "args": [ + "bar", + "baz" + ], + "env": { + "BIG_BAD": "WOLF" + }, + "outputs": {} + })", + makeDynDepDerivation(*store)) + +TEST_ATERM(DynDerivationTest, dynDerivationDeps, + R"(DrvWithVersion("xp-dyn-drv",[],[("/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep2.drv",(["cat","dog"],[("cat",["kitten"]),("goose",["gosling"])]))],["/nix/store/c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"],"wasm-sel4","foo",["bar","baz"],[("BIG_BAD","WOLF")]))", + makeDynDepDerivation(*store), + "dyn-dep-derivation") + #undef TEST_JSON #undef TEST_ATERM diff --git a/src/libutil/comparator.hh b/src/libutil/comparator.hh index 7982fdc5e..a4d20a675 100644 --- a/src/libutil/comparator.hh +++ b/src/libutil/comparator.hh @@ -1,21 +1,48 @@ #pragma once ///@file +#define DECLARE_ONE_CMP(PRE, QUAL, COMPARATOR, MY_TYPE) \ + PRE bool QUAL operator COMPARATOR(const MY_TYPE & other) const; +#define DECLARE_EQUAL(prefix, qualification, my_type) \ + DECLARE_ONE_CMP(prefix, qualification, ==, my_type) +#define DECLARE_LEQ(prefix, qualification, my_type) \ + DECLARE_ONE_CMP(prefix, qualification, <, my_type) +#define DECLARE_NEQ(prefix, qualification, my_type) \ + DECLARE_ONE_CMP(prefix, qualification, !=, my_type) + +#define GENERATE_ONE_CMP(PRE, QUAL, COMPARATOR, MY_TYPE, ...) \ + PRE bool QUAL operator COMPARATOR(const MY_TYPE & other) const { \ + __VA_OPT__(const MY_TYPE * me = this;) \ + auto fields1 = std::make_tuple( __VA_ARGS__ ); \ + __VA_OPT__(me = &other;) \ + auto fields2 = std::make_tuple( __VA_ARGS__ ); \ + return fields1 COMPARATOR fields2; \ + } +#define GENERATE_EQUAL(prefix, qualification, my_type, args...) \ + GENERATE_ONE_CMP(prefix, qualification, ==, my_type, args) +#define GENERATE_LEQ(prefix, qualification, my_type, args...) \ + GENERATE_ONE_CMP(prefix, qualification, <, my_type, args) +#define GENERATE_NEQ(prefix, qualification, my_type, args...) \ + GENERATE_ONE_CMP(prefix, qualification, !=, my_type, args) + /** * Declare comparison methods without defining them. */ -#define DECLARE_ONE_CMP(COMPARATOR, MY_TYPE) \ - bool operator COMPARATOR(const MY_TYPE & other) const; -#define DECLARE_EQUAL(my_type) \ - DECLARE_ONE_CMP(==, my_type) -#define DECLARE_LEQ(my_type) \ - DECLARE_ONE_CMP(<, my_type) -#define DECLARE_NEQ(my_type) \ - DECLARE_ONE_CMP(!=, my_type) #define DECLARE_CMP(my_type) \ - DECLARE_EQUAL(my_type) \ - DECLARE_LEQ(my_type) \ - DECLARE_NEQ(my_type) + DECLARE_EQUAL(,,my_type) \ + DECLARE_LEQ(,,my_type) \ + DECLARE_NEQ(,,my_type) + +/** + * @param prefix This is for something before each declaration like + * `template`. + * + * @param my_type the type are defining operators for. + */ +#define DECLARE_CMP_EXT(prefix, qualification, my_type) \ + DECLARE_EQUAL(prefix, qualification, my_type) \ + DECLARE_LEQ(prefix, qualification, my_type) \ + DECLARE_NEQ(prefix, qualification, my_type) /** * Awful hacky generation of the comparison operators by doing a lexicographic @@ -33,18 +60,18 @@ * } * ``` */ -#define GENERATE_ONE_CMP(COMPARATOR, MY_TYPE, ...) \ - bool operator COMPARATOR(const MY_TYPE& other) const { \ - __VA_OPT__(const MY_TYPE* me = this;) \ - auto fields1 = std::make_tuple( __VA_ARGS__ ); \ - __VA_OPT__(me = &other;) \ - auto fields2 = std::make_tuple( __VA_ARGS__ ); \ - return fields1 COMPARATOR fields2; \ - } -#define GENERATE_EQUAL(args...) GENERATE_ONE_CMP(==, args) -#define GENERATE_LEQ(args...) GENERATE_ONE_CMP(<, args) -#define GENERATE_NEQ(args...) GENERATE_ONE_CMP(!=, args) #define GENERATE_CMP(args...) \ - GENERATE_EQUAL(args) \ - GENERATE_LEQ(args) \ - GENERATE_NEQ(args) + GENERATE_EQUAL(,,args) \ + GENERATE_LEQ(,,args) \ + GENERATE_NEQ(,,args) + +/** + * @param prefix This is for something before each declaration like + * `template`. + * + * @param my_type the type are defining operators for. + */ +#define GENERATE_CMP_EXT(prefix, my_type, args...) \ + GENERATE_EQUAL(prefix, my_type ::, my_type, args) \ + GENERATE_LEQ(prefix, my_type ::, my_type, args) \ + GENERATE_NEQ(prefix, my_type ::, my_type, args) diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index 66f319c3e..e2189fc66 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -406,8 +406,22 @@ static void main_nix_build(int argc, char * * argv) } } + std::function, const DerivedPathMap::ChildNode &)> accumDerivedPath; + + accumDerivedPath = [&](ref inputDrv, const DerivedPathMap::ChildNode & inputNode) { + if (!inputNode.value.empty()) + pathsToBuild.push_back(DerivedPath::Built { + .drvPath = inputDrv, + .outputs = OutputsSpec::Names { inputNode.value }, + }); + for (const auto & [outputName, childNode] : inputNode.childMap) + accumDerivedPath( + make_ref(SingleDerivedPath::Built { inputDrv, outputName }), + childNode); + }; + // Build or fetch all dependencies of the derivation. - for (const auto & [inputDrv0, inputOutputs] : drv.inputDrvs) { + for (const auto & [inputDrv0, inputNode] : drv.inputDrvs.map) { // To get around lambda capturing restrictions in the // standard. const auto & inputDrv = inputDrv0; @@ -416,10 +430,7 @@ static void main_nix_build(int argc, char * * argv) return !std::regex_search(store->printStorePath(inputDrv), std::regex(exclude)); })) { - pathsToBuild.push_back(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(inputDrv), - .outputs = OutputsSpec::Names { inputOutputs }, - }); + accumDerivedPath(makeConstantStorePathRef(inputDrv), inputNode); pathsToCopy.insert(inputDrv); } } @@ -482,13 +493,21 @@ static void main_nix_build(int argc, char * * argv) if (env.count("__json")) { StorePathSet inputs; - for (auto & [depDrvPath, wantedDepOutputs] : drv.inputDrvs) { - auto outputs = evalStore->queryPartialDerivationOutputMap(depDrvPath); - for (auto & i : wantedDepOutputs) { + + std::function::ChildNode &)> accumInputClosure; + + accumInputClosure = [&](const StorePath & inputDrv, const DerivedPathMap::ChildNode & inputNode) { + auto outputs = evalStore->queryPartialDerivationOutputMap(inputDrv); + for (auto & i : inputNode.value) { auto o = outputs.at(i); store->computeFSClosure(*o, inputs); } - } + for (const auto & [outputName, childNode] : inputNode.childMap) + accumInputClosure(*outputs.at(outputName), childNode); + }; + + for (const auto & [inputDrv, inputNode] : drv.inputDrvs.map) + accumInputClosure(inputDrv, inputNode); ParsedDerivation parsedDrv(drvInfo.requireDrvPath(), drv); diff --git a/src/nix/app.cc b/src/nix/app.cc index 67c5ef344..34fac9935 100644 --- a/src/nix/app.cc +++ b/src/nix/app.cc @@ -20,15 +20,22 @@ StringPairs resolveRewrites( const std::vector & dependencies) { StringPairs res; - for (auto & dep : dependencies) - if (auto drvDep = std::get_if(&dep.path)) - if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) - for (auto & [ outputName, outputPath ] : drvDep->outputs) + if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { + for (auto & dep : dependencies) { + if (auto drvDep = std::get_if(&dep.path)) { + for (auto & [ outputName, outputPath ] : drvDep->outputs) { res.emplace( - DownstreamPlaceholder::unknownCaOutput( - drvDep->drvPath->outPath(), outputName).render(), + DownstreamPlaceholder::fromSingleDerivedPathBuilt( + SingleDerivedPath::Built { + .drvPath = make_ref(drvDep->drvPath->discardOutputPath()), + .output = outputName, + }).render(), store.printStorePath(outputPath) ); + } + } + } + } return res; } diff --git a/tests/dyn-drv/dep-built-drv.sh b/tests/dyn-drv/dep-built-drv.sh index 8c4a45e3b..c3daf2c59 100644 --- a/tests/dyn-drv/dep-built-drv.sh +++ b/tests/dyn-drv/dep-built-drv.sh @@ -6,4 +6,6 @@ out1=$(nix-build ./text-hashed-output.nix -A hello --no-out-link) clearStore -expectStderr 1 nix-build ./text-hashed-output.nix -A wrapper --no-out-link | grepQuiet "Dependencies on the outputs of dynamic derivations are not yet supported" +out2=$(nix-build ./text-hashed-output.nix -A wrapper --no-out-link) + +diff -r $out1 $out2 From 80d7994f52ccefca2f2fbe4ee64741a6f49884ff Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 6 Sep 2023 22:57:37 -0400 Subject: [PATCH 100/299] Special-case error message to add extra information The Derivation parser and old ATerm unfortunately leaves few ways to get nice errors when an old version of Nix encounters a new version of the format. The most likely scenario for this to occur is with a new client making a derivation that the old daemon it is communicating with cannot understand. The extensions we just created for dynamic derivation deps will add a version field, solving the problem going forward, but there is still the issue of what to do about old versions of Nix up to now. The solution here is to carefully catch the bad error from the daemon that is likely to indicate this problem, and add some extra context to it. There is another "Ugly backwards compatibility hack" in `remote-store.cc` that also works by transforming an error. Co-authored-by: Robert Hensing --- src/libstore/remote-store.cc | 19 ++++++++++++++++++- tests/dyn-drv/local.mk | 3 ++- tests/dyn-drv/old-daemon-error-hack.nix | 20 ++++++++++++++++++++ tests/dyn-drv/old-daemon-error-hack.sh | 11 +++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/dyn-drv/old-daemon-error-hack.nix create mode 100644 tests/dyn-drv/old-daemon-error-hack.sh diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 58f72beb9..a639346d1 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -172,7 +172,24 @@ void RemoteStore::ConnectionHandle::processStderr(Sink * sink, Source * source, auto ex = handle->processStderr(sink, source, flush); if (ex) { daemonException = true; - std::rethrow_exception(ex); + try { + std::rethrow_exception(ex); + } catch (const Error & e) { + // Nix versions before #4628 did not have an adequate behavior for reporting that the derivation format was upgraded. + // To avoid having to add compatibility logic in many places, we expect to catch almost all occurrences of the + // old incomprehensible error here, so that we can explain to users what's going on when their daemon is + // older than #4628 (2023). + if (experimentalFeatureSettings.isEnabled(Xp::DynamicDerivations) && + GET_PROTOCOL_MINOR(handle->daemonVersion) <= 35) + { + auto m = e.msg(); + if (m.find("parsing derivation") != std::string::npos && + m.find("expected string") != std::string::npos && + m.find("Derive([") != std::string::npos) + throw Error("%s, this might be because the daemon is too old to understand dependencies on dynamic derivations. Check to see if the raw dervation is in the form '%s'", std::move(m), "DrvWithVersion(..)"); + } + throw; + } } } diff --git a/tests/dyn-drv/local.mk b/tests/dyn-drv/local.mk index 0ce7cd37d..6b435499b 100644 --- a/tests/dyn-drv/local.mk +++ b/tests/dyn-drv/local.mk @@ -3,7 +3,8 @@ dyn-drv-tests := \ $(d)/recursive-mod-json.sh \ $(d)/build-built-drv.sh \ $(d)/eval-outputOf.sh \ - $(d)/dep-built-drv.sh + $(d)/dep-built-drv.sh \ + $(d)/old-daemon-error-hack.sh install-tests-groups += dyn-drv diff --git a/tests/dyn-drv/old-daemon-error-hack.nix b/tests/dyn-drv/old-daemon-error-hack.nix new file mode 100644 index 000000000..c9d4a62d4 --- /dev/null +++ b/tests/dyn-drv/old-daemon-error-hack.nix @@ -0,0 +1,20 @@ +with import ./config.nix; + +# A simple content-addressed derivation. +# The derivation can be arbitrarily modified by passing a different `seed`, +# but the output will always be the same +rec { + stub = mkDerivation { + name = "stub"; + buildCommand = '' + echo stub > $out + ''; + }; + wrapper = mkDerivation { + name = "has-dynamic-drv-dep"; + buildCommand = '' + exit 1 # we're not building this derivation + ${builtins.outputOf stub.outPath "out"} + ''; + }; +} diff --git a/tests/dyn-drv/old-daemon-error-hack.sh b/tests/dyn-drv/old-daemon-error-hack.sh new file mode 100644 index 000000000..43b049973 --- /dev/null +++ b/tests/dyn-drv/old-daemon-error-hack.sh @@ -0,0 +1,11 @@ +# Purposely bypassing our usual common for this subgroup +source ../common.sh + +# Need backend to support text-hashing too +isDaemonNewer "2.18.0pre20230906" && skipTest "Daemon is too new" + +enableFeatures "ca-derivations dynamic-derivations" + +restartDaemon + +expectStderr 1 nix-instantiate --read-write-mode ./old-daemon-error-hack.nix | grepQuiet "the daemon is too old to understand dependencies on dynamic derivations" From 5473e10249e02f95375b6126e232700d51bdf429 Mon Sep 17 00:00:00 2001 From: thenbe <33713262+thenbe@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:25:32 +0000 Subject: [PATCH 101/299] fix: `nix shell` multiple commands example (#8950) The `-c` flag belongs to `sh` not `nix shell`. As it stands, the command errors with: ``` $ nix shell nixpkgs#gnumake --command sh --command "cd src && make" sh: --command: invalid option ``` https://github.com/NixOS/nix/pull/8276 was good for readability, but it missed this since that PR used a find/replace script. --- src/nix/shell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nix/shell.md b/src/nix/shell.md index 1668104b1..f36919575 100644 --- a/src/nix/shell.md +++ b/src/nix/shell.md @@ -26,7 +26,7 @@ R""( * Run multiple commands in a shell environment: ```console - # nix shell nixpkgs#gnumake --command sh --command "cd src && make" + # nix shell nixpkgs#gnumake --command sh -c "cd src && make" ``` * Run GNU Hello in a chroot store: From 2cdc9c32e746a620458a1dd87655d2a6c3800f64 Mon Sep 17 00:00:00 2001 From: Emil Nikolov Date: Sat, 9 Sep 2023 08:54:39 +0200 Subject: [PATCH 102/299] docs: fixed the default priority of nix-env --install (#8945) --- doc/manual/src/command-ref/nix-env/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/command-ref/nix-env/install.md b/doc/manual/src/command-ref/nix-env/install.md index ad179cbc7..5dc04c385 100644 --- a/doc/manual/src/command-ref/nix-env/install.md +++ b/doc/manual/src/command-ref/nix-env/install.md @@ -30,7 +30,7 @@ a number of possible ways: derivation with the highest *priority* is used. A derivation can define a priority by declaring the `meta.priority` attribute. This attribute should be a number, with a higher value denoting a lower - priority. The default priority is `0`. + priority. The default priority is `5`. If there are multiple matching derivations with the same priority, then the derivation with the highest version will be installed. From 682dbcab9a7a81c210dc9c8aa3cfc58ccc1723aa Mon Sep 17 00:00:00 2001 From: maralorn Date: Sat, 9 Sep 2023 18:01:10 +0200 Subject: [PATCH 103/299] Print parent activity field in json log --- src/libutil/logging.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/logging.cc b/src/libutil/logging.cc index 54702e4ea..9d7a141b3 100644 --- a/src/libutil/logging.cc +++ b/src/libutil/logging.cc @@ -220,8 +220,8 @@ struct JSONLogger : Logger { json["level"] = lvl; json["type"] = type; json["text"] = s; + json["parent"] = parent; addFields(json, fields); - // FIXME: handle parent write(json); } From 07545add53ab39b4560d8e7cec1748423ae1a22e Mon Sep 17 00:00:00 2001 From: Matthew Kenigsberg Date: Sun, 10 Sep 2023 12:18:03 +0200 Subject: [PATCH 104/299] Drop dead code localPath is unused --- src/libexpr/flake/flake.cc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 6a27ea2e8..90427e064 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -520,11 +520,6 @@ LockedFlake lockFlake( } } - auto localPath(parentPath); - // If this input is a path, recurse it down. - // This allows us to resolve path inputs relative to the current flake. - if ((*input.ref).input.getType() == "path") - localPath = absPath(*input.ref->input.getSourcePath(), parentPath); computeLocks( mustRefetch ? getFlake(state, oldLock->lockedRef, false, flakeCache, inputPath).inputs From 3720e811fa2883bca1a2698543146070e4d8d32c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 12 Sep 2023 13:21:55 +0200 Subject: [PATCH 105/299] libexpr: Add nrExprs to NIX_SHOW_STATS --- src/libexpr/eval.cc | 1 + src/libexpr/nixexpr.hh | 4 ++++ src/libexpr/parser.y | 1 + 3 files changed, 6 insertions(+) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 6d445fd96..afa864730 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2506,6 +2506,7 @@ void EvalState::printStats() {"elements", nrValuesInEnvs}, {"bytes", bEnvs}, }; + topObj["nrExprs"] = Expr::nrExprs; topObj["list"] = { {"elements", nrListElems}, {"bytes", bLists}, diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index 5ca3d1fa6..b80c07f46 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -155,6 +155,10 @@ std::string showAttrPath(const SymbolTable & symbols, const AttrPath & attrPath) struct Expr { + static unsigned long nrExprs; + inline Expr() { + nrExprs++; + } virtual ~Expr() { }; virtual void show(const SymbolTable & symbols, std::ostream & str) const; virtual void bindVars(EvalState & es, const std::shared_ptr & env); diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 792f51fde..a3d3c7041 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -653,6 +653,7 @@ formal namespace nix { +unsigned long Expr::nrExprs = 0; Expr * EvalState::parse( char * text, From bf8deb4991286a44e1b6cc2142721a594276dd12 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 12 Sep 2023 13:45:45 +0200 Subject: [PATCH 106/299] Expr: remove redundant int and float fields --- src/libexpr/nixexpr.cc | 4 ++-- src/libexpr/nixexpr.hh | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libexpr/nixexpr.cc b/src/libexpr/nixexpr.cc index 4566a1388..22be8e68c 100644 --- a/src/libexpr/nixexpr.cc +++ b/src/libexpr/nixexpr.cc @@ -76,12 +76,12 @@ void Expr::show(const SymbolTable & symbols, std::ostream & str) const void ExprInt::show(const SymbolTable & symbols, std::ostream & str) const { - str << n; + str << v.integer; } void ExprFloat::show(const SymbolTable & symbols, std::ostream & str) const { - str << nf; + str << v.fpoint; } void ExprString::show(const SymbolTable & symbols, std::ostream & str) const diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index b80c07f46..17b2d1136 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -175,18 +175,16 @@ struct Expr struct ExprInt : Expr { - NixInt n; Value v; - ExprInt(NixInt n) : n(n) { v.mkInt(n); }; + ExprInt(NixInt n) { v.mkInt(n); }; Value * maybeThunk(EvalState & state, Env & env) override; COMMON_METHODS }; struct ExprFloat : Expr { - NixFloat nf; Value v; - ExprFloat(NixFloat nf) : nf(nf) { v.mkFloat(nf); }; + ExprFloat(NixFloat nf) { v.mkFloat(nf); }; Value * maybeThunk(EvalState & state, Env & env) override; COMMON_METHODS }; From 21783cff1649f236cb31f27f788e3934802c42c9 Mon Sep 17 00:00:00 2001 From: Emil Nikolov Date: Tue, 12 Sep 2023 17:15:36 +0200 Subject: [PATCH 107/299] docs: make the nix develop --command example unambiguous (#8952) --- src/nix/develop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nix/develop.md b/src/nix/develop.md index 1b5a8aeba..c49b39669 100644 --- a/src/nix/develop.md +++ b/src/nix/develop.md @@ -69,7 +69,7 @@ R""( * Run a series of script commands: ```console - # nix develop --command bash --command "mkdir build && cmake .. && make" + # nix develop --command bash -c "mkdir build && cmake .. && make" ``` # Description From 408055a9ddf855e32a59e07e564ad893b2cb2cdf Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 18 Sep 2023 20:08:48 +0200 Subject: [PATCH 108/299] add clarifying example to `nix-env` output selection there is a very confusing warning in the Nixpkgs manual that mischaracterises `nix-env` behavior, and this example shows what's really happening. note that it doesn't use `pkgs.runCommand` or other `pkgs.stdenv` facilities, as deep down those set `meta.outputsToInstall` to very particular defaults that do not generally apply to Nix. --- doc/manual/src/command-ref/nix-env/install.md | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/doc/manual/src/command-ref/nix-env/install.md b/doc/manual/src/command-ref/nix-env/install.md index 5dc04c385..95648e9e0 100644 --- a/doc/manual/src/command-ref/nix-env/install.md +++ b/doc/manual/src/command-ref/nix-env/install.md @@ -19,12 +19,15 @@ current generation of the active profile, to which a set of store paths described by *args* is added. The arguments *args* map to store paths in a number of possible ways: - - By default, *args* is a set of derivation names denoting derivations + + - By default, *args* is a set of [derivation] names denoting derivations in the active Nix expression. These are realised, and the resulting output paths are installed. Currently installed derivations with a name equal to the name of a derivation being added are removed unless the option `--preserve-installed` is specified. + [derivation]: @docroot@/language/derivations.md + If there are multiple derivations matching a name in *args* that have the same name (e.g., `gcc-3.3.6` and `gcc-4.1.1`), then the derivation with the highest *priority* is used. A derivation can @@ -66,8 +69,59 @@ a number of possible ways: - If *args* are store paths that are not store derivations, then these are [realised](@docroot@/command-ref/nix-store/realise.md) and installed. - - By default all outputs are installed for each derivation. That can - be reduced by setting `meta.outputsToInstall`. + - By default all outputs are installed for each derivation. + This can be overridden by adding a `meta.outputsToInstall` attribute on the derivation listing a subset of the output names. + + + + Example: + + The file `example.nix` defines a [derivation] with two outputs `foo` and `bar`, each containing a file. + + ```nix + # example.nix + let + pkgs = import {}; + command = '' + ${pkgs.coreutils}/bin/mkdir -p $foo $bar + echo foo > $foo/foo-file + echo bar > $bar/bar-file + ''; + in + derivation { + name = "example"; + builder = "${pkgs.bash}/bin/bash"; + args = [ "-c" command ]; + outputs = [ "foo" "bar" ]; + system = builtins.currentSystem; + } + ``` + + Installing from this Nix expression will make files from both outputs appear in the current profile. + + ```console + $ nix-env --install --file example.nix + installing 'example' + $ ls ~/.nix-profile + foo-file + bar-file + manifest.nix + ``` + + Adding `meta.outputsToInstall` to that derivation will make `nix-env` only install files from the specified outputs. + + ```nix + # example-outputs.nix + import ./example.nix // { meta.outputsToInstall = [ "bar" ]; } + ``` + + ```console + $ nix-env --install --file example-outputs.nix + installing 'example' + $ ls ~/.nix-profile + bar-file + manifest.nix + ``` # Flags From dd3bf4dbda02b83a185efc1f8f4bced06a6b07ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:16:55 +0000 Subject: [PATCH 109/299] Bump docker/login-action from 2 to 3 Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v2...v3) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 515548a4e..0c9c24dad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: - run: docker tag nix:$NIX_VERSION nixos/nix:$NIX_VERSION - run: docker tag nix:$NIX_VERSION nixos/nix:master - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From c8afa01bc25ba81883f8007ef83532ca550c731d Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 19 Sep 2023 14:51:50 +0200 Subject: [PATCH 110/299] Try aws-sdk-cpp fix --- flake.lock | 12 ++++++------ flake.nix | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/flake.lock b/flake.lock index 2bc503258..75b6ae6a7 100644 --- a/flake.lock +++ b/flake.lock @@ -34,16 +34,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1693494129, - "narHash": "sha256-YrHlSbniFmhcz0ORe8MMFttifKR4hTRzyX2OQUO9VxA=", - "owner": "NixOS", + "lastModified": 1695124524, + "narHash": "sha256-trXDytVCqf3KryQQQrHOZKUabu1/lB8/ndOAuZKQrOE=", + "owner": "edolstra", "repo": "nixpkgs", - "rev": "22a584b861ab31a2dca52121999079832f0e0f73", + "rev": "a3d30b525535e3158221abc1a957ce798ab159fe", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-23.05-small", + "owner": "edolstra", + "ref": "fix-aws-sdk-cpp", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 49aa9c4ce..249da2a04 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,8 @@ { description = "The purely functional package manager"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05-small"; + #inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05-small"; + inputs.nixpkgs.url = "github:edolstra/nixpkgs/fix-aws-sdk-cpp"; inputs.nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2"; inputs.lowdown-src = { url = "github:kristapsdz/lowdown"; flake = false; }; inputs.flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; From 126e2645f2a060197655f9a699ffa4dc3a464bdd Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 19 Sep 2023 16:04:00 +0200 Subject: [PATCH 111/299] Disable rapidcheck tests in the coverage run https://hydra.nixos.org/build/233688539 --- flake.nix | 2 ++ src/libexpr/tests/derived-path.cc | 4 ++++ src/libexpr/tests/value/context.cc | 4 ++++ src/libstore/tests/derived-path.cc | 4 ++++ src/libstore/tests/outputs-spec.cc | 4 ++++ src/libstore/tests/path.cc | 4 ++++ 6 files changed, 22 insertions(+) diff --git a/flake.nix b/flake.nix index 249da2a04..dfb2dc1f8 100644 --- a/flake.nix +++ b/flake.nix @@ -588,6 +588,8 @@ lcovFilter = [ "*/boost/*" "*-tab.*" ]; hardeningDisable = ["fortify"]; + + NIX_CFLAGS_COMPILE = "-DCOVERAGE=1"; }; # API docs for Nix's unstable internal C++ interfaces. diff --git a/src/libexpr/tests/derived-path.cc b/src/libexpr/tests/derived-path.cc index d01735db8..d5fc6f201 100644 --- a/src/libexpr/tests/derived-path.cc +++ b/src/libexpr/tests/derived-path.cc @@ -18,6 +18,8 @@ TEST_F(DerivedPathExpressionTest, force_init) { } +#ifndef COVERAGE + RC_GTEST_FIXTURE_PROP( DerivedPathExpressionTest, prop_opaque_path_round_trip, @@ -61,4 +63,6 @@ RC_GTEST_FIXTURE_PROP( RC_ASSERT(SingleDerivedPath { b } == d); } +#endif + } /* namespace nix */ diff --git a/src/libexpr/tests/value/context.cc b/src/libexpr/tests/value/context.cc index 19407d071..92d4889ab 100644 --- a/src/libexpr/tests/value/context.cc +++ b/src/libexpr/tests/value/context.cc @@ -147,6 +147,8 @@ Gen Arbitrary::arbitrary() namespace nix { +#ifndef COVERAGE + RC_GTEST_PROP( NixStringContextElemTest, prop_round_rip, @@ -155,4 +157,6 @@ RC_GTEST_PROP( RC_ASSERT(o == NixStringContextElem::parse(o.to_string())); } +#endif + } diff --git a/src/libstore/tests/derived-path.cc b/src/libstore/tests/derived-path.cc index d6549f66f..3fa3c0801 100644 --- a/src/libstore/tests/derived-path.cc +++ b/src/libstore/tests/derived-path.cc @@ -130,6 +130,8 @@ TEST_F(DerivedPathTest, built_built_xp) { MissingExperimentalFeature); } +#ifndef COVERAGE + RC_GTEST_FIXTURE_PROP( DerivedPathTest, prop_legacy_round_rip, @@ -146,4 +148,6 @@ RC_GTEST_FIXTURE_PROP( RC_ASSERT(o == DerivedPath::parse(*store, o.to_string(*store))); } +#endif + } diff --git a/src/libstore/tests/outputs-spec.cc b/src/libstore/tests/outputs-spec.cc index bf8deaa9d..952945185 100644 --- a/src/libstore/tests/outputs-spec.cc +++ b/src/libstore/tests/outputs-spec.cc @@ -224,6 +224,8 @@ Gen Arbitrary::arbitrary() namespace nix { +#ifndef COVERAGE + RC_GTEST_PROP( OutputsSpec, prop_round_rip, @@ -232,4 +234,6 @@ RC_GTEST_PROP( RC_ASSERT(o == OutputsSpec::parse(o.to_string())); } +#endif + } diff --git a/src/libstore/tests/path.cc b/src/libstore/tests/path.cc index 430aa0099..efa35ef2b 100644 --- a/src/libstore/tests/path.cc +++ b/src/libstore/tests/path.cc @@ -134,6 +134,8 @@ Gen Arbitrary::arbitrary() namespace nix { +#ifndef COVERAGE + RC_GTEST_FIXTURE_PROP( StorePathTest, prop_regex_accept, @@ -150,4 +152,6 @@ RC_GTEST_FIXTURE_PROP( RC_ASSERT(p == store->parseStorePath(store->printStorePath(p))); } +#endif + } From c6953d1ff62fb6dc4fbd89c03e7949c552c19382 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 19 Sep 2023 17:03:21 +0200 Subject: [PATCH 112/299] Disable systemd-nspawn test This is broken because of a change in systemd in NixOS 23.05. It fails with Failed to mount proc (type proc) on /proc (MS_NOSUID|MS_NODEV|MS_NOEXEC ""): Operation not permitted --- tests/nixos/containers/containers.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/nixos/containers/containers.nix b/tests/nixos/containers/containers.nix index c8ee78a4a..e721be48f 100644 --- a/tests/nixos/containers/containers.nix +++ b/tests/nixos/containers/containers.nix @@ -56,8 +56,8 @@ host.fail("nix build -v --auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-6 --arg uidRange true") # Run systemd-nspawn in a Nix build. - host.succeed("nix build -v --auto-allocate-uids --sandbox -L --offline --impure --file ${./systemd-nspawn.nix} --argstr nixpkgs ${nixpkgs}") - host.succeed("[[ $(cat ./result/msg) = 'Hello World' ]]") + #host.succeed("nix build -v --auto-allocate-uids --sandbox -L --offline --impure --file ${./systemd-nspawn.nix} --argstr nixpkgs ${nixpkgs}") + #host.succeed("[[ $(cat ./result/msg) = 'Hello World' ]]") ''; } From b6b2a0aea99995d73f0faa6115fb33a137e57b23 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 19 Sep 2023 17:21:07 +0200 Subject: [PATCH 113/299] Use "touch -h" https://hydra.nixos.org/build/235888160 This is needed because Nixpkgs now contains dangling symlinks (pkgs/test/nixpkgs-check-by-name/tests/symlink-invalid/pkgs/by-name/fo/foo/foo.nix). --- tests/nixos/github-flakes.nix | 2 +- tests/nixos/sourcehut-flakes.nix | 2 +- tests/nixos/tarball-flakes.nix | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nixos/github-flakes.nix b/tests/nixos/github-flakes.nix index e4d347691..6de702d17 100644 --- a/tests/nixos/github-flakes.nix +++ b/tests/nixos/github-flakes.nix @@ -82,7 +82,7 @@ let dir=NixOS-nixpkgs-${nixpkgs.shortRev} cp -prd ${nixpkgs} $dir # Set the correct timestamp in the tarball. - find $dir -print0 | xargs -0 touch -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- + find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- tar cfz $out/archive/${nixpkgs.rev}.tar.gz $dir --hard-dereference ''; in diff --git a/tests/nixos/sourcehut-flakes.nix b/tests/nixos/sourcehut-flakes.nix index a76fed020..6e8d884a0 100644 --- a/tests/nixos/sourcehut-flakes.nix +++ b/tests/nixos/sourcehut-flakes.nix @@ -47,7 +47,7 @@ let cp -prd ${nixpkgs} $dir # Set the correct timestamp in the tarball. - find $dir -print0 | xargs -0 touch -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- + find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- mkdir -p $out/archive tar cfz $out/archive/${nixpkgs.rev}.tar.gz $dir --hard-dereference diff --git a/tests/nixos/tarball-flakes.nix b/tests/nixos/tarball-flakes.nix index 1d43a5d04..e30d15739 100644 --- a/tests/nixos/tarball-flakes.nix +++ b/tests/nixos/tarball-flakes.nix @@ -11,7 +11,7 @@ let dir=nixpkgs-${nixpkgs.shortRev} cp -prd ${nixpkgs} $dir # Set the correct timestamp in the tarball. - find $dir -print0 | xargs -0 touch -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- + find $dir -print0 | xargs -0 touch -h -t ${builtins.substring 0 12 nixpkgs.lastModifiedDate}.${builtins.substring 12 2 nixpkgs.lastModifiedDate} -- tar cfz $out/stable/${nixpkgs.rev}.tar.gz $dir --hard-dereference echo 'Redirect "/latest.tar.gz" "/stable/${nixpkgs.rev}.tar.gz"' > $out/.htaccess From 10ad052f7d14101525daabbd63a147eb0247c3a1 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 20 Sep 2023 11:42:49 +0200 Subject: [PATCH 114/299] Release notes --- doc/manual/src/SUMMARY.md.in | 1 + doc/manual/src/release-notes/rl-2.18.md | 28 ++++++++++++++++++++++++ doc/manual/src/release-notes/rl-next.md | 29 ------------------------- 3 files changed, 29 insertions(+), 29 deletions(-) create mode 100644 doc/manual/src/release-notes/rl-2.18.md diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index 701a6ea69..c23186511 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -110,6 +110,7 @@ - [C++ style guide](contributing/cxx.md) - [Release Notes](release-notes/release-notes.md) - [Release X.Y (202?-??-??)](release-notes/rl-next.md) + - [Release 2.18 (2023-09-20)](release-notes/rl-2.18.md) - [Release 2.17 (2023-07-24)](release-notes/rl-2.17.md) - [Release 2.16 (2023-05-31)](release-notes/rl-2.16.md) - [Release 2.15 (2023-04-11)](release-notes/rl-2.15.md) diff --git a/doc/manual/src/release-notes/rl-2.18.md b/doc/manual/src/release-notes/rl-2.18.md new file mode 100644 index 000000000..4bbc52b50 --- /dev/null +++ b/doc/manual/src/release-notes/rl-2.18.md @@ -0,0 +1,28 @@ +# Release 2.18 (2023-09-20) + +- Two new builtin functions, + [`builtins.parseFlakeRef`](@docroot@/language/builtins.md#builtins-parseFlakeRef) + and + [`builtins.flakeRefToString`](@docroot@/language/builtins.md#builtins-flakeRefToString), + have been added. + These functions are useful for converting between flake references encoded as attribute sets and URLs. + +- [`builtins.toJSON`](@docroot@/language/builtins.md#builtins-parseFlakeRef) now prints [--show-trace](@docroot@/command-ref/conf-file.html#conf-show-trace) items for the path in which it finds an evaluation error. + +- Error messages regarding malformed input to [`nix derivation add`](@docroot@/command-ref/new-cli/nix3-derivation-add.md) are now clearer and more detailed. + +- The `discard-references` feature has been stabilized. + This means that the + [unsafeDiscardReferences](@docroot@/contributing/experimental-features.md#xp-feature-discard-references) + attribute is no longer guarded by an experimental flag and can be used + freely. + +- The JSON output for derived paths which are store paths is now a string, not an object with a single `path` field. + This only affects `nix-build --json` when "building" non-derivation things like fetched sources, which is a no-op. + +- A new builtin [`outputOf`](@docroot@/language/builtins.md#builtins-outputOf) has been added. + It is part of the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. + +- Flake follow paths at depths greater than 2 are now handled correctly, preventing "follows a non-existent input" errors. + +- [`nix-store --query`](@docroot@/command-ref/nix-store/query.md) gained a new type of query: `--valid-derivers`. It returns all `.drv` files in the local store that *can be* used to build the output passed in argument. This is in contrast to `--deriver`, which returns the single `.drv` file that *was actually* used to build the output passed in argument. In case the output was substituted from a binary cache, this `.drv` file may only exist on said binary cache and not locally. diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 02ad0a661..c869b5e2f 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -1,30 +1 @@ # Release X.Y (202?-??-??) - -- Two new builtin functions, - [`builtins.parseFlakeRef`](@docroot@/language/builtins.md#builtins-parseFlakeRef) - and - [`builtins.flakeRefToString`](@docroot@/language/builtins.md#builtins-flakeRefToString), - have been added. - These functions are useful for converting between flake references encoded as attribute sets and URLs. - -- [`builtins.toJSON`](@docroot@/language/builtins.md#builtins-parseFlakeRef) now prints [--show-trace](@docroot@/command-ref/conf-file.html#conf-show-trace) items for the path in which it finds an evaluation error. - -- Error messages regarding malformed input to [`derivation add`](@docroot@/command-ref/new-cli/nix3-derivation-add.md) are now clearer and more detailed. - -- The `discard-references` feature has been stabilized. - This means that the - [unsafeDiscardReferences](@docroot@/contributing/experimental-features.md#xp-feature-discard-references) - attribute is no longer guarded by an experimental flag and can be used - freely. - -- The JSON output for derived paths with are store paths is now a string, not an object with a single `path` field. - This only affects `nix-build --json` when "building" non-derivation things like fetched sources, which is a no-op. - -- Introduce a new [`outputOf`](@docroot@/language/builtins.md#builtins-outputOf) builtin. - It is part of the [`dynamic-derivations`](@docroot@/contributing/experimental-features.md#xp-feature-dynamic-derivations) experimental feature. - -- Flake follow paths at depths greater than 2 are now handled correctly, preventing "follows a non-existent input" errors. - -- [`nix-store --query`](@docroot@/command-ref/nix-store/query.md) gained a new type of query: `--valid-derivers`. It returns all `.drv` files in the local store that *can be* used to build the output passed in argument. -This is in contrast to `--deriver`, which returns the single `.drv` file that *was actually* used to build the output passed in argument. In case the output was substituted from a binary cache, -this `.drv` file may only exist on said binary cache and not locally. From e44d2a6bbef113b3d8f162f75bd5c94e0101075f Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 30 Aug 2023 22:57:59 -0400 Subject: [PATCH 115/299] Add FreeBSD and NetBSD cross to Nix's flake --- flake.nix | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index dfb2dc1f8..871f0d7ba 100644 --- a/flake.nix +++ b/flake.nix @@ -25,8 +25,11 @@ linuxSystems = linux32BitSystems ++ linux64BitSystems; darwinSystems = [ "x86_64-darwin" "aarch64-darwin" ]; systems = linuxSystems ++ darwinSystems; - - crossSystems = [ "armv6l-linux" "armv7l-linux" ]; + + crossSystems = [ + "armv6l-linux" "armv7l-linux" + "x86_64-freebsd13" "x86_64-netbsd" + ]; stdenvs = [ "gccStdenv" "clangStdenv" "clang11Stdenv" "stdenv" "libcxxStdenv" "ccacheStdenv" ]; @@ -94,7 +97,14 @@ nixpkgsFor = forAllSystems (system: let make-pkgs = crossSystem: stdenv: import nixpkgs { - inherit system crossSystem; + localSystem = { + inherit system; + }; + crossSystem = if crossSystem == null then null else { + system = crossSystem; + } // lib.optionalAttrs (crossSystem == "x86_64-freebsd13") { + useLLVM = true; + }; overlays = [ (overlayFor (p: p.${stdenv})) ]; From 28850ee90095e337bf47fbe03a0d937cb98784e2 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 30 Aug 2023 23:27:53 -0400 Subject: [PATCH 116/299] Make dev shells work for cross Need to get tools from right package set. Could build clang tools but I don't want to wait :D. --- flake.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 871f0d7ba..24d9520ea 100644 --- a/flake.nix +++ b/flake.nix @@ -750,7 +750,11 @@ outputs = [ "out" "dev" "doc" ]; nativeBuildInputs = nativeBuildDeps - ++ (lib.optionals stdenv.cc.isClang [ pkgs.bear pkgs.clang-tools ]); + ++ lib.optional stdenv.cc.isClang pkgs.buildPackages.bear + ++ lib.optional + (stdenv.cc.isClang && stdenv.hostPlatform == stdenv.buildPlatform) + pkgs.buildPackages.clang-tools + ; buildInputs = buildDeps ++ propagatedDeps ++ awsDeps ++ checkDeps ++ internalApiDocsDeps; From 564392b57bc44c407303e4eaf6138d61a8ea50b0 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 30 Aug 2023 23:29:17 -0400 Subject: [PATCH 117/299] Make libsodium an unconditional dependency The configure script will not tolerate it being missing. --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 24d9520ea..50a226347 100644 --- a/flake.nix +++ b/flake.nix @@ -190,9 +190,9 @@ libarchive boost lowdown-nix + libsodium ] ++ lib.optionals stdenv.isLinux [libseccomp] - ++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium ++ lib.optional stdenv.hostPlatform.isx86_64 libcpuid; checkDeps = [ From 0db251e4ad07338375fd59134fb467e9ebc4176a Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 30 Aug 2023 23:43:48 -0400 Subject: [PATCH 118/299] Do not build docs in cross devShell Coppied from the main build; we really should deduplicate this more. --- flake.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 50a226347..36937057d 100644 --- a/flake.nix +++ b/flake.nix @@ -743,6 +743,9 @@ devShells = let makeShell = pkgs: stdenv: + let + canRunInstalled = stdenv.buildPlatform.canExecute stdenv.hostPlatform; + in with commonDeps { inherit pkgs; }; stdenv.mkDerivation { name = "nix"; @@ -760,7 +763,8 @@ ++ awsDeps ++ checkDeps ++ internalApiDocsDeps; configureFlags = configureFlags - ++ testConfigureFlags ++ internalApiDocsConfigureFlags; + ++ testConfigureFlags ++ internalApiDocsConfigureFlags + ++ lib.optional (!canRunInstalled) "--disable-doc-gen"; enableParallelBuilding = true; From 7f76d7f038fbb5cb7982cf9aa951b9730566e275 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 30 Aug 2023 23:44:23 -0400 Subject: [PATCH 119/299] Rename an identifier of ours called `stdout` This is a reserved identifier on NetBSD --- it is replaced by a macro on that platform --- and so we cannot use it. --- src/libmain/shared.cc | 6 +++--- src/libmain/shared.hh | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 56f47a4ac..9c2ad039a 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -379,9 +379,9 @@ RunPager::RunPager() }); pid.setKillSignal(SIGINT); - stdout = fcntl(STDOUT_FILENO, F_DUPFD_CLOEXEC, 0); + std_out = fcntl(STDOUT_FILENO, F_DUPFD_CLOEXEC, 0); if (dup2(toPager.writeSide.get(), STDOUT_FILENO) == -1) - throw SysError("dupping stdout"); + throw SysError("dupping standard output"); } @@ -390,7 +390,7 @@ RunPager::~RunPager() try { if (pid != -1) { std::cout.flush(); - dup2(stdout, STDOUT_FILENO); + dup2(std_out, STDOUT_FILENO); pid.wait(); } } catch (...) { diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh index 7a9e83c6c..9415be78a 100644 --- a/src/libmain/shared.hh +++ b/src/libmain/shared.hh @@ -85,8 +85,9 @@ struct LegacyArgs : public MixCommonArgs void showManPage(const std::string & name); /** - * The constructor of this class starts a pager if stdout is a - * terminal and $PAGER is set. Stdout is redirected to the pager. + * The constructor of this class starts a pager if standard output is a + * terminal and $PAGER is set. Standard output is redirected to the + * pager. */ class RunPager { @@ -96,7 +97,7 @@ public: private: Pid pid; - int stdout; + int std_out; }; extern volatile ::sig_atomic_t blockInt; From c18911602eb4260d59acf8c17f1c3b4c7fcf7cee Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 31 Aug 2023 00:07:49 -0400 Subject: [PATCH 120/299] Fix `boehmgc-coroutine-sp-fallback.diff` for FreeBSD Our FreeBSD headers have `pthread_getattr_np`, but we get a link-time error that is missing. The good news is that there is another similar function which does exist, and the upstream project elsewhere does just the [fallback code] we need. As the fallback code indicates, the two functions are not identical however as the other one needs explicit initialization. NetBSD supports both in fact, and its [manpage] is therefore a good resource on what the differences are. [fallback code]: https://github.com/ivmai/bdwgc/blob/07a6d0ee8889bca5eaeadc13cabadc363725d216/os_dep.c#L1266-L1272 [manpage]: https://man.netbsd.org/pthread_attr_get_np.3 --- boehmgc-coroutine-sp-fallback.diff | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/boehmgc-coroutine-sp-fallback.diff b/boehmgc-coroutine-sp-fallback.diff index 5066d8278..2afbe9671 100644 --- a/boehmgc-coroutine-sp-fallback.diff +++ b/boehmgc-coroutine-sp-fallback.diff @@ -59,12 +59,18 @@ index b5d71e62..aed7b0bf 100644 GC_bool found_me = FALSE; size_t nthreads = 0; int i; -@@ -851,6 +853,31 @@ GC_INNER void GC_push_all_stacks(void) +@@ -851,6 +853,37 @@ GC_INNER void GC_push_all_stacks(void) hi = p->altstack + p->altstack_size; /* FIXME: Need to scan the normal stack too, but how ? */ /* FIXME: Assume stack grows down */ + } else { -+ if (pthread_getattr_np(p->id, &pattr)) { ++#ifdef HAVE_PTHREAD_ATTR_GET_NP ++ if (!pthread_attr_init(&pattr) ++ || !pthread_attr_get_np(p->id, &pattr)) ++#else /* HAVE_PTHREAD_GETATTR_NP */ ++ if (pthread_getattr_np(p->id, &pattr)) ++#endif ++ { + ABORT("GC_push_all_stacks: pthread_getattr_np failed!"); + } + if (pthread_attr_getstacksize(&pattr, &stack_limit)) { From b7acef1ceb756c72462a896d90c1b17ddc32a6df Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 20 Sep 2023 15:21:21 +0200 Subject: [PATCH 121/299] Bump version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index cf8690732..ef0f38abe 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.18.0 +2.19.0 From 883092e3f78d4efb1066a2e24e343b307035a04c Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Wed, 20 Sep 2023 09:09:01 -0700 Subject: [PATCH 122/299] Re-enable systemd-nspawn test It was disabled in c6953d1ff62fb6dc4fbd89c03e7949c552c19382 because a recent Nixpkgs bump brought in a new systemd which changed how systemd-nspawn worked. As far as I can tell, the issue was caused by this upstream systemd commit: https://github.com/systemd/systemd/commit/b71a0192c040f585397cfc6fc2ca025bf839733d Bind-mounting the host's `/sys` and `/proc` into the container's `/run/host/{sys,proc}` fixes the issue and allows the test to succeed. --- tests/nixos/containers/containers.nix | 4 ++-- tests/nixos/containers/systemd-nspawn.nix | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/nixos/containers/containers.nix b/tests/nixos/containers/containers.nix index e721be48f..c8ee78a4a 100644 --- a/tests/nixos/containers/containers.nix +++ b/tests/nixos/containers/containers.nix @@ -56,8 +56,8 @@ host.fail("nix build -v --auto-allocate-uids --no-sandbox -L --offline --impure --file ${./id-test.nix} --argstr name id-test-6 --arg uidRange true") # Run systemd-nspawn in a Nix build. - #host.succeed("nix build -v --auto-allocate-uids --sandbox -L --offline --impure --file ${./systemd-nspawn.nix} --argstr nixpkgs ${nixpkgs}") - #host.succeed("[[ $(cat ./result/msg) = 'Hello World' ]]") + host.succeed("nix build -v --auto-allocate-uids --sandbox -L --offline --impure --file ${./systemd-nspawn.nix} --argstr nixpkgs ${nixpkgs}") + host.succeed("[[ $(cat ./result/msg) = 'Hello World' ]]") ''; } diff --git a/tests/nixos/containers/systemd-nspawn.nix b/tests/nixos/containers/systemd-nspawn.nix index f54f32f2a..1dad4ebd7 100644 --- a/tests/nixos/containers/systemd-nspawn.nix +++ b/tests/nixos/containers/systemd-nspawn.nix @@ -73,6 +73,8 @@ runCommand "test" --resolv-conf=off \ --bind-ro=/nix/store \ --bind=$out \ + --bind=/proc:/run/host/proc \ + --bind=/sys:/run/host/sys \ --private-network \ $toplevel/init '' From e0e5943db28271cd6f1dcf56f3a0b12d0bc37b6c Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:51:39 +0200 Subject: [PATCH 123/299] remove IRC from links in README the community has moved away from IRC a long time ago --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 85b0902b1..46a4218d9 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ to set up a development environment and build Nix from source. - [Nix jobsets on hydra.nixos.org](https://hydra.nixos.org/project/nix) - [NixOS Discourse](https://discourse.nixos.org/) - [Matrix - #nix:nixos.org](https://matrix.to/#/#nix:nixos.org) -- [IRC - #nixos on libera.chat](irc://irc.libera.chat/#nixos) ## License From 747b2baf21f5bc3924945b02d10cd37b17e2e1bb Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:51:54 +0200 Subject: [PATCH 124/299] fix rendering error for consecutive spaces --- doc/manual/src/language/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/language/index.md b/doc/manual/src/language/index.md index 29950a52d..a26e43a05 100644 --- a/doc/manual/src/language/index.md +++ b/doc/manual/src/language/index.md @@ -83,7 +83,8 @@ This is an incomplete overview of language features, by example. - A multi-line string. Strips common prefixed whitespace. Evaluates to `"multi\n line\n string"`. + + A multi-line string. Strips common prefixed whitespace. Evaluates to `"multi\n line\n  string"`. From 954890a42f143896d5892066f7b5a0f2b120fbfe Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:52:05 +0200 Subject: [PATCH 125/299] add information on release cycle and backports --- doc/manual/src/release-notes/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/manual/src/release-notes/release-notes.md b/doc/manual/src/release-notes/release-notes.md index b05d5ee0a..dbda0babd 100644 --- a/doc/manual/src/release-notes/release-notes.md +++ b/doc/manual/src/release-notes/release-notes.md @@ -1 +1,7 @@ # Nix Release Notes + +Nix has a release cycle of roughly 6 weeks. +Notable changes and additions are announced in the release notes for each version. + +Bugfixes can be backported on request to the Nix version used in the latest NixOS release, which is announced in the [NixOS release notes](https://nixos.org/manual/nixos/stable/release-notes.html#ch-release-notes). + From 02649d247b7336697f059186ea8e9f3076dcda0a Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:52:28 +0200 Subject: [PATCH 126/299] move test coverage section to testing page --- doc/manual/src/contributing/hacking.md | 14 -------------- doc/manual/src/contributing/testing.md | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md index 4b0a3a3e5..22f629c6f 100644 --- a/doc/manual/src/contributing/hacking.md +++ b/doc/manual/src/contributing/hacking.md @@ -271,17 +271,3 @@ or inside a `nix develop` shell by running: # make internal-api-html # xdg-open ./outputs/doc/share/doc/nix/internal-api/html/index.html ``` - -## Coverage analysis - -A coverage analysis report is [available -online](https://hydra.nixos.org/job/nix/master/coverage/latest/download-by-type/report/coverage). You -can build it yourself: - -``` -# nix build .#hydraJobs.coverage -# xdg-open ./result/coverage/index.html -``` - -Metrics about the change in line/function coverage over time are also -[available](https://hydra.nixos.org/job/nix/master/coverage#tabs-charts). diff --git a/doc/manual/src/contributing/testing.md b/doc/manual/src/contributing/testing.md index cd94d5cfb..881118505 100644 --- a/doc/manual/src/contributing/testing.md +++ b/doc/manual/src/contributing/testing.md @@ -1,5 +1,19 @@ # Running tests +## Coverage analysis + +A [coverage analysis report] is available online +You can build it yourself: + +[coverage analysis report]: https://hydra.nixos.org/job/nix/master/coverage/latest/download-by-type/report/coverage + +``` +# nix build .#hydraJobs.coverage +# xdg-open ./result/coverage/index.html +``` + +[Extensive records of build metrics](https://hydra.nixos.org/job/nix/master/coverage#tabs-charts), such as test coverage over time, are also available online. + ## Unit-tests The unit-tests for each Nix library (`libexpr`, `libstore`, etc..) are defined From 4685260a77266382b5c2c2b68ff1e6e0ddc8a908 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:52:58 +0200 Subject: [PATCH 127/299] fix links to configuration settings --- src/libutil/experimental-features.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 782331283..211374bf6 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -171,7 +171,7 @@ constexpr std::array xpFeatureDetails = {{ .name = "auto-allocate-uids", .description = R"( Allows Nix to automatically pick UIDs for builds, rather than creating - `nixbld*` user accounts. See the [`auto-allocate-uids`](#conf-auto-allocate-uids) setting for details. + `nixbld*` user accounts. See the [`auto-allocate-uids`](@docroot@/command-ref/conf-file.md#conf-auto-allocate-uids) setting for details. )", }, { @@ -179,7 +179,7 @@ constexpr std::array xpFeatureDetails = {{ .name = "cgroups", .description = R"( Allows Nix to execute builds inside cgroups. See - the [`use-cgroups`](#conf-use-cgroups) setting for details. + the [`use-cgroups`](@docroot@/command-ref/conf-file.md#conf-use-cgroups) setting for details. )", }, { From 8e25450ff4bbf2b2c6951ebc78fd1bdec7097660 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:53:11 +0200 Subject: [PATCH 128/299] refer to nix.dev for installation instructions there are currently multiple places with installation instructions that all have to be updated when a change to any of them is accepted. this reduces the number of places by one, and directs beginners to the maintained and curated resource for Nix learning materials. --- README.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 85b0902b1..1440239bc 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,11 @@ Nix is a powerful package manager for Linux and other Unix systems that makes pa management reliable and reproducible. Please refer to the [Nix manual](https://nixos.org/nix/manual) for more details. -## Installation +## Installation and first steps -On Linux and macOS the easiest way to install Nix is to run the following shell command -(as a user other than root): +Visit [nix.dev](https://nix.dev) for installation instructions and beginner tutorials. -```console -$ curl -L https://nixos.org/nix/install | sh -``` - -Information on additional installation methods is available on the [Nix download page](https://nixos.org/download.html). +Full reference documentation can be found in the [Nix manual](https://nixos.org/nix/manual). ## Building And Developing From 984bd4cb0e2973ad756b6b1692919980f14f5df2 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:53:36 +0200 Subject: [PATCH 129/299] README: link to CONTRIBUTING --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 85b0902b1..3b31b4d93 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Information on additional installation methods is available on the [Nix download See our [Hacking guide](https://nixos.org/manual/nix/unstable/contributing/hacking.html) in our manual for instruction on how to to set up a development environment and build Nix from source. +## Contributing + +Check the [contributing guide](./CONTRIBUTING.md) if you want to get involved with developing Nix. + ## Additional Resources - [Nix manual](https://nixos.org/nix/manual) From cf6ba7256f849ec1fe209b18c311e61ecd7719d3 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:57:00 +0200 Subject: [PATCH 130/299] delete unused files --- doc/manual/src/command-ref/opt-common-syn.md | 57 -------------------- doc/manual/src/command-ref/opt-inst-syn.md | 15 ------ 2 files changed, 72 deletions(-) delete mode 100644 doc/manual/src/command-ref/opt-common-syn.md delete mode 100644 doc/manual/src/command-ref/opt-inst-syn.md diff --git a/doc/manual/src/command-ref/opt-common-syn.md b/doc/manual/src/command-ref/opt-common-syn.md deleted file mode 100644 index b66d318c2..000000000 --- a/doc/manual/src/command-ref/opt-common-syn.md +++ /dev/null @@ -1,57 +0,0 @@ -\--help - -\--version - -\--verbose - -\-v - -\--quiet - -\--log-format - -format - -\--no-build-output - -\-Q - -\--max-jobs - -\-j - -number - -\--cores - -number - -\--max-silent-time - -number - -\--timeout - -number - -\--keep-going - -\-k - -\--keep-failed - -\-K - -\--fallback - -\--readonly-mode - -\-I - -path - -\--option - -name - -value diff --git a/doc/manual/src/command-ref/opt-inst-syn.md b/doc/manual/src/command-ref/opt-inst-syn.md deleted file mode 100644 index 1703c40e3..000000000 --- a/doc/manual/src/command-ref/opt-inst-syn.md +++ /dev/null @@ -1,15 +0,0 @@ -\--prebuilt-only - -\-b - -\--attr - -\-A - -\--from-expression - -\-E - -\--from-profile - -path From 1a412a8d782b11b3af08be5b7ee3b4000b72cb32 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 11:38:13 +0200 Subject: [PATCH 131/299] fix typo in docstring --- src/libutil/config.hh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libutil/config.hh b/src/libutil/config.hh index cc8532587..96c2cd75d 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -36,8 +36,8 @@ namespace nix { * * std::map settings; * config.getSettings(settings); - * config["system"].description == "the current system" - * config["system"].value == "x86_64-linux" + * settings["system"].description == "the current system" + * settings["system"].value == "x86_64-linux" * * * The above retrieves all currently known settings from the `Config` object From 1b560ea502cf0308b829e4b3425307f80fc3528a Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 23:23:14 +0200 Subject: [PATCH 132/299] more detail on backports Co-authored-by: John Ericson --- doc/manual/src/release-notes/release-notes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/release-notes/release-notes.md b/doc/manual/src/release-notes/release-notes.md index dbda0babd..1fa2f5580 100644 --- a/doc/manual/src/release-notes/release-notes.md +++ b/doc/manual/src/release-notes/release-notes.md @@ -3,5 +3,9 @@ Nix has a release cycle of roughly 6 weeks. Notable changes and additions are announced in the release notes for each version. -Bugfixes can be backported on request to the Nix version used in the latest NixOS release, which is announced in the [NixOS release notes](https://nixos.org/manual/nixos/stable/release-notes.html#ch-release-notes). +Bugfixes can be backported on request to previous Nix releases. +We typically backport only as far back as the Nix version used in the latest NixOS release, which is announced in the [NixOS release notes](https://nixos.org/manual/nixos/stable/release-notes.html#ch-release-notes). + +Backports never skip releases (if a feature is backported to `x.y`, it must also be available in `x.(y+1)`; +This ensures that upgrading from an older version with backports is still safe and no backported functionality will go missing. From f264d9ff0862277523a207c7afaed4894a40dc11 Mon Sep 17 00:00:00 2001 From: Gerg-L Date: Thu, 21 Sep 2023 21:00:35 -0400 Subject: [PATCH 133/299] flake: complete update to 23.05 --- flake.lock | 12 ++++++------ flake.nix | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 75b6ae6a7..56df9c3fb 100644 --- a/flake.lock +++ b/flake.lock @@ -34,16 +34,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1695124524, - "narHash": "sha256-trXDytVCqf3KryQQQrHOZKUabu1/lB8/ndOAuZKQrOE=", - "owner": "edolstra", + "lastModified": 1695283060, + "narHash": "sha256-CJz71xhCLlRkdFUSQEL0pIAAfcnWFXMzd9vXhPrnrEg=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "a3d30b525535e3158221abc1a957ce798ab159fe", + "rev": "31ed632c692e6a36cfc18083b88ece892f863ed4", "type": "github" }, "original": { - "owner": "edolstra", - "ref": "fix-aws-sdk-cpp", + "owner": "NixOS", + "ref": "nixos-23.05-small", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index dfb2dc1f8..a7680a759 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,7 @@ { description = "The purely functional package manager"; - #inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05-small"; - inputs.nixpkgs.url = "github:edolstra/nixpkgs/fix-aws-sdk-cpp"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05-small"; inputs.nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2"; inputs.lowdown-src = { url = "github:kristapsdz/lowdown"; flake = false; }; inputs.flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; From d8cebae939932f2a5b458b5d92c82cd1a025dc0a Mon Sep 17 00:00:00 2001 From: Rasmus Rendal Date: Sat, 4 Jun 2022 12:07:08 +0200 Subject: [PATCH 134/299] Add a test for flake paths with spaces in them --- tests/flakes/common.sh | 10 +- tests/flakes/flakes.sh | 302 +++++++++++++++++++++-------------------- 2 files changed, 157 insertions(+), 155 deletions(-) diff --git a/tests/flakes/common.sh b/tests/flakes/common.sh index 427abcdde..8aed296e6 100644 --- a/tests/flakes/common.sh +++ b/tests/flakes/common.sh @@ -61,10 +61,10 @@ createGitRepo() { local repo="$1" local extraArgs="${2-}" - rm -rf $repo $repo.tmp - mkdir -p $repo + rm -rf "$repo" "$repo".tmp + mkdir -p "$repo" - git -C $repo init $extraArgs - git -C $repo config user.email "foobar@example.com" - git -C $repo config user.name "Foobar" + git -C "$repo" init $extraArgs + git -C "$repo" config user.email "foobar@example.com" + git -C "$repo" config user.name "Foobar" } diff --git a/tests/flakes/flakes.sh b/tests/flakes/flakes.sh index 128f759ea..70de28628 100644 --- a/tests/flakes/flakes.sh +++ b/tests/flakes/flakes.sh @@ -6,27 +6,29 @@ clearStore rm -rf $TEST_HOME/.cache $TEST_HOME/.config flake1Dir=$TEST_ROOT/flake1 -flake2Dir=$TEST_ROOT/flake2 -flake3Dir=$TEST_ROOT/flake3 +flake2Dir=$TEST_ROOT/flake\ 2 +percentEncodedFlake2Dir=$TEST_ROOT/flake%202 +flake3Dir=$TEST_ROOT/flake%20 +percentEncodedFlake3Dir=$TEST_ROOT/flake%2520 flake5Dir=$TEST_ROOT/flake5 flake7Dir=$TEST_ROOT/flake7 nonFlakeDir=$TEST_ROOT/nonFlake badFlakeDir=$TEST_ROOT/badFlake flakeGitBare=$TEST_ROOT/flakeGitBare -for repo in $flake1Dir $flake2Dir $flake3Dir $flake7Dir $nonFlakeDir; do +for repo in "$flake1Dir" "$flake2Dir" "$flake3Dir" "$flake7Dir" "$nonFlakeDir"; do # Give one repo a non-main initial branch. extraArgs= - if [[ $repo == $flake2Dir ]]; then + if [[ "$repo" == "$flake2Dir" ]]; then extraArgs="--initial-branch=main" fi createGitRepo "$repo" "$extraArgs" done -createSimpleGitFlake $flake1Dir +createSimpleGitFlake "$flake1Dir" -cat > $flake2Dir/flake.nix < "$flake2Dir/flake.nix" < $flake2Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/default.nix < "$flake3Dir/default.nix" < $nonFlakeDir/README.md < "$nonFlakeDir/README.md" < $flake1Dir/foo -git -C $flake1Dir add $flake1Dir/foo +echo foo > "$flake1Dir/foo" +git -C "$flake1Dir" add $flake1Dir/foo [[ $(nix flake metadata flake1 --json --refresh | jq -r .dirtyRevision) == "$hash1-dirty" ]] -echo -n '# foo' >> $flake1Dir/flake.nix -flake1OriginalCommit=$(git -C $flake1Dir rev-parse HEAD) -git -C $flake1Dir commit -a -m 'Foo' -flake1NewCommit=$(git -C $flake1Dir rev-parse HEAD) +echo -n '# foo' >> "$flake1Dir/flake.nix" +flake1OriginalCommit=$(git -C "$flake1Dir" rev-parse HEAD) +git -C "$flake1Dir" commit -a -m 'Foo' +flake1NewCommit=$(git -C "$flake1Dir" rev-parse HEAD) hash2=$(nix flake metadata flake1 --json --refresh | jq -r .revision) [[ $(nix flake metadata flake1 --json --refresh | jq -r .dirtyRevision) == "null" ]] [[ $hash1 != $hash2 ]] # Test 'nix build' on a flake. -nix build -o $TEST_ROOT/result flake1#foo -[[ -e $TEST_ROOT/result/hello ]] +nix build -o "$TEST_ROOT/result" flake1#foo +[[ -e "$TEST_ROOT/result/hello" ]] # Test packages.default. -nix build -o $TEST_ROOT/result flake1 -[[ -e $TEST_ROOT/result/hello ]] +nix build -o "$TEST_ROOT/result" flake1 +[[ -e "$TEST_ROOT/result/hello" ]] -nix build -o $TEST_ROOT/result $flake1Dir -nix build -o $TEST_ROOT/result git+file://$flake1Dir +nix build -o "$TEST_ROOT/result" "$flake1Dir" +nix build -o "$TEST_ROOT/result" "git+file://$flake1Dir" # Check that store symlinks inside a flake are not interpreted as flakes. -nix build -o $flake1Dir/result git+file://$flake1Dir -nix path-info $flake1Dir/result +nix build -o "$flake1Dir/result" "git+file://$flake1Dir" +nix path-info "$flake1Dir/result" # 'getFlake' on an unlocked flakeref should fail in pure mode, but # succeed in impure mode. -(! nix build -o $TEST_ROOT/result --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default") -nix build -o $TEST_ROOT/result --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default" --impure +(! nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default") +nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"$flake1Dir\").packages.$system.default" --impure # 'getFlake' on a locked flakeref should succeed even in pure mode. -nix build -o $TEST_ROOT/result --expr "(builtins.getFlake \"git+file://$flake1Dir?rev=$hash2\").packages.$system.default" +nix build -o "$TEST_ROOT/result" --expr "(builtins.getFlake \"git+file://$flake1Dir?rev=$hash2\").packages.$system.default" # Building a flake with an unlocked dependency should fail in pure mode. -(! nix build -o $TEST_ROOT/result flake2#bar --no-registries) -(! nix build -o $TEST_ROOT/result flake2#bar --no-use-registries) +(! nix build -o "$TEST_ROOT/result" flake2#bar --no-registries) +(! nix build -o "$TEST_ROOT/result" flake2#bar --no-use-registries) (! nix eval --expr "builtins.getFlake \"$flake2Dir\"") # But should succeed in impure mode. -(! nix build -o $TEST_ROOT/result flake2#bar --impure) -nix build -o $TEST_ROOT/result flake2#bar --impure --no-write-lock-file +(! nix build -o "$TEST_ROOT/result" flake2#bar --impure) +nix build -o "$TEST_ROOT/result" flake2#bar --impure --no-write-lock-file nix eval --expr "builtins.getFlake \"$flake2Dir\"" --impure # Building a local flake with an unlocked dependency should fail with --no-update-lock-file. -expect 1 nix build -o $TEST_ROOT/result $flake2Dir#bar --no-update-lock-file 2>&1 | grep 'requires lock file changes' +expect 1 nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --no-update-lock-file 2>&1 | grep 'requires lock file changes' # But it should succeed without that flag. -nix build -o $TEST_ROOT/result $flake2Dir#bar --no-write-lock-file -expect 1 nix build -o $TEST_ROOT/result $flake2Dir#bar --no-update-lock-file 2>&1 | grep 'requires lock file changes' -nix build -o $TEST_ROOT/result $flake2Dir#bar --commit-lock-file -[[ -e $flake2Dir/flake.lock ]] -[[ -z $(git -C $flake2Dir diff main || echo failed) ]] +nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --no-write-lock-file +expect 1 nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --no-update-lock-file 2>&1 | grep 'requires lock file changes' +nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" --commit-lock-file +[[ -e "$flake2Dir/flake.lock" ]] +[[ -z $(git -C "$flake2Dir" diff main || echo failed) ]] # Rerunning the build should not change the lockfile. -nix build -o $TEST_ROOT/result $flake2Dir#bar -[[ -z $(git -C $flake2Dir diff main || echo failed) ]] +nix build -o "$TEST_ROOT/result" "$flake2Dir#bar" +[[ -z $(git -C "$flake2Dir" diff main || echo failed) ]] # Building with a lockfile should not require a fetch of the registry. -nix build -o $TEST_ROOT/result --flake-registry file:///no-registry.json $flake2Dir#bar --refresh -nix build -o $TEST_ROOT/result --no-registries $flake2Dir#bar --refresh -nix build -o $TEST_ROOT/result --no-use-registries $flake2Dir#bar --refresh +nix build -o "$TEST_ROOT/result" --flake-registry file:///no-registry.json "$flake2Dir#bar" --refresh +nix build -o "$TEST_ROOT/result" --no-registries "$flake2Dir#bar" --refresh +nix build -o "$TEST_ROOT/result" --no-use-registries "$flake2Dir#bar" --refresh # Updating the flake should not change the lockfile. -nix flake lock $flake2Dir -[[ -z $(git -C $flake2Dir diff main || echo failed) ]] +nix flake lock "$flake2Dir" +[[ -z $(git -C "$flake2Dir" diff main || echo failed) ]] # Now we should be able to build the flake in pure mode. -nix build -o $TEST_ROOT/result flake2#bar +nix build -o "$TEST_ROOT/result" flake2#bar # Or without a registry. -nix build -o $TEST_ROOT/result --no-registries git+file://$flake2Dir#bar --refresh -nix build -o $TEST_ROOT/result --no-use-registries git+file://$flake2Dir#bar --refresh +nix build -o "$TEST_ROOT/result" --no-registries "git+file://$percentEncodedFlake2Dir#bar" --refresh +nix build -o "$TEST_ROOT/result" --no-use-registries "git+file://$percentEncodedFlake2Dir#bar" --refresh # Test whether indirect dependencies work. -nix build -o $TEST_ROOT/result $flake3Dir#xyzzy -git -C $flake3Dir add flake.lock +nix build -o "$TEST_ROOT/result" "$flake3Dir#xyzzy" +git -C "$flake3Dir" add flake.lock # Add dependency to flake3. -rm $flake3Dir/flake.nix +rm "$flake3Dir/flake.nix" -cat > $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix < $flake3Dir/flake.nix < "$flake3Dir/flake.nix" < $flake3Dir/flake.nix <' -A x) = 123 ]] [[ $(NIX_PATH=flake3=flake:flake3 nix-instantiate --eval '' -A x) = 123 ]] # Test alternate lockfile paths. -nix flake lock $flake2Dir --output-lock-file $TEST_ROOT/flake2.lock -cmp $flake2Dir/flake.lock $TEST_ROOT/flake2.lock >/dev/null # lockfiles should be identical, since we're referencing flake2's original one +nix flake lock "$flake2Dir" --output-lock-file $TEST_ROOT/flake2.lock +cmp "$flake2Dir/flake.lock" $TEST_ROOT/flake2.lock >/dev/null # lockfiles should be identical, since we're referencing flake2's original one -nix flake lock $flake2Dir --output-lock-file $TEST_ROOT/flake2-overridden.lock --override-input flake1 git+file://$flake1Dir?rev=$flake1OriginalCommit -expectStderr 1 cmp $flake2Dir/flake.lock $TEST_ROOT/flake2-overridden.lock -nix flake metadata $flake2Dir --reference-lock-file $TEST_ROOT/flake2-overridden.lock | grepQuiet $flake1OriginalCommit +nix flake lock "$flake2Dir" --output-lock-file $TEST_ROOT/flake2-overridden.lock --override-input flake1 git+file://$flake1Dir?rev=$flake1OriginalCommit +expectStderr 1 cmp "$flake2Dir/flake.lock" $TEST_ROOT/flake2-overridden.lock +nix flake metadata "$flake2Dir" --reference-lock-file $TEST_ROOT/flake2-overridden.lock | grepQuiet $flake1OriginalCommit # reference-lock-file can only be used if allow-dirty is set. -expectStderr 1 nix flake metadata $flake2Dir --no-allow-dirty --reference-lock-file $TEST_ROOT/flake2-overridden.lock +expectStderr 1 nix flake metadata "$flake2Dir" --no-allow-dirty --reference-lock-file $TEST_ROOT/flake2-overridden.lock From 50e61f579cfc8cf64031845f3e73f82593ad2ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?= Date: Wed, 31 May 2023 10:36:43 +0200 Subject: [PATCH 135/299] Allow special characters in flake paths Support using nix flakes in paths with spaces or abitrary unicode characters. This introduces the convention that the path part of the URL should be percent-encoded when dealing with `path:` urls and not when using filepaths (following the convention of firefox). Co-authored-by: Rendal --- src/libexpr/flake/flakeref.cc | 85 +++++++++++++++++++---------------- src/libutil/tests/url.cc | 9 ++++ src/libutil/url.cc | 2 +- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index e1bce90bc..2155c5f12 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -77,14 +77,6 @@ std::pair parseFlakeRefWithFragment( { using namespace fetchers; - static std::string fnRegex = "[0-9a-zA-Z-._~!$&'\"()*+,;=]+"; - - static std::regex pathUrlRegex( - "(/?" + fnRegex + "(?:/" + fnRegex + ")*/?)" - + "(?:\\?(" + queryRegex + "))?" - + "(?:#(" + queryRegex + "))?", - std::regex::ECMAScript); - static std::regex flakeRegex( "((" + flakeIdRegexS + ")(?:/(?:" + refAndOrRevRegex + "))?)" + "(?:#(" + queryRegex + "))?", @@ -92,26 +84,23 @@ std::pair parseFlakeRefWithFragment( std::smatch match; - /* Check if 'url' is a flake ID. This is an abbreviated syntax for - 'flake:?ref=&rev='. */ - - if (std::regex_match(url, match, flakeRegex)) { - auto parsedURL = ParsedURL{ - .url = url, - .base = "flake:" + match.str(1), - .scheme = "flake", - .authority = "", - .path = match[1], - }; - - return std::make_pair( - FlakeRef(Input::fromURL(parsedURL, isFlake), ""), - percentDecode(match.str(6))); - } - - else if (std::regex_match(url, match, pathUrlRegex)) { - std::string path = match[1]; - std::string fragment = percentDecode(match.str(3)); + auto parsePathFlakeRef = [&]() { + std::string path = url; + std::string fragment = ""; + std::map query = {}; + auto pathEnd = url.find_first_of("#?"); + auto fragmentStart = pathEnd; + if (pathEnd != std::string::npos && url[pathEnd] == '?') + fragmentStart = url.find("#"); + if (pathEnd != std::string::npos) { + path = url.substr(0, pathEnd); + } + if (fragmentStart != std::string::npos) { + fragment = percentDecode(url.substr(fragmentStart+1)); + } + if (fragmentStart != std::string::npos && pathEnd != std::string::npos) { + query = decodeQuery(url.substr(pathEnd+1, fragmentStart)); + } if (baseDir) { /* Check if 'url' is a path (either absolute or relative @@ -163,7 +152,7 @@ std::pair parseFlakeRefWithFragment( .scheme = "git+file", .authority = "", .path = flakeRoot, - .query = decodeQuery(match[2]), + .query = query, }; if (subdir != "") { @@ -188,7 +177,6 @@ std::pair parseFlakeRefWithFragment( } else { if (!hasPrefix(path, "/")) throw BadURL("flake reference '%s' is not an absolute path", url); - auto query = decodeQuery(match[2]); path = canonPath(path + "/" + getOr(query, "dir", "")); } @@ -197,19 +185,40 @@ std::pair parseFlakeRefWithFragment( attrs.insert_or_assign("path", path); return std::make_pair(FlakeRef(Input::fromAttrs(std::move(attrs)), ""), fragment); + }; + + /* Check if 'url' is a flake ID. This is an abbreviated syntax for + 'flake:?ref=&rev='. */ + + if (std::regex_match(url, match, flakeRegex)) { + auto parsedURL = ParsedURL{ + .url = url, + .base = "flake:" + match.str(1), + .scheme = "flake", + .authority = "", + .path = match[1], + }; + + return std::make_pair( + FlakeRef(Input::fromURL(parsedURL), ""), + percentDecode(match.str(6))); } else { - auto parsedURL = parseURL(url); - std::string fragment; - std::swap(fragment, parsedURL.fragment); + try { + auto parsedURL = parseURL(url); + std::string fragment; + std::swap(fragment, parsedURL.fragment); - auto input = Input::fromURL(parsedURL, isFlake); - input.parent = baseDir; + auto input = Input::fromURL(parsedURL, isFlake); + input.parent = baseDir; - return std::make_pair( - FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")), - fragment); + return std::make_pair( + FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")), + fragment); + } catch (BadURL &) { + return parsePathFlakeRef(); + } } } diff --git a/src/libutil/tests/url.cc b/src/libutil/tests/url.cc index a908631e6..a678dad20 100644 --- a/src/libutil/tests/url.cc +++ b/src/libutil/tests/url.cc @@ -335,4 +335,13 @@ namespace nix { ASSERT_EQ(d, s); } + TEST(percentEncode, yen) { + // https://en.wikipedia.org/wiki/Percent-encoding#Character_data + std::string s = reinterpret_cast(u8"円"); + std::string e = "%E5%86%86"; + + ASSERT_EQ(percentEncode(s), e); + ASSERT_EQ(percentDecode(e), s); + } + } diff --git a/src/libutil/url.cc b/src/libutil/url.cc index a8f7d39fd..e700c8eaf 100644 --- a/src/libutil/url.cc +++ b/src/libutil/url.cc @@ -103,7 +103,7 @@ std::string percentEncode(std::string_view s, std::string_view keep) || keep.find(c) != std::string::npos) res += c; else - res += fmt("%%%02X", (unsigned int) c); + res += fmt("%%%02X", c & 0xFF); return res; } From e8113747e1abf7fa6225f7fcd31ca9e71e03cfbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?= Date: Thu, 20 Apr 2023 06:51:47 +0200 Subject: [PATCH 136/299] Split the parseFlakeRefWithFragment function Was starting to be very complex and hard to follow. Now the different cases should be easier to understand. --- src/libexpr/flake/flakeref.cc | 279 +++++++++++++++++++--------------- 1 file changed, 155 insertions(+), 124 deletions(-) diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index 2155c5f12..f7ef19b3f 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -69,127 +69,130 @@ std::optional maybeParseFlakeRef( } } -std::pair parseFlakeRefWithFragment( +std::pair parsePathFlakeRefWithFragment( const std::string & url, const std::optional & baseDir, bool allowMissing, bool isFlake) { - using namespace fetchers; + std::string path = url; + std::string fragment = ""; + std::map query; + auto pathEnd = url.find_first_of("#?"); + auto fragmentStart = pathEnd; + if (pathEnd != std::string::npos && url[pathEnd] == '?') { + fragmentStart = url.find("#"); + } + if (pathEnd != std::string::npos) { + path = url.substr(0, pathEnd); + } + if (fragmentStart != std::string::npos) { + fragment = percentDecode(url.substr(fragmentStart+1)); + } + if (pathEnd != std::string::npos && fragmentStart != std::string::npos) { + query = decodeQuery(url.substr(pathEnd+1, fragmentStart)); + } + + if (baseDir) { + /* Check if 'url' is a path (either absolute or relative + to 'baseDir'). If so, search upward to the root of the + repo (i.e. the directory containing .git). */ + + path = absPath(path, baseDir); + + if (isFlake) { + + if (!allowMissing && !pathExists(path + "/flake.nix")){ + notice("path '%s' does not contain a 'flake.nix', searching up",path); + + // Save device to detect filesystem boundary + dev_t device = lstat(path).st_dev; + bool found = false; + while (path != "/") { + if (pathExists(path + "/flake.nix")) { + found = true; + break; + } else if (pathExists(path + "/.git")) + throw Error("path '%s' is not part of a flake (neither it nor its parent directories contain a 'flake.nix' file)", path); + else { + if (lstat(path).st_dev != device) + throw Error("unable to find a flake before encountering filesystem boundary at '%s'", path); + } + path = dirOf(path); + } + if (!found) + throw BadURL("could not find a flake.nix file"); + } + + if (!S_ISDIR(lstat(path).st_mode)) + throw BadURL("path '%s' is not a flake (because it's not a directory)", path); + + if (!allowMissing && !pathExists(path + "/flake.nix")) + throw BadURL("path '%s' is not a flake (because it doesn't contain a 'flake.nix' file)", path); + + auto flakeRoot = path; + std::string subdir; + + while (flakeRoot != "/") { + if (pathExists(flakeRoot + "/.git")) { + auto base = std::string("git+file://") + flakeRoot; + + auto parsedURL = ParsedURL{ + .url = base, // FIXME + .base = base, + .scheme = "git+file", + .authority = "", + .path = flakeRoot, + .query = query, + }; + + if (subdir != "") { + if (parsedURL.query.count("dir")) + throw Error("flake URL '%s' has an inconsistent 'dir' parameter", url); + parsedURL.query.insert_or_assign("dir", subdir); + } + + if (pathExists(flakeRoot + "/.git/shallow")) + parsedURL.query.insert_or_assign("shallow", "1"); + + return std::make_pair( + FlakeRef(fetchers::Input::fromURL(parsedURL), getOr(parsedURL.query, "dir", "")), + fragment); + } + + subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir); + flakeRoot = dirOf(flakeRoot); + } + } + + } else { + if (!hasPrefix(path, "/")) + throw BadURL("flake reference '%s' is not an absolute path", url); + path = canonPath(path + "/" + getOr(query, "dir", "")); + } + + fetchers::Attrs attrs; + attrs.insert_or_assign("type", "path"); + attrs.insert_or_assign("path", path); + + return std::make_pair(FlakeRef(fetchers::Input::fromAttrs(std::move(attrs)), ""), fragment); +}; + + +/* Check if 'url' is a flake ID. This is an abbreviated syntax for + 'flake:?ref=&rev='. */ +std::optional> parseFlakeIdRef( + const std::string & url, + bool isFlake +) +{ + std::smatch match; static std::regex flakeRegex( "((" + flakeIdRegexS + ")(?:/(?:" + refAndOrRevRegex + "))?)" + "(?:#(" + queryRegex + "))?", std::regex::ECMAScript); - std::smatch match; - - auto parsePathFlakeRef = [&]() { - std::string path = url; - std::string fragment = ""; - std::map query = {}; - auto pathEnd = url.find_first_of("#?"); - auto fragmentStart = pathEnd; - if (pathEnd != std::string::npos && url[pathEnd] == '?') - fragmentStart = url.find("#"); - if (pathEnd != std::string::npos) { - path = url.substr(0, pathEnd); - } - if (fragmentStart != std::string::npos) { - fragment = percentDecode(url.substr(fragmentStart+1)); - } - if (fragmentStart != std::string::npos && pathEnd != std::string::npos) { - query = decodeQuery(url.substr(pathEnd+1, fragmentStart)); - } - - if (baseDir) { - /* Check if 'url' is a path (either absolute or relative - to 'baseDir'). If so, search upward to the root of the - repo (i.e. the directory containing .git). */ - - path = absPath(path, baseDir); - - if (isFlake) { - - if (!allowMissing && !pathExists(path + "/flake.nix")){ - notice("path '%s' does not contain a 'flake.nix', searching up",path); - - // Save device to detect filesystem boundary - dev_t device = lstat(path).st_dev; - bool found = false; - while (path != "/") { - if (pathExists(path + "/flake.nix")) { - found = true; - break; - } else if (pathExists(path + "/.git")) - throw Error("path '%s' is not part of a flake (neither it nor its parent directories contain a 'flake.nix' file)", path); - else { - if (lstat(path).st_dev != device) - throw Error("unable to find a flake before encountering filesystem boundary at '%s'", path); - } - path = dirOf(path); - } - if (!found) - throw BadURL("could not find a flake.nix file"); - } - - if (!S_ISDIR(lstat(path).st_mode)) - throw BadURL("path '%s' is not a flake (because it's not a directory)", path); - - if (!allowMissing && !pathExists(path + "/flake.nix")) - throw BadURL("path '%s' is not a flake (because it doesn't contain a 'flake.nix' file)", path); - - auto flakeRoot = path; - std::string subdir; - - while (flakeRoot != "/") { - if (pathExists(flakeRoot + "/.git")) { - auto base = std::string("git+file://") + flakeRoot; - - auto parsedURL = ParsedURL{ - .url = base, // FIXME - .base = base, - .scheme = "git+file", - .authority = "", - .path = flakeRoot, - .query = query, - }; - - if (subdir != "") { - if (parsedURL.query.count("dir")) - throw Error("flake URL '%s' has an inconsistent 'dir' parameter", url); - parsedURL.query.insert_or_assign("dir", subdir); - } - - if (pathExists(flakeRoot + "/.git/shallow")) - parsedURL.query.insert_or_assign("shallow", "1"); - - return std::make_pair( - FlakeRef(Input::fromURL(parsedURL, isFlake), getOr(parsedURL.query, "dir", "")), - fragment); - } - - subdir = std::string(baseNameOf(flakeRoot)) + (subdir.empty() ? "" : "/" + subdir); - flakeRoot = dirOf(flakeRoot); - } - } - - } else { - if (!hasPrefix(path, "/")) - throw BadURL("flake reference '%s' is not an absolute path", url); - path = canonPath(path + "/" + getOr(query, "dir", "")); - } - - fetchers::Attrs attrs; - attrs.insert_or_assign("type", "path"); - attrs.insert_or_assign("path", path); - - return std::make_pair(FlakeRef(Input::fromAttrs(std::move(attrs)), ""), fragment); - }; - - /* Check if 'url' is a flake ID. This is an abbreviated syntax for - 'flake:?ref=&rev='. */ - if (std::regex_match(url, match, flakeRegex)) { auto parsedURL = ParsedURL{ .url = url, @@ -200,25 +203,53 @@ std::pair parseFlakeRefWithFragment( }; return std::make_pair( - FlakeRef(Input::fromURL(parsedURL), ""), + FlakeRef(fetchers::Input::fromURL(parsedURL, isFlake), ""), percentDecode(match.str(6))); } - else { - try { - auto parsedURL = parseURL(url); - std::string fragment; - std::swap(fragment, parsedURL.fragment); + return {}; +} - auto input = Input::fromURL(parsedURL, isFlake); - input.parent = baseDir; +std::optional> parseURLFlakeRef( + const std::string & url, + const std::optional & baseDir, + bool isFlake +) +{ + ParsedURL parsedURL; + try { + parsedURL = parseURL(url); + } catch (BadURL &) { + return std::nullopt; + } - return std::make_pair( - FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")), - fragment); - } catch (BadURL &) { - return parsePathFlakeRef(); - } + std::string fragment; + std::swap(fragment, parsedURL.fragment); + + auto input = fetchers::Input::fromURL(parsedURL, isFlake); + input.parent = baseDir; + + return std::make_pair( + FlakeRef(std::move(input), getOr(parsedURL.query, "dir", "")), + fragment); +} + +std::pair parseFlakeRefWithFragment( + const std::string & url, + const std::optional & baseDir, + bool allowMissing, + bool isFlake) +{ + using namespace fetchers; + + std::smatch match; + + if (auto res = parseFlakeIdRef(url, isFlake)) { + return *res; + } else if (auto res = parseURLFlakeRef(url, baseDir, isFlake)) { + return *res; + } else { + return parsePathFlakeRefWithFragment(url, baseDir, allowMissing, isFlake); } } From 34115076969f20be935ae8eaf579928ac07981e3 Mon Sep 17 00:00:00 2001 From: Rasmus Rendal Date: Tue, 14 Jun 2022 17:38:48 +0200 Subject: [PATCH 137/299] Document the percent-encoding mechanism --- doc/manual/src/release-notes/rl-next.md | 4 ++++ src/nix/flake.md | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index c869b5e2f..2467e7ccc 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -1 +1,5 @@ # Release X.Y (202?-??-??) + +- [URL flake references](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references) now support [percent-encoded](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1) characters. + +- [Path-like flake references](@docroot@/command-ref/new-cli/nix3-flake.md#path-like-syntax) now accept arbitrary unicode characters (except `#` and `?`). diff --git a/src/nix/flake.md b/src/nix/flake.md index 92f477917..1c512b315 100644 --- a/src/nix/flake.md +++ b/src/nix/flake.md @@ -67,6 +67,13 @@ inputs.nixpkgs = { }; ``` +Following [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1), +characters outside of the allowed range (i.e. are neither [reserved +character](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) or +[unreserved +characters](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3)) must be +percent-encoded. + ### Examples Here are some examples of flake references in their URL-like representation: @@ -103,10 +110,15 @@ The semantic of such a path is as follows: 2. The filesystem root (/), or 3. A folder on a different mount point. +Contrary to URL-like reference, path-like flake references can contain +arbitrary unicode characters (except `#` and `?`). + ### Examples * `.`: The flake to which the current directory belongs to. * `/home/alice/src/patchelf`: A flake in some other directory. +* `./../sub directory/with Ûñî©ôδ€`: A flake in another relative directory that + has Unicode characters in its name. ## Flake reference attributes From 39ba81a4eb7404f9a12cf0768da51c0e0e26b588 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 7 Sep 2023 22:52:57 -0400 Subject: [PATCH 138/299] Improve internal API docs for two file hashing functions Co-Authored-By: Matthew Bauer Co-Authored-By: Carlo Nucera --- src/libutil/hash.hh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index ae3ee40f4..c3aa5cd81 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -145,13 +145,17 @@ std::string printHash16or32(const Hash & hash); Hash hashString(HashType ht, std::string_view s); /** - * Compute the hash of the given file. + * Compute the hash of the given file, hashing its contents directly. + * + * (Metadata, such as the executable permission bit, is ignored.) */ Hash hashFile(HashType ht, const Path & path); /** - * Compute the hash of the given path. The hash is defined as - * (essentially) hashString(ht, dumpPath(path)). + * Compute the hash of the given path, serializing as a Nix Archive and + * then hashing that. + * + * The hash is defined as (essentially) hashString(ht, dumpPath(path)). */ typedef std::pair HashResult; HashResult hashPath(HashType ht, const Path & path, From 9d6114313b1b06a020bed230cfe7f05e6075b875 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 7 Sep 2023 22:31:19 -0400 Subject: [PATCH 139/299] Move `ParseSink` to its own header We will soon add a new implemenation so the one for NARs in `archive.cc` isn't the only one. Co-Authored-By: Matthew Bauer Co-Authored-By: Carlo Nucera --- src/libutil/archive.hh | 17 +---------------- src/libutil/fs-sink.hh | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 src/libutil/fs-sink.hh diff --git a/src/libutil/archive.hh b/src/libutil/archive.hh index 2cf164a41..3530783c1 100644 --- a/src/libutil/archive.hh +++ b/src/libutil/archive.hh @@ -3,6 +3,7 @@ #include "types.hh" #include "serialise.hh" +#include "fs-sink.hh" namespace nix { @@ -72,22 +73,6 @@ time_t dumpPathAndGetMtime(const Path & path, Sink & sink, */ void dumpString(std::string_view s, Sink & sink); -/** - * \todo Fix this API, it sucks. - */ -struct ParseSink -{ - virtual void createDirectory(const Path & path) { }; - - virtual void createRegularFile(const Path & path) { }; - virtual void closeRegularFile() { }; - virtual void isExecutable() { }; - virtual void preallocateContents(uint64_t size) { }; - virtual void receiveContents(std::string_view data) { }; - - virtual void createSymlink(const Path & path, const std::string & target) { }; -}; - /** * If the NAR archive contains a single file at top-level, then save * the contents of the file to `s`. Otherwise barf. diff --git a/src/libutil/fs-sink.hh b/src/libutil/fs-sink.hh new file mode 100644 index 000000000..ed6484e61 --- /dev/null +++ b/src/libutil/fs-sink.hh @@ -0,0 +1,25 @@ +#pragma once +///@file + +#include "types.hh" +#include "serialise.hh" + +namespace nix { + +/** + * \todo Fix this API, it sucks. + */ +struct ParseSink +{ + virtual void createDirectory(const Path & path) { }; + + virtual void createRegularFile(const Path & path) { }; + virtual void closeRegularFile() { }; + virtual void isExecutable() { }; + virtual void preallocateContents(uint64_t size) { }; + virtual void receiveContents(std::string_view data) { }; + + virtual void createSymlink(const Path & path, const std::string & target) { }; +}; + +} From 8a416e819c9727c2e9efd70f1d2a4b0bfda11663 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 7 Sep 2023 22:49:01 -0400 Subject: [PATCH 140/299] Move `RestoreSink` to `fs-sink.cc` Co-Authored-By: Matthew Bauer Co-Authored-By: Carlo Nucera --- src/libutil/archive.cc | 67 ---------------------------------- src/libutil/fs-sink.cc | 82 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 67 deletions(-) create mode 100644 src/libutil/fs-sink.cc diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 268a798d9..0f0cae7c0 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -27,8 +27,6 @@ struct ArchiveSettings : Config #endif "use-case-hack", "Whether to enable a Darwin-specific hack for dealing with file name collisions."}; - Setting preallocateContents{this, false, "preallocate-contents", - "Whether to preallocate files when writing objects with known size."}; }; static ArchiveSettings archiveSettings; @@ -302,71 +300,6 @@ void parseDump(ParseSink & sink, Source & source) } -struct RestoreSink : ParseSink -{ - Path dstPath; - AutoCloseFD fd; - - void createDirectory(const Path & path) override - { - Path p = dstPath + path; - if (mkdir(p.c_str(), 0777) == -1) - throw SysError("creating directory '%1%'", p); - }; - - void createRegularFile(const Path & path) override - { - Path p = dstPath + path; - fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666); - if (!fd) throw SysError("creating file '%1%'", p); - } - - void closeRegularFile() override - { - /* Call close explicitly to make sure the error is checked */ - fd.close(); - } - - void isExecutable() override - { - struct stat st; - if (fstat(fd.get(), &st) == -1) - throw SysError("fstat"); - if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1) - throw SysError("fchmod"); - } - - void preallocateContents(uint64_t len) override - { - if (!archiveSettings.preallocateContents) - return; - -#if HAVE_POSIX_FALLOCATE - if (len) { - errno = posix_fallocate(fd.get(), 0, len); - /* Note that EINVAL may indicate that the underlying - filesystem doesn't support preallocation (e.g. on - OpenSolaris). Since preallocation is just an - optimisation, ignore it. */ - if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS) - throw SysError("preallocating file of %1% bytes", len); - } -#endif - } - - void receiveContents(std::string_view data) override - { - writeFull(fd.get(), data); - } - - void createSymlink(const Path & path, const std::string & target) override - { - Path p = dstPath + path; - nix::createSymlink(target, p); - } -}; - - void restorePath(const Path & path, Source & source) { RestoreSink sink; diff --git a/src/libutil/fs-sink.cc b/src/libutil/fs-sink.cc new file mode 100644 index 000000000..fcffd3216 --- /dev/null +++ b/src/libutil/fs-sink.cc @@ -0,0 +1,82 @@ +#include + +#include "config.hh" +#include "fs-sink.hh" + +namespace nix { + +struct RestoreSinkSettings : Config +{ + Setting preallocateContents{this, false, "preallocate-contents", + "Whether to preallocate files when writing objects with known size."}; +}; + +static RestoreSinkSettings restoreSinkSettings; + +static GlobalConfig::Register r1(&restoreSinkSettings); + +struct RestoreSink : ParseSink +{ + Path dstPath; + AutoCloseFD fd; + + void createDirectory(const Path & path) override + { + Path p = dstPath + path; + if (mkdir(p.c_str(), 0777) == -1) + throw SysError("creating directory '%1%'", p); + }; + + void createRegularFile(const Path & path) override + { + Path p = dstPath + path; + fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666); + if (!fd) throw SysError("creating file '%1%'", p); + } + + void closeRegularFile() override + { + /* Call close explicitly to make sure the error is checked */ + fd.close(); + } + + void isExecutable() override + { + struct stat st; + if (fstat(fd.get(), &st) == -1) + throw SysError("fstat"); + if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1) + throw SysError("fchmod"); + } + + void preallocateContents(uint64_t len) override + { + if (!archiveSettings.preallocateContents) + return; + +#if HAVE_POSIX_FALLOCATE + if (len) { + errno = posix_fallocate(fd.get(), 0, len); + /* Note that EINVAL may indicate that the underlying + filesystem doesn't support preallocation (e.g. on + OpenSolaris). Since preallocation is just an + optimisation, ignore it. */ + if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS) + throw SysError("preallocating file of %1% bytes", len); + } +#endif + } + + void receiveContents(std::string_view data) override + { + writeFull(fd.get(), data); + } + + void createSymlink(const Path & path, const std::string & target) override + { + Path p = dstPath + path; + nix::createSymlink(target, p); + } +}; + +} From f2e201fbdb93925b9588f78a03d67fcd91c74b65 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 7 Sep 2023 22:49:01 -0400 Subject: [PATCH 141/299] Expose `RestoreSink` in header (`fs-sink.hh`) Co-Authored-By: Matthew Bauer Co-Authored-By: Carlo Nucera --- src/libutil/fs-sink.cc | 117 ++++++++++++++++++++--------------------- src/libutil/fs-sink.hh | 17 ++++++ 2 files changed, 73 insertions(+), 61 deletions(-) diff --git a/src/libutil/fs-sink.cc b/src/libutil/fs-sink.cc index fcffd3216..a08a723a4 100644 --- a/src/libutil/fs-sink.cc +++ b/src/libutil/fs-sink.cc @@ -15,68 +15,63 @@ static RestoreSinkSettings restoreSinkSettings; static GlobalConfig::Register r1(&restoreSinkSettings); -struct RestoreSink : ParseSink + +void RestoreSink::createDirectory(const Path & path) { - Path dstPath; - AutoCloseFD fd; - - void createDirectory(const Path & path) override - { - Path p = dstPath + path; - if (mkdir(p.c_str(), 0777) == -1) - throw SysError("creating directory '%1%'", p); - }; - - void createRegularFile(const Path & path) override - { - Path p = dstPath + path; - fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666); - if (!fd) throw SysError("creating file '%1%'", p); - } - - void closeRegularFile() override - { - /* Call close explicitly to make sure the error is checked */ - fd.close(); - } - - void isExecutable() override - { - struct stat st; - if (fstat(fd.get(), &st) == -1) - throw SysError("fstat"); - if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1) - throw SysError("fchmod"); - } - - void preallocateContents(uint64_t len) override - { - if (!archiveSettings.preallocateContents) - return; - -#if HAVE_POSIX_FALLOCATE - if (len) { - errno = posix_fallocate(fd.get(), 0, len); - /* Note that EINVAL may indicate that the underlying - filesystem doesn't support preallocation (e.g. on - OpenSolaris). Since preallocation is just an - optimisation, ignore it. */ - if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS) - throw SysError("preallocating file of %1% bytes", len); - } -#endif - } - - void receiveContents(std::string_view data) override - { - writeFull(fd.get(), data); - } - - void createSymlink(const Path & path, const std::string & target) override - { - Path p = dstPath + path; - nix::createSymlink(target, p); - } + Path p = dstPath + path; + if (mkdir(p.c_str(), 0777) == -1) + throw SysError("creating directory '%1%'", p); }; +void RestoreSink::createRegularFile(const Path & path) +{ + Path p = dstPath + path; + fd = open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666); + if (!fd) throw SysError("creating file '%1%'", p); +} + +void RestoreSink::closeRegularFile() +{ + /* Call close explicitly to make sure the error is checked */ + fd.close(); +} + +void RestoreSink::isExecutable() +{ + struct stat st; + if (fstat(fd.get(), &st) == -1) + throw SysError("fstat"); + if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1) + throw SysError("fchmod"); +} + +void RestoreSink::preallocateContents(uint64_t len) +{ + if (!restoreSinkSettings.preallocateContents) + return; + +#if HAVE_POSIX_FALLOCATE + if (len) { + errno = posix_fallocate(fd.get(), 0, len); + /* Note that EINVAL may indicate that the underlying + filesystem doesn't support preallocation (e.g. on + OpenSolaris). Since preallocation is just an + optimisation, ignore it. */ + if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS) + throw SysError("preallocating file of %1% bytes", len); + } +#endif +} + +void RestoreSink::receiveContents(std::string_view data) +{ + writeFull(fd.get(), data); +} + +void RestoreSink::createSymlink(const Path & path, const std::string & target) +{ + Path p = dstPath + path; + nix::createSymlink(target, p); +} + } diff --git a/src/libutil/fs-sink.hh b/src/libutil/fs-sink.hh index ed6484e61..6837e2fc4 100644 --- a/src/libutil/fs-sink.hh +++ b/src/libutil/fs-sink.hh @@ -22,4 +22,21 @@ struct ParseSink virtual void createSymlink(const Path & path, const std::string & target) { }; }; +struct RestoreSink : ParseSink +{ + Path dstPath; + AutoCloseFD fd; + + + void createDirectory(const Path & path) override; + + void createRegularFile(const Path & path) override; + void closeRegularFile() override; + void isExecutable() override; + void preallocateContents(uint64_t size) override; + void receiveContents(std::string_view data) override; + + void createSymlink(const Path & path, const std::string & target) override; +}; + } From 694810ba34a74226eba22daf5c6264b9d81e2926 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 22 Sep 2023 23:54:04 -0400 Subject: [PATCH 142/299] doc: `showOptions`: Move union to caller `showOptions` itself doesn't care, so it shouldn't take two separate arguments. --- doc/manual/generate-manpage.nix | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 87891fa7a..b85c488c9 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -78,16 +78,15 @@ let maybeOptions = optionalString (details.flags != {}) '' # Options - ${showOptions details.flags toplevel.flags} + ${showOptions (details.flags // toplevel.flags)} > **Note** > > See [`man nix.conf`](@docroot@/command-ref/conf-file.md#command-line-flags) for overriding configuration settings with command line flags. ''; - showOptions = options: commonOptions: + showOptions = allOptions: let - allOptions = options // commonOptions; showCategory = cat: '' ${optionalString (cat != "") "**${cat}:**"} From 9c640c122931aa6515e1f3c80cb4d415a1352cc7 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 23 Sep 2023 00:28:16 -0400 Subject: [PATCH 143/299] doc: `showOptions`: Simplify code with `builtins.groupBy` This makes grouping options by category much nicer. No behavior should be changed. --- doc/manual/generate-manpage.nix | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index b85c488c9..e18810594 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -1,8 +1,8 @@ let inherit (builtins) - attrNames attrValues fromJSON listToAttrs mapAttrs + attrNames attrValues fromJSON listToAttrs mapAttrs groupBy concatStringsSep concatMap length lessThan replaceStrings sort; - inherit (import ./utils.nix) concatStrings optionalString filterAttrs trim squash unique showSettings; + inherit (import ./utils.nix) attrsToList concatStrings optionalString filterAttrs trim squash unique showSettings; in commandDump: @@ -87,12 +87,11 @@ let showOptions = allOptions: let - showCategory = cat: '' + showCategory = cat: opts: '' ${optionalString (cat != "") "**${cat}:**"} - ${listOptions (filterAttrs (n: v: v.category == cat) allOptions)} + ${concatStringsSep "\n" (attrValues (mapAttrs showOption opts))} ''; - listOptions = opts: concatStringsSep "\n" (attrValues (mapAttrs showOption opts)); showOption = name: option: let shortName = optionalString @@ -106,8 +105,12 @@ let ${option.description} ''; - categories = sort lessThan (unique (map (cmd: cmd.category) (attrValues allOptions))); - in concatStrings (map showCategory categories); + categories = mapAttrs + (_: listToAttrs) + (groupBy + (cmd: cmd.value.category) + (attrsToList allOptions)); + in concatStrings (attrValues (mapAttrs showCategory categories)); in squash result; appendName = filename: name: (if filename == "nix" then "nix3" else filename) + "-" + name; From 1d9fd3a6f8ac53be4ba7d409defe291c0dbdf97a Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 22 Sep 2023 23:55:06 -0400 Subject: [PATCH 144/299] manual / manpages: Adjust option filter filtering, move from C++ to Nix Behavior change: Before we only showed uption if the command-specific options were non-empty. But that is somewhat odd since we also show common options. Now, we do everything based on the union of both sorts of options (with hidden-categories filtered, as before). Implementation change: The JSON dumping once again includes all options; the filtering of hidden categories is done in the Nix instead. This is better separation of "content" vs "presentation", and prepare the way for the HTML manual vs manpages / `--help` doing different things. --- doc/manual/generate-manpage.nix | 8 ++++++-- src/libutil/args.cc | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index e18810594..b41730bc2 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -75,10 +75,14 @@ let (details ? doc) (replaceStrings ["@stores@"] [storeDocs] details.doc); - maybeOptions = optionalString (details.flags != {}) '' + maybeOptions = let + allVisibleOptions = filterAttrs + (_: o: ! o.hiddenCategory) + (details.flags // toplevel.flags); + in optionalString (allVisibleOptions != {}) '' # Options - ${showOptions (details.flags // toplevel.flags)} + ${showOptions allVisibleOptions} > **Note** > diff --git a/src/libutil/args.cc b/src/libutil/args.cc index 8db293762..e410c7eec 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -236,7 +236,7 @@ nlohmann::json Args::toJSON() for (auto & [name, flag] : longFlags) { auto j = nlohmann::json::object(); - if (hiddenCategories.count(flag->category)) continue; + j["hiddenCategory"] = hiddenCategories.count(flag->category) > 0; if (flag->aliases.count(name)) continue; if (flag->shortName) j["shortName"] = std::string(1, flag->shortName); From 9f93972c4d73abdcde789112229209af2cbda520 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sat, 23 Sep 2023 00:35:03 -0400 Subject: [PATCH 145/299] manual / manpages: Make option category names a proper subheader Before they were an "ad-hoc" header with bold and a colon; now they are a proper subheader. For the man pages, this doesn't make much of a difference, but it will help more on for the HTML manual, where things can be restyled. Again, good separation of content vs presentation. --- doc/manual/generate-manpage.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index b41730bc2..21a4802e5 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -92,7 +92,7 @@ let showOptions = allOptions: let showCategory = cat: opts: '' - ${optionalString (cat != "") "**${cat}:**"} + ${optionalString (cat != "") "## ${cat}"} ${concatStringsSep "\n" (attrValues (mapAttrs showOption opts))} ''; From e4b83fbfe2e08ddd2ebd409c26f2dc4c11ee1d4a Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Sun, 24 Sep 2023 14:54:41 +0800 Subject: [PATCH 146/299] libexpr: const rvalue reference -> value for nix::Expr nodes --- src/libexpr/nixexpr.hh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index 5ca3d1fa6..944977e50 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -238,7 +238,7 @@ struct ExprSelect : Expr PosIdx pos; Expr * e, * def; AttrPath attrPath; - ExprSelect(const PosIdx & pos, Expr * e, const AttrPath && attrPath, Expr * def) : pos(pos), e(e), def(def), attrPath(std::move(attrPath)) { }; + ExprSelect(const PosIdx & pos, Expr * e, AttrPath attrPath, Expr * def) : pos(pos), e(e), def(def), attrPath(std::move(attrPath)) { }; ExprSelect(const PosIdx & pos, Expr * e, Symbol name) : pos(pos), e(e), def(0) { attrPath.push_back(AttrName(name)); }; PosIdx getPos() const override { return pos; } COMMON_METHODS @@ -248,7 +248,7 @@ struct ExprOpHasAttr : Expr { Expr * e; AttrPath attrPath; - ExprOpHasAttr(Expr * e, const AttrPath && attrPath) : e(e), attrPath(std::move(attrPath)) { }; + ExprOpHasAttr(Expr * e, AttrPath attrPath) : e(e), attrPath(std::move(attrPath)) { }; PosIdx getPos() const override { return e->getPos(); } COMMON_METHODS }; From 89e5e68799132bc2f1d2551002dab84b63fde9ac Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Sun, 24 Sep 2023 20:28:34 +0200 Subject: [PATCH 147/299] doc/hacking: fix `make` target to build the docs (#9033) Was confused why `make html` didn't work while working on #9032, but then I realized that after this section was written, the target was renamed to `manual-html` in 6910f5dcb6784360589b860b8f80487a5c97fe08. --- doc/manual/src/contributing/hacking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md index 22f629c6f..a2446b53a 100644 --- a/doc/manual/src/contributing/hacking.md +++ b/doc/manual/src/contributing/hacking.md @@ -228,7 +228,7 @@ This happens late in the process, so `nix build` is not suitable for iterating. To build the manual incrementally, run: ```console -make html -j $NIX_BUILD_CORES +make manual-html -j $NIX_BUILD_CORES ``` In order to reflect changes to the [Makefile], clear all generated files before re-building: From b3433099d4ef3fbc2ef86f97cf19dcd543bd05e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?= <7226587+thufschmitt@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:56:49 +0200 Subject: [PATCH 148/299] Apply suggestions from code review Co-authored-by: Valentin Gagarin --- src/nix/flake.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/nix/flake.md b/src/nix/flake.md index 1c512b315..939269c6a 100644 --- a/src/nix/flake.md +++ b/src/nix/flake.md @@ -68,11 +68,9 @@ inputs.nixpkgs = { ``` Following [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1), -characters outside of the allowed range (i.e. are neither [reserved -character](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) or -[unreserved -characters](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3)) must be -percent-encoded. +characters outside of the allowed range (i.e. neither [reserved characters](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) +nor [unreserved characters](https://datatracker.ietf.org/doc/html/rfc3986#section-2.3)) +must be percent-encoded. ### Examples @@ -110,8 +108,7 @@ The semantic of such a path is as follows: 2. The filesystem root (/), or 3. A folder on a different mount point. -Contrary to URL-like reference, path-like flake references can contain -arbitrary unicode characters (except `#` and `?`). +Contrary to URL-like references, path-like flake references can contain arbitrary unicode characters (except `#` and `?`). ### Examples From 4606a07bb62c650d2261f2d8d5e93e547acf0af6 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 25 Sep 2023 08:20:39 -0400 Subject: [PATCH 149/299] generate-manpage.nix: Add comment explaining one bit --- doc/manual/generate-manpage.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 21a4802e5..41725aed6 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -110,6 +110,7 @@ let ${option.description} ''; categories = mapAttrs + # Convert each group from a list of key-value pairs back to an attrset (_: listToAttrs) (groupBy (cmd: cmd.value.category) From 70b5e6050cd135b75942f731ad892e4f473c6c21 Mon Sep 17 00:00:00 2001 From: waalge <47293755+waalge@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:39:11 +0000 Subject: [PATCH 150/299] fix docstring --- src/libexpr/primops.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index cd9a05bb2..0d503ae26 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -2991,7 +2991,7 @@ static RegisterPrimOp primop_tail({ .name = "__tail", .args = {"list"}, .doc = R"( - Return the second to last elements of a list; abort evaluation if + Return the list without its first item; abort evaluation if the argument isn’t a list or is an empty list. > **Warning** From ad213103d85d22b52c608362cb2127fa10e49620 Mon Sep 17 00:00:00 2001 From: Sergei Trofimovich Date: Mon, 25 Sep 2023 17:45:13 +0100 Subject: [PATCH 151/299] src/libstore/profiles.cc: fix comparison of sign difference Detected by `gcc` as: CXX src/libstore/profiles.o src/libstore/profiles.cc: In function 'void nix::deleteGenerationsGreaterThan(const Path&, GenerationNumber, bool)': src/libstore/profiles.cc:186:50: warning: comparison of integer expressions of different signedness: 'int' and 'nix::GenerationNumber' {aka 'long unsigned int'} [-Wsign-compare] 186 | for (auto keep = 0; i != gens.rend() && keep < max; ++i, ++keep); | ~~~~~^~~~~ --- src/libstore/profiles.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libstore/profiles.cc b/src/libstore/profiles.cc index 4e9955948..239047dd6 100644 --- a/src/libstore/profiles.cc +++ b/src/libstore/profiles.cc @@ -183,7 +183,7 @@ void deleteGenerationsGreaterThan(const Path & profile, GenerationNumber max, bo iterDropUntil(gens, i, [&](auto & g) { return g.number == curGen; }); // Skip over `max` generations, preserving them - for (auto keep = 0; i != gens.rend() && keep < max; ++i, ++keep); + for (GenerationNumber keep = 0; i != gens.rend() && keep < max; ++i, ++keep); // Delete the rest for (; i != gens.rend(); ++i) From bd24176ac57b9d418b4ec51448b188cb74b87945 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 25 Sep 2023 17:51:17 +0100 Subject: [PATCH 152/299] libexpr/nixexpr.hh: Remove redundant inline This is redundant since definitions in C++ record are implicitly inline-ed. Co-authored-by: Yingchi Long --- src/libexpr/nixexpr.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index 17b2d1136..5c9242759 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -156,7 +156,7 @@ std::string showAttrPath(const SymbolTable & symbols, const AttrPath & attrPath) struct Expr { static unsigned long nrExprs; - inline Expr() { + Expr() { nrExprs++; } virtual ~Expr() { }; From cba53b3a18c2758e3d969d9d5781294560afe1b7 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 00:39:11 +0200 Subject: [PATCH 153/299] reformat for readability --- doc/manual/src/command-ref/nix-store/realise.md | 8 ++++++-- doc/manual/src/glossary.md | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/doc/manual/src/command-ref/nix-store/realise.md b/doc/manual/src/command-ref/nix-store/realise.md index a421a4220..5428d57fa 100644 --- a/doc/manual/src/command-ref/nix-store/realise.md +++ b/doc/manual/src/command-ref/nix-store/realise.md @@ -15,8 +15,12 @@ Each of *paths* is processed as follows: 1. If it is not [valid], substitute the store derivation file itself. 2. Realise its [output paths]: - Try to fetch from [substituters] the [store objects] associated with the output paths in the store derivation's [closure]. - - With [content-addressed derivations] (experimental): Determine the output paths to realise by querying content-addressed realisation entries in the [Nix database]. - - For any store paths that cannot be substituted, produce the required store objects. This involves first realising all outputs of the derivation's dependencies and then running the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable. + - With [content-addressed derivations] (experimental): + Determine the output paths to realise by querying content-addressed realisation entries in the [Nix database]. + - For any store paths that cannot be substituted, produce the required store objects: + 1. Realise all outputs of the derivation's dependencies + 2. Run the derivation's [`builder`](@docroot@/language/derivations.md#attr-builder) executable + - Otherwise, and if the path is not already valid: Try to fetch the associated [store objects] in the path's [closure] from [substituters]. If no substitutes are available and no store derivation is given, realisation fails. diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index b7c340d36..5345f2deb 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -33,11 +33,15 @@ Ensure a [store path] is [valid][validity]. - This means either running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation], or fetching a pre-built [store object] from a [substituter], or delegating to a [remote builder](@docroot@/advanced-topics/distributed-builds.html) and retrieving the outputs. + This can be achieved by: + - Fetching a pre-built [store object] from a [substituter] + - Running the [`builder`](@docroot@/language/derivations.md#attr-builder) executable as specified in the corresponding [derivation] + - Delegating to a [remote builder](@docroot@/advanced-topics/distributed-builds.html) and retrieving the outputs + - See [`nix-build`](./command-ref/nix-build.md) and [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md). + See [`nix-store --realise`](@docroot@/command-ref/nix-store/realise.md) for a detailed description of the algorithm. - See [`nix build`](./command-ref/new-cli/nix3-build.md) (experimental). + See also [`nix-build`](./command-ref/nix-build.md) and [`nix build`](./command-ref/new-cli/nix3-build.md) (experimental). [realise]: #gloss-realise From 541890463d83d1a818dfabc79053d1302bd828a2 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 18 Jul 2023 13:49:59 +0200 Subject: [PATCH 154/299] make separate section for builder execution --- doc/manual/src/language/derivations.md | 116 +++++++++++++------------ 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 79a09122a..df4673cf5 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -93,69 +93,71 @@ wrapper around `derivation` that adds a default value for `system` and always uses Bash as the builder, to which the supplied builder is passed as a command-line argument. See the Nixpkgs manual for details. -The builder is executed as follows: +## Builder execution - - A temporary directory is created under the directory specified by - `TMPDIR` (default `/tmp`) where the build will take place. The - current directory is changed to this directory. +The [`builder`](#attr-builder) is executed as follows: - - The environment is cleared and set to the derivation attributes, as - specified above. +- A temporary directory is created under the directory specified by + `TMPDIR` (default `/tmp`) where the build will take place. The + current directory is changed to this directory. - - In addition, the following variables are set: - - - `NIX_BUILD_TOP` contains the path of the temporary directory for - this build. - - - Also, `TMPDIR`, `TEMPDIR`, `TMP`, `TEMP` are set to point to the - temporary directory. This is to prevent the builder from - accidentally writing temporary files anywhere else. Doing so - might cause interference by other processes. - - - `PATH` is set to `/path-not-set` to prevent shells from - initialising it to their built-in default value. - - - `HOME` is set to `/homeless-shelter` to prevent programs from - using `/etc/passwd` or the like to find the user's home - directory, which could cause impurity. Usually, when `HOME` is - set, it is used as the location of the home directory, even if - it points to a non-existent path. - - - `NIX_STORE` is set to the path of the top-level Nix store - directory (typically, `/nix/store`). - - - For each output declared in `outputs`, the corresponding - environment variable is set to point to the intended path in the - Nix store for that output. Each output path is a concatenation - of the cryptographic hash of all build inputs, the `name` - attribute and the output name. (The output name is omitted if - it’s `out`.) +- The environment is cleared and set to the derivation attributes, as + specified above. - - If an output path already exists, it is removed. Also, locks are - acquired to prevent multiple Nix instances from performing the same - build at the same time. +- In addition, the following variables are set: - - A log of the combined standard output and error is written to - `/nix/var/log/nix`. + - `NIX_BUILD_TOP` contains the path of the temporary directory for + this build. - - The builder is executed with the arguments specified by the - attribute `args`. If it exits with exit code 0, it is considered to - have succeeded. + - Also, `TMPDIR`, `TEMPDIR`, `TMP`, `TEMP` are set to point to the + temporary directory. This is to prevent the builder from + accidentally writing temporary files anywhere else. Doing so + might cause interference by other processes. - - The temporary directory is removed (unless the `-K` option was - specified). + - `PATH` is set to `/path-not-set` to prevent shells from + initialising it to their built-in default value. - - If the build was successful, Nix scans each output path for - references to input paths by looking for the hash parts of the input - paths. Since these are potential runtime dependencies, Nix registers - them as dependencies of the output paths. + - `HOME` is set to `/homeless-shelter` to prevent programs from + using `/etc/passwd` or the like to find the user's home + directory, which could cause impurity. Usually, when `HOME` is + set, it is used as the location of the home directory, even if + it points to a non-existent path. - - After the build, Nix sets the last-modified timestamp on all files - in the build result to 1 (00:00:01 1/1/1970 UTC), sets the group to - the default group, and sets the mode of the file to 0444 or 0555 - (i.e., read-only, with execute permission enabled if the file was - originally executable). Note that possible `setuid` and `setgid` - bits are cleared. Setuid and setgid programs are not currently - supported by Nix. This is because the Nix archives used in - deployment have no concept of ownership information, and because it - makes the build result dependent on the user performing the build. + - `NIX_STORE` is set to the path of the top-level Nix store + directory (typically, `/nix/store`). + + - For each output declared in `outputs`, the corresponding + environment variable is set to point to the intended path in the + Nix store for that output. Each output path is a concatenation + of the cryptographic hash of all build inputs, the `name` + attribute and the output name. (The output name is omitted if + it’s `out`.) + +- If an output path already exists, it is removed. Also, locks are + acquired to prevent multiple Nix instances from performing the same + build at the same time. + +- A log of the combined standard output and error is written to + `/nix/var/log/nix`. + +- The builder is executed with the arguments specified by the + attribute `args`. If it exits with exit code 0, it is considered to + have succeeded. + +- The temporary directory is removed (unless the `-K` option was + specified). + +- If the build was successful, Nix scans each output path for + references to input paths by looking for the hash parts of the input + paths. Since these are potential runtime dependencies, Nix registers + them as dependencies of the output paths. + +- After the build, Nix sets the last-modified timestamp on all files + in the build result to 1 (00:00:01 1/1/1970 UTC), sets the group to + the default group, and sets the mode of the file to 0444 or 0555 + (i.e., read-only, with execute permission enabled if the file was + originally executable). Note that possible `setuid` and `setgid` + bits are cleared. Setuid and setgid programs are not currently + supported by Nix. This is because the Nix archives used in + deployment have no concept of ownership information, and because it + makes the build result dependent on the user performing the build. From e2f118efeda5c0a9b1380db736eb2b86f691b1fa Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 18 Jul 2023 14:03:17 +0200 Subject: [PATCH 155/299] restructure and reword input attributes section on `derivation` --- doc/manual/src/language/derivations.md | 208 +++++++++++++++---------- 1 file changed, 123 insertions(+), 85 deletions(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index df4673cf5..56c6818ca 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -1,97 +1,135 @@ # Derivations -The most important built-in function is `derivation`, which is used to -describe a single derivation (a build task). It takes as input a set, -the attributes of which specify the inputs of the build. +The most important built-in function is `derivation`, which is used to describe a single derivation: +a build task to run a process on precisely defined input files and repeatably produce output files at uniquely determined file system paths. - - There must be an attribute named [`system`]{#attr-system} whose value must be a - string specifying a Nix system type, such as `"i686-linux"` or - `"x86_64-darwin"`. (To figure out your system type, run `nix -vv - --version`.) The build can only be performed on a machine and - operating system matching the system type. (Nix can automatically - [forward builds for other - platforms](../advanced-topics/distributed-builds.md) by forwarding - them to other machines.) +It takes as input an attribute set, the attributes of which specify the inputs to the build. +It outputs an attribute set, and produces a [store derivation](@docroot@/glossary.md#gloss-store-derivation) as a side effect of evaluation. - - There must be an attribute named `name` whose value must be a - string. This is used as a symbolic name for the package by - `nix-env`, and it is appended to the output paths of the derivation. + - - There must be an attribute named [`builder`]{#attr-builder} that identifies the - program that is executed to perform the build. It can be either a - derivation or a source (a local file reference, e.g., - `./builder.sh`). +## Input attributes - - Every attribute is passed as an environment variable to the builder. - Attribute values are translated to environment variables as follows: - - - Strings and numbers are just passed verbatim. - - - A *path* (e.g., `../foo/sources.tar`) causes the referenced file - to be copied to the store; its location in the store is put in - the environment variable. The idea is that all sources should - reside in the Nix store, since all inputs to a derivation should - reside in the Nix store. - - - A *derivation* causes that derivation to be built prior to the - present derivation; its default output path is put in the - environment variable. - - - Lists of the previous types are also allowed. They are simply - concatenated, separated by spaces. - - - `true` is passed as the string `1`, `false` and `null` are - passed as an empty string. +### Required - - The optional attribute `args` specifies command-line arguments to be - passed to the builder. It should be a list. +- [`name`]{#attr-name} ([String](@docroot@/language/values.md#type-string)) - - The optional attribute `outputs` specifies a list of symbolic - outputs of the derivation. By default, a derivation produces a - single output path, denoted as `out`. However, derivations can - produce multiple output paths. This is useful because it allows - outputs to be downloaded or garbage-collected separately. For - instance, imagine a library package that provides a dynamic library, - header files, and documentation. A program that links against the - library doesn’t need the header files and documentation at runtime, - and it doesn’t need the documentation at build time. Thus, the - library package could specify: - - ```nix - outputs = [ "lib" "headers" "doc" ]; - ``` - - This will cause Nix to pass environment variables `lib`, `headers` - and `doc` to the builder containing the intended store paths of each - output. The builder would typically do something like - - ```bash - ./configure \ - --libdir=$lib/lib \ - --includedir=$headers/include \ - --docdir=$doc/share/doc - ``` - - for an Autoconf-style package. You can refer to each output of a - derivation by selecting it as an attribute, e.g. - - ```nix - buildInputs = [ pkg.lib pkg.headers ]; - ``` - - The first element of `outputs` determines the *default output*. - Thus, you could also write - - ```nix - buildInputs = [ pkg pkg.headers ]; - ``` - - since `pkg` is equivalent to `pkg.lib`. + Symbolic name for the derivation. + It is appended to the [store paths](@docroot@/glossary.md#gloss-store-path) of the resulting store derivation and its [output paths][output path]. -The function `mkDerivation` in the Nixpkgs standard environment is a -wrapper around `derivation` that adds a default value for `system` and -always uses Bash as the builder, to which the supplied builder is passed -as a command-line argument. See the Nixpkgs manual for details. + Example: `name = "hello";` + +- [`system`]{#attr-system} ([String](@docroot@/language/values.md#type-string)) + + The system type on which the [`builder`](#attr-builder) executable can be run. + + Nix will only build derivations where the `system` attribute matches the current [`system` configuration option]. + It can automatically [build on other platforms](../advanced-topics/distributed-builds.md) by forwarding build requests to other machines. + + Examples: + + `system = "x86_64-linux";` + + `system = builtins.currentSystem;` + + [`builtins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) has the value of the [`system` configuration option], and defaults to the system type of the current Nix installation. + + [`system` configuration option]: @docroot@/command-ref/conf-file.md#conf-system + +- [`builder`]{#attr-builder} ([Path](@docroot@/language/values.md#type-path) | [String](@docroot@/language/values.md#type-string)) + + Path to an executable that will perform the build. + + Examples: + + `builder = "/bin/bash";` + + `builder = ./builder.sh;` + + `builder = "${pkgs.python}/bin/python";` + +### Optional + +- [`args`]{#attr-args} ([List](@docroot@/language/values.md#list) of [String](@docroot@/language/values.md#type-string)) Default: `[ ]` + + Command-line arguments to be passed to the [`builder`](#attr-builder) executable. + + Example: `args = [ "-c" "echo hello world > $out" ];` + +- [`outputs`]{#attr-outputs} ([List](@docroot@/language/values.md#list) of [String](@docroot@/language/values.md#type-string)) Default: `[ "out" ]` + + Symbolic outputs of the derivation. + Each output name is passed to the [`builder`](#attr-builder) executable as an environment variable with its value set to the corresponding [output path]. + + [output path]: @docroot@/glossary.md#gloss-output-path + + By default, a derivation produces a single output path called `out`. + However, derivations can produce multiple output paths. + This allows the associated [store objects](@docroot@/glossary.md#gloss-store-object) and their [closures](@docroot@/glossary.md#gloss-closure) to be copied or garbage-collected separately. + + Examples: + + Imagine a library package that provides a dynamic library, header files, and documentation. + A program that links against the library doesn’t need the header files and documentation at runtime, and it doesn’t need the documentation at build time. + Thus, the library package could specify: + + ```nix + outputs = [ "lib" "headers" "doc" ]; + ``` + + This will cause Nix to pass environment variables `lib`, `headers`, and `doc` to the builder containing the intended store paths of each output. + The builder would typically do something like + + ```bash + ./configure \ + --libdir=$lib/lib \ + --includedir=$headers/include \ + --docdir=$doc/share/doc + ``` + + for an Autoconf-style package. + + You can refer to each output of a + derivation by selecting it as an attribute, e.g. + + ```nix + buildInputs = [ pkg.lib pkg.headers ]; + ``` + + + The first element of `outputs` determines the *default output*. + Thus, you could also write + + ```nix + buildInputs = [ pkg pkg.headers ]; + ``` + + since `pkg` is equivalent to `pkg.lib`. + +- See [Advanced Attributes](./advanced-attributes.md) for more, infrequently used, optional attributes. + + + +- Every other attribute is passed as an environment variable to the builder. + Attribute values are translated to environment variables as follows: + + - Strings and numbers are just passed verbatim. + + - A *path* (e.g., `../foo/sources.tar`) causes the referenced file + to be copied to the store; its location in the store is put in + the environment variable. The idea is that all sources should + reside in the Nix store, since all inputs to a derivation should + reside in the Nix store. + + - A *derivation* causes that derivation to be built prior to the + present derivation; its default output path is put in the + environment variable. + + - Lists of the previous types are also allowed. They are simply + concatenated, separated by spaces. + + - `true` is passed as the string `1`, `false` and `null` are + passed as an empty string. ## Builder execution From d621dd17f23023d4d6a84daba90ef38e848a1375 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 15 Sep 2023 10:31:31 +0200 Subject: [PATCH 156/299] more precise wording Co-authored-by: Robert Hensing --- doc/manual/src/language/derivations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 56c6818ca..dfefe446f 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -14,14 +14,14 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar - [`name`]{#attr-name} ([String](@docroot@/language/values.md#type-string)) - Symbolic name for the derivation. - It is appended to the [store paths](@docroot@/glossary.md#gloss-store-path) of the resulting store derivation and its [output paths][output path]. + A symbolic name for the derivation. + It is added to the [store derivation]'s [path](@docroot@/glossary.md#gloss-store-path) and its [output paths][output path]. Example: `name = "hello";` - [`system`]{#attr-system} ([String](@docroot@/language/values.md#type-string)) - The system type on which the [`builder`](#attr-builder) executable can be run. + The system type on which the [`builder`](#attr-builder) executable is meant to be run. Nix will only build derivations where the `system` attribute matches the current [`system` configuration option]. It can automatically [build on other platforms](../advanced-topics/distributed-builds.md) by forwarding build requests to other machines. From 026c24e3787d7b1cf81c49fdcb66818fd5dad2e4 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 15 Sep 2023 15:59:07 +0200 Subject: [PATCH 157/299] add example for store path using the given name --- doc/manual/src/language/derivations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index dfefe446f..3ced976f6 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -19,6 +19,7 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar Example: `name = "hello";` + The store derivation's path will be `/nix/store/-hello.drv`, and the output paths will be of the form `/nix/store/-hello[-]` - [`system`]{#attr-system} ([String](@docroot@/language/values.md#type-string)) The system type on which the [`builder`](#attr-builder) executable is meant to be run. From 17884f54d18b6d69dbfefe6c08272ea51b8879f8 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 22 Sep 2023 09:53:29 +0200 Subject: [PATCH 158/299] clarification on extra attributes Co-authored-by: Robert Hensing --- doc/manual/src/language/derivations.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 3ced976f6..77eac03b6 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -114,7 +114,11 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar - Every other attribute is passed as an environment variable to the builder. Attribute values are translated to environment variables as follows: - - Strings and numbers are just passed verbatim. + - Strings are passed unchanged. + + - Integral numbers are converted to decimal notation. + + - Floating point numbers are converted to simple decimal or scientific notation with a preset precision. - A *path* (e.g., `../foo/sources.tar`) causes the referenced file to be copied to the store; its location in the store is put in From 5e4734a08b8fb3f80b9617c190a2b5dfaba4a1da Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 00:13:56 +0200 Subject: [PATCH 159/299] reword to avoid saying "build" derivations are about data transformation, so the term "build" does not add any information. there was also some feedback that "build task" is not more helpful than "derivation" if you have no prior experience with Nix or build systems, while existing associations may be misleading. --- doc/manual/src/language/derivations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 77eac03b6..da3cca4ad 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -1,9 +1,9 @@ # Derivations The most important built-in function is `derivation`, which is used to describe a single derivation: -a build task to run a process on precisely defined input files and repeatably produce output files at uniquely determined file system paths. +a specification for running an executable on precisely defined input files to repeatably produce output files at uniquely determined file system paths. -It takes as input an attribute set, the attributes of which specify the inputs to the build. +It takes as input an attribute set, the attributes of which specify the inputs to the process. It outputs an attribute set, and produces a [store derivation](@docroot@/glossary.md#gloss-store-derivation) as a side effect of evaluation. From 75a231147f2d6acd32321e088a5030a69f188854 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 00:43:41 +0200 Subject: [PATCH 160/299] be more precise about `system` semantics --- doc/manual/src/language/derivations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index da3cca4ad..c13c81e7d 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -24,7 +24,7 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar The system type on which the [`builder`](#attr-builder) executable is meant to be run. - Nix will only build derivations where the `system` attribute matches the current [`system` configuration option]. + A necessary condition for Nix to build derivations locally is that the `system` attribute matches the current [`system` configuration option]. It can automatically [build on other platforms](../advanced-topics/distributed-builds.md) by forwarding build requests to other machines. Examples: From 7de66f19f84edc0d617e00f6e0cc37f2dbc4f5f6 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 00:46:26 +0200 Subject: [PATCH 161/299] example: headers -> dev make the example more realistic, since `headers` is not an output name used in Nixpkgs Co-authored-by: Robert Hensing --- doc/manual/src/language/derivations.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index c13c81e7d..6e6d3298b 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -75,16 +75,16 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar Thus, the library package could specify: ```nix - outputs = [ "lib" "headers" "doc" ]; + outputs = [ "lib" "dev" "doc" ]; ``` - This will cause Nix to pass environment variables `lib`, `headers`, and `doc` to the builder containing the intended store paths of each output. + This will cause Nix to pass environment variables `lib`, `dev`, and `doc` to the builder containing the intended store paths of each output. The builder would typically do something like ```bash ./configure \ --libdir=$lib/lib \ - --includedir=$headers/include \ + --includedir=$dev/include \ --docdir=$doc/share/doc ``` @@ -94,7 +94,7 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar derivation by selecting it as an attribute, e.g. ```nix - buildInputs = [ pkg.lib pkg.headers ]; + buildInputs = [ pkg.lib pkg.dev ]; ``` @@ -102,7 +102,7 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar Thus, you could also write ```nix - buildInputs = [ pkg pkg.headers ]; + buildInputs = [ pkg pkg.dev ]; ``` since `pkg` is equivalent to `pkg.lib`. From 5b0336b3b14551762f37126423ea8b4590ebb560 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 00:58:50 +0200 Subject: [PATCH 162/299] reword example for clarity --- doc/manual/src/language/derivations.md | 28 ++++++++++---------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 6e6d3298b..e0b9d021a 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -71,11 +71,15 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar Examples: Imagine a library package that provides a dynamic library, header files, and documentation. - A program that links against the library doesn’t need the header files and documentation at runtime, and it doesn’t need the documentation at build time. + A program that links against such a library doesn’t need the header files and documentation at runtime, and it doesn’t need the documentation at build time. Thus, the library package could specify: ```nix - outputs = [ "lib" "dev" "doc" ]; + derivation { + # ... + outputs = [ "lib" "dev" "doc" ]; + # ... + } ``` This will cause Nix to pass environment variables `lib`, `dev`, and `doc` to the builder containing the intended store paths of each output. @@ -90,22 +94,12 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar for an Autoconf-style package. - You can refer to each output of a - derivation by selecting it as an attribute, e.g. - - ```nix - buildInputs = [ pkg.lib pkg.dev ]; - ``` - + You can refer to each output of a derivation by selecting it as an attribute, e.g. `myPackage.lib` or `myPackage.doc`. The first element of `outputs` determines the *default output*. - Thus, you could also write + Therefore, in the given example, `myPackage` is equivalent to `myPackage.lib`. - ```nix - buildInputs = [ pkg pkg.dev ]; - ``` - - since `pkg` is equivalent to `pkg.lib`. + - See [Advanced Attributes](./advanced-attributes.md) for more, infrequently used, optional attributes. @@ -115,9 +109,9 @@ It outputs an attribute set, and produces a [store derivation](@docroot@/glossar Attribute values are translated to environment variables as follows: - Strings are passed unchanged. - + - Integral numbers are converted to decimal notation. - + - Floating point numbers are converted to simple decimal or scientific notation with a preset precision. - A *path* (e.g., `../foo/sources.tar`) causes the referenced file From 887cbcd3958abed7cf726f7eb23ba530e021ddfb Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 21 Sep 2023 02:53:50 +0200 Subject: [PATCH 163/299] add contributing guide for documentation --- doc/manual/local.mk | 6 +- doc/manual/src/SUMMARY.md.in | 1 + doc/manual/src/contributing/contributing.md | 9 +- doc/manual/src/contributing/documentation.md | 181 +++++++++++++++++++ doc/manual/src/contributing/hacking.md | 51 ------ 5 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 doc/manual/src/contributing/documentation.md diff --git a/doc/manual/local.mk b/doc/manual/local.mk index abdfd6a62..ab95af53c 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -173,6 +173,10 @@ doc/manual/generated/man1/nix3-manpages: $(d)/src/command-ref/new-cli done @touch $@ +# the `! -name 'contributing.md'` filter excludes the one place where +# `@docroot@` is to be preserved for documenting the mechanism +# FIXME: maybe contributing guides should live right next to the code +# instead of in the manual $(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/custom.css $(d)/src/SUMMARY.md $(d)/src/command-ref/new-cli $(d)/src/contributing/experimental-feature-descriptions.md $(d)/src/command-ref/conf-file.md $(d)/src/language/builtins.md $(d)/src/language/builtin-constants.md $(trace-gen) \ tmp="$$(mktemp -d)"; \ @@ -180,7 +184,7 @@ $(docdir)/manual/index.html: $(MANUAL_SRCS) $(d)/book.toml $(d)/anchors.jq $(d)/ find "$$tmp" -name '*.md' | while read -r file; do \ $(call process-includes,$$file,$$file); \ done; \ - find "$$tmp" -name '*.md' | while read -r file; do \ + find "$$tmp" -name '*.md' ! -name 'documentation.md' | while read -r file; do \ docroot="$$(realpath --relative-to="$$(dirname "$$file")" $$tmp/manual/src)"; \ sed -i "s,@docroot@,$$docroot,g" "$$file"; \ done; \ diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index c23186511..5c3667690 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -105,6 +105,7 @@ - [Contributing](contributing/contributing.md) - [Hacking](contributing/hacking.md) - [Testing](contributing/testing.md) + - [Documentation](contributing/documentation.md) - [Experimental Features](contributing/experimental-features.md) - [CLI guideline](contributing/cli-guideline.md) - [C++ style guide](contributing/cxx.md) diff --git a/doc/manual/src/contributing/contributing.md b/doc/manual/src/contributing/contributing.md index 854139a31..4d55c17a4 100644 --- a/doc/manual/src/contributing/contributing.md +++ b/doc/manual/src/contributing/contributing.md @@ -1 +1,8 @@ -# Contributing +# Development + +Nix is developed on GitHub. +Check the [contributing guide](https://github.com/NixOS/nix/blob/master/CONTRIBUTING.md) if you want to get involved. + +This chapter is a collection of guides for making changes to the code and documentation. + +If you're not sure where to start, try to [compile Nix from source](./hacking.md) and consider [making improvements to documentation](./documentation.md). diff --git a/doc/manual/src/contributing/documentation.md b/doc/manual/src/contributing/documentation.md new file mode 100644 index 000000000..f73ab2149 --- /dev/null +++ b/doc/manual/src/contributing/documentation.md @@ -0,0 +1,181 @@ +# Contributing documentation + +Improvements to documentation are very much appreciated, and a good way to start out with contributing to Nix. + +This is how you can help: +- Address [open issues with documentation](https://github.com/NixOS/nix/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation) +- Review [pull requests concerning documentation](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+label%3Adocumentation) + +Incremental refactorings of the documentation build setup to make it faster or easier to understand and maintain are also welcome. + +## Building the manual + +Build the manual from scratch: + +```console +nix-build $(nix-instantiate)'!doc' +``` + +or + +```console +nix build .#^doc +``` + +and open `./result-doc/share/doc/nix/manual/index.html`. + +To build the manual incrementally, [enter the development shell](./hacking.md) and run: + +```console +make manual-html -j $NIX_BUILD_CORES +``` + +and open `./outputs/out/share/doc/nix/manual/language/index.html`. + +In order to reflect changes to the [Makefile for the manual], clear all generated files before re-building: + +[Makefile for the manual]: https://github.com/NixOS/nix/blob/master/doc/manual/local.mk + +```console +rm $(git ls-files doc/manual/ -o | grep -F '.md') && rmdir doc/manual/src/command-ref/new-cli && make manual-html -j $NIX_BUILD_CORES +``` + +## Style guide + +The goal of this style guide is to make it such that +- The manual is easy to search and skim for relevant information +- Documentation sources are easy to edit +- Changes to documentation are easy to review + +You will notice that this is not implemented consistently yet. +Please follow the guide when making additions or changes to existing documentation. +Do not make sweeping changes, unless they are programmatic and can be validated easily. + +### Language + +This manual is [reference documentation](https://diataxis.fr/reference/). +The typical usage pattern is to look up isolated pieces of information. +It should therefore aim to be correct, consistent, complete, and easy to navigate at a glance. + +- Aim for clarity and brevity. + + Please take the time to read the [plain language guidelines](https://www.plainlanguage.gov/guidelines/) for details. + +- Describe the subject factually. + + In particular, do not make value judgements or recommendations. + Check the code or add tests if in doubt. + +- Provide complete, minimal examples, and explain them. + + Readers should be able to try examples verbatim and get the same results as shown in the manual. + Always describe in words what a given example does. + + Non-trivial examples may need additional explanation, especially if they use concepts from outside the given context. + +- Use British English. + + This is a somewhat arbitrary choice to force consistency, and accounts for the fact that a majority of Nix users and developers are from Europe. + +### Links and anchors + +Reference documentation must be readable in arbitrary order. +Readers cannot be expected to have any particular prerequisite knowledge about Nix. +While the table of contents can provide guidance and full-text search can help, they are most likely to find what they need by following sensible cross-references. + +- Link to technical terms + + When mentioning Nix-specific concepts, commands, options, settings, etc., link to appropriate documentation. + Also link to external tools or concepts, especially if their meaning may be ambiguous. + You may also want to link to definitions of less common technical terms. + + Then readers won't have to actively search for definitions and are more likely to discover relevant information on their own. + + > **Note** + > + > `man` and `--help` pages don't display links. + > Use appropriate link texts such that readers of terminal output can infer search terms. + +- Do not break existing URLs between releases. + + There are countless links in the wild pointing to old versions of the manual. + We want people to find up-to-date documentation when following popular advice. + + - When moving files, update [redirects on nixos.org](https://github.com/NixOS/nixos-homepage/blob/master/netlify.toml). + + This is especially important when moving information out of the Nix manual to other resources. + + - When changing anchors, update [client-side redirects](https://github.com/NixOS/nix/blob/master/doc/manual/redirects.js) + + The current setup is cumbersome, and help making better automation is appreciated. + +The build checks for broken internal links with. +This happens late in the process, so [building the whole manual](#building-the-manual) is not suitable for iterating quickly. +[`mdbook-linkcheck`] does not implement checking [URI fragments] yet. + +[`mdbook-linkcheck`]: https://github.com/Michael-F-Bryan/mdbook-linkcheck +[URI fragments]: https://en.wikipedia.org/wiki/URI_fragment + +### Markdown conventions + +The manual is written in markdown, and rendered with [mdBook](https://github.com/rust-lang/mdBook) for the web and with [lowdown](https://github.com/kristapsdz/lowdown) for `man` pages and `--help` output. + +For supported markdown features, refer to: +- [mdBook documentation](https://rust-lang.github.io/mdBook/format/markdown.html) +- [lowdown documentation](https://kristaps.bsd.lv/lowdown/) + +Please observe these guidelines to ease reviews: + +- Write one sentence per line. + + This makes long sentences immediately visible, and makes it easier to review changes and make direct suggestions. + +- Use reference links – sparingly – to ease source readability. + Put definitions close to their first use. + + Example: + + ``` + A [store object] contains a [file system object] and [references] to other store objects. + + [store object]: @docroot@/glossary.md#gloss-store-object + [file system object]: @docroot@/architecture/file-system-object.md + [references]: @docroot@/glossary.md#gloss-reference + ``` + +- Use admonitions of the following form: + + ``` + > **Note** + > + > This is a note. + ``` + +### The `@docroot@` variable + +`@docroot@` provides a base path for links that occur in reusable snippets or other documentation that doesn't have a base path of its own. + +If a broken link occurs in a snippet that was inserted into multiple generated files in different directories, use `@docroot@` to reference the `doc/manual/src` directory. + +If the `@docroot@` literal appears in an error message from the [`mdbook-linkcheck`] tool, the `@docroot@` replacement needs to be applied to the generated source file that mentions it. +See existing `@docroot@` logic in the [Makefile for the manual]. +Regular markdown files used for the manual have a base path of their own and they can use relative paths instead of `@docroot@`. + +## API documentation + +[Doxygen API documentation] is available online. +You can also build and view it yourself: + +[Doxygen API documentation]: https://hydra.nixos.org/job/nix/master/internal-api-docs/latest/download-by-type/doc/internal-api-docs + +```console +# nix build .#hydraJobs.internal-api-docs +# xdg-open ./result/share/doc/nix/internal-api/html/index.html +``` + +or inside `nix-shell` or `nix develop`: + +``` +# make internal-api-html +# xdg-open ./outputs/doc/share/doc/nix/internal-api/html/index.html +``` diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md index a2446b53a..db393942c 100644 --- a/doc/manual/src/contributing/hacking.md +++ b/doc/manual/src/contributing/hacking.md @@ -220,54 +220,3 @@ Configure your editor to use the `clangd` from the shell, either by running it i > For some editors (e.g. Visual Studio Code), you may need to install a [special extension](https://open-vsx.org/extension/llvm-vs-code-extensions/vscode-clangd) for the editor to interact with `clangd`. > Some other editors (e.g. Emacs, Vim) need a plugin to support LSP servers in general (e.g. [lsp-mode](https://github.com/emacs-lsp/lsp-mode) for Emacs and [vim-lsp](https://github.com/prabirshrestha/vim-lsp) for vim). > Editor-specific setup is typically opinionated, so we will not cover it here in more detail. - -### Checking links in the manual - -The build checks for broken internal links. -This happens late in the process, so `nix build` is not suitable for iterating. -To build the manual incrementally, run: - -```console -make manual-html -j $NIX_BUILD_CORES -``` - -In order to reflect changes to the [Makefile], clear all generated files before re-building: - -[Makefile]: https://github.com/NixOS/nix/blob/master/doc/manual/local.mk - -```console -rm $(git ls-files doc/manual/ -o | grep -F '.md') && rmdir doc/manual/src/command-ref/new-cli && make html -j $NIX_BUILD_CORES -``` - -[`mdbook-linkcheck`] does not implement checking [URI fragments] yet. - -[`mdbook-linkcheck`]: https://github.com/Michael-F-Bryan/mdbook-linkcheck -[URI fragments]: https://en.wikipedia.org/wiki/URI_fragment - -#### `@docroot@` variable - -`@docroot@` provides a base path for links that occur in reusable snippets or other documentation that doesn't have a base path of its own. - -If a broken link occurs in a snippet that was inserted into multiple generated files in different directories, use `@docroot@` to reference the `doc/manual/src` directory. - -If the `@docroot@` literal appears in an error message from the `mdbook-linkcheck` tool, the `@docroot@` replacement needs to be applied to the generated source file that mentions it. -See existing `@docroot@` logic in the [Makefile]. -Regular markdown files used for the manual have a base path of their own and they can use relative paths instead of `@docroot@`. - -## API documentation - -Doxygen API documentation is [available -online](https://hydra.nixos.org/job/nix/master/internal-api-docs/latest/download-by-type/doc/internal-api-docs). You -can also build and view it yourself: - -```console -# nix build .#hydraJobs.internal-api-docs -# xdg-open ./result/share/doc/nix/internal-api/html/index.html -``` - -or inside a `nix develop` shell by running: - -``` -# make internal-api-html -# xdg-open ./outputs/doc/share/doc/nix/internal-api/html/index.html -``` From 45de35bcf14d1bdad5dca2e24a113d26d1985aed Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 01:24:07 +0200 Subject: [PATCH 164/299] fix broken redirects script --- doc/manual/redirects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/redirects.js b/doc/manual/redirects.js index b43622ed6..b2fad19bb 100644 --- a/doc/manual/redirects.js +++ b/doc/manual/redirects.js @@ -343,7 +343,7 @@ const redirects = { "linux": "uninstall.html#linux", "macos": "uninstall.html#macos", "uninstalling": "uninstall.html", - } + }, "contributing/hacking.html": { "nix-with-flakes": "#building-nix-with-flakes", "classic-nix": "#building-nix", From c6f8247032612b4cc4a9f79213a887aa9ddddfda Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 03:10:12 +0200 Subject: [PATCH 165/299] fix broken reference link --- doc/manual/src/language/derivations.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index e0b9d021a..4a1b70fd1 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -4,7 +4,9 @@ The most important built-in function is `derivation`, which is used to describe a specification for running an executable on precisely defined input files to repeatably produce output files at uniquely determined file system paths. It takes as input an attribute set, the attributes of which specify the inputs to the process. -It outputs an attribute set, and produces a [store derivation](@docroot@/glossary.md#gloss-store-derivation) as a side effect of evaluation. +It outputs an attribute set, and produces a [store derivation] as a side effect of evaluation. + +[store derivation]: @docroot@/glossary.md#gloss-store-derivation From 7e24dc606b1cf7fd4e4dfd7027ba1469a59784d5 Mon Sep 17 00:00:00 2001 From: Tom Bereknyei Date: Fri, 22 Sep 2023 13:05:55 -0400 Subject: [PATCH 166/299] fix(tests): fix assumption that string.s is a char* --- src/libexpr/tests/libexpr.hh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libexpr/tests/libexpr.hh b/src/libexpr/tests/libexpr.hh index b8e65aafe..97b05ac46 100644 --- a/src/libexpr/tests/libexpr.hh +++ b/src/libexpr/tests/libexpr.hh @@ -71,7 +71,7 @@ namespace nix { if (arg.type() != nString) { return false; } - return std::string_view(arg.string.s) == s; + return std::string_view(arg.c_str()) == s; } MATCHER_P(IsIntEq, v, fmt("The string is equal to \"%1%\"", v)) { @@ -106,8 +106,8 @@ namespace nix { if (arg.type() != nPath) { *result_listener << "Expected a path got " << arg.type(); return false; - } else if (std::string_view(arg.string.s) != p) { - *result_listener << "Expected a path that equals \"" << p << "\" but got: " << arg.string.s; + } else if (std::string_view(arg._path) != p) { + *result_listener << "Expected a path that equals \"" << p << "\" but got: " << arg.c_str(); return false; } return true; From b17f200b11b3901c4f975c9901f1e2c6fc54124f Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 03:49:03 +0200 Subject: [PATCH 167/299] Document "Import From Derivation" (#7332) * document "Import From Derivation" Co-authored-by: Robert Hensing Co-authored-by: John Ericson --- doc/manual/src/SUMMARY.md.in | 1 + doc/manual/src/glossary.md | 5 +- .../src/language/import-from-derivation.md | 139 ++++++++++++++++++ src/libexpr/eval-settings.hh | 11 +- 4 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 doc/manual/src/language/import-from-derivation.md diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index c23186511..9e2ad0618 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -33,6 +33,7 @@ - [Operators](language/operators.md) - [Derivations](language/derivations.md) - [Advanced Attributes](language/advanced-attributes.md) + - [Import From Derivation](language/import-from-derivation.md) - [Built-in Constants](language/builtin-constants.md) - [Built-in Functions](language/builtins.md) - [Advanced Topics](advanced-topics/advanced-topics.md) diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index b7c340d36..c23466351 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -105,12 +105,15 @@ - [store object]{#gloss-store-object} - A store object consists of a [file system object], [reference]s to other store objects, and other metadata. It can be referred to by a [store path]. [store object]: #gloss-store-object +- [IFD]{#gloss-ifd} + + [Import From Derivation](./language/import-from-derivation.md) + - [input-addressed store object]{#gloss-input-addressed-store-object} A store object produced by building a diff --git a/doc/manual/src/language/import-from-derivation.md b/doc/manual/src/language/import-from-derivation.md new file mode 100644 index 000000000..03b3f9d91 --- /dev/null +++ b/doc/manual/src/language/import-from-derivation.md @@ -0,0 +1,139 @@ +# Import From Derivation + +The value of a Nix expression can depend on the contents of a [store object](@docroot@/glossary.md#gloss-store-object). + +Passing an expression `expr` that evaluates to a [store path](@docroot@/glossary.md#gloss-store-path) to any built-in function which reads from the filesystem constitutes Import From Derivation (IFD): + +- [`import`](./builtins.md#builtins-import)` expr` +- [`builtins.readFile`](./builtins.md#builtins-readFile)` expr` +- [`builtins.readFileType`](./builtins.md#builtins-readFileType)` expr` +- [`builtins.readDir`](./builtins.md#builtins-readDir)` expr` +- [`builtins.pathExists`](./builtins.md#builtins-pathExists)` expr` +- [`builtins.filterSource`](./builtins.md#builtins-filterSource)` f expr` +- [`builtins.path`](./builtins.md#builtins-path)` { path = expr; }` +- [`builtins.hashFile`](./builtins.md#builtins-hashFile)` t expr` +- `builtins.scopedImport x drv` + +When the store path needs to be accessed, evaluation will be paused, the corresponding store object [realised], and then evaluation resumed. + +[realised]: @docroot@/glossary.md#gloss-realise + +This has performance implications: +Evaluation can only finish when all required store objects are realised. +Since the Nix language evaluator is sequential, it only finds store paths to read from one at a time. +While realisation is always parallel, in this case it cannot be done for all required store paths at once, and is therefore much slower than otherwise. + +Realising store objects during evaluation can be disabled by setting [`allow-import-from-derivation`](../command-ref/conf-file.md#conf-allow-import-from-derivation) to `false`. +Without IFD it is ensured that evaluation is complete and Nix can produce a build plan before starting any realisation. + +## Example + +In the following Nix expression, the inner derivation `drv` produces a file with contents `hello`. + +```nix +# IFD.nix +let + drv = derivation { + name = "hello"; + builder = "/bin/sh"; + args = [ "-c" "echo -n hello > $out" ]; + system = builtins.currentSystem; + }; +in "${builtins.readFile drv} world" +``` + +```shellSession +nix-instantiate IFD.nix --eval --read-write-mode +``` + +``` +building '/nix/store/348q1cal6sdgfxs8zqi9v8llrsn4kqkq-hello.drv'... +"hello world" +``` + +The contents of the derivation's output have to be [realised] before they can be read with [`readFile`](./builtins.md#builtins-readFile). +Only then evaluation can continue to produce the final result. + +## Illustration + +As a first approximation, the following data flow graph shows how evaluation and building are interleaved, if the value of a Nix expression depends on realising a [store object]. +Boxes are data structures, arrow labels are transformations. + +``` ++----------------------+ +------------------------+ +| Nix evaluator | | Nix store | +| .----------------. | | | +| | Nix expression | | | | +| '----------------' | | | +| | | | | +| evaluate | | | +| | | | | +| V | | | +| .------------. | | .------------------. | +| | derivation |----|-instantiate-|->| store derivation | | +| '------------' | | '------------------' | +| | | | | +| | | realise | +| | | | | +| | | V | +| .----------------. | | .--------------. | +| | Nix expression |<-|----read-----|----| store object | | +| '----------------' | | '--------------' | +| | | | | +| evaluate | | | +| | | | | +| V | | | +| .------------. | | | +| | value | | | | +| '------------' | | | ++----------------------+ +------------------------+ +``` + +In more detail, the following sequence diagram shows how the expression is evaluated step by step, and where evaluation is blocked to wait for the build output to appear. + +``` +.-------. .-------------. .---------. +|Nix CLI| |Nix evaluator| |Nix store| +'-------' '-------------' '---------' + | | | + |evaluate IFD.nix| | + |--------------->| | + | | | + | evaluate `"${readFile drv} world"` | + | | | + | evaluate `readFile drv` | + | | | + | evaluate `drv` as string | + | | | + | |instantiate /nix/store/...-hello.drv| + | |----------------------------------->| + | : | + | : realise /nix/store/...-hello.drv | + | :----------------------------------->| + | : | + | |--------. + | : | | + | (evaluation blocked) | echo hello > $out + | : | | + | |<-------' + | : /nix/store/...-hello | + | |<-----------------------------------| + | | | + | resume `readFile /nix/store/...-hello` | + | | | + | | readFile /nix/store/...-hello | + | |----------------------------------->| + | | | + | | hello | + | |<-----------------------------------| + | | | + | resume `"${"hello"} world"` | + | | | + | resume `"hello world"` | + | | | + | "hello world" | | + |<---------------| | +.-------. .-------------. .---------. +|Nix CLI| |Nix evaluator| |Nix store| +'-------' '-------------' '---------' +``` diff --git a/src/libexpr/eval-settings.hh b/src/libexpr/eval-settings.hh index 19122bc31..19ba679be 100644 --- a/src/libexpr/eval-settings.hh +++ b/src/libexpr/eval-settings.hh @@ -47,11 +47,12 @@ struct EvalSettings : Config Setting enableImportFromDerivation{ this, true, "allow-import-from-derivation", R"( - By default, Nix allows you to `import` from a derivation, allowing - building at evaluation time. With this option set to false, Nix will - throw an error when evaluating an expression that uses this feature, - allowing users to ensure their evaluation will not require any - builds to take place. + By default, Nix allows [Import from Derivation](@docroot@/language/import-from-derivation.md). + + With this option set to `false`, Nix will throw an error when evaluating an expression that uses this feature, + even when the required store object is readily available. + This ensures that evaluation will not require any builds to take place, + regardless of the state of the store. )"}; Setting allowedUris{this, {}, "allowed-uris", From a757749fcfa810f699fa096f98821efccba86522 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Tue, 26 Sep 2023 06:28:17 +0200 Subject: [PATCH 168/299] reword for readability --- doc/manual/src/release-notes/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/manual/src/release-notes/release-notes.md b/doc/manual/src/release-notes/release-notes.md index 1fa2f5580..cc805e631 100644 --- a/doc/manual/src/release-notes/release-notes.md +++ b/doc/manual/src/release-notes/release-notes.md @@ -6,6 +6,7 @@ Notable changes and additions are announced in the release notes for each versio Bugfixes can be backported on request to previous Nix releases. We typically backport only as far back as the Nix version used in the latest NixOS release, which is announced in the [NixOS release notes](https://nixos.org/manual/nixos/stable/release-notes.html#ch-release-notes). -Backports never skip releases (if a feature is backported to `x.y`, it must also be available in `x.(y+1)`; +Backports never skip releases. +If a feature is backported to version `x.y`, it must also be available in version `x.(y+1)`. This ensures that upgrading from an older version with backports is still safe and no backported functionality will go missing. From 5b902ce9d60fca33c0f5bb76fcd31792c0bb03a6 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 26 Sep 2023 23:30:32 +0800 Subject: [PATCH 169/299] libexpr: construct ExprPath by move ctor, not copy cotr --- src/libexpr/parser.y | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 792f51fde..509f4fcc4 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -520,7 +520,7 @@ path_start /* add back in the trailing '/' to the first segment */ if ($1.p[$1.l-1] == '/' && $1.l > 1) path += "/"; - $$ = new ExprPath(path); + $$ = new ExprPath(std::move(path)); } | HPATH { if (evalSettings.pureEval) { @@ -530,7 +530,7 @@ path_start ); } Path path(getHome() + std::string($1.p + 1, $1.l - 1)); - $$ = new ExprPath(path); + $$ = new ExprPath(std::move(path)); } ; From 1eeea01931d20d32c917e92d860513b7cb32ba10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Wed, 27 Sep 2023 02:07:43 +0000 Subject: [PATCH 170/299] Fix repl.md duplicate typo Seems like `legacyPackages.x86_64-linux.emacs.name` is accidentally shown twice. --- src/nix/repl.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/nix/repl.md b/src/nix/repl.md index c5113be61..9dc61fd35 100644 --- a/src/nix/repl.md +++ b/src/nix/repl.md @@ -43,9 +43,6 @@ R""( nix-repl> legacyPackages.x86_64-linux.emacs.name "emacs-27.1" - nix-repl> legacyPackages.x86_64-linux.emacs.name - "emacs-27.1" - nix-repl> :q # nix repl --expr 'import {}' From 399ef8442012308f480ea5841fb6ac732207e03d Mon Sep 17 00:00:00 2001 From: Tom Bereknyei Date: Mon, 25 Sep 2023 21:30:41 -0400 Subject: [PATCH 171/299] refactor: use string accessors Create context, string_view, and c_str, accessors throughout in order to better support improvements to the underlying string representation. --- src/libcmd/repl.cc | 2 +- src/libexpr/eval-cache.cc | 8 ++++---- src/libexpr/eval.cc | 18 +++++++++--------- src/libexpr/flake/flake.cc | 10 +++++----- src/libexpr/get-drvs.cc | 12 ++++++------ src/libexpr/primops.cc | 10 +++++----- src/libexpr/primops/fetchClosure.cc | 2 +- src/libexpr/tests/primops.cc | 4 ++-- src/libexpr/value-to-json.cc | 2 +- src/libexpr/value-to-xml.cc | 6 +++--- src/libexpr/value.hh | 20 +++++++++++++++----- src/nix-env/nix-env.cc | 6 +++--- src/nix/eval.cc | 2 +- 13 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 944609f22..2e17a29a7 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -922,7 +922,7 @@ std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int m case nString: str << ANSI_WARNING; - printLiteralString(str, v.string.s); + printLiteralString(str, v.string_view()); str << ANSI_NORMAL; break; diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index 2c9aa5532..391b32a77 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -440,8 +440,8 @@ Value & AttrCursor::forceValue() if (root->db && (!cachedValue || std::get_if(&cachedValue->second))) { if (v.type() == nString) - cachedValue = {root->db->setString(getKey(), v.string.s, v.string.context), - string_t{v.string.s, {}}}; + cachedValue = {root->db->setString(getKey(), v.c_str(), v.context()), + string_t{v.c_str(), {}}}; else if (v.type() == nPath) { auto path = v.path().path; cachedValue = {root->db->setString(getKey(), path.abs()), string_t{path.abs(), {}}}; @@ -582,7 +582,7 @@ std::string AttrCursor::getString() if (v.type() != nString && v.type() != nPath) root->state.error("'%s' is not a string but %s", getAttrPathStr()).debugThrow(); - return v.type() == nString ? v.string.s : v.path().to_string(); + return v.type() == nString ? v.c_str() : v.path().to_string(); } string_t AttrCursor::getStringWithContext() @@ -624,7 +624,7 @@ string_t AttrCursor::getStringWithContext() if (v.type() == nString) { NixStringContext context; copyContext(v, context); - return {v.string.s, std::move(context)}; + return {v.c_str(), std::move(context)}; } else if (v.type() == nPath) return {v.path().to_string(), {}}; diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index afa864730..a78c0afb1 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -114,7 +114,7 @@ void Value::print(const SymbolTable &symbols, std::ostream &str, printLiteralBool(str, boolean); break; case tString: - printLiteralString(str, string.s); + printLiteralString(str, string_view()); break; case tPath: str << path().to_string(); // !!! escaping? @@ -339,7 +339,7 @@ static Symbol getName(const AttrName & name, EvalState & state, Env & env) Value nameValue; name.expr->eval(state, env, nameValue); state.forceStringNoCtx(nameValue, noPos, "while evaluating an attribute name"); - return state.symbols.create(nameValue.string.s); + return state.symbols.create(nameValue.string_view()); } } @@ -1343,7 +1343,7 @@ void ExprAttrs::eval(EvalState & state, Env & env, Value & v) if (nameVal.type() == nNull) continue; state.forceStringNoCtx(nameVal, i.pos, "while evaluating the name of a dynamic attribute"); - auto nameSym = state.symbols.create(nameVal.string.s); + auto nameSym = state.symbols.create(nameVal.string_view()); Bindings::iterator j = v.attrs->find(nameSym); if (j != v.attrs->end()) state.error("dynamic attribute '%1%' already defined at %2%", state.symbols[nameSym], state.positions[j->pos]).atPos(i.pos).withFrame(env, *this).debugThrow(); @@ -2155,7 +2155,7 @@ std::string_view EvalState::forceString(Value & v, const PosIdx pos, std::string forceValue(v, pos); if (v.type() != nString) error("value is %1% while a string was expected", showType(v)).debugThrow(); - return v.string.s; + return v.string_view(); } catch (Error & e) { e.addTrace(positions[pos], errorCtx); throw; @@ -2182,8 +2182,8 @@ std::string_view EvalState::forceString(Value & v, NixStringContext & context, c std::string_view EvalState::forceStringNoCtx(Value & v, const PosIdx pos, std::string_view errorCtx) { auto s = forceString(v, pos, errorCtx); - if (v.string.context) { - error("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string.s, v.string.context[0]).withTrace(pos, errorCtx).debugThrow(); + if (v.context()) { + error("the string '%1%' is not allowed to refer to a store path (such as '%2%')", v.string_view(), v.context()[0]).withTrace(pos, errorCtx).debugThrow(); } return s; } @@ -2196,7 +2196,7 @@ bool EvalState::isDerivation(Value & v) if (i == v.attrs->end()) return false; forceValue(*i->value, i->pos); if (i->value->type() != nString) return false; - return strcmp(i->value->string.s, "derivation") == 0; + return i->value->string_view().compare("derivation") == 0; } @@ -2228,7 +2228,7 @@ BackedStringView EvalState::coerceToString( if (v.type() == nString) { copyContext(v, context); - return std::string_view(v.string.s); + return v.string_view(); } if (v.type() == nPath) { @@ -2426,7 +2426,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v return v1.boolean == v2.boolean; case nString: - return strcmp(v1.string.s, v2.string.s) == 0; + return v1.string_view().compare(v2.string_view()) == 0; case nPath: return strcmp(v1._path, v2._path) == 0; diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 90427e064..a6212c12f 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -113,7 +113,7 @@ static FlakeInput parseFlakeInput(EvalState & state, try { if (attr.name == sUrl) { expectType(state, nString, *attr.value, attr.pos); - url = attr.value->string.s; + url = attr.value->string_view(); attrs.emplace("url", *url); } else if (attr.name == sFlake) { expectType(state, nBool, *attr.value, attr.pos); @@ -122,7 +122,7 @@ static FlakeInput parseFlakeInput(EvalState & state, input.overrides = parseFlakeInputs(state, attr.value, attr.pos, baseDir, lockRootPath); } else if (attr.name == sFollows) { expectType(state, nString, *attr.value, attr.pos); - auto follows(parseInputPath(attr.value->string.s)); + auto follows(parseInputPath(attr.value->c_str())); follows.insert(follows.begin(), lockRootPath.begin(), lockRootPath.end()); input.follows = follows; } else { @@ -131,7 +131,7 @@ static FlakeInput parseFlakeInput(EvalState & state, #pragma GCC diagnostic ignored "-Wswitch-enum" switch (attr.value->type()) { case nString: - attrs.emplace(state.symbols[attr.name], attr.value->string.s); + attrs.emplace(state.symbols[attr.name], attr.value->c_str()); break; case nBool: attrs.emplace(state.symbols[attr.name], Explicit { attr.value->boolean }); @@ -229,7 +229,7 @@ static Flake getFlake( if (auto description = vInfo.attrs->get(state.sDescription)) { expectType(state, nString, *description->value, description->pos); - flake.description = description->value->string.s; + flake.description = description->value->c_str(); } auto sInputs = state.symbols.create("inputs"); @@ -850,7 +850,7 @@ static void prim_flakeRefToString( Explicit { attr.value->boolean }); } else if (t == nString) { attrs.emplace(state.symbols[attr.name], - std::string(attr.value->str())); + std::string(attr.value->string_view())); } else { state.error( "flake reference attribute sets may only contain integers, Booleans, " diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc index 506a63677..fe3e6f7ee 100644 --- a/src/libexpr/get-drvs.cc +++ b/src/libexpr/get-drvs.cc @@ -156,7 +156,7 @@ DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall Outputs result; for (auto elem : outTI->listItems()) { if (elem->type() != nString) throw errMsg; - auto out = outputs.find(elem->string.s); + auto out = outputs.find(elem->c_str()); if (out == outputs.end()) throw errMsg; result.insert(*out); } @@ -230,7 +230,7 @@ std::string DrvInfo::queryMetaString(const std::string & name) { Value * v = queryMeta(name); if (!v || v->type() != nString) return ""; - return v->string.s; + return v->c_str(); } @@ -242,7 +242,7 @@ NixInt DrvInfo::queryMetaInt(const std::string & name, NixInt def) if (v->type() == nString) { /* Backwards compatibility with before we had support for integer meta fields. */ - if (auto n = string2Int(v->string.s)) + if (auto n = string2Int(v->c_str())) return *n; } return def; @@ -256,7 +256,7 @@ NixFloat DrvInfo::queryMetaFloat(const std::string & name, NixFloat def) if (v->type() == nString) { /* Backwards compatibility with before we had support for float meta fields. */ - if (auto n = string2Float(v->string.s)) + if (auto n = string2Float(v->c_str())) return *n; } return def; @@ -271,8 +271,8 @@ bool DrvInfo::queryMetaBool(const std::string & name, bool def) if (v->type() == nString) { /* Backwards compatibility with before we had support for Boolean meta fields. */ - if (strcmp(v->string.s, "true") == 0) return true; - if (strcmp(v->string.s, "false") == 0) return false; + if (v->string_view() == "true") return true; + if (v->string_view() == "false") return false; } return def; } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index cd9a05bb2..1cb5cdf30 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -590,7 +590,7 @@ struct CompareValues case nFloat: return v1->fpoint < v2->fpoint; case nString: - return strcmp(v1->string.s, v2->string.s) < 0; + return v1->string_view().compare(v2->string_view()) < 0; case nPath: return strcmp(v1->_path, v2->_path) < 0; case nList: @@ -982,7 +982,7 @@ static void prim_trace(EvalState & state, const PosIdx pos, Value * * args, Valu { state.forceValue(*args[0], pos); if (args[0]->type() == nString) - printError("trace: %1%", args[0]->string.s); + printError("trace: %1%", args[0]->string_view()); else printError("trace: %1%", printValue(state, *args[0])); state.forceValue(*args[1], pos); @@ -1528,7 +1528,7 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args, auto path = realisePath(state, pos, arg, { .checkForPureEval = false }); /* SourcePath doesn't know about trailing slash. */ - auto mustBeDir = arg.type() == nString && arg.str().ends_with("/"); + auto mustBeDir = arg.type() == nString && arg.string_view().ends_with("/"); try { auto checked = state.checkSourcePath(path); @@ -2400,7 +2400,7 @@ static void prim_attrNames(EvalState & state, const PosIdx pos, Value * * args, (v.listElems()[n++] = state.allocValue())->mkString(state.symbols[i.name]); std::sort(v.listElems(), v.listElems() + n, - [](Value * v1, Value * v2) { return strcmp(v1->string.s, v2->string.s) < 0; }); + [](Value * v1, Value * v2) { return v1->string_view().compare(v2->string_view()) < 0; }); } static RegisterPrimOp primop_attrNames({ @@ -2541,7 +2541,7 @@ static void prim_removeAttrs(EvalState & state, const PosIdx pos, Value * * args names.reserve(args[1]->listSize()); for (auto elem : args[1]->listItems()) { state.forceStringNoCtx(*elem, pos, "while evaluating the values of the second argument passed to builtins.removeAttrs"); - names.emplace_back(state.symbols.create(elem->string.s), nullptr); + names.emplace_back(state.symbols.create(elem->string_view()), nullptr); } std::sort(names.begin(), names.end()); diff --git a/src/libexpr/primops/fetchClosure.cc b/src/libexpr/primops/fetchClosure.cc index 7fe8203f4..b86ef6b93 100644 --- a/src/libexpr/primops/fetchClosure.cc +++ b/src/libexpr/primops/fetchClosure.cc @@ -133,7 +133,7 @@ static void prim_fetchClosure(EvalState & state, const PosIdx pos, Value * * arg else if (attrName == "toPath") { state.forceValue(*attr.value, attr.pos); - bool isEmptyString = attr.value->type() == nString && attr.value->string.s == std::string(""); + bool isEmptyString = attr.value->type() == nString && attr.value->string_view() == ""; if (isEmptyString) { toPath = StorePathOrGap {}; } diff --git a/src/libexpr/tests/primops.cc b/src/libexpr/tests/primops.cc index ce3b5d11f..d820b860e 100644 --- a/src/libexpr/tests/primops.cc +++ b/src/libexpr/tests/primops.cc @@ -711,14 +711,14 @@ namespace nix { // FIXME: add a test that verifies the string context is as expected auto v = eval("builtins.replaceStrings [\"oo\" \"a\"] [\"a\" \"i\"] \"foobar\""); ASSERT_EQ(v.type(), nString); - ASSERT_EQ(v.string.s, std::string_view("fabir")); + ASSERT_EQ(v.string_view(), "fabir"); } TEST_F(PrimOpTest, concatStringsSep) { // FIXME: add a test that verifies the string context is as expected auto v = eval("builtins.concatStringsSep \"%\" [\"foo\" \"bar\" \"baz\"]"); ASSERT_EQ(v.type(), nString); - ASSERT_EQ(std::string_view(v.string.s), "foo%bar%baz"); + ASSERT_EQ(v.string_view(), "foo%bar%baz"); } TEST_F(PrimOpTest, split1) { diff --git a/src/libexpr/value-to-json.cc b/src/libexpr/value-to-json.cc index ac3986c87..cbc91f509 100644 --- a/src/libexpr/value-to-json.cc +++ b/src/libexpr/value-to-json.cc @@ -31,7 +31,7 @@ json printValueAsJSON(EvalState & state, bool strict, case nString: copyContext(v, context); - out = v.string.s; + out = v.c_str(); break; case nPath: diff --git a/src/libexpr/value-to-xml.cc b/src/libexpr/value-to-xml.cc index 2539ad1c1..bd7a4ae30 100644 --- a/src/libexpr/value-to-xml.cc +++ b/src/libexpr/value-to-xml.cc @@ -74,7 +74,7 @@ static void printValueAsXML(EvalState & state, bool strict, bool location, case nString: /* !!! show the context? */ copyContext(v, context); - doc.writeEmptyElement("string", singletonAttrs("value", v.string.s)); + doc.writeEmptyElement("string", singletonAttrs("value", v.c_str())); break; case nPath: @@ -96,14 +96,14 @@ static void printValueAsXML(EvalState & state, bool strict, bool location, if (a != v.attrs->end()) { if (strict) state.forceValue(*a->value, a->pos); if (a->value->type() == nString) - xmlAttrs["drvPath"] = drvPath = a->value->string.s; + xmlAttrs["drvPath"] = drvPath = a->value->c_str(); } a = v.attrs->find(state.sOutPath); if (a != v.attrs->end()) { if (strict) state.forceValue(*a->value, a->pos); if (a->value->type() == nString) - xmlAttrs["outPath"] = a->value->string.s; + xmlAttrs["outPath"] = a->value->c_str(); } XMLOpenElement _(doc, "derivation", xmlAttrs); diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index c44683e50..7a1165cac 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -186,10 +186,9 @@ public: * For canonicity, the store paths should be in sorted order. */ struct { - const char * s; + const char * c_str; const char * * context; // must be in sorted order } string; - const char * _path; Bindings * attrs; struct { @@ -270,7 +269,7 @@ public: inline void mkString(const char * s, const char * * context = 0) { internalType = tString; - string.s = s; + string.c_str = s; string.context = context; } @@ -441,10 +440,21 @@ public: return SourcePath{CanonPath(_path)}; } - std::string_view str() const + std::string_view string_view() const { assert(internalType == tString); - return std::string_view(string.s); + return std::string_view(string.c_str); + } + + const char * const c_str() const + { + assert(internalType == tString); + return string.c_str; + } + + const char * * context() const + { + return string.context; } }; diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index b112e8cb3..e455a50fd 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1228,7 +1228,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) else { if (v->type() == nString) { attrs2["type"] = "string"; - attrs2["value"] = v->string.s; + attrs2["value"] = v->c_str(); xml.writeEmptyElement("meta", attrs2); } else if (v->type() == nInt) { attrs2["type"] = "int"; @@ -1248,7 +1248,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) for (auto elem : v->listItems()) { if (elem->type() != nString) continue; XMLAttrs attrs3; - attrs3["value"] = elem->string.s; + attrs3["value"] = elem->c_str(); xml.writeEmptyElement("string", attrs3); } } else if (v->type() == nAttrs) { @@ -1260,7 +1260,7 @@ static void opQuery(Globals & globals, Strings opFlags, Strings opArgs) if(a.value->type() != nString) continue; XMLAttrs attrs3; attrs3["type"] = globals.state->symbols[i.name]; - attrs3["value"] = a.value->string.s; + attrs3["value"] = a.value->c_str(); xml.writeEmptyElement("string", attrs3); } } diff --git a/src/nix/eval.cc b/src/nix/eval.cc index d880bef0a..b34af34e0 100644 --- a/src/nix/eval.cc +++ b/src/nix/eval.cc @@ -85,7 +85,7 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption state->forceValue(v, pos); if (v.type() == nString) // FIXME: disallow strings with contexts? - writeFile(path, v.string.s); + writeFile(path, v.string_view()); else if (v.type() == nAttrs) { if (mkdir(path.c_str(), 0777) == -1) throw SysError("creating directory '%s'", path); From 5bc540a8ca7ada282839ab58225a1a24cc723c4e Mon Sep 17 00:00:00 2001 From: Alex Ameen Date: Wed, 27 Sep 2023 14:49:52 -0500 Subject: [PATCH 172/299] Respect `NOCOLOR` While `nix` has always been respectful towards requests for `NO_COLOR=1`, this change asks represents a new stage of maturity for `nix` - making it also respect quests for `NOCOLOR=1`. This ideally makes the tool more accessible to folks like me, who are exhausted by guessing whether `NO_COLOR` or `NOCOLOR` is the right environment variable to set. <3 --- src/libutil/util.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 5a10c69e2..3b4c181e5 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -1515,7 +1515,7 @@ bool shouldANSI() { return isatty(STDERR_FILENO) && getEnv("TERM").value_or("dumb") != "dumb" - && !getEnv("NO_COLOR").has_value(); + && !(getEnv("NO_COLOR").has_value() || getEnv("NOCOLOR").has_value()); } std::string filterANSIEscapes(std::string_view s, bool filterAll, unsigned int width) From dafa38213bda956525fb692eea33f2ec4fed7ff3 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 27 Sep 2023 23:10:39 +0100 Subject: [PATCH 173/299] Update doc/manual/src/advanced-topics/post-build-hook.md Co-authored-by: Valentin Gagarin --- doc/manual/src/advanced-topics/post-build-hook.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/advanced-topics/post-build-hook.md b/doc/manual/src/advanced-topics/post-build-hook.md index 17b67111c..f7b3b1317 100644 --- a/doc/manual/src/advanced-topics/post-build-hook.md +++ b/doc/manual/src/advanced-topics/post-build-hook.md @@ -69,7 +69,7 @@ exec nix copy --to "s3://example-nix-cache" $OUT_PATHS > store sign`. Nix guarantees the paths will not contain any spaces, > however a store path might contain glob characters. The `set -f` > disables globbing in the shell. -> If you want to upload the .drv file too, the `$DRV_PATH` variable +> If you want to upload the `.drv` file too, the `$DRV_PATH` variable > is also defined for the script and works just like `$OUT_PATHS`. Then make sure the hook program is executable by the `root` user: From 13ed5d710674a9467c8959fed12641b40e38c791 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Tue, 26 Sep 2023 00:31:00 +0200 Subject: [PATCH 174/299] flakes: adopt repl-flake behavior as default --- doc/manual/src/release-notes/rl-next.md | 2 ++ src/libutil/experimental-features.cc | 2 ++ src/nix/repl.cc | 2 +- src/nix/repl.md | 2 +- tests/repl.sh | 11 +++-------- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 2467e7ccc..a69cad1db 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -3,3 +3,5 @@ - [URL flake references](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references) now support [percent-encoded](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1) characters. - [Path-like flake references](@docroot@/command-ref/new-cli/nix3-flake.md#path-like-syntax) now accept arbitrary unicode characters (except `#` and `?`). + +- The experimental feature `repl-flake` is no longer needed, as its functionality is now part of the `flakes` experimental feature. To get the previous behavior, use the `--file/--expr` flags accordingly. diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 6d92222b9..203455b63 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -163,6 +163,8 @@ constexpr std::array xpFeatureDetails = {{ .tag = Xp::ReplFlake, .name = "repl-flake", .description = R"( + *Enabled with [`flakes`](#xp-feature-flakes) since 2.19* + Allow passing [installables](@docroot@/command-ref/new-cli/nix.md#installables) to `nix repl`, making its interface consistent with the other experimental commands. )", }, diff --git a/src/nix/repl.cc b/src/nix/repl.cc index 9677c1b48..63fe3044b 100644 --- a/src/nix/repl.cc +++ b/src/nix/repl.cc @@ -47,7 +47,7 @@ struct CmdRepl : RawInstallablesCommand void applyDefaultInstallables(std::vector & rawInstallables) override { - if (!experimentalFeatureSettings.isEnabled(Xp::ReplFlake) && !(file) && rawInstallables.size() >= 1) { + if (!experimentalFeatureSettings.isEnabled(Xp::Flakes) && !(file) && rawInstallables.size() >= 1) { warn("future versions of Nix will require using `--file` to load a file"); if (rawInstallables.size() > 1) warn("more than one input file is not currently supported"); diff --git a/src/nix/repl.md b/src/nix/repl.md index 9dc61fd35..32c08e24b 100644 --- a/src/nix/repl.md +++ b/src/nix/repl.md @@ -36,7 +36,7 @@ R""( Loading Installable ''... Added 1 variables. - # nix repl --extra-experimental-features 'flakes repl-flake' nixpkgs + # nix repl --extra-experimental-features 'flakes' nixpkgs Loading Installable 'flake:nixpkgs#'... Added 5 variables. diff --git a/tests/repl.sh b/tests/repl.sh index bb8b60e50..1b779c1f5 100644 --- a/tests/repl.sh +++ b/tests/repl.sh @@ -105,18 +105,13 @@ testReplResponseNoRegex ' testReplResponse ' drvPath ' '".*-simple.drv"' \ -$testDir/simple.nix +--file $testDir/simple.nix testReplResponse ' drvPath ' '".*-simple.drv"' \ --file $testDir/simple.nix --experimental-features 'ca-derivations' -testReplResponse ' -drvPath -' '".*-simple.drv"' \ ---file $testDir/simple.nix --extra-experimental-features 'repl-flake ca-derivations' - mkdir -p flake && cat < flake/flake.nix { outputs = { self }: { @@ -130,7 +125,7 @@ EOF testReplResponse ' foo + baz ' "3" \ - ./flake ./flake\#bar --experimental-features 'flakes repl-flake' + ./flake ./flake\#bar --experimental-features 'flakes' # Test the `:reload` mechansim with flakes: # - Eval `./flake#changingThing` @@ -143,7 +138,7 @@ sleep 1 # Leave the repl the time to eval 'foo' sed -i 's/beforeChange/afterChange/' flake/flake.nix echo ":reload" echo "changingThing" -) | nix repl ./flake --experimental-features 'flakes repl-flake') +) | nix repl ./flake --experimental-features 'flakes') echo "$replResult" | grepQuiet -s beforeChange echo "$replResult" | grepQuiet -s afterChange From add7c99c3b559e531b7426e7a0a987231e0028ed Mon Sep 17 00:00:00 2001 From: Andrea Bedini Date: Mon, 18 Sep 2023 16:57:18 +0800 Subject: [PATCH 175/299] Include "original" and "locked" in nix flake prefetch --json --- src/nix/flake.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 87dd4da1b..f8df032c8 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -1342,6 +1342,8 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON auto res = nlohmann::json::object(); res["storePath"] = store->printStorePath(tree.storePath); res["hash"] = hash.to_string(SRI, true); + res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs()); + res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs()); logger->cout(res.dump()); } else { notice("Downloaded '%s' to '%s' (hash '%s').", From cede94dbf7733c2c64bdc4a4f53645cffdd73379 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 28 Sep 2023 20:51:25 -0400 Subject: [PATCH 176/299] `builtins.fetchTree`: Mark experimental the new way This helps ensure uniform docs/error message. --- src/libexpr/primops/fetchTree.cc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 46d31a4e9..124165896 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -195,7 +195,6 @@ static void fetchTree( static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - experimentalFeatureSettings.require(Xp::Flakes); fetchTree(state, pos, args, v, std::nullopt, FetchTreeParams { .allowNameArgument = false }); } @@ -237,12 +236,9 @@ static RegisterPrimOp primop_fetchTree({ ```nix builtins.fetchTree "https://example.com/" ``` - - > **Note** - > - > This function requires the [`flakes` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-flakes) to be enabled. )", - .fun = prim_fetchTree + .fun = prim_fetchTree, + .experimentalFeature = Xp::Flakes, }); static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v, From b912f3a9377ce0b7302b8833934bc1af448ea996 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 28 Sep 2023 20:55:41 -0400 Subject: [PATCH 177/299] Move `flakeIdRegex{,S}` from `libutil` to `flakeref.{cc,hh` It isn't used, and doesn't belong in `libutil`. --- src/libexpr/flake/flakeref.cc | 2 ++ src/libexpr/flake/flakeref.hh | 3 +++ src/libutil/url-parts.hh | 3 --- src/libutil/url.cc | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index f7ef19b3f..93ad33a33 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -289,4 +289,6 @@ std::tuple parseFlakeRefWithFragment return {std::move(flakeRef), fragment, std::move(extendedOutputsSpec)}; } +std::regex flakeIdRegex(flakeIdRegexS, std::regex::ECMAScript); + } diff --git a/src/libexpr/flake/flakeref.hh b/src/libexpr/flake/flakeref.hh index a7c9208c0..ffb2e50de 100644 --- a/src/libexpr/flake/flakeref.hh +++ b/src/libexpr/flake/flakeref.hh @@ -6,6 +6,7 @@ #include "fetchers.hh" #include "outputs-spec.hh" +#include #include namespace nix { @@ -91,5 +92,7 @@ std::tuple parseFlakeRefWithFragment bool allowMissing = false, bool isFlake = true); +const static std::string flakeIdRegexS = "[a-zA-Z][a-zA-Z0-9_-]*"; +extern std::regex flakeIdRegex; } diff --git a/src/libutil/url-parts.hh b/src/libutil/url-parts.hh index 98162b0f7..4712c0c91 100644 --- a/src/libutil/url-parts.hh +++ b/src/libutil/url-parts.hh @@ -41,7 +41,4 @@ extern std::regex revRegex; /// A ref or revision, or a ref followed by a revision. const static std::string refAndOrRevRegex = "(?:(" + revRegexS + ")|(?:(" + refRegexS + ")(?:/(" + revRegexS + "))?))"; -const static std::string flakeIdRegexS = "[a-zA-Z][a-zA-Z0-9_-]*"; -extern std::regex flakeIdRegex; - } diff --git a/src/libutil/url.cc b/src/libutil/url.cc index e700c8eaf..2970f8d2d 100644 --- a/src/libutil/url.cc +++ b/src/libutil/url.cc @@ -8,7 +8,6 @@ namespace nix { std::regex refRegex(refRegexS, std::regex::ECMAScript); std::regex badGitRefRegex(badGitRefRegexS, std::regex::ECMAScript); std::regex revRegex(revRegexS, std::regex::ECMAScript); -std::regex flakeIdRegex(flakeIdRegexS, std::regex::ECMAScript); ParsedURL parseURL(const std::string & url) { From c816c67eed6d8508a84f40ca00ac93f5926b44be Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 28 Sep 2023 21:10:17 -0400 Subject: [PATCH 178/299] Reword some comments/API docs to reflect `libfetcher`'s multiple users It's not just flakes, but also `builtins.fetchTree`. Also try to provide some more info in general. --- src/libfetchers/attrs.hh | 6 ++++++ src/libfetchers/fetchers.cc | 3 ++- src/libfetchers/fetchers.hh | 31 ++++++++++++++++++++----------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/libfetchers/attrs.hh b/src/libfetchers/attrs.hh index 9f885a793..b9a2c824e 100644 --- a/src/libfetchers/attrs.hh +++ b/src/libfetchers/attrs.hh @@ -13,6 +13,12 @@ namespace nix::fetchers { typedef std::variant> Attr; + +/** + * An `Attrs` can be thought of a JSON object restricted or simplified + * to be "flat", not containing any subcontainers (arrays or objects) + * and also not containing any `null`s. + */ typedef std::map Attrs; Attrs jsonToAttrs(const nlohmann::json & json); diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index e683b9f80..7bffb04ff 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -254,7 +254,8 @@ std::optional Input::getRev() const try { hash = Hash::parseAnyPrefixed(*s); } catch (BadHash &e) { - // Default to sha1 for backwards compatibility with existing flakes + // Default to sha1 for backwards compatibility with existing + // usages (e.g. `builtins.fetchTree` calls or flake inputs). hash = Hash::parseAny(*s, htSHA1); } } diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index 6e10e9513..b27ac4128 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -22,12 +22,11 @@ struct Tree struct InputScheme; /** - * The Input object is generated by a specific fetcher, based on the - * user-supplied input attribute in the flake.nix file, and contains + * The `Input` object is generated by a specific fetcher, based on + * user-supplied information, and contains * the information that the specific fetcher needs to perform the * actual fetch. The Input object is most commonly created via the - * "fromURL()" or "fromAttrs()" static functions which are provided - * the url or attrset specified in the flake file. + * `fromURL()` or `fromAttrs()` static functions. */ struct Input { @@ -44,10 +43,20 @@ struct Input std::optional parent; public: + /** + * Create an `Input` from a URL. + * + * The URL indicate which sort of fetcher, and provides information to that fetcher. + */ static Input fromURL(const std::string & url, bool requireTree = true); static Input fromURL(const ParsedURL & url, bool requireTree = true); + /** + * Create an `Input` from a an `Attrs`. + * + * The URL indicate which sort of fetcher, and provides information to that fetcher. + */ static Input fromAttrs(Attrs && attrs); ParsedURL toURL() const; @@ -116,13 +125,13 @@ public: /** - * The InputScheme represents a type of fetcher. Each fetcher - * registers with nix at startup time. When processing an input for a - * flake, each scheme is given an opportunity to "recognize" that - * input from the url or attributes in the flake file's specification - * and return an Input object to represent the input if it is - * recognized. The Input object contains the information the fetcher - * needs to actually perform the "fetch()" when called. + * The `InputScheme` represents a type of fetcher. Each fetcher + * registers with nix at startup time. When processing an `Input`, + * each scheme is given an opportunity to "recognize" that + * input from the user-provided url or attributes + * and return an `Input` object to represent the input if it is + * recognized. The `Input` object contains the information the fetcher + * needs to actually perform the `fetch()` when called. */ struct InputScheme { From bfe1308d3ff7f3dfd4facf4318adcb459e57ece9 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 28 Sep 2023 21:21:58 -0400 Subject: [PATCH 179/299] Add infra for `InputSchemes` to be experimental --- src/libfetchers/fetchers.cc | 7 +++++++ src/libfetchers/fetchers.hh | 5 +++++ src/libutil/args.hh | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index 7bffb04ff..e3d3ac562 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -36,6 +36,7 @@ Input Input::fromURL(const ParsedURL & url, bool requireTree) for (auto & inputScheme : *inputSchemes) { auto res = inputScheme->inputFromURL(url, requireTree); if (res) { + experimentalFeatureSettings.require(inputScheme->experimentalFeature()); res->scheme = inputScheme; fixupInput(*res); return std::move(*res); @@ -50,6 +51,7 @@ Input Input::fromAttrs(Attrs && attrs) for (auto & inputScheme : *inputSchemes) { auto res = inputScheme->inputFromAttrs(attrs); if (res) { + experimentalFeatureSettings.require(inputScheme->experimentalFeature()); res->scheme = inputScheme; fixupInput(*res); return std::move(*res); @@ -309,4 +311,9 @@ void InputScheme::clone(const Input & input, const Path & destDir) const throw Error("do not know how to clone input '%s'", input.to_string()); } +std::optional InputScheme::experimentalFeature() +{ + return {}; +} + } diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index b27ac4128..b55aabb6f 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -158,6 +158,11 @@ struct InputScheme virtual void markChangedFile(const Input & input, std::string_view file, std::optional commitMsg); virtual std::pair fetch(ref store, const Input & input) = 0; + + /** + * Is this `InputScheme` part of an experimental feature? + */ + virtual std::optional experimentalFeature(); }; void registerInputScheme(std::shared_ptr && fetcher); diff --git a/src/libutil/args.hh b/src/libutil/args.hh index d90129796..1e5bac650 100644 --- a/src/libutil/args.hh +++ b/src/libutil/args.hh @@ -236,7 +236,7 @@ struct Command : virtual public Args static constexpr Category catDefault = 0; - virtual std::optional experimentalFeature (); + virtual std::optional experimentalFeature(); virtual Category category() { return catDefault; } }; From 89b39520630726b7c48e8cbc065643134352e43d Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 28 Sep 2023 21:35:36 -0400 Subject: [PATCH 180/299] Make the indirect fetcher input scheme part of the Flakes XP feature I don't know much about it, but by the number of times "flake" appears in the code it seems like is part of flakes, at least for now. --- src/libfetchers/indirect.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libfetchers/indirect.cc b/src/libfetchers/indirect.cc index 4874a43ff..e09d8bfb8 100644 --- a/src/libfetchers/indirect.cc +++ b/src/libfetchers/indirect.cc @@ -98,6 +98,11 @@ struct IndirectInputScheme : InputScheme { throw Error("indirect input '%s' cannot be fetched directly", input.to_string()); } + + std::optional experimentalFeature() override + { + return Xp::Flakes; + } }; static auto rIndirectInputScheme = OnStartup([] { registerInputScheme(std::make_unique()); }); From 1dd03c62ad8f09b50dd77c18b973d4aee2112f7a Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 29 Sep 2023 10:46:42 +0200 Subject: [PATCH 181/299] add hint for troubleshooting tests --- doc/manual/src/contributing/testing.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/manual/src/contributing/testing.md b/doc/manual/src/contributing/testing.md index 881118505..4a7f1c15c 100644 --- a/doc/manual/src/contributing/testing.md +++ b/doc/manual/src/contributing/testing.md @@ -130,6 +130,15 @@ GNU gdb (GDB) 12.1 One can debug the Nix invocation in all the usual ways. For example, enter `run` to start the Nix invocation. +### Troubleshooting + +Sometimes running tests in the development shell may leave artefacts in the local repository. +To remove any traces of that: + +```console +git clean -x --force tests +``` + ### Characterization testing Occasionally, Nix utilizes a technique called [Characterization Testing](https://en.wikipedia.org/wiki/Characterization_test) as part of the functional tests. From 08145a5be5cbc963a7006f65d9417f710b3fb8b5 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 29 Sep 2023 16:17:51 +0200 Subject: [PATCH 182/299] contributor guide: emphasize solving a well-specified problem with each pull request this moves the orientation step to the beginning, and adds notes how to make sure that a problem is well-spefified and the according change more likely to get accepted Co-authored-by: Robert Hensing --- CONTRIBUTING.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index facbf0eb0..4a9cff3b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,25 +24,30 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). ## Making changes to Nix -1. Check for [pull requests](https://github.com/NixOS/nix/pulls) that might already cover the contribution you are about to make. - There are many open pull requests that might already do what you intent to work on. - You can use [labels](https://github.com/NixOS/nix/labels) to filter for relevant topics. - -2. Search for related issues that cover what you're going to work on. It could help to mention there that you will work on the issue. +1. Search for related issues that cover what you're going to work on. + It could help to mention there that you will work on the issue. Issues labeled [good first issue](https://github.com/NixOS/nix/labels/good-first-issue) should be relatively easy to fix and are likely to get merged quickly. - Pull requests addressing issues labeled [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) are especially welcomed by maintainers and will receive prioritised review. + Pull requests addressing issues labeled [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) or [RFC](https://github.com/NixOS/nix/labels/RFC) are especially welcomed by maintainers and will receive prioritised review. + + If there is no relevant issue yet and you're not sure whether your change is likely to be accepted, [open an issue](https://github.com/NixOS/nix/issues/new/choose) yourself. + +2. Check for [pull requests](https://github.com/NixOS/nix/pulls) that might already cover the contribution you are about to make. + There are many open pull requests that might already do what you intend to work on. + You can use [labels](https://github.com/NixOS/nix/labels) to filter for relevant topics. 3. Check the [Nix reference manual](https://nixos.org/manual/nix/unstable/contributing/hacking.html) for information on building Nix and running its tests. For contributions to the command line interface, please check the [CLI guidelines](https://nixos.org/manual/nix/unstable/contributing/cli-guideline.html). -4. Make your changes! +4. Make your change! 5. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) for your changes. - * Link related issues in your pull request to inform interested parties and future contributors about your change. + * Clearly explain the problem that you're solving. + + Link related issues to inform interested parties and future contributors about your change. + If your pull request closes one or multiple issues, mention that in the description using `Closes: #`, as it will then happen automatically when your change is merged. * Make sure to have [a clean history of commits on your branch by using rebase](https://www.digitalocean.com/community/tutorials/how-to-rebase-and-update-a-pull-request). - If your pull request closes one or multiple issues, note that in the description using `Closes: #`, as it will then happen automatically when your change is merged. * [Mark the pull request as draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request) if you're not done with the changes. 6. Do not expect your pull request to be reviewed immediately. From f8a3893e8d77ce4a6e23719a0b2d88464cb84b9c Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 2 Sep 2023 22:47:38 +0200 Subject: [PATCH 183/299] pathExists: isDir when endswith /. --- src/libexpr/primops.cc | 4 +++- tests/lang/eval-okay-pathexists.nix | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index cb1f99acb..5de1b2828 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1528,7 +1528,9 @@ static void prim_pathExists(EvalState & state, const PosIdx pos, Value * * args, auto path = realisePath(state, pos, arg, { .checkForPureEval = false }); /* SourcePath doesn't know about trailing slash. */ - auto mustBeDir = arg.type() == nString && arg.string_view().ends_with("/"); + auto mustBeDir = arg.type() == nString + && (arg.string_view().ends_with("/") + || arg.string_view().ends_with("/.")); try { auto checked = state.checkSourcePath(path); diff --git a/tests/lang/eval-okay-pathexists.nix b/tests/lang/eval-okay-pathexists.nix index e1246e370..c5e7a62de 100644 --- a/tests/lang/eval-okay-pathexists.nix +++ b/tests/lang/eval-okay-pathexists.nix @@ -2,6 +2,27 @@ builtins.pathExists (./lib.nix) && builtins.pathExists (builtins.toPath ./lib.nix) && builtins.pathExists (builtins.toString ./lib.nix) && !builtins.pathExists (builtins.toString ./lib.nix + "/") +&& !builtins.pathExists (builtins.toString ./lib.nix + "/.") +# FIXME +# && !builtins.pathExists (builtins.toString ./lib.nix + "/..") +# && !builtins.pathExists (builtins.toString ./lib.nix + "/a/..") +# && !builtins.pathExists (builtins.toString ./lib.nix + "/../lib.nix") +&& !builtins.pathExists (builtins.toString ./lib.nix + "/./") +&& !builtins.pathExists (builtins.toString ./lib.nix + "/./.") +&& builtins.pathExists (builtins.toString ./.. + "/lang/lib.nix") +&& !builtins.pathExists (builtins.toString ./.. + "lang/lib.nix") +&& builtins.pathExists (builtins.toString ./. + "/../lang/lib.nix") +&& builtins.pathExists (builtins.toString ./. + "/../lang/./lib.nix") +&& builtins.pathExists (builtins.toString ./.) +&& builtins.pathExists (builtins.toString ./. + "/") +&& builtins.pathExists (builtins.toString ./. + "/../lang") +&& builtins.pathExists (builtins.toString ./. + "/../lang/") +&& builtins.pathExists (builtins.toString ./. + "/../lang/.") +&& builtins.pathExists (builtins.toString ./. + "/../lang/./") +&& builtins.pathExists (builtins.toString ./. + "/../lang//./") +&& builtins.pathExists (builtins.toString ./. + "/../lang/..") +&& builtins.pathExists (builtins.toString ./. + "/../lang/../") +&& builtins.pathExists (builtins.toString ./. + "/../lang/..//") && builtins.pathExists (builtins.toPath (builtins.toString ./lib.nix)) && !builtins.pathExists (builtins.toPath (builtins.toString ./bla.nix)) && builtins.pathExists ./lib.nix From bfdd908f7dee5ae04c6a501167ea0fe1f4e08591 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Sun, 24 Sep 2023 13:19:04 +0200 Subject: [PATCH 184/299] structured attrs: improve support / usage of NIX_ATTRS_{SH,JSON}_FILE In #4770 I implemented proper `nix-shell(1)` support for derivations using `__structuredAttrs = true;`. Back then we decided to introduce two new environment variables, `NIX_ATTRS_SH_FILE` for `.attrs.sh` and `NIX_ATTRS_JSON_FILE` for `.attrs.json`. This was to avoid having to copy these files to `$NIX_BUILD_TOP` in a `nix-shell(1)` session which effectively meant copying these files to the project dir without cleaning up afterwords[1]. On last NixCon I resumed hacking on `__structuredAttrs = true;` by default for `nixpkgs` with a few other folks and getting back to it, I identified a few problems with the how it's used in `nixpkgs`: * A lot of builders in `nixpkgs` don't care about the env vars and assume that `.attrs.sh` and `.attrs.json` are in `$NIX_BUILD_TOP`. The sole reason why this works is that `nix-shell(1)` sources the contents of `.attrs.sh` and then sources `$stdenv/setup` if it exists. This may not be pretty, but it mostly works. One notable difference when using nixpkgs' stdenv as of now is however that `$__structuredAttrs` is set to `1` on regular builds, but set to an empty string in a shell session. Also, `.attrs.json` cannot be used in shell sessions because it can only be accessed by `$NIX_ATTRS_JSON_FILE` and not by `$NIX_BUILD_TOP/.attrs.json`. I considered changing Nix to be compatible with what nixpkgs effectively does, but then we'd have to either move $NIX_BUILD_TOP for shell sessions to a temporary location (and thus breaking a lot of assumptions) or we'd reintroduce all the problems we solved back then by using these two env vars. This is partly because I didn't document these variables back then (mea culpa), so I decided to drop all mentions of `.attrs.{json,sh}` in the manual and only refer to `$NIX_ATTRS_SH_FILE` and `$NIX_ATTRS_JSON_FILE`. The same applies to all our integration tests. Theoretically we could deprecated using `"$NIX_BUILD_TOP"/.attrs.sh` in the future now. * `nix develop` and `nix print-dev-env` don't support this environment variable at all even though they're supposed to be part of the replacement for `nix-shell` - for the drv debugging part to be precise. This isn't a big deal for the vast majority of derivations, i.e. derivations relying on nixpkgs' `stdenv` wiring things together properly. This is because `nix develop` effectively "clones" the derivation and replaces the builder with a script that dumps all of the environment, shell variables, functions etc, so the state of structured attrs being "sourced" is transmitted into the dev shell and most of the time you don't need to worry about `.attrs.sh` not existing because the shell is correctly configured and the if [ -e .attrs.sh ]; then source .attrs.sh; fi is simply omitted. However, this will break when having a derivation that reads e.g. from `.attrs.json` like with import {}; runCommand "foo" { __structuredAttrs = true; foo.bar = 23; } '' cat $NIX_ATTRS_JSON_FILE # doesn't work because it points to /build/.attrs.json '' To work around this I employed a similar approach as it exists for `nix-shell`: the `NIX_ATTRS_{JSON,SH}_FILE` vars are replaced with temporary locations. The contents of `.attrs.sh` and `.attrs.json` are now written into the JSON by `get-env.sh`, the builder that `nix develop` injects into the derivation it's debugging. So finally the exact file contents are present and exported by `nix develop`. I also made `.attrs.json` a JSON string in the JSON printed by `get-env.sh` on purpose because then it's not necessary to serialize the object structure again. `nix develop` only needs the JSON as string because it's only written into the temporary file. I'm not entirely sure if it makes sense to also use a temporary location for `nix print-dev-env` (rather than just skipping the rewrite in there), but this would probably break certain cases where it's relied upon `$NIX_ATTRS_SH_FILE` to exist (prime example are the `nix print-dev-env` test-cases I wrote in this patch using `tests/shell.nix`, these would fail because the env var exists, but it cannot read from it). [1] https://github.com/NixOS/nix/pull/4770#issuecomment-836799719 --- .../src/language/advanced-attributes.md | 17 ++-- src/nix/develop.cc | 86 +++++++++++++++++-- src/nix/get-env.sh | 20 ++++- tests/build-hook-ca-fixed.nix | 5 +- tests/build-hook.nix | 5 +- tests/config.nix.in | 5 +- tests/failing.nix | 5 +- tests/hermetic.nix | 5 +- tests/structured-attrs.sh | 14 ++- 9 files changed, 141 insertions(+), 21 deletions(-) diff --git a/doc/manual/src/language/advanced-attributes.md b/doc/manual/src/language/advanced-attributes.md index 86f0f24b5..7d97b24b6 100644 --- a/doc/manual/src/language/advanced-attributes.md +++ b/doc/manual/src/language/advanced-attributes.md @@ -273,18 +273,21 @@ Derivations can declare some infrequently used optional attributes. - [`__structuredAttrs`]{#adv-attr-structuredAttrs}\ If the special attribute `__structuredAttrs` is set to `true`, the other derivation - attributes are serialised in JSON format and made available to the - builder via the file `.attrs.json` in the builder’s temporary - directory. This obviates the need for [`passAsFile`](#adv-attr-passAsFile) since JSON files - have no size restrictions, unlike process environments. + attributes are serialised into a file in JSON format. The environment variable + `NIX_ATTRS_JSON_FILE` points to the exact location of that file both in a build + and a [`nix-shell`](../command-ref/nix-shell.md). This obviates the need for + [`passAsFile`](#adv-attr-passAsFile) since JSON files have no size restrictions, + unlike process environments. It also makes it possible to tweak derivation settings in a structured way; see [`outputChecks`](#adv-attr-outputChecks) for example. As a convenience to Bash builders, - Nix writes a script named `.attrs.sh` to the builder’s directory - that initialises shell variables corresponding to all attributes - that are representable in Bash. This includes non-nested + Nix writes a script that initialises shell variables + corresponding to all attributes that are representable in Bash. The + environment variable `NIX_ATTRS_SH_FILE` points to the exact + location of the script, both in a build and a + [`nix-shell`](../command-ref/nix-shell.md). This includes non-nested (associative) arrays. For example, the attribute `hardening.format = true` ends up as the Bash associative array element `${hardening[format]}`. diff --git a/src/nix/develop.cc b/src/nix/develop.cc index c01ef1a2a..b080a3939 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -8,9 +8,12 @@ #include "derivations.hh" #include "progress-bar.hh" #include "run.hh" +#include "util.hh" +#include #include #include +#include using namespace nix; @@ -51,6 +54,7 @@ struct BuildEnvironment std::map vars; std::map bashFunctions; + std::optional> structuredAttrs; static BuildEnvironment fromJSON(std::string_view in) { @@ -74,6 +78,10 @@ struct BuildEnvironment res.bashFunctions.insert({name, def}); } + if (json.contains("structuredAttrs")) { + res.structuredAttrs = {json["structuredAttrs"][".attrs.json"], json["structuredAttrs"][".attrs.sh"]}; + } + return res; } @@ -102,6 +110,13 @@ struct BuildEnvironment res["bashFunctions"] = bashFunctions; + if (providesStructuredAttrs()) { + auto contents = nlohmann::json::object(); + contents[".attrs.sh"] = getAttrsSH(); + contents[".attrs.json"] = getAttrsJSON(); + res["structuredAttrs"] = std::move(contents); + } + auto json = res.dump(); assert(BuildEnvironment::fromJSON(json) == *this); @@ -109,6 +124,23 @@ struct BuildEnvironment return json; } + bool providesStructuredAttrs() const + { + return structuredAttrs.has_value(); + } + + std::string getAttrsJSON() const + { + assert(providesStructuredAttrs()); + return structuredAttrs->first; + } + + std::string getAttrsSH() const + { + assert(providesStructuredAttrs()); + return structuredAttrs->second; + } + void toBash(std::ostream & out, const std::set & ignoreVars) const { for (auto & [name, value] : vars) { @@ -291,6 +323,7 @@ struct Common : InstallableCommand, MixProfile std::string makeRcScript( ref store, const BuildEnvironment & buildEnvironment, + const Path & tmpDir, const Path & outputsDir = absPath(".") + "/outputs") { // A list of colon-separated environment variables that should be @@ -353,9 +386,48 @@ struct Common : InstallableCommand, MixProfile } } + if (buildEnvironment.providesStructuredAttrs()) { + fixupStructuredAttrs( + "sh", + "NIX_ATTRS_SH_FILE", + buildEnvironment.getAttrsSH(), + rewrites, + buildEnvironment, + tmpDir + ); + fixupStructuredAttrs( + "json", + "NIX_ATTRS_JSON_FILE", + buildEnvironment.getAttrsJSON(), + rewrites, + buildEnvironment, + tmpDir + ); + } + return rewriteStrings(script, rewrites); } + /** + * Replace the value of NIX_ATTRS_*_FILE (`/build/.attrs.*`) with a tmp file + * that's accessible from the interactive shell session. + */ + void fixupStructuredAttrs( + const std::string & ext, + const std::string & envVar, + const std::string & content, + StringMap & rewrites, + const BuildEnvironment & buildEnvironment, + const Path & tmpDir) + { + auto targetFilePath = tmpDir + "/.attrs." + ext; + writeFile(targetFilePath, content); + + auto fileInBuilderEnv = buildEnvironment.vars.find(envVar); + assert(fileInBuilderEnv != buildEnvironment.vars.end()); + rewrites.insert({BuildEnvironment::getString(fileInBuilderEnv->second), targetFilePath}); + } + Strings getDefaultFlakeAttrPaths() override { Strings paths{ @@ -487,7 +559,9 @@ struct CmdDevelop : Common, MixEnvironment auto [rcFileFd, rcFilePath] = createTempFile("nix-shell"); - auto script = makeRcScript(store, buildEnvironment); + AutoDelete tmpDir(createTempDir("", "nix-develop"), true); + + auto script = makeRcScript(store, buildEnvironment, (Path) tmpDir); if (verbosity >= lvlDebug) script += "set -x\n"; @@ -615,10 +689,12 @@ struct CmdPrintDevEnv : Common, MixJSON stopProgressBar(); - logger->writeToStdout( - json - ? buildEnvironment.toJSON() - : makeRcScript(store, buildEnvironment)); + if (json) { + logger->writeToStdout(buildEnvironment.toJSON()); + } else { + AutoDelete tmpDir(createTempDir("", "nix-dev-env"), true); + logger->writeToStdout(makeRcScript(store, buildEnvironment, tmpDir)); + } } }; diff --git a/src/nix/get-env.sh b/src/nix/get-env.sh index a7a8a01b9..832cc2f11 100644 --- a/src/nix/get-env.sh +++ b/src/nix/get-env.sh @@ -1,5 +1,5 @@ set -e -if [ -e .attrs.sh ]; then source .attrs.sh; fi +if [ -e "$NIX_ATTRS_SH_FILE" ]; then source "$NIX_ATTRS_SH_FILE"; fi export IN_NIX_SHELL=impure export dontAddDisableDepTrack=1 @@ -101,7 +101,21 @@ __dumpEnv() { printf "}" done < <(printf "%s\n" "$__vars") - printf '\n }\n}' + printf '\n }' + + if [ -e "$NIX_ATTRS_SH_FILE" ]; then + printf ',\n "structuredAttrs": {\n ' + __escapeString ".attrs.sh" + printf ': ' + __escapeString "$(<"$NIX_ATTRS_SH_FILE")" + printf ',\n ' + __escapeString ".attrs.json" + printf ': ' + __escapeString "$(<"$NIX_ATTRS_JSON_FILE")" + printf '\n }' + fi + + printf '\n}' } __escapeString() { @@ -117,7 +131,7 @@ __escapeString() { # In case of `__structuredAttrs = true;` the list of outputs is an associative # array with a format like `outname => /nix/store/hash-drvname-outname`, so `__olist` # must contain the array's keys (hence `${!...[@]}`) in this case. -if [ -e .attrs.sh ]; then +if [ -e "$NIX_ATTRS_SH_FILE" ]; then __olist="${!outputs[@]}" else __olist=$outputs diff --git a/tests/build-hook-ca-fixed.nix b/tests/build-hook-ca-fixed.nix index 4cb9e85d1..0ce6d9b12 100644 --- a/tests/build-hook-ca-fixed.nix +++ b/tests/build-hook-ca-fixed.nix @@ -8,7 +8,10 @@ let derivation ({ inherit system; builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; + args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '')]; outputHashMode = "recursive"; outputHashAlgo = "sha256"; } // removeAttrs args ["builder" "meta" "passthru"]) diff --git a/tests/build-hook.nix b/tests/build-hook.nix index 7effd7903..99a13aee4 100644 --- a/tests/build-hook.nix +++ b/tests/build-hook.nix @@ -14,7 +14,10 @@ let derivation ({ inherit system; builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; + args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '')]; } // removeAttrs args ["builder" "meta" "passthru"] // caArgs) // { meta = args.meta or {}; passthru = args.passthru or {}; }; diff --git a/tests/config.nix.in b/tests/config.nix.in index 7facbdcbc..00dc007e1 100644 --- a/tests/config.nix.in +++ b/tests/config.nix.in @@ -20,7 +20,10 @@ rec { derivation ({ inherit system; builder = shell; - args = ["-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; + args = ["-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '')]; PATH = path; } // caArgs // removeAttrs args ["builder" "meta"]) // { meta = args.meta or {}; }; diff --git a/tests/failing.nix b/tests/failing.nix index 2a0350d4d..d25e2d6b6 100644 --- a/tests/failing.nix +++ b/tests/failing.nix @@ -6,7 +6,10 @@ let derivation ({ inherit system; builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; + args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '')]; } // removeAttrs args ["builder" "meta"]) // { meta = args.meta or {}; }; in diff --git a/tests/hermetic.nix b/tests/hermetic.nix index 4c9d7a51f..0513540f0 100644 --- a/tests/hermetic.nix +++ b/tests/hermetic.nix @@ -14,7 +14,10 @@ let derivation ({ inherit system; builder = busybox; - args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" "if [ -e .attrs.sh ]; then source .attrs.sh; fi; eval \"$buildCommand\"")]; + args = ["sh" "-e" args.builder or (builtins.toFile "builder-${args.name}.sh" '' + if [ -e "$NIX_ATTRS_SH_FILE" ]; then source $NIX_ATTRS_SH_FILE; fi; + eval "$buildCommand" + '')]; } // removeAttrs args ["builder" "meta" "passthru"] // caArgs) // { meta = args.meta or {}; passthru = args.passthru or {}; }; diff --git a/tests/structured-attrs.sh b/tests/structured-attrs.sh index 378dbc735..f2835a761 100644 --- a/tests/structured-attrs.sh +++ b/tests/structured-attrs.sh @@ -15,9 +15,21 @@ nix-build structured-attrs.nix -A all -o $TEST_ROOT/result export NIX_BUILD_SHELL=$SHELL env NIX_PATH=nixpkgs=shell.nix nix-shell structured-attrs-shell.nix \ - --run 'test -e .attrs.json; test "3" = "$(jq ".my.list|length" < $NIX_ATTRS_JSON_FILE)"' + --run 'test "3" = "$(jq ".my.list|length" < $NIX_ATTRS_JSON_FILE)"' + +nix develop -f structured-attrs-shell.nix -c bash -c 'test "3" = "$(jq ".my.list|length" < $NIX_ATTRS_JSON_FILE)"' # `nix develop` is a slightly special way of dealing with environment vars, it parses # these from a shell-file exported from a derivation. This is to test especially `outputs` # (which is an associative array in thsi case) being fine. nix develop -f structured-attrs-shell.nix -c bash -c 'test -n "$out"' + +nix print-dev-env -f structured-attrs-shell.nix | grep -q 'NIX_ATTRS_JSON_FILE=' +nix print-dev-env -f structured-attrs-shell.nix | grep -q 'NIX_ATTRS_SH_FILE=' +nix print-dev-env -f shell.nix shellDrv | grepQuietInverse 'NIX_ATTRS_SH_FILE' + +jsonOut="$(nix print-dev-env -f structured-attrs-shell.nix --json)" + +test "$(<<<"$jsonOut" jq '.structuredAttrs|keys|.[]' -r)" = "$(printf ".attrs.json\n.attrs.sh")" + +test "$(<<<"$jsonOut" jq '.variables.out.value' -r)" = "$(<<<"$jsonOut" jq '.structuredAttrs.".attrs.json"' -r | jq -r '.outputs.out')" From 42e3c6d658ebdb60eeef179d91e71ca311f69da8 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Fri, 29 Sep 2023 17:14:31 +0200 Subject: [PATCH 185/299] doc: reference NIX_ATTRS_*_FILE vars at the env var reference for drvs --- doc/manual/src/language/derivations.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index 4a1b70fd1..d80a7dd48 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -165,6 +165,11 @@ The [`builder`](#attr-builder) is executed as follows: - `NIX_STORE` is set to the path of the top-level Nix store directory (typically, `/nix/store`). + - `NIX_ATTRS_JSON_FILE` & `NIX_ATTRS_SH_FILE` if `__structuredAttrs` + is set to `true` for the dervation. A detailed explanation of this + behavior can be found in the + [section about structured attrs](./advanced-attributes.md#adv-attr-structuredAttrs). + - For each output declared in `outputs`, the corresponding environment variable is set to point to the intended path in the Nix store for that output. Each output path is a concatenation From 7a0886e3ccbab7da645dae6ecd015b3084aadc54 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 30 Sep 2023 22:04:38 +0100 Subject: [PATCH 186/299] tests/structured-attrs.sh: grep -q -> grepQuiet --- tests/structured-attrs.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/structured-attrs.sh b/tests/structured-attrs.sh index f2835a761..f11992dcd 100644 --- a/tests/structured-attrs.sh +++ b/tests/structured-attrs.sh @@ -24,8 +24,8 @@ nix develop -f structured-attrs-shell.nix -c bash -c 'test "3" = "$(jq ".my.list # (which is an associative array in thsi case) being fine. nix develop -f structured-attrs-shell.nix -c bash -c 'test -n "$out"' -nix print-dev-env -f structured-attrs-shell.nix | grep -q 'NIX_ATTRS_JSON_FILE=' -nix print-dev-env -f structured-attrs-shell.nix | grep -q 'NIX_ATTRS_SH_FILE=' +nix print-dev-env -f structured-attrs-shell.nix | grepQuiet 'NIX_ATTRS_JSON_FILE=' +nix print-dev-env -f structured-attrs-shell.nix | grepQuiet 'NIX_ATTRS_SH_FILE=' nix print-dev-env -f shell.nix shellDrv | grepQuietInverse 'NIX_ATTRS_SH_FILE' jsonOut="$(nix print-dev-env -f structured-attrs-shell.nix --json)" From 78e886bc5fd9e4d85f8503799540c0b71bb270be Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 29 Sep 2023 15:59:11 +0200 Subject: [PATCH 187/299] refine the maintainer's process to unblock discussions more quickly this addresses that we're too often running into open-ended discussions about attempts to solve problems where neither the problem nor the solution is well-understood enough to make decisions in a reasonable amount of time. this also prevents us from doing more work asynchronously. Co-authored-by: Robert Hensing --- maintainers/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/maintainers/README.md b/maintainers/README.md index 0d520cb0c..8eeb47e8b 100644 --- a/maintainers/README.md +++ b/maintainers/README.md @@ -96,8 +96,10 @@ What constitutes a trivial pull request is up to maintainers' judgement. Pull requests and issues that are deemed important and controversial are discussed by the team during discussion meetings. This may be where the merit of the change itself or the implementation strategy is contested by a team member. +Whenever the discussion opens up questions about the process or this team's goals, this may indicate that the change is too large in scope. +In that case it is taken off the board to be reconsidered by the author or broken down into smaller pieces that are less far-reaching and can be reviewed independently. -As a general guideline, the order of items is determined as follows: +As a general guideline, the order of items to discuss is determined as follows: - Prioritise pull requests over issues From 8440afbed756254784d9fea3eaab06649dffd390 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 1 Oct 2023 23:29:45 -0400 Subject: [PATCH 188/299] Revert "Adapt scheduler to work with dynamic derivations" This reverts commit 5e3986f59cb58f48186a49dcec7aa317b4787522. This un-implements RFC 92 but fixes the critical bug #9052 which many people are hitting. This is a decent stop-gap until a minimal reproduction of that bug is found and a proper fix can be made. Mostly fixed #9052, but I would like to leave that issue open until we have a regression test, so I can then properly fix the bug (unbreaking RFC 92) later. --- .../create-derivation-and-realise-goal.cc | 157 ------------------ .../create-derivation-and-realise-goal.hh | 96 ----------- src/libstore/build/derivation-goal.cc | 33 +++- src/libstore/build/derivation-goal.hh | 5 +- src/libstore/build/entry-points.cc | 11 +- src/libstore/build/goal.hh | 10 -- src/libstore/build/worker.cc | 110 +++--------- src/libstore/build/worker.hh | 24 --- src/libstore/derived-path-map.cc | 3 - src/libstore/derived-path-map.hh | 7 +- tests/dyn-drv/build-built-drv.sh | 4 +- tests/dyn-drv/dep-built-drv.sh | 4 +- 12 files changed, 55 insertions(+), 409 deletions(-) delete mode 100644 src/libstore/build/create-derivation-and-realise-goal.cc delete mode 100644 src/libstore/build/create-derivation-and-realise-goal.hh diff --git a/src/libstore/build/create-derivation-and-realise-goal.cc b/src/libstore/build/create-derivation-and-realise-goal.cc deleted file mode 100644 index 60f67956d..000000000 --- a/src/libstore/build/create-derivation-and-realise-goal.cc +++ /dev/null @@ -1,157 +0,0 @@ -#include "create-derivation-and-realise-goal.hh" -#include "worker.hh" - -namespace nix { - -CreateDerivationAndRealiseGoal::CreateDerivationAndRealiseGoal(ref drvReq, - const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode) - : Goal(worker, DerivedPath::Built { .drvPath = drvReq, .outputs = wantedOutputs }) - , drvReq(drvReq) - , wantedOutputs(wantedOutputs) - , buildMode(buildMode) -{ - state = &CreateDerivationAndRealiseGoal::getDerivation; - name = fmt( - "outer obtaining drv from '%s' and then building outputs %s", - drvReq->to_string(worker.store), - std::visit(overloaded { - [&](const OutputsSpec::All) -> std::string { - return "* (all of them)"; - }, - [&](const OutputsSpec::Names os) { - return concatStringsSep(", ", quoteStrings(os)); - }, - }, wantedOutputs.raw)); - trace("created outer"); - - worker.updateProgress(); -} - - -CreateDerivationAndRealiseGoal::~CreateDerivationAndRealiseGoal() -{ -} - - -static StorePath pathPartOfReq(const SingleDerivedPath & req) -{ - return std::visit(overloaded { - [&](const SingleDerivedPath::Opaque & bo) { - return bo.path; - }, - [&](const SingleDerivedPath::Built & bfd) { - return pathPartOfReq(*bfd.drvPath); - }, - }, req.raw()); -} - - -std::string CreateDerivationAndRealiseGoal::key() -{ - /* Ensure that derivations get built in order of their name, - i.e. a derivation named "aardvark" always comes before "baboon". And - substitution goals and inner derivation goals always happen before - derivation goals (due to "b$"). */ - return "c$" + std::string(pathPartOfReq(*drvReq).name()) + "$" + drvReq->to_string(worker.store); -} - - -void CreateDerivationAndRealiseGoal::timedOut(Error && ex) -{ -} - - -void CreateDerivationAndRealiseGoal::work() -{ - (this->*state)(); -} - - -void CreateDerivationAndRealiseGoal::addWantedOutputs(const OutputsSpec & outputs) -{ - /* If we already want all outputs, there is nothing to do. */ - auto newWanted = wantedOutputs.union_(outputs); - bool needRestart = !newWanted.isSubsetOf(wantedOutputs); - wantedOutputs = newWanted; - - if (!needRestart) return; - - if (!optDrvPath) - // haven't started steps where the outputs matter yet - return; - worker.makeDerivationGoal(*optDrvPath, outputs, buildMode); -} - - -void CreateDerivationAndRealiseGoal::getDerivation() -{ - trace("outer init"); - - /* The first thing to do is to make sure that the derivation - exists. If it doesn't, it may be created through a - substitute. */ - if (auto optDrvPath = [this]() -> std::optional { - if (buildMode != bmNormal) return std::nullopt; - - auto drvPath = StorePath::dummy; - try { - drvPath = resolveDerivedPath(worker.store, *drvReq); - } catch (MissingRealisation &) { - return std::nullopt; - } - return worker.evalStore.isValidPath(drvPath) || worker.store.isValidPath(drvPath) - ? std::optional { drvPath } - : std::nullopt; - }()) { - trace(fmt("already have drv '%s' for '%s', can go straight to building", - worker.store.printStorePath(*optDrvPath), - drvReq->to_string(worker.store))); - - loadAndBuildDerivation(); - } else { - trace("need to obtain drv we want to build"); - - addWaitee(worker.makeGoal(DerivedPath::fromSingle(*drvReq))); - - state = &CreateDerivationAndRealiseGoal::loadAndBuildDerivation; - if (waitees.empty()) work(); - } -} - - -void CreateDerivationAndRealiseGoal::loadAndBuildDerivation() -{ - trace("outer load and build derivation"); - - if (nrFailed != 0) { - amDone(ecFailed, Error("cannot build missing derivation '%s'", drvReq->to_string(worker.store))); - return; - } - - StorePath drvPath = resolveDerivedPath(worker.store, *drvReq); - /* Build this step! */ - concreteDrvGoal = worker.makeDerivationGoal(drvPath, wantedOutputs, buildMode); - addWaitee(upcast_goal(concreteDrvGoal)); - state = &CreateDerivationAndRealiseGoal::buildDone; - optDrvPath = std::move(drvPath); - if (waitees.empty()) work(); -} - - -void CreateDerivationAndRealiseGoal::buildDone() -{ - trace("outer build done"); - - buildResult = upcast_goal(concreteDrvGoal)->getBuildResult(DerivedPath::Built { - .drvPath = drvReq, - .outputs = wantedOutputs, - }); - - if (buildResult.success()) - amDone(ecSuccess); - else - amDone(ecFailed, Error("building '%s' failed", drvReq->to_string(worker.store))); -} - - -} diff --git a/src/libstore/build/create-derivation-and-realise-goal.hh b/src/libstore/build/create-derivation-and-realise-goal.hh deleted file mode 100644 index ca936fc95..000000000 --- a/src/libstore/build/create-derivation-and-realise-goal.hh +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "parsed-derivations.hh" -#include "lock.hh" -#include "store-api.hh" -#include "pathlocks.hh" -#include "goal.hh" - -namespace nix { - -struct DerivationGoal; - -/** - * This goal type is essentially the serial composition (like function - * composition) of a goal for getting a derivation, and then a - * `DerivationGoal` using the newly-obtained derivation. - * - * In the (currently experimental) general inductive case of derivations - * that are themselves build outputs, that first goal will be *another* - * `CreateDerivationAndRealiseGoal`. In the (much more common) base-case - * where the derivation has no provence and is just referred to by - * (content-addressed) store path, that first goal is a - * `SubstitutionGoal`. - * - * If we already have the derivation (e.g. if the evalutator has created - * the derivation locally and then instructured the store to build it), - * we can skip the first goal entirely as a small optimization. - */ -struct CreateDerivationAndRealiseGoal : public Goal -{ - /** - * How to obtain a store path of the derivation to build. - */ - ref drvReq; - - /** - * The path of the derivation, once obtained. - **/ - std::optional optDrvPath; - - /** - * The goal for the corresponding concrete derivation. - **/ - std::shared_ptr concreteDrvGoal; - - /** - * The specific outputs that we need to build. - */ - OutputsSpec wantedOutputs; - - typedef void (CreateDerivationAndRealiseGoal::*GoalState)(); - GoalState state; - - /** - * The final output paths of the build. - * - * - For input-addressed derivations, always the precomputed paths - * - * - For content-addressed derivations, calcuated from whatever the - * hash ends up being. (Note that fixed outputs derivations that - * produce the "wrong" output still install that data under its - * true content-address.) - */ - OutputPathMap finalOutputs; - - BuildMode buildMode; - - CreateDerivationAndRealiseGoal(ref drvReq, - const OutputsSpec & wantedOutputs, Worker & worker, - BuildMode buildMode = bmNormal); - virtual ~CreateDerivationAndRealiseGoal(); - - void timedOut(Error && ex) override; - - std::string key() override; - - void work() override; - - /** - * Add wanted outputs to an already existing derivation goal. - */ - void addWantedOutputs(const OutputsSpec & outputs); - - /** - * The states. - */ - void getDerivation(); - void loadAndBuildDerivation(); - void buildDone(); - - JobCategory jobCategory() const override { - return JobCategory::Administration; - }; -}; - -} diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 6472ecd99..83c0a3135 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -71,7 +71,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, , wantedOutputs(wantedOutputs) , buildMode(buildMode) { - state = &DerivationGoal::loadDerivation; + state = &DerivationGoal::getDerivation; name = fmt( "building of '%s' from .drv file", DerivedPath::Built { makeConstantStorePathRef(drvPath), wantedOutputs }.to_string(worker.store)); @@ -164,6 +164,24 @@ void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs) } +void DerivationGoal::getDerivation() +{ + trace("init"); + + /* The first thing to do is to make sure that the derivation + exists. If it doesn't, it may be created through a + substitute. */ + if (buildMode == bmNormal && worker.evalStore.isValidPath(drvPath)) { + loadDerivation(); + return; + } + + addWaitee(upcast_goal(worker.makePathSubstitutionGoal(drvPath))); + + state = &DerivationGoal::loadDerivation; +} + + void DerivationGoal::loadDerivation() { trace("loading derivation"); @@ -1498,24 +1516,23 @@ void DerivationGoal::waiteeDone(GoalPtr waitee, ExitCode result) if (!useDerivation) return; auto & fullDrv = *dynamic_cast(drv.get()); - std::optional info = tryGetConcreteDrvGoal(waitee); - if (!info) return; - const auto & [dg, drvReq] = *info; + auto * dg = dynamic_cast(&*waitee); + if (!dg) return; - auto * nodeP = fullDrv.inputDrvs.findSlot(drvReq.get()); + auto * nodeP = fullDrv.inputDrvs.findSlot(DerivedPath::Opaque { .path = dg->drvPath }); if (!nodeP) return; auto & outputs = nodeP->value; for (auto & outputName : outputs) { - auto buildResult = dg.get().getBuildResult(DerivedPath::Built { - .drvPath = makeConstantStorePathRef(dg.get().drvPath), + auto buildResult = dg->getBuildResult(DerivedPath::Built { + .drvPath = makeConstantStorePathRef(dg->drvPath), .outputs = OutputsSpec::Names { outputName }, }); if (buildResult.success()) { auto i = buildResult.builtOutputs.find(outputName); if (i != buildResult.builtOutputs.end()) inputDrvOutputs.insert_or_assign( - { dg.get().drvPath, outputName }, + { dg->drvPath, outputName }, i->second.outPath); } } diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index 62b122c27..ddb5ee1e3 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -52,10 +52,6 @@ struct InitialOutput { /** * A goal for building some or all of the outputs of a derivation. - * - * The derivation must already be present, either in the store in a drv - * or in memory. If the derivation itself needs to be gotten first, a - * `CreateDerivationAndRealiseGoal` goal must be used instead. */ struct DerivationGoal : public Goal { @@ -235,6 +231,7 @@ struct DerivationGoal : public Goal /** * The states. */ + void getDerivation(); void loadDerivation(); void haveDerivation(); void outputsSubstitutionTried(); diff --git a/src/libstore/build/entry-points.cc b/src/libstore/build/entry-points.cc index f0f0e5519..13ff22f45 100644 --- a/src/libstore/build/entry-points.cc +++ b/src/libstore/build/entry-points.cc @@ -1,6 +1,5 @@ #include "worker.hh" #include "substitution-goal.hh" -#include "create-derivation-and-realise-goal.hh" #include "derivation-goal.hh" #include "local-store.hh" @@ -16,7 +15,7 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod worker.run(goals); - StringSet failed; + StorePathSet failed; std::optional ex; for (auto & i : goals) { if (i->ex) { @@ -26,10 +25,10 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod ex = std::move(i->ex); } if (i->exitCode != Goal::ecSuccess) { - if (auto i2 = dynamic_cast(i.get())) - failed.insert(i2->drvReq->to_string(*this)); + if (auto i2 = dynamic_cast(i.get())) + failed.insert(i2->drvPath); else if (auto i2 = dynamic_cast(i.get())) - failed.insert(printStorePath(i2->storePath)); + failed.insert(i2->storePath); } } @@ -38,7 +37,7 @@ void Store::buildPaths(const std::vector & reqs, BuildMode buildMod throw std::move(*ex); } else if (!failed.empty()) { if (ex) logError(ex->info()); - throw Error(worker.failingExitStatus(), "build of %s failed", concatStringsSep(", ", quoteStrings(failed))); + throw Error(worker.failingExitStatus(), "build of %s failed", showPaths(failed)); } } diff --git a/src/libstore/build/goal.hh b/src/libstore/build/goal.hh index 01d3c3491..9af083230 100644 --- a/src/libstore/build/goal.hh +++ b/src/libstore/build/goal.hh @@ -49,16 +49,6 @@ enum struct JobCategory { * A substitution an arbitrary store object; it will use network resources. */ Substitution, - /** - * A goal that does no "real" work by itself, and just exists to depend on - * other goals which *do* do real work. These goals therefore are not - * limited. - * - * These goals cannot infinitely create themselves, so there is no risk of - * a "fork bomb" type situation (which would be a problem even though the - * goal do no real work) either. - */ - Administration, }; struct Goal : public std::enable_shared_from_this diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc index b4a634e7b..37cb86b91 100644 --- a/src/libstore/build/worker.cc +++ b/src/libstore/build/worker.cc @@ -2,7 +2,6 @@ #include "worker.hh" #include "substitution-goal.hh" #include "drv-output-substitution-goal.hh" -#include "create-derivation-and-realise-goal.hh" #include "local-derivation-goal.hh" #include "hook-instance.hh" @@ -42,24 +41,6 @@ Worker::~Worker() } -std::shared_ptr Worker::makeCreateDerivationAndRealiseGoal( - ref drvReq, - const OutputsSpec & wantedOutputs, - BuildMode buildMode) -{ - std::weak_ptr & goal_weak = outerDerivationGoals.ensureSlot(*drvReq).value; - std::shared_ptr goal = goal_weak.lock(); - if (!goal) { - goal = std::make_shared(drvReq, wantedOutputs, *this, buildMode); - goal_weak = goal; - wakeUp(goal); - } else { - goal->addWantedOutputs(wantedOutputs); - } - return goal; -} - - std::shared_ptr Worker::makeDerivationGoalCommon( const StorePath & drvPath, const OutputsSpec & wantedOutputs, @@ -130,7 +111,10 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) { return std::visit(overloaded { [&](const DerivedPath::Built & bfd) -> GoalPtr { - return makeCreateDerivationAndRealiseGoal(bfd.drvPath, bfd.outputs, buildMode); + if (auto bop = std::get_if(&*bfd.drvPath)) + return makeDerivationGoal(bop->path, bfd.outputs, buildMode); + else + throw UnimplementedError("Building dynamic derivations in one shot is not yet implemented."); }, [&](const DerivedPath::Opaque & bo) -> GoalPtr { return makePathSubstitutionGoal(bo.path, buildMode == bmRepair ? Repair : NoRepair); @@ -139,46 +123,24 @@ GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode) } -template -static void cullMap(std::map & goalMap, F f) -{ - for (auto i = goalMap.begin(); i != goalMap.end();) - if (!f(i->second)) - i = goalMap.erase(i); - else ++i; -} - - template static void removeGoal(std::shared_ptr goal, std::map> & goalMap) { /* !!! inefficient */ - cullMap(goalMap, [&](const std::weak_ptr & gp) -> bool { - return gp.lock() != goal; - }); -} - -template -static void removeGoal(std::shared_ptr goal, std::map>::ChildNode> & goalMap); - -template -static void removeGoal(std::shared_ptr goal, std::map>::ChildNode> & goalMap) -{ - /* !!! inefficient */ - cullMap(goalMap, [&](DerivedPathMap>::ChildNode & node) -> bool { - if (node.value.lock() == goal) - node.value.reset(); - removeGoal(goal, node.childMap); - return !node.value.expired() || !node.childMap.empty(); - }); + for (auto i = goalMap.begin(); + i != goalMap.end(); ) + if (i->second.lock() == goal) { + auto j = i; ++j; + goalMap.erase(i); + i = j; + } + else ++i; } void Worker::removeGoal(GoalPtr goal) { - if (auto drvGoal = std::dynamic_pointer_cast(goal)) - nix::removeGoal(drvGoal, outerDerivationGoals.map); - else if (auto drvGoal = std::dynamic_pointer_cast(goal)) + if (auto drvGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(drvGoal, derivationGoals); else if (auto subGoal = std::dynamic_pointer_cast(goal)) nix::removeGoal(subGoal, substitutionGoals); @@ -236,19 +198,8 @@ void Worker::childStarted(GoalPtr goal, const std::set & fds, child.respectTimeouts = respectTimeouts; children.emplace_back(child); if (inBuildSlot) { - switch (goal->jobCategory()) { - case JobCategory::Substitution: - nrSubstitutions++; - break; - case JobCategory::Build: - nrLocalBuilds++; - break; - case JobCategory::Administration: - /* Intentionally not limited, see docs */ - break; - default: - abort(); - } + if (goal->jobCategory() == JobCategory::Substitution) nrSubstitutions++; + else nrLocalBuilds++; } } @@ -260,20 +211,12 @@ void Worker::childTerminated(Goal * goal, bool wakeSleepers) if (i == children.end()) return; if (i->inBuildSlot) { - switch (goal->jobCategory()) { - case JobCategory::Substitution: + if (goal->jobCategory() == JobCategory::Substitution) { assert(nrSubstitutions > 0); nrSubstitutions--; - break; - case JobCategory::Build: + } else { assert(nrLocalBuilds > 0); nrLocalBuilds--; - break; - case JobCategory::Administration: - /* Intentionally not limited, see docs */ - break; - default: - abort(); } } @@ -324,9 +267,9 @@ void Worker::run(const Goals & _topGoals) for (auto & i : _topGoals) { topGoals.insert(i); - if (auto goal = dynamic_cast(i.get())) { + if (auto goal = dynamic_cast(i.get())) { topPaths.push_back(DerivedPath::Built { - .drvPath = goal->drvReq, + .drvPath = makeConstantStorePathRef(goal->drvPath), .outputs = goal->wantedOutputs, }); } else if (auto goal = dynamic_cast(i.get())) { @@ -589,19 +532,4 @@ GoalPtr upcast_goal(std::shared_ptr subGoal) return subGoal; } -GoalPtr upcast_goal(std::shared_ptr subGoal) -{ - return subGoal; -} - -std::optional, std::reference_wrapper>> tryGetConcreteDrvGoal(GoalPtr waitee) -{ - auto * odg = dynamic_cast(&*waitee); - if (!odg) return std::nullopt; - return {{ - std::cref(*odg->concreteDrvGoal), - std::cref(*odg->drvReq), - }}; -} - } diff --git a/src/libstore/build/worker.hh b/src/libstore/build/worker.hh index 6f6d25d7d..23ad87914 100644 --- a/src/libstore/build/worker.hh +++ b/src/libstore/build/worker.hh @@ -4,7 +4,6 @@ #include "types.hh" #include "lock.hh" #include "store-api.hh" -#include "derived-path-map.hh" #include "goal.hh" #include "realisation.hh" @@ -14,7 +13,6 @@ namespace nix { /* Forward definition. */ -struct CreateDerivationAndRealiseGoal; struct DerivationGoal; struct PathSubstitutionGoal; class DrvOutputSubstitutionGoal; @@ -33,25 +31,9 @@ class DrvOutputSubstitutionGoal; */ GoalPtr upcast_goal(std::shared_ptr subGoal); GoalPtr upcast_goal(std::shared_ptr subGoal); -GoalPtr upcast_goal(std::shared_ptr subGoal); typedef std::chrono::time_point steady_time_point; -/** - * The current implementation of impure derivations has - * `DerivationGoal`s accumulate realisations from their waitees. - * Unfortunately, `DerivationGoal`s don't directly depend on other - * goals, but instead depend on `CreateDerivationAndRealiseGoal`s. - * - * We try not to share any of the details of any goal type with any - * other, for sake of modularity and quicker rebuilds. This means we - * cannot "just" downcast and fish out the field. So as an escape hatch, - * we have made the function, written in `worker.cc` where all the goal - * types are visible, and use it instead. - */ - -std::optional, std::reference_wrapper>> tryGetConcreteDrvGoal(GoalPtr waitee); - /** * A mapping used to remember for each child process to what goal it * belongs, and file descriptors for receiving log data and output @@ -119,9 +101,6 @@ private: * Maps used to prevent multiple instantiations of a goal for the * same derivation / path. */ - - DerivedPathMap> outerDerivationGoals; - std::map> derivationGoals; std::map> substitutionGoals; std::map> drvOutputSubstitutionGoals; @@ -209,9 +188,6 @@ public: * @ref DerivationGoal "derivation goal" */ private: - std::shared_ptr makeCreateDerivationAndRealiseGoal( - ref drvPath, - const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal); std::shared_ptr makeDerivationGoalCommon( const StorePath & drvPath, const OutputsSpec & wantedOutputs, std::function()> mkDrvGoal); diff --git a/src/libstore/derived-path-map.cc b/src/libstore/derived-path-map.cc index 437b6a71a..5982c04b3 100644 --- a/src/libstore/derived-path-map.cc +++ b/src/libstore/derived-path-map.cc @@ -51,11 +51,8 @@ typename DerivedPathMap::ChildNode * DerivedPathMap::findSlot(const Single // instantiations -#include "create-derivation-and-realise-goal.hh" namespace nix { -template struct DerivedPathMap>; - GENERATE_CMP_EXT( template<>, DerivedPathMap>::ChildNode, diff --git a/src/libstore/derived-path-map.hh b/src/libstore/derived-path-map.hh index 4a2c90733..4d72b301e 100644 --- a/src/libstore/derived-path-map.hh +++ b/src/libstore/derived-path-map.hh @@ -20,11 +20,8 @@ namespace nix { * * @param V A type to instantiate for each output. It should probably * should be an "optional" type so not every interior node has to have a - * value. For example, the scheduler uses - * `DerivedPathMap>` to - * remember which goals correspond to which outputs. `* const Something` - * or `std::optional` would also be good choices for - * "optional" types. + * value. `* const Something` or `std::optional` would be + * good choices for "optional" types. */ template struct DerivedPathMap { diff --git a/tests/dyn-drv/build-built-drv.sh b/tests/dyn-drv/build-built-drv.sh index 94f3550bd..647be9457 100644 --- a/tests/dyn-drv/build-built-drv.sh +++ b/tests/dyn-drv/build-built-drv.sh @@ -18,6 +18,4 @@ clearStore drvDep=$(nix-instantiate ./text-hashed-output.nix -A producingDrv) -out2=$(nix build "${drvDep}^out^out" --no-link) - -test $out1 == $out2 +expectStderr 1 nix build "${drvDep}^out^out" --no-link | grepQuiet "Building dynamic derivations in one shot is not yet implemented" diff --git a/tests/dyn-drv/dep-built-drv.sh b/tests/dyn-drv/dep-built-drv.sh index c3daf2c59..4f6e9b080 100644 --- a/tests/dyn-drv/dep-built-drv.sh +++ b/tests/dyn-drv/dep-built-drv.sh @@ -6,6 +6,6 @@ out1=$(nix-build ./text-hashed-output.nix -A hello --no-out-link) clearStore -out2=$(nix-build ./text-hashed-output.nix -A wrapper --no-out-link) +expectStderr 1 nix-build ./text-hashed-output.nix -A wrapper --no-out-link | grepQuiet "Building dynamic derivations in one shot is not yet implemented" -diff -r $out1 $out2 +# diff -r $out1 $out2 From 632f24166d9d22e329d9d8626fa7dcd6b9ac83ce Mon Sep 17 00:00:00 2001 From: John Ericson Date: Tue, 26 Sep 2023 14:44:15 -0400 Subject: [PATCH 189/299] Test the rest of the worker protocol serializers Part of the `BuildResult` test is commented out because we have caught a roundtrip bug! A future PR will fix the bug and uncomment that test. --- src/libstore/build-result.cc | 18 ++ src/libstore/build-result.hh | 3 + src/libstore/tests/worker-protocol.cc | 189 ++++++++++++++++++ src/libstore/worker-protocol.hh | 4 +- .../libstore/worker-protocol/build-result.bin | Bin 0 -> 144 bytes .../libstore/worker-protocol/drv-output.bin | Bin 0 -> 176 bytes .../worker-protocol/keyed-build-result.bin | Bin 0 -> 264 bytes .../optional-content-address.bin | Bin 0 -> 64 bytes .../worker-protocol/optional-store-path.bin | Bin 0 -> 72 bytes .../worker-protocol/optional-trusted-flag.bin | Bin 0 -> 24 bytes .../libstore/worker-protocol/realisation.bin | Bin 0 -> 520 bytes .../libstore/worker-protocol/set.bin | Bin 0 -> 152 bytes .../libstore/worker-protocol/vector.bin | Bin 0 -> 152 bytes 13 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 src/libstore/build-result.cc create mode 100644 unit-test-data/libstore/worker-protocol/build-result.bin create mode 100644 unit-test-data/libstore/worker-protocol/drv-output.bin create mode 100644 unit-test-data/libstore/worker-protocol/keyed-build-result.bin create mode 100644 unit-test-data/libstore/worker-protocol/optional-content-address.bin create mode 100644 unit-test-data/libstore/worker-protocol/optional-store-path.bin create mode 100644 unit-test-data/libstore/worker-protocol/optional-trusted-flag.bin create mode 100644 unit-test-data/libstore/worker-protocol/realisation.bin create mode 100644 unit-test-data/libstore/worker-protocol/set.bin create mode 100644 unit-test-data/libstore/worker-protocol/vector.bin diff --git a/src/libstore/build-result.cc b/src/libstore/build-result.cc new file mode 100644 index 000000000..18f519c5c --- /dev/null +++ b/src/libstore/build-result.cc @@ -0,0 +1,18 @@ +#include "build-result.hh" + +namespace nix { + +GENERATE_CMP_EXT( + , + BuildResult, + me->status, + me->errorMsg, + me->timesBuilt, + me->isNonDeterministic, + me->builtOutputs, + me->startTime, + me->stopTime, + me->cpuUser, + me->cpuSystem); + +} diff --git a/src/libstore/build-result.hh b/src/libstore/build-result.hh index b7a56e791..8840fa7e3 100644 --- a/src/libstore/build-result.hh +++ b/src/libstore/build-result.hh @@ -3,6 +3,7 @@ #include "realisation.hh" #include "derived-path.hh" +#include "comparator.hh" #include #include @@ -100,6 +101,8 @@ struct BuildResult */ std::optional cpuUser, cpuSystem; + DECLARE_CMP(BuildResult); + bool success() { return status == Built || status == Substituted || status == AlreadyValid || status == ResolvesToAlreadyValid; diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index 4a6ccf7c0..24a06503c 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -6,6 +6,7 @@ #include "worker-protocol.hh" #include "worker-protocol-impl.hh" #include "derived-path.hh" +#include "build-result.hh" #include "tests/libstore.hh" namespace nix { @@ -136,4 +137,192 @@ CHARACTERIZATION_TEST( }, })) +CHARACTERIZATION_TEST( + drvOutput, + "drv-output", + (std::tuple { + { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "quux", + }, + })) + +CHARACTERIZATION_TEST( + realisation, + "realisation", + (std::tuple { + Realisation { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + .signatures = { "asdf", "qwer" }, + }, + Realisation { + .id = { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + .signatures = { "asdf", "qwer" }, + .dependentRealisations = { + { + DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "quux", + }, + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + }, + }, + })) + +CHARACTERIZATION_TEST( + buildResult, + "build-result", + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + BuildResult { + .status = BuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::NotDeterministic, + .errorMsg = "no idea why", + .timesBuilt = 3, + .isNonDeterministic = true, + .startTime = 30, + .stopTime = 50, + }, +#if 0 + // This is commented because this test would fail! + // FIXME uncomment this and fix the underlying bug. + BuildResult { + .status = BuildResult::Built, + .timesBuilt = 1, + .builtOutputs = { + { + "foo", + { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "foo", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + }, + { + "bar", + { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "bar", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar" }, + }, + }, + }, + .startTime = 30, + .stopTime = 50, + .cpuUser = std::chrono::milliseconds(500s), + .cpuSystem = std::chrono::milliseconds(604s), + }, +#endif + }; + t; + })) + +CHARACTERIZATION_TEST( + keyedBuildResult, + "keyed-build-result", + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + KeyedBuildResult { + { + .status = KeyedBuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + /* .path = */ DerivedPath::Opaque { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-xxx" }, + }, + }, + KeyedBuildResult { + { + .status = KeyedBuildResult::NotDeterministic, + .errorMsg = "no idea why", + .timesBuilt = 3, + .isNonDeterministic = true, + .startTime = 30, + .stopTime = 50, + }, + /* .path = */ DerivedPath::Built { + .drvPath = makeConstantStorePathRef(StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }), + .outputs = OutputsSpec::Names { "out" }, + }, + }, + }; + t; + })) + +CHARACTERIZATION_TEST( + optionalTrustedFlag, + "optional-trusted-flag", + (std::tuple, std::optional, std::optional> { + std::nullopt, + std::optional { Trusted }, + std::optional { NotTrusted }, + })) + +CHARACTERIZATION_TEST( + vector, + "vector", + (std::tuple, std::vector, std::vector, std::vector>> { + { }, + { "" }, + { "", "foo", "bar" }, + { {}, { "" }, { "", "1", "2" } }, + })) + +CHARACTERIZATION_TEST( + set, + "set", + (std::tuple, std::set, std::set, std::set>> { + { }, + { "" }, + { "", "foo", "bar" }, + { {}, { "" }, { "", "1", "2" } }, + })) + +CHARACTERIZATION_TEST( + optionalStorePath, + "optional-store-path", + (std::tuple, std::optional> { + std::nullopt, + std::optional { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, + }, + })) + +CHARACTERIZATION_TEST( + optionalContentAddress, + "optional-content-address", + (std::tuple, std::optional> { + std::nullopt, + std::optional { + ContentAddress { + .method = FileIngestionMethod::Flat, + .hash = hashString(HashType::htSHA1, "blob blob..."), + }, + }, + })) + } diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index 70a5bddb9..b7f42f24d 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -208,10 +208,10 @@ MAKE_WORKER_PROTO(ContentAddress); template<> MAKE_WORKER_PROTO(DerivedPath); template<> -MAKE_WORKER_PROTO(Realisation); -template<> MAKE_WORKER_PROTO(DrvOutput); template<> +MAKE_WORKER_PROTO(Realisation); +template<> MAKE_WORKER_PROTO(BuildResult); template<> MAKE_WORKER_PROTO(KeyedBuildResult); diff --git a/unit-test-data/libstore/worker-protocol/build-result.bin b/unit-test-data/libstore/worker-protocol/build-result.bin new file mode 100644 index 0000000000000000000000000000000000000000..f981fd91f7f32724dc2b9ce1938398aad3d73cdf GIT binary patch literal 144 tcmZQ&fB%U7Sq@}q)U|6h9n2j1!MT?w+C~^gO WO9=?G&ojrYjy`x4ZufnEe#tjMpDPdm literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/keyed-build-result.bin b/unit-test-data/libstore/worker-protocol/keyed-build-result.bin new file mode 100644 index 0000000000000000000000000000000000000000..c5b3c7f3669a906fef3ef450e8fc8cf8d87d1d04 GIT binary patch literal 264 zcmdOAfB^lx%nJSDlKi4n{dB`}^NdR4LR_?NMMVVzD^wphl+Mdn$V^F1R4C7=go+SH onM0M4Vt-O%kzPtsnPPrv2?GyQKQ4DLL**Hvv>cQ+g3>Sw03m8EMgRZ+ literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/optional-content-address.bin b/unit-test-data/libstore/worker-protocol/optional-content-address.bin new file mode 100644 index 0000000000000000000000000000000000000000..f8cfe65ba27fd78ec7787eed7ebec8dfdead2fba GIT binary patch literal 64 zcmZQzfB#ECTBU06lLKJOBUy literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/optional-trusted-flag.bin b/unit-test-data/libstore/worker-protocol/optional-trusted-flag.bin new file mode 100644 index 0000000000000000000000000000000000000000..51b239409bc815c19b00cb97f62996fb782f24c3 GIT binary patch literal 24 PcmZQzfB;4)%><3#3x z?rW}!>Uc!$VBY>Sr2mUC`<2<)qY;)1KN Po44)luetw6d9AunL$;dR literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/set.bin b/unit-test-data/libstore/worker-protocol/set.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce11ede7fe7aa061a30b211a18cd7a336f879de8 GIT binary patch literal 152 ucmZQzfB;4)4WpQ03@8obCnXkvMPU52{CpHXOdBEdVDg4g4KThDln(&+h63;a literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/vector.bin b/unit-test-data/libstore/worker-protocol/vector.bin new file mode 100644 index 0000000000000000000000000000000000000000..7a37c8cd1093201c56f384747ae9c2aa8aa700f9 GIT binary patch literal 152 ucmZQzfB;4)4WpQ03@8obr{(8^MPU4-#3B?vOdBEdVDg4g4KThDln(&-b^`DK literal 0 HcmV?d00001 From e1af175707007591113b0d6fc56036782b0e347c Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 4 Oct 2023 17:36:49 -0400 Subject: [PATCH 190/299] Enable most of the third `BuildResult` worker protocol test This was somewhat of a false alarm. The problem was not that the protocol implementation actually failed to round trip, but that two of the fields were ignored entirely --- not serialized and deserialized at all. For reference, those fields were added in fa68eb367e79297bb1c0451cd92ad18a06edce96. --- src/libstore/tests/worker-protocol.cc | 11 ++++++----- .../libstore/worker-protocol/build-result.bin | Bin 144 -> 744 bytes 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index 24a06503c..fa7cbe121 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -187,7 +187,7 @@ CHARACTERIZATION_TEST( "build-result", ({ using namespace std::literals::chrono_literals; - std::tuple t { + std::tuple t { BuildResult { .status = BuildResult::OutputRejected, .errorMsg = "no idea why", @@ -200,9 +200,6 @@ CHARACTERIZATION_TEST( .startTime = 30, .stopTime = 50, }, -#if 0 - // This is commented because this test would fail! - // FIXME uncomment this and fix the underlying bug. BuildResult { .status = BuildResult::Built, .timesBuilt = 1, @@ -230,10 +227,14 @@ CHARACTERIZATION_TEST( }, .startTime = 30, .stopTime = 50, +#if 0 + // These fields are not yet serialized. + // FIXME Include in next version of protocol or document + // why they are skipped. .cpuUser = std::chrono::milliseconds(500s), .cpuSystem = std::chrono::milliseconds(604s), - }, #endif + }, }; t; })) diff --git a/unit-test-data/libstore/worker-protocol/build-result.bin b/unit-test-data/libstore/worker-protocol/build-result.bin index f981fd91f7f32724dc2b9ce1938398aad3d73cdf..b02c706eab8de4d60eadd6afdcc2c888ab605ad3 100644 GIT binary patch literal 744 zcmc(b&uRiO5XSolPxTp!=B%)rxXHS&(2IC2JXe~O6CwQ^%woR?}>#sX4il$bfs;n zmv%`YOQ~vvTo;t-%xH@l(n4vSK8bRZQHc`kI^B)Ih0TkNGRhXy8rqZN5BnYj(ieFo zAJ+t*u7l`;??iPt&V)lzi91dfGZFf@g4iVAZN4+jUVRU7o>ol_o!fedeM@Pl_mAVf T^ROZOQyyvZZF!s Date: Thu, 7 Sep 2023 00:31:33 +0200 Subject: [PATCH 191/299] always show anchors on setting listings refactor the templates for readability --- doc/manual/generate-manpage.nix | 55 ++++++-------------------- doc/manual/generate-settings.nix | 63 ++++++++++++++++++++++++++++++ doc/manual/generate-store-info.nix | 45 +++++++++++++++++++++ doc/manual/local.mk | 6 +-- doc/manual/utils.nix | 57 --------------------------- 5 files changed, 124 insertions(+), 102 deletions(-) create mode 100644 doc/manual/generate-settings.nix create mode 100644 doc/manual/generate-store-info.nix diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 41725aed6..425eea002 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -2,7 +2,8 @@ let inherit (builtins) attrNames attrValues fromJSON listToAttrs mapAttrs groupBy concatStringsSep concatMap length lessThan replaceStrings sort; - inherit (import ./utils.nix) attrsToList concatStrings optionalString filterAttrs trim squash unique showSettings; + inherit (import ./utils.nix) attrsToList concatStrings optionalString filterAttrs trim squash unique; + showStoreDocs = import ./generate-store-info.nix; in commandDump: @@ -30,7 +31,7 @@ let ${maybeSubcommands} - ${maybeDocumentation} + ${maybeStoreDocs} ${maybeOptions} ''; @@ -40,15 +41,15 @@ let showArgument = arg: "*${arg.label}*" + optionalString (! arg ? arity) "..."; arguments = concatStringsSep " " (map showArgument args); in '' - `${command}` [*option*...] ${arguments} + `${command}` [*option*...] ${arguments} ''; maybeSubcommands = optionalString (details ? commands && details.commands != {}) - '' - where *subcommand* is one of the following: + '' + where *subcommand* is one of the following: - ${subcommands} - ''; + ${subcommands} + ''; subcommands = if length categories > 1 then listCategories @@ -70,10 +71,11 @@ let * [`${command} ${name}`](./${appendName filename name}.md) - ${subcmd.description} ''; - # TODO: move this confusing special case out of here when implementing #8496 - maybeDocumentation = optionalString - (details ? doc) - (replaceStrings ["@stores@"] [storeDocs] details.doc); + # FIXME: this is a hack. + # store parameters should not be part of command documentation to begin + # with, but instead be rendered on separate pages. + maybeStoreDocs = optionalString (details ? doc) + (replaceStrings [ "@stores@" ] [ (showStoreDocs commandInfo.stores) ] details.doc); maybeOptions = let allVisibleOptions = filterAttrs @@ -147,35 +149,4 @@ let " - [${page.command}](command-ref/new-cli/${page.name})"; in concatStringsSep "\n" (map showEntry manpages) + "\n"; - storeDocs = - let - showStore = name: { settings, doc, experimentalFeature }: - let - experimentalFeatureNote = optionalString (experimentalFeature != null) '' - > **Warning** - > This store is part of an - > [experimental feature](@docroot@/contributing/experimental-features.md). - - To use this store, you need to make sure the corresponding experimental feature, - [`${experimentalFeature}`](@docroot@/contributing/experimental-features.md#xp-feature-${experimentalFeature}), - is enabled. - For example, include the following in [`nix.conf`](@docroot@/command-ref/conf-file.md): - - ``` - extra-experimental-features = ${experimentalFeature} - ``` - ''; - in '' - ## ${name} - - ${doc} - - ${experimentalFeatureNote} - - **Settings**: - - ${showSettings { useAnchors = false; } settings} - ''; - in concatStrings (attrValues (mapAttrs showStore commandInfo.stores)); - in (listToAttrs manpages) // { "SUMMARY.md" = tableOfContents; } diff --git a/doc/manual/generate-settings.nix b/doc/manual/generate-settings.nix new file mode 100644 index 000000000..450771f73 --- /dev/null +++ b/doc/manual/generate-settings.nix @@ -0,0 +1,63 @@ +let + inherit (builtins) attrValues concatStringsSep isAttrs isBool mapAttrs; + inherit (import ./utils.nix) concatStrings indent optionalString squash; +in + +prefix: settingsInfo: + +let + + showSetting = prefix: setting: { description, documentDefault, defaultValue, aliases, value, experimentalFeature }: + let + result = squash '' + - [`${setting}`](#${prefix}-${setting}) + + ${indent " " body} + ''; + + # separate body to cleanly handle indentation + body = '' + ${description} + + ${experimentalFeatureNote} + + **Default:** ${showDefault documentDefault defaultValue} + + ${showAliases aliases} + ''; + + experimentalFeatureNote = optionalString (experimentalFeature != null) '' + > **Warning** + > This setting is part of an + > [experimental feature](@docroot@/contributing/experimental-features.md). + + To change this setting, you need to make sure the corresponding experimental feature, + [`${experimentalFeature}`](@docroot@/contributing/experimental-features.md#xp-feature-${experimentalFeature}), + is enabled. + For example, include the following in [`nix.conf`](#): + + ``` + extra-experimental-features = ${experimentalFeature} + ${setting} = ... + ``` + ''; + + showDefault = documentDefault: defaultValue: + if documentDefault then + # a StringMap value type is specified as a string, but + # this shows the value type. The empty stringmap is `null` in + # JSON, but that converts to `{ }` here. + if defaultValue == "" || defaultValue == [] || isAttrs defaultValue + then "*empty*" + else if isBool defaultValue then + if defaultValue then "`true`" else "`false`" + else "`${toString defaultValue}`" + else "*machine-specific*"; + + showAliases = aliases: + optionalString (aliases != []) + "**Deprecated alias:** ${(concatStringsSep ", " (map (s: "`${s}`") aliases))}"; + + in result; + +in concatStrings (attrValues (mapAttrs (showSetting prefix) settingsInfo)) diff --git a/doc/manual/generate-store-info.nix b/doc/manual/generate-store-info.nix new file mode 100644 index 000000000..7bb2ebad3 --- /dev/null +++ b/doc/manual/generate-store-info.nix @@ -0,0 +1,45 @@ +let + inherit (builtins) attrValues mapAttrs; + inherit (import ./utils.nix) concatStrings optionalString; + showSettings = import ./generate-settings.nix; +in + +storesInfo: + +let + + showStore = name: { settings, doc, experimentalFeature }: + let + + result = '' + ## ${name} + + ${doc} + + ${experimentalFeatureNote} + + ### Settings + + ${showSettings "store-${slug}" settings} + ''; + + # markdown doesn't like spaces in URLs + slug = builtins.replaceStrings [ " " ] [ "-" ] name; + + experimentalFeatureNote = optionalString (experimentalFeature != null) '' + > **Warning** + > This store is part of an + > [experimental feature](@docroot@/contributing/experimental-features.md). + + To use this store, you need to make sure the corresponding experimental feature, + [`${experimentalFeature}`](@docroot@/contributing/experimental-features.md#xp-feature-${experimentalFeature}), + is enabled. + For example, include the following in [`nix.conf`](#): + + ``` + extra-experimental-features = ${experimentalFeature} + ``` + ''; + in result; + +in concatStrings (attrValues (mapAttrs showStore storesInfo)) diff --git a/doc/manual/local.mk b/doc/manual/local.mk index abdfd6a62..927854066 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -96,14 +96,14 @@ $(d)/src/SUMMARY.md: $(d)/src/SUMMARY.md.in $(d)/src/command-ref/new-cli $(d)/sr @cp $< $@ @$(call process-includes,$@,$@) -$(d)/src/command-ref/new-cli: $(d)/nix.json $(d)/utils.nix $(d)/generate-manpage.nix $(bindir)/nix +$(d)/src/command-ref/new-cli: $(d)/nix.json $(d)/utils.nix $(d)/generate-manpage.nix $(d)/generate-settings.nix $(d)/generate-store-info.nix $(bindir)/nix @rm -rf $@ $@.tmp $(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-manpage.nix (builtins.readFile $<)' @mv $@.tmp $@ -$(d)/src/command-ref/conf-file.md: $(d)/conf-file.json $(d)/utils.nix $(d)/src/command-ref/conf-file-prefix.md $(d)/src/command-ref/experimental-features-shortlist.md $(bindir)/nix +$(d)/src/command-ref/conf-file.md: $(d)/conf-file.json $(d)/utils.nix $(d)/generate-settings.nix $(d)/src/command-ref/conf-file-prefix.md $(d)/src/command-ref/experimental-features-shortlist.md $(bindir)/nix @cat doc/manual/src/command-ref/conf-file-prefix.md > $@.tmp - $(trace-gen) $(nix-eval) --expr '(import doc/manual/utils.nix).showSettings { useAnchors = true; } (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp; + $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-settings.nix "opt-" (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp; @mv $@.tmp $@ $(d)/nix.json: $(bindir)/nix diff --git a/doc/manual/utils.nix b/doc/manual/utils.nix index 9043dd8cd..849832b2c 100644 --- a/doc/manual/utils.nix +++ b/doc/manual/utils.nix @@ -44,63 +44,6 @@ rec { optionalString = cond: string: if cond then string else ""; - showSetting = { useAnchors }: name: { description, documentDefault, defaultValue, aliases, value, experimentalFeature }: - let - result = squash '' - - ${if useAnchors - then ''[`${name}`](#conf-${name})'' - else ''`${name}`''} - - ${indent " " body} - ''; - - experimentalFeatureNote = optionalString (experimentalFeature != null) '' - > **Warning** - > This setting is part of an - > [experimental feature](@docroot@/contributing/experimental-features.md). - - To change this setting, you need to make sure the corresponding experimental feature, - [`${experimentalFeature}`](@docroot@/contributing/experimental-features.md#xp-feature-${experimentalFeature}), - is enabled. - For example, include the following in [`nix.conf`](#): - - ``` - extra-experimental-features = ${experimentalFeature} - ${name} = ... - ``` - ''; - - # separate body to cleanly handle indentation - body = '' - ${description} - - ${experimentalFeatureNote} - - **Default:** ${showDefault documentDefault defaultValue} - - ${showAliases aliases} - ''; - - showDefault = documentDefault: defaultValue: - if documentDefault then - # a StringMap value type is specified as a string, but - # this shows the value type. The empty stringmap is `null` in - # JSON, but that converts to `{ }` here. - if defaultValue == "" || defaultValue == [] || isAttrs defaultValue - then "*empty*" - else if isBool defaultValue then - if defaultValue then "`true`" else "`false`" - else "`${toString defaultValue}`" - else "*machine-specific*"; - - showAliases = aliases: - optionalString (aliases != []) - "**Deprecated alias:** ${(concatStringsSep ", " (map (s: "`${s}`") aliases))}"; - - in result; - indent = prefix: s: concatStringsSep "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s)); - - showSettings = args: settingsInfo: concatStrings (attrValues (mapAttrs (showSetting args) settingsInfo)); } From 8232711c9fa0ba831c3554e1d19e6707dd093c39 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 4 Oct 2023 23:07:13 +0200 Subject: [PATCH 192/299] fix wiring of baked-in Nix expressions --- src/nix/local.mk | 2 +- src/nix/main.cc | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/nix/local.mk b/src/nix/local.mk index 20ea29d10..57f8259c4 100644 --- a/src/nix/local.mk +++ b/src/nix/local.mk @@ -31,7 +31,7 @@ src/nix/develop.cc: src/nix/get-env.sh.gen.hh src/nix-channel/nix-channel.cc: src/nix-channel/unpack-channel.nix.gen.hh -src/nix/main.cc: doc/manual/generate-manpage.nix.gen.hh doc/manual/utils.nix.gen.hh +src/nix/main.cc: doc/manual/generate-manpage.nix.gen.hh doc/manual/utils.nix.gen.hh doc/manual/generate-settings.nix.gen.hh doc/manual/generate-store-info.nix.gen.hh src/nix/doc/files/%.md: doc/manual/src/command-ref/files/%.md @mkdir -p $$(dirname $@) diff --git a/src/nix/main.cc b/src/nix/main.cc index d05bac68e..031dc2348 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -214,6 +214,22 @@ static void showHelp(std::vector subcommand, NixArgs & toplevel) , CanonPath::root), *vUtils); + auto vSettingsInfo = state.allocValue(); + state.cacheFile( + CanonPath("/generate-settings.nix"), CanonPath("/generate-settings.nix"), + state.parseExprFromString( + #include "generate-settings.nix.gen.hh" + , CanonPath::root), + *vSettingsInfo); + + auto vStoreInfo = state.allocValue(); + state.cacheFile( + CanonPath("/generate-store-info.nix"), CanonPath("/generate-store-info.nix"), + state.parseExprFromString( + #include "generate-store-info.nix.gen.hh" + , CanonPath::root), + *vStoreInfo); + auto vDump = state.allocValue(); vDump->mkString(toplevel.dumpCli()); From 24bda0c7b381e1a017023c6f7cb9661fae8560bd Mon Sep 17 00:00:00 2001 From: edef Date: Wed, 4 Oct 2023 16:50:24 +0000 Subject: [PATCH 193/299] StorePath: reject names starting with '.' This has been the behaviour before Nix 2.4. It was dropped in a rewrite in 759947bf72c134592f0ce23d385e48095bd0a301, allowing the creation of store paths that aren't considered valid by older Nix versions or other Nix tooling. Nix 2.4 didn't ship in NixOS until 22.05, and stdenv.mkDerivation in nixpkgs drops leading periods since April 2022, so it's unlikely anyone is relying on the current lax behaviour. Closes #9091. Change-Id: I4a57bd9899e1b0dba56870ae5a1b680918a18ce9 --- src/libstore/path-regex.hh | 2 +- src/libstore/path.cc | 2 ++ src/libstore/tests/path.cc | 9 +++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libstore/path-regex.hh b/src/libstore/path-regex.hh index 4f8dc4c1f..a44e6a2eb 100644 --- a/src/libstore/path-regex.hh +++ b/src/libstore/path-regex.hh @@ -3,6 +3,6 @@ namespace nix { -static constexpr std::string_view nameRegexStr = R"([0-9a-zA-Z\+\-\._\?=]+)"; +static constexpr std::string_view nameRegexStr = R"([0-9a-zA-Z\+\-_\?=][0-9a-zA-Z\+\-\._\?=]*)"; } diff --git a/src/libstore/path.cc b/src/libstore/path.cc index 552e83114..3c6b9fc10 100644 --- a/src/libstore/path.cc +++ b/src/libstore/path.cc @@ -11,6 +11,8 @@ static void checkName(std::string_view path, std::string_view name) if (name.size() > StorePath::MaxPathLen) throw BadStorePath("store path '%s' has a name longer than %d characters", path, StorePath::MaxPathLen); + if (name[0] == '.') + throw BadStorePath("store path '%s' starts with illegal character '.'", path); // See nameRegexStr for the definition for (auto c : name) if (!((c >= '0' && c <= '9') diff --git a/src/libstore/tests/path.cc b/src/libstore/tests/path.cc index efa35ef2b..5a84d646c 100644 --- a/src/libstore/tests/path.cc +++ b/src/libstore/tests/path.cc @@ -39,6 +39,7 @@ TEST_DONT_PARSE(double_star, "**") TEST_DONT_PARSE(star_first, "*,foo") TEST_DONT_PARSE(star_second, "foo,*") TEST_DONT_PARSE(bang, "foo!o") +TEST_DONT_PARSE(dotfile, ".gitignore") #undef TEST_DONT_PARSE @@ -101,8 +102,12 @@ Gen Arbitrary::arbitrary() pre += '-'; break; case 64: - pre += '.'; - break; + // names aren't permitted to start with a period, + // so just fall through to the next case here + if (c != 0) { + pre += '.'; + break; + } case 65: pre += '_'; break; From e0e47c0a68f7e1b6b0f9e5065e8113b32b12c212 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 5 Oct 2023 01:20:26 +0200 Subject: [PATCH 194/299] accommodate inconsistent output from `lowdown` the `term` output mode leaves inline HTML around verbatim, while `nroff` mode (used for `man` pages) does not. the correct solution would be to pre-render all output with a more benign tool so we have less liabilities in our own code, but this has to do for now. --- doc/manual/generate-manpage.nix | 22 +++++++++++++--------- doc/manual/generate-settings.nix | 9 ++++++--- doc/manual/generate-store-info.nix | 4 ++-- doc/manual/local.mk | 4 ++-- src/nix/main.cc | 3 ++- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 425eea002..d81eac182 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -6,7 +6,7 @@ let showStoreDocs = import ./generate-store-info.nix; in -commandDump: +inlineHTML: commandDump: let @@ -75,7 +75,7 @@ let # store parameters should not be part of command documentation to begin # with, but instead be rendered on separate pages. maybeStoreDocs = optionalString (details ? doc) - (replaceStrings [ "@stores@" ] [ (showStoreDocs commandInfo.stores) ] details.doc); + (replaceStrings [ "@stores@" ] [ (showStoreDocs inlineHTML commandInfo.stores) ] details.doc); maybeOptions = let allVisibleOptions = filterAttrs @@ -84,14 +84,14 @@ let in optionalString (allVisibleOptions != {}) '' # Options - ${showOptions allVisibleOptions} + ${showOptions inlineHTML allVisibleOptions} > **Note** > > See [`man nix.conf`](@docroot@/command-ref/conf-file.md#command-line-flags) for overriding configuration settings with command line flags. ''; - showOptions = allOptions: + showOptions = inlineHTML: allOptions: let showCategory = cat: opts: '' ${optionalString (cat != "") "## ${cat}"} @@ -100,17 +100,21 @@ let ''; showOption = name: option: let + result = trim '' + - ${item} + + ${option.description} + ''; + item = if inlineHTML + then ''[`--${name}`](#opt-${name}) ${shortName} ${labels}'' + else "`--${name}` ${shortName} ${labels}"; shortName = optionalString (option ? shortName) ("/ `-${option.shortName}`"); labels = optionalString (option ? labels) (concatStringsSep " " (map (s: "*${s}*") option.labels)); - in trim '' - - [`--${name}`](#opt-${name}) ${shortName} ${labels} - - ${option.description} - ''; + in result; categories = mapAttrs # Convert each group from a list of key-value pairs back to an attrset (_: listToAttrs) diff --git a/doc/manual/generate-settings.nix b/doc/manual/generate-settings.nix index 450771f73..8736bb793 100644 --- a/doc/manual/generate-settings.nix +++ b/doc/manual/generate-settings.nix @@ -3,18 +3,21 @@ let inherit (import ./utils.nix) concatStrings indent optionalString squash; in -prefix: settingsInfo: +# `inlineHTML` is a hack to accommodate inconsistent output from `lowdown` +{ prefix, inlineHTML ? true }: settingsInfo: let showSetting = prefix: setting: { description, documentDefault, defaultValue, aliases, value, experimentalFeature }: let result = squash '' - - [`${setting}`](#${prefix}-${setting}) + - ${item} ${indent " " body} ''; - + item = if inlineHTML + then ''[`${setting}`](#${prefix}-${setting})'' + else "`${setting}`"; # separate body to cleanly handle indentation body = '' ${description} diff --git a/doc/manual/generate-store-info.nix b/doc/manual/generate-store-info.nix index 7bb2ebad3..36215aadf 100644 --- a/doc/manual/generate-store-info.nix +++ b/doc/manual/generate-store-info.nix @@ -4,7 +4,7 @@ let showSettings = import ./generate-settings.nix; in -storesInfo: +inlineHTML: storesInfo: let @@ -20,7 +20,7 @@ let ### Settings - ${showSettings "store-${slug}" settings} + ${showSettings { prefix = "store-${slug}"; inherit inlineHTML; } settings} ''; # markdown doesn't like spaces in URLs diff --git a/doc/manual/local.mk b/doc/manual/local.mk index 927854066..2d1db66f3 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -98,12 +98,12 @@ $(d)/src/SUMMARY.md: $(d)/src/SUMMARY.md.in $(d)/src/command-ref/new-cli $(d)/sr $(d)/src/command-ref/new-cli: $(d)/nix.json $(d)/utils.nix $(d)/generate-manpage.nix $(d)/generate-settings.nix $(d)/generate-store-info.nix $(bindir)/nix @rm -rf $@ $@.tmp - $(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-manpage.nix (builtins.readFile $<)' + $(trace-gen) $(nix-eval) --write-to $@.tmp --expr 'import doc/manual/generate-manpage.nix true (builtins.readFile $<)' @mv $@.tmp $@ $(d)/src/command-ref/conf-file.md: $(d)/conf-file.json $(d)/utils.nix $(d)/generate-settings.nix $(d)/src/command-ref/conf-file-prefix.md $(d)/src/command-ref/experimental-features-shortlist.md $(bindir)/nix @cat doc/manual/src/command-ref/conf-file-prefix.md > $@.tmp - $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-settings.nix "opt-" (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp; + $(trace-gen) $(nix-eval) --expr 'import doc/manual/generate-settings.nix { prefix = "opt-"; } (builtins.fromJSON (builtins.readFile $<))' >> $@.tmp; @mv $@.tmp $@ $(d)/nix.json: $(bindir)/nix diff --git a/src/nix/main.cc b/src/nix/main.cc index 031dc2348..879baa5f5 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -234,7 +234,8 @@ static void showHelp(std::vector subcommand, NixArgs & toplevel) vDump->mkString(toplevel.dumpCli()); auto vRes = state.allocValue(); - state.callFunction(*vGenerateManpage, *vDump, *vRes, noPos); + state.callFunction(*vGenerateManpage, state.getBuiltin("false"), *vRes, noPos); + state.callFunction(*vRes, *vDump, *vRes, noPos); auto attr = vRes->attrs->get(state.symbols.create(mdName + ".md")); if (!attr) From 80f734a803d3170eadd51758c71390ff87be22f9 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 5 Oct 2023 09:20:42 +0200 Subject: [PATCH 195/299] more specific links to nix.dev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1440239bc..65daa22e4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ for more details. ## Installation and first steps -Visit [nix.dev](https://nix.dev) for installation instructions and beginner tutorials. +Visit [nix.dev](https://nix.dev) for [installation instructions](https://nix.dev/tutorials/install-nix) and [beginner tutorials](https://nix.dev/tutorials/first-steps). Full reference documentation can be found in the [Nix manual](https://nixos.org/nix/manual). From eb68454be61bd6e8baf40136ae69c2b5fec3006a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?= Date: Wed, 4 Oct 2023 17:10:54 +0200 Subject: [PATCH 196/299] Don't run the tests that require building if we're not building A couple of tests require building some libraries that depend on Nix, and assume it to be built locally. Don't run these if we only want to run the install tests. This prevents the CI from rebuilding several times Nix (like in https://github.com/NixOS/nix/actions/runs/6404422275/job/17384964033#step:6:6412), thus removing a fair amount of build time. --- flake.nix | 1 + tests/local.mk | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/flake.nix b/flake.nix index e3b9477a5..d74085db7 100644 --- a/flake.nix +++ b/flake.nix @@ -260,6 +260,7 @@ testNixVersions = pkgs: client: daemon: with commonDeps { inherit pkgs; }; with pkgs.lib; pkgs.stdenv.mkDerivation { NIX_DAEMON_PACKAGE = daemon; NIX_CLIENT_PACKAGE = client; + HAVE_LOCAL_NIX_BUILD = false; name = "nix-tests" + optionalString diff --git a/tests/local.mk b/tests/local.mk index 7479b1576..ac418dc0d 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -1,3 +1,7 @@ +# whether to run the tests that assume that we have a local build of +# Nix +HAVE_LOCAL_NIX_BUILD ?= 1 + nix_tests = \ test-infra.sh \ init.sh \ @@ -104,7 +108,6 @@ nix_tests = \ case-hack.sh \ placeholders.sh \ ssh-relay.sh \ - plugins.sh \ build.sh \ build-delete.sh \ output-normalization.sh \ @@ -120,7 +123,6 @@ nix_tests = \ flakes/show.sh \ impure-derivations.sh \ path-from-hash-part.sh \ - test-libstoreconsumer.sh \ toString-path.sh \ read-only-store.sh \ nested-sandboxing.sh @@ -129,6 +131,17 @@ ifeq ($(HAVE_LIBCPUID), 1) nix_tests += compute-levels.sh endif +ifeq ($(HAVE_LOCAL_NIX_BUILD), 1) + nix_tests += test-libstoreconsumer.sh + + ifeq ($(BUILD_SHARED_LIBS), 1) + nix_tests += plugins.sh + endif +endif + +tests/test-libstoreconsumer.sh.test: tests/test-libstoreconsumer/test-libstoreconsumer +tests/plugins.sh: tests/plugins/libplugintest.$(SO_EXT) + install-tests += $(foreach x, $(nix_tests), $(d)/$(x)) clean-files += \ @@ -137,9 +150,4 @@ clean-files += \ test-deps += \ tests/common/vars-and-functions.sh \ - tests/config.nix \ - tests/test-libstoreconsumer/test-libstoreconsumer - -ifeq ($(BUILD_SHARED_LIBS), 1) - test-deps += tests/plugins/libplugintest.$(SO_EXT) -endif + tests/config.nix From 92e8e1b1bbc59d3a7e9efd5bff146b0a2726c7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?= Date: Wed, 4 Oct 2023 17:22:45 +0200 Subject: [PATCH 197/299] Poison the build on the test derivation Make sure that we're not accidentally rebuilding Nix here as it's just wasteful and awful for CI times. --- flake.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index d74085db7..4de155362 100644 --- a/flake.nix +++ b/flake.nix @@ -280,9 +280,13 @@ enableParallelBuilding = true; configureFlags = testConfigureFlags; # otherwise configure fails - dontBuild = true; doInstallCheck = true; + buildPhase = '' + # Remove the source files to make sure that we're not accidentally rebuilding Nix + rm src/**/*.cc + ''; + installPhase = '' mkdir -p $out ''; From f95364a803ddf694e4d7a7f6841101b93d9741fe Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 1 Sep 2023 12:20:54 +0200 Subject: [PATCH 198/299] eval: Run a full GC before printing stats This makes the numbers more deterministic, especially when it comes to the final heap size. --- src/libexpr/eval.cc | 212 +++++++++++++++++++++++++------------------- src/libexpr/eval.hh | 18 +++- 2 files changed, 136 insertions(+), 94 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index a78c0afb1..600444955 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2477,10 +2477,37 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v } } +bool EvalState::fullGC() { +#if HAVE_BOEHMGC + GC_gcollect(); + // Check that it ran. We might replace this with a version that uses more + // of the boehm API to get this reliably, at a maintenance cost. + // We use a 1K margin because technically this has a race condtion, but we + // probably won't encounter it in practice, because the CLI isn't concurrent + // like that. + return GC_get_bytes_since_gc() < 1024; +#else + return false; +#endif +} + void EvalState::printStats() { bool showStats = getEnv("NIX_SHOW_STATS").value_or("0") != "0"; + if (showStats) { + // Make the final heap size more deterministic. +#if HAVE_BOEHMGC + if (!fullGC()) { + warn("failed to perform a full GC before reporting stats"); + } +#endif + printStatistics(); + } +} + +void EvalState::printStatistics() +{ struct rusage buf; getrusage(RUSAGE_SELF, &buf); float cpuTime = buf.ru_utime.tv_sec + ((float) buf.ru_utime.tv_usec / 1000000); @@ -2494,106 +2521,105 @@ void EvalState::printStats() GC_word heapSize, totalBytes; GC_get_heap_usage_safe(&heapSize, 0, 0, 0, &totalBytes); #endif - if (showStats) { - auto outPath = getEnv("NIX_SHOW_STATS_PATH").value_or("-"); - std::fstream fs; - if (outPath != "-") - fs.open(outPath, std::fstream::out); - json topObj = json::object(); - topObj["cpuTime"] = cpuTime; - topObj["envs"] = { - {"number", nrEnvs}, - {"elements", nrValuesInEnvs}, - {"bytes", bEnvs}, - }; - topObj["nrExprs"] = Expr::nrExprs; - topObj["list"] = { - {"elements", nrListElems}, - {"bytes", bLists}, - {"concats", nrListConcats}, - }; - topObj["values"] = { - {"number", nrValues}, - {"bytes", bValues}, - }; - topObj["symbols"] = { - {"number", symbols.size()}, - {"bytes", symbols.totalSize()}, - }; - topObj["sets"] = { - {"number", nrAttrsets}, - {"bytes", bAttrsets}, - {"elements", nrAttrsInAttrsets}, - }; - topObj["sizes"] = { - {"Env", sizeof(Env)}, - {"Value", sizeof(Value)}, - {"Bindings", sizeof(Bindings)}, - {"Attr", sizeof(Attr)}, - }; - topObj["nrOpUpdates"] = nrOpUpdates; - topObj["nrOpUpdateValuesCopied"] = nrOpUpdateValuesCopied; - topObj["nrThunks"] = nrThunks; - topObj["nrAvoided"] = nrAvoided; - topObj["nrLookups"] = nrLookups; - topObj["nrPrimOpCalls"] = nrPrimOpCalls; - topObj["nrFunctionCalls"] = nrFunctionCalls; + + auto outPath = getEnv("NIX_SHOW_STATS_PATH").value_or("-"); + std::fstream fs; + if (outPath != "-") + fs.open(outPath, std::fstream::out); + json topObj = json::object(); + topObj["cpuTime"] = cpuTime; + topObj["envs"] = { + {"number", nrEnvs}, + {"elements", nrValuesInEnvs}, + {"bytes", bEnvs}, + }; + topObj["nrExprs"] = Expr::nrExprs; + topObj["list"] = { + {"elements", nrListElems}, + {"bytes", bLists}, + {"concats", nrListConcats}, + }; + topObj["values"] = { + {"number", nrValues}, + {"bytes", bValues}, + }; + topObj["symbols"] = { + {"number", symbols.size()}, + {"bytes", symbols.totalSize()}, + }; + topObj["sets"] = { + {"number", nrAttrsets}, + {"bytes", bAttrsets}, + {"elements", nrAttrsInAttrsets}, + }; + topObj["sizes"] = { + {"Env", sizeof(Env)}, + {"Value", sizeof(Value)}, + {"Bindings", sizeof(Bindings)}, + {"Attr", sizeof(Attr)}, + }; + topObj["nrOpUpdates"] = nrOpUpdates; + topObj["nrOpUpdateValuesCopied"] = nrOpUpdateValuesCopied; + topObj["nrThunks"] = nrThunks; + topObj["nrAvoided"] = nrAvoided; + topObj["nrLookups"] = nrLookups; + topObj["nrPrimOpCalls"] = nrPrimOpCalls; + topObj["nrFunctionCalls"] = nrFunctionCalls; #if HAVE_BOEHMGC - topObj["gc"] = { - {"heapSize", heapSize}, - {"totalBytes", totalBytes}, - }; + topObj["gc"] = { + {"heapSize", heapSize}, + {"totalBytes", totalBytes}, + }; #endif - if (countCalls) { - topObj["primops"] = primOpCalls; - { - auto& list = topObj["functions"]; - list = json::array(); - for (auto & [fun, count] : functionCalls) { - json obj = json::object(); - if (fun->name) - obj["name"] = (std::string_view) symbols[fun->name]; - else - obj["name"] = nullptr; - if (auto pos = positions[fun->pos]) { - if (auto path = std::get_if(&pos.origin)) - obj["file"] = path->to_string(); - obj["line"] = pos.line; - obj["column"] = pos.column; - } - obj["count"] = count; - list.push_back(obj); - } - } - { - auto list = topObj["attributes"]; - list = json::array(); - for (auto & i : attrSelects) { - json obj = json::object(); - if (auto pos = positions[i.first]) { - if (auto path = std::get_if(&pos.origin)) - obj["file"] = path->to_string(); - obj["line"] = pos.line; - obj["column"] = pos.column; - } - obj["count"] = i.second; - list.push_back(obj); + if (countCalls) { + topObj["primops"] = primOpCalls; + { + auto& list = topObj["functions"]; + list = json::array(); + for (auto & [fun, count] : functionCalls) { + json obj = json::object(); + if (fun->name) + obj["name"] = (std::string_view) symbols[fun->name]; + else + obj["name"] = nullptr; + if (auto pos = positions[fun->pos]) { + if (auto path = std::get_if(&pos.origin)) + obj["file"] = path->to_string(); + obj["line"] = pos.line; + obj["column"] = pos.column; } + obj["count"] = count; + list.push_back(obj); } } + { + auto list = topObj["attributes"]; + list = json::array(); + for (auto & i : attrSelects) { + json obj = json::object(); + if (auto pos = positions[i.first]) { + if (auto path = std::get_if(&pos.origin)) + obj["file"] = path->to_string(); + obj["line"] = pos.line; + obj["column"] = pos.column; + } + obj["count"] = i.second; + list.push_back(obj); + } + } + } - if (getEnv("NIX_SHOW_SYMBOLS").value_or("0") != "0") { - // XXX: overrides earlier assignment - topObj["symbols"] = json::array(); - auto &list = topObj["symbols"]; - symbols.dump([&](const std::string & s) { list.emplace_back(s); }); - } - if (outPath == "-") { - std::cerr << topObj.dump(2) << std::endl; - } else { - fs << topObj.dump(2) << std::endl; - } + if (getEnv("NIX_SHOW_SYMBOLS").value_or("0") != "0") { + // XXX: overrides earlier assignment + topObj["symbols"] = json::array(); + auto &list = topObj["symbols"]; + symbols.dump([&](const std::string & s) { list.emplace_back(s); }); + } + if (outPath == "-") { + std::cerr << topObj.dump(2) << std::endl; + } else { + fs << topObj.dump(2) << std::endl; } } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index fa8fa462b..3bfa34ef6 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -709,10 +709,26 @@ public: void concatLists(Value & v, size_t nrLists, Value * * lists, const PosIdx pos, std::string_view errorCtx); /** - * Print statistics. + * Print statistics, if enabled. + * + * Performs a full memory GC before printing the statistics, so that the + * GC statistics are more accurate. */ void printStats(); + /** + * Print statistics, unconditionally, cheaply, without performing a GC first. + */ + void printStatistics(); + + /** + * Perform a full memory garbage collection - not incremental. + * + * @return true if Nix was built with GC and a GC was performed, false if not. + * The return value is currently not thread safe - just the return value. + */ + bool fullGC(); + /** * Realise the given context, and return a mapping from the placeholders * used to construct the associated value to their final store path From 369b076986531f3fbf6933539a0379b5d3fb1970 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 6 Oct 2023 11:46:41 +0200 Subject: [PATCH 199/299] add links and anchors --- doc/manual/src/command-ref/nix-env/install.md | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/doc/manual/src/command-ref/nix-env/install.md b/doc/manual/src/command-ref/nix-env/install.md index 95648e9e0..c1fff50e8 100644 --- a/doc/manual/src/command-ref/nix-env/install.md +++ b/doc/manual/src/command-ref/nix-env/install.md @@ -14,19 +14,21 @@ # Description -The install operation creates a new user environment, based on the -current generation of the active profile, to which a set of store paths -described by *args* is added. The arguments *args* map to store paths in -a number of possible ways: +The install operation creates a new user environment. +It is based on the current generation of the active [profile](@docroot@/command-ref/files/profiles.md), to which a set of [store paths] described by *args* is added. + +[store paths]: @docroot@/glossary.md#gloss-store-path + +The arguments *args* map to store paths in a number of possible ways: - - By default, *args* is a set of [derivation] names denoting derivations - in the active Nix expression. These are realised, and the resulting - output paths are installed. Currently installed derivations with a - name equal to the name of a derivation being added are removed - unless the option `--preserve-installed` is specified. + - By default, *args* is a set of [derivation] names denoting derivations in the [default Nix expression]. + These are [realised], and the resulting output paths are installed. + Currently installed derivations with a name equal to the name of a derivation being added are removed unless the option `--preserve-installed` is specified. - [derivation]: @docroot@/language/derivations.md + [derivation]: @docroot@/glossary.md#gloss-derivation + [default Nix expression]: @docroot@/command-ref/files/default-nix-expression.md + [realised]: @docroot@/glossary.md#gloss-realise If there are multiple derivations matching a name in *args* that have the same name (e.g., `gcc-3.3.6` and `gcc-4.1.1`), then the @@ -43,40 +45,33 @@ a number of possible ways: gcc-3.3.6 gcc-4.1.1` will install both version of GCC (and will probably cause a user environment conflict\!). - - If `--attr` (`-A`) is specified, the arguments are *attribute - paths* that select attributes from the top-level Nix - expression. This is faster than using derivation names and - unambiguous. To find out the attribute paths of available - packages, use `nix-env --query --available --attr-path `. + - If [`--attr`](#opt-attr) / `-A` is specified, the arguments are *attribute paths* that select attributes from the [default Nix expression]. + This is faster than using derivation names and unambiguous. + Show the attribute paths of available packages with [`nix-env --query`](./query.md): + + ```console + nix-env --query --available --attr-path` + ``` - If `--from-profile` *path* is given, *args* is a set of names - denoting installed store paths in the profile *path*. This is an + denoting installed [store paths] in the profile *path*. This is an easy way to copy user environment elements from one profile to another. - - If `--from-expression` is given, *args* are Nix - [functions](@docroot@/language/constructs.md#functions) - that are called with the active Nix expression as their single - argument. The derivations returned by those function calls are - installed. This allows derivations to be specified in an - unambiguous way, which is necessary if there are multiple - derivations with the same name. + - If `--from-expression` is given, *args* are [Nix language functions](@docroot@/language/constructs.md#functions) that are called with the [default Nix expression] as their single argument. + The derivations returned by those function calls are installed. + This allows derivations to be specified in an unambiguous way, which is necessary if there are multiple derivations with the same name. - - If *args* are [store derivations](@docroot@/glossary.md#gloss-store-derivation), then these are - [realised](@docroot@/command-ref/nix-store/realise.md), and the resulting output paths - are installed. + - If *args* are [store derivations](@docroot@/glossary.md#gloss-store-derivation), then these are [realised], and the resulting output paths are installed. - - If *args* are store paths that are not store derivations, then these - are [realised](@docroot@/command-ref/nix-store/realise.md) and installed. + - If *args* are [store paths] that are not store derivations, then these are [realised] and installed. - - By default all outputs are installed for each derivation. + - By default all [outputs](@docroot@/language/derivations.md#attr-outputs) are installed for each [derivation]. This can be overridden by adding a `meta.outputsToInstall` attribute on the derivation listing a subset of the output names. - - Example: - The file `example.nix` defines a [derivation] with two outputs `foo` and `bar`, each containing a file. + The file `example.nix` defines a derivation with two outputs `foo` and `bar`, each containing a file. ```nix # example.nix @@ -123,15 +118,17 @@ a number of possible ways: manifest.nix ``` -# Flags +# Options + + - `--prebuilt-only` / `-b` - - `--prebuilt-only` / `-b`\ Use only derivations for which a substitute is registered, i.e., there is a pre-built binary available that can be downloaded in lieu of building the derivation. Thus, no packages will be built from source. - - `--preserve-installed` / `-P`\ + - `--preserve-installed` / `-P` + Do not remove derivations with a name matching one of the derivations being installed. Usually, trying to have two versions of the same package installed in the same generation of a profile will @@ -139,7 +136,8 @@ a number of possible ways: clashes between the two versions. However, this is not the case for all packages. - - `--remove-all` / `-r`\ + - `--remove-all` / `-r` + Remove all previously installed packages first. This is equivalent to running `nix-env --uninstall '.*'` first, except that everything happens in a single transaction. From 68c81c737571794f7246db53fb4774e94fcf4b7e Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 5 Oct 2023 12:12:18 -0400 Subject: [PATCH 200/299] Put functional tests in `tests/functional` I think it is bad for these reasons when `tests/` contains a mix of functional and integration tests - Concepts is harder to understand, the documentation makes a good unit vs functional vs integration distinction, but when the integration tests are just two subdirs within `tests/` this is not clear. - Source filtering in the `flake.nix` is more complex. We need to filter out some of the dirs from `tests/`, rather than simply pick the dirs we want and take all of them. This is a good sign the structure of what we are trying to do is not matching the structure of the files. With this change we have a clean: ```shell-session $ git show 'HEAD:tests' tree HEAD:tests functional/ installer/ nixos/ ``` --- .github/labeler.yml | 2 +- .gitignore | 34 ++++++------ CONTRIBUTING.md | 2 +- Makefile | 10 ++-- doc/manual/src/contributing/testing.md | 26 ++++----- flake.nix | 52 ++++++++---------- mk/common-test.sh | 8 ++- src/libstore/gc.cc | 2 +- src/libutil/url-parts.hh | 2 +- tests/{ => functional}/add.sh | 0 tests/{ => functional}/bad.tar.xz | Bin tests/{ => functional}/bash-profile.sh | 2 +- .../{ => functional}/big-derivation-attr.nix | 0 .../binary-cache-build-remote.sh | 0 tests/{ => functional}/binary-cache.sh | 0 tests/{ => functional}/brotli.sh | 0 tests/{ => functional}/build-delete.sh | 0 tests/{ => functional}/build-dry.sh | 0 .../{ => functional}/build-hook-ca-fixed.nix | 0 .../build-hook-ca-floating.nix | 0 tests/{ => functional}/build-hook.nix | 0 .../build-remote-content-addressed-fixed.sh | 0 ...build-remote-content-addressed-floating.sh | 0 .../build-remote-input-addressed.sh | 0 .../build-remote-trustless-after.sh | 0 .../build-remote-trustless-should-fail-0.sh | 0 .../build-remote-trustless-should-pass-0.sh | 0 .../build-remote-trustless-should-pass-1.sh | 0 .../build-remote-trustless-should-pass-2.sh | 0 .../build-remote-trustless-should-pass-3.sh | 0 .../build-remote-trustless.sh | 2 +- tests/{ => functional}/build-remote.sh | 0 tests/{ => functional}/build.sh | 0 tests/{ => functional}/ca-shell.nix | 0 tests/{ => functional}/ca/build-cache.sh | 0 tests/{ => functional}/ca/build-dry.sh | 0 .../ca/build-with-garbage-path.sh | 0 tests/{ => functional}/ca/build.sh | 0 tests/{ => functional}/ca/common.sh | 0 .../{ => functional}/ca/concurrent-builds.sh | 0 tests/{ => functional}/ca/config.nix.in | 0 .../{ => functional}/ca/content-addressed.nix | 0 tests/{ => functional}/ca/derivation-json.sh | 0 .../ca/duplicate-realisation-in-closure.sh | 0 tests/{ => functional}/ca/flake.nix | 0 tests/{ => functional}/ca/gc.sh | 0 .../{ => functional}/ca/import-derivation.sh | 0 tests/{ => functional}/ca/local.mk | 2 +- tests/{ => functional}/ca/new-build-cmd.sh | 0 tests/{ => functional}/ca/nix-copy.sh | 0 tests/{ => functional}/ca/nix-run.sh | 0 tests/{ => functional}/ca/nix-shell.sh | 0 .../{ => functional}/ca/nondeterministic.nix | 0 tests/{ => functional}/ca/post-hook.sh | 0 tests/{ => functional}/ca/racy.nix | 0 tests/{ => functional}/ca/recursive.sh | 0 tests/{ => functional}/ca/repl.sh | 0 tests/{ => functional}/ca/selfref-gc.sh | 0 tests/{ => functional}/ca/signatures.sh | 0 tests/{ => functional}/ca/substitute.sh | 0 tests/{ => functional}/ca/why-depends.sh | 0 tests/{ => functional}/case-hack.sh | 0 tests/{ => functional}/case.nar | Bin tests/{ => functional}/check-refs.nix | 0 tests/{ => functional}/check-refs.sh | 0 tests/{ => functional}/check-reqs.nix | 0 tests/{ => functional}/check-reqs.sh | 0 tests/{ => functional}/check.nix | 0 tests/{ => functional}/check.sh | 0 tests/{ => functional}/common.sh | 0 .../common/vars-and-functions.sh.in | 2 +- tests/{ => functional}/completions.sh | 0 tests/{ => functional}/compression-levels.sh | 0 tests/{ => functional}/compute-levels.sh | 0 tests/{ => functional}/config.nix.in | 0 tests/{ => functional}/config.sh | 0 .../config/nix-with-substituters.conf | 0 tests/{ => functional}/db-migration.sh | 0 .../{ => functional}/dependencies.builder0.sh | 0 tests/{ => functional}/dependencies.nix | 0 tests/{ => functional}/dependencies.sh | 0 tests/{ => functional}/derivation-json.sh | 0 tests/{ => functional}/dummy | 0 tests/{ => functional}/dump-db.sh | 0 .../dyn-drv/build-built-drv.sh | 0 tests/{ => functional}/dyn-drv/common.sh | 0 tests/{ => functional}/dyn-drv/config.nix.in | 0 .../{ => functional}/dyn-drv/dep-built-drv.sh | 0 .../{ => functional}/dyn-drv/eval-outputOf.sh | 0 tests/{ => functional}/dyn-drv/local.mk | 2 +- .../dyn-drv/old-daemon-error-hack.nix | 0 .../dyn-drv/old-daemon-error-hack.sh | 0 .../dyn-drv/recursive-mod-json.nix | 0 .../dyn-drv/recursive-mod-json.sh | 0 .../dyn-drv/text-hashed-output.nix | 0 .../dyn-drv/text-hashed-output.sh | 0 tests/{ => functional}/eval-store.sh | 0 tests/{ => functional}/eval.nix | 0 tests/{ => functional}/eval.sh | 0 .../{ => functional}/experimental-features.sh | 0 tests/{ => functional}/export-graph.nix | 0 tests/{ => functional}/export-graph.sh | 0 tests/{ => functional}/export.sh | 0 tests/{ => functional}/failing.nix | 0 tests/{ => functional}/fetchClosure.sh | 0 tests/{ => functional}/fetchGit.sh | 0 tests/{ => functional}/fetchGitRefs.sh | 0 tests/{ => functional}/fetchGitSubmodules.sh | 0 tests/{ => functional}/fetchMercurial.sh | 0 tests/{ => functional}/fetchPath.sh | 0 tests/{ => functional}/fetchTree-file.sh | 0 tests/{ => functional}/fetchurl.sh | 0 tests/{ => functional}/filter-source.nix | 0 tests/{ => functional}/filter-source.sh | 0 tests/{ => functional}/fixed.builder1.sh | 0 tests/{ => functional}/fixed.builder2.sh | 0 tests/{ => functional}/fixed.nix | 0 tests/{ => functional}/fixed.sh | 0 .../flakes/absolute-attr-paths.sh | 0 .../{ => functional}/flakes/absolute-paths.sh | 0 tests/{ => functional}/flakes/build-paths.sh | 0 tests/{ => functional}/flakes/bundle.sh | 0 tests/{ => functional}/flakes/check.sh | 0 tests/{ => functional}/flakes/circular.sh | 0 tests/{ => functional}/flakes/common.sh | 0 tests/{ => functional}/flakes/config.sh | 0 .../flakes/flake-in-submodule.sh | 0 tests/{ => functional}/flakes/flakes.sh | 0 tests/{ => functional}/flakes/follow-paths.sh | 0 tests/{ => functional}/flakes/init.sh | 0 tests/{ => functional}/flakes/inputs.sh | 0 tests/{ => functional}/flakes/mercurial.sh | 0 tests/{ => functional}/flakes/run.sh | 0 tests/{ => functional}/flakes/search-root.sh | 0 tests/{ => functional}/flakes/show.sh | 0 .../flakes/unlocked-override.sh | 0 tests/{ => functional}/fmt.sh | 0 tests/{ => functional}/fmt.simple.sh | 0 tests/{ => functional}/function-trace.sh | 0 tests/{ => functional}/gc-auto.sh | 0 .../{ => functional}/gc-concurrent.builder.sh | 0 tests/{ => functional}/gc-concurrent.nix | 0 tests/{ => functional}/gc-concurrent.sh | 0 .../gc-concurrent2.builder.sh | 0 tests/{ => functional}/gc-non-blocking.sh | 0 tests/{ => functional}/gc-runtime.nix | 0 tests/{ => functional}/gc-runtime.sh | 0 tests/{ => functional}/gc.sh | 0 tests/{ => functional}/hash-check.nix | 0 tests/{ => functional}/hash.sh | 0 tests/{ => functional}/hermetic.nix | 0 tests/{ => functional}/import-derivation.nix | 0 tests/{ => functional}/import-derivation.sh | 0 tests/{ => functional}/impure-derivations.nix | 0 tests/{ => functional}/impure-derivations.sh | 0 tests/{ => functional}/init.sh | 0 tests/{ => functional}/install-darwin.sh | 0 tests/{ => functional}/lang-test-infra.sh | 0 tests/{ => functional}/lang.sh | 2 +- tests/{ => functional}/lang/binary-data | Bin tests/{ => functional}/lang/data | 0 tests/{ => functional}/lang/dir1/a.nix | 0 tests/{ => functional}/lang/dir2/a.nix | 0 tests/{ => functional}/lang/dir2/b.nix | 0 tests/{ => functional}/lang/dir3/a.nix | 0 tests/{ => functional}/lang/dir3/b.nix | 0 tests/{ => functional}/lang/dir3/c.nix | 0 tests/{ => functional}/lang/dir4/a.nix | 0 tests/{ => functional}/lang/dir4/c.nix | 0 tests/{ => functional}/lang/empty.exp | 0 .../lang/eval-fail-abort.err.exp | 0 .../{ => functional}/lang/eval-fail-abort.nix | 0 .../lang/eval-fail-antiquoted-path.err.exp | 0 .../lang/eval-fail-assert.err.exp | 0 .../lang/eval-fail-assert.nix | 0 .../lang/eval-fail-bad-antiquote-1.err.exp | 0 .../lang/eval-fail-bad-antiquote-2.err.exp | 0 .../lang/eval-fail-bad-antiquote-3.err.exp | 0 ...al-fail-bad-string-interpolation-1.err.exp | 0 .../eval-fail-bad-string-interpolation-1.nix | 0 ...al-fail-bad-string-interpolation-2.err.exp | 0 .../eval-fail-bad-string-interpolation-2.nix | 0 ...al-fail-bad-string-interpolation-3.err.exp | 0 .../eval-fail-bad-string-interpolation-3.nix | 0 .../lang/eval-fail-blackhole.err.exp | 0 .../lang/eval-fail-blackhole.nix | 0 .../lang/eval-fail-deepseq.err.exp | 0 .../lang/eval-fail-deepseq.nix | 0 .../lang/eval-fail-dup-dynamic-attrs.err.exp | 0 .../lang/eval-fail-dup-dynamic-attrs.nix | 0 ...-foldlStrict-strict-op-application.err.exp | 0 ...fail-foldlStrict-strict-op-application.nix | 0 .../eval-fail-fromTOML-timestamps.err.exp | 0 .../lang/eval-fail-fromTOML-timestamps.nix | 0 .../lang/eval-fail-hashfile-missing.err.exp | 0 .../lang/eval-fail-hashfile-missing.nix | 0 .../lang/eval-fail-list.err.exp | 0 .../{ => functional}/lang/eval-fail-list.nix | 0 .../lang/eval-fail-missing-arg.err.exp | 0 .../lang/eval-fail-missing-arg.nix | 0 .../lang/eval-fail-nonexist-path.err.exp | 0 .../lang/eval-fail-nonexist-path.nix | 0 .../lang/eval-fail-path-slash.err.exp | 0 .../lang/eval-fail-path-slash.nix | 0 .../lang/eval-fail-recursion.err.exp | 0 .../lang/eval-fail-recursion.nix | 0 .../lang/eval-fail-remove.err.exp | 0 .../lang/eval-fail-remove.nix | 0 .../lang/eval-fail-scope-5.err.exp | 0 .../lang/eval-fail-scope-5.nix | 0 .../lang/eval-fail-seq.err.exp | 0 tests/{ => functional}/lang/eval-fail-seq.nix | 0 .../lang/eval-fail-set-override.err.exp | 0 .../lang/eval-fail-set-override.nix | 0 .../lang/eval-fail-set.err.exp | 0 tests/{ => functional}/lang/eval-fail-set.nix | 0 .../lang/eval-fail-substring.err.exp | 0 .../lang/eval-fail-substring.nix | 0 .../lang/eval-fail-to-path.err.exp | 0 .../lang/eval-fail-to-path.nix | 0 .../lang/eval-fail-toJSON.err.exp | 0 .../lang/eval-fail-toJSON.nix | 0 .../lang/eval-fail-undeclared-arg.err.exp | 0 .../lang/eval-fail-undeclared-arg.nix | 0 .../lang/eval-okay-any-all.exp | 0 .../lang/eval-okay-any-all.nix | 0 .../lang/eval-okay-arithmetic.exp | 0 .../lang/eval-okay-arithmetic.nix | 0 .../lang/eval-okay-attrnames.exp | 0 .../lang/eval-okay-attrnames.nix | 0 .../{ => functional}/lang/eval-okay-attrs.exp | 0 .../{ => functional}/lang/eval-okay-attrs.nix | 0 .../lang/eval-okay-attrs2.exp | 0 .../lang/eval-okay-attrs2.nix | 0 .../lang/eval-okay-attrs3.exp | 0 .../lang/eval-okay-attrs3.nix | 0 .../lang/eval-okay-attrs4.exp | 0 .../lang/eval-okay-attrs4.nix | 0 .../lang/eval-okay-attrs5.exp | 0 .../lang/eval-okay-attrs5.nix | 0 .../lang/eval-okay-attrs6.exp | 0 .../lang/eval-okay-attrs6.nix | 0 .../lang/eval-okay-autoargs.exp | 0 .../lang/eval-okay-autoargs.flags | 0 .../lang/eval-okay-autoargs.nix | 0 .../lang/eval-okay-backslash-newline-1.exp | 0 .../lang/eval-okay-backslash-newline-1.nix | 0 .../lang/eval-okay-backslash-newline-2.exp | 0 .../lang/eval-okay-backslash-newline-2.nix | 0 .../lang/eval-okay-builtins-add.exp | 0 .../lang/eval-okay-builtins-add.nix | 0 .../lang/eval-okay-builtins.exp | 0 .../lang/eval-okay-builtins.nix | 0 .../lang/eval-okay-callable-attrs.exp | 0 .../lang/eval-okay-callable-attrs.nix | 0 .../lang/eval-okay-catattrs.exp | 0 .../lang/eval-okay-catattrs.nix | 0 .../lang/eval-okay-closure.exp | 0 .../lang/eval-okay-closure.exp.xml | 0 .../lang/eval-okay-closure.nix | 0 .../lang/eval-okay-comments.exp | 0 .../lang/eval-okay-comments.nix | 0 .../lang/eval-okay-concat.exp | 0 .../lang/eval-okay-concat.nix | 0 .../lang/eval-okay-concatmap.exp | 0 .../lang/eval-okay-concatmap.nix | 0 .../lang/eval-okay-concatstringssep.exp | 0 .../lang/eval-okay-concatstringssep.nix | 0 .../lang/eval-okay-context-introspection.exp | 0 .../lang/eval-okay-context-introspection.nix | 0 .../lang/eval-okay-context.exp | 0 .../lang/eval-okay-context.nix | 0 .../lang/eval-okay-curpos.exp | 0 .../lang/eval-okay-curpos.nix | 0 .../lang/eval-okay-deepseq.exp | 0 .../lang/eval-okay-deepseq.nix | 0 .../lang/eval-okay-delayed-with-inherit.exp | 0 .../lang/eval-okay-delayed-with-inherit.nix | 0 .../lang/eval-okay-delayed-with.exp | 0 .../lang/eval-okay-delayed-with.nix | 0 .../lang/eval-okay-dynamic-attrs-2.exp | 0 .../lang/eval-okay-dynamic-attrs-2.nix | 0 .../lang/eval-okay-dynamic-attrs-bare.exp | 0 .../lang/eval-okay-dynamic-attrs-bare.nix | 0 .../lang/eval-okay-dynamic-attrs.exp | 0 .../lang/eval-okay-dynamic-attrs.nix | 0 .../{ => functional}/lang/eval-okay-elem.exp | 0 .../{ => functional}/lang/eval-okay-elem.nix | 0 .../lang/eval-okay-empty-args.exp | 0 .../lang/eval-okay-empty-args.nix | 0 .../lang/eval-okay-eq-derivations.exp | 0 .../lang/eval-okay-eq-derivations.nix | 0 tests/{ => functional}/lang/eval-okay-eq.exp | 0 tests/{ => functional}/lang/eval-okay-eq.nix | 0 .../lang/eval-okay-filter.exp | 0 .../lang/eval-okay-filter.nix | 0 .../lang/eval-okay-flake-ref-to-string.exp | 0 .../lang/eval-okay-flake-ref-to-string.nix | 0 .../lang/eval-okay-flatten.exp | 0 .../lang/eval-okay-flatten.nix | 0 .../{ => functional}/lang/eval-okay-float.exp | 0 .../{ => functional}/lang/eval-okay-float.nix | 0 .../lang/eval-okay-floor-ceil.exp | 0 .../lang/eval-okay-floor-ceil.nix | 0 .../eval-okay-foldlStrict-lazy-elements.exp | 0 .../eval-okay-foldlStrict-lazy-elements.nix | 0 ...y-foldlStrict-lazy-initial-accumulator.exp | 0 ...y-foldlStrict-lazy-initial-accumulator.nix | 0 .../lang/eval-okay-foldlStrict.exp | 0 .../lang/eval-okay-foldlStrict.nix | 0 .../lang/eval-okay-fromTOML-timestamps.exp | 0 .../lang/eval-okay-fromTOML-timestamps.flags | 0 .../lang/eval-okay-fromTOML-timestamps.nix | 0 .../lang/eval-okay-fromTOML.exp | 0 .../lang/eval-okay-fromTOML.nix | 0 .../lang/eval-okay-fromjson-escapes.exp | 0 .../lang/eval-okay-fromjson-escapes.nix | 0 .../lang/eval-okay-fromjson.exp | 0 .../lang/eval-okay-fromjson.nix | 0 .../lang/eval-okay-functionargs.exp | 0 .../lang/eval-okay-functionargs.exp.xml | 0 .../lang/eval-okay-functionargs.nix | 0 .../eval-okay-getattrpos-functionargs.exp | 0 .../eval-okay-getattrpos-functionargs.nix | 0 .../lang/eval-okay-getattrpos-undefined.exp | 0 .../lang/eval-okay-getattrpos-undefined.nix | 0 .../lang/eval-okay-getattrpos.exp | 0 .../lang/eval-okay-getattrpos.nix | 0 .../lang/eval-okay-getenv.exp | 0 .../lang/eval-okay-getenv.nix | 0 .../lang/eval-okay-groupBy.exp | 0 .../lang/eval-okay-groupBy.nix | 0 .../{ => functional}/lang/eval-okay-hash.exp | 0 .../lang/eval-okay-hashfile.exp | 0 .../lang/eval-okay-hashfile.nix | 0 .../lang/eval-okay-hashstring.exp | 0 .../lang/eval-okay-hashstring.nix | 0 tests/{ => functional}/lang/eval-okay-if.exp | 0 tests/{ => functional}/lang/eval-okay-if.nix | 0 .../lang/eval-okay-import.exp | 0 .../lang/eval-okay-import.nix | 0 .../lang/eval-okay-ind-string.exp | 0 .../lang/eval-okay-ind-string.nix | 0 .../lang/eval-okay-intersectAttrs.exp | 0 .../lang/eval-okay-intersectAttrs.nix | 0 tests/{ => functional}/lang/eval-okay-let.exp | 0 tests/{ => functional}/lang/eval-okay-let.nix | 0 .../{ => functional}/lang/eval-okay-list.exp | 0 .../{ => functional}/lang/eval-okay-list.nix | 0 .../lang/eval-okay-listtoattrs.exp | 0 .../lang/eval-okay-listtoattrs.nix | 0 .../{ => functional}/lang/eval-okay-logic.exp | 0 .../{ => functional}/lang/eval-okay-logic.nix | 0 tests/{ => functional}/lang/eval-okay-map.exp | 0 tests/{ => functional}/lang/eval-okay-map.nix | 0 .../lang/eval-okay-mapattrs.exp | 0 .../lang/eval-okay-mapattrs.nix | 0 .../lang/eval-okay-merge-dynamic-attrs.exp | 0 .../lang/eval-okay-merge-dynamic-attrs.nix | 0 .../lang/eval-okay-nested-with.exp | 0 .../lang/eval-okay-nested-with.nix | 0 .../lang/eval-okay-new-let.exp | 0 .../lang/eval-okay-new-let.nix | 0 .../lang/eval-okay-null-dynamic-attrs.exp | 0 .../lang/eval-okay-null-dynamic-attrs.nix | 0 .../lang/eval-okay-overrides.exp | 0 .../lang/eval-okay-overrides.nix | 0 .../lang/eval-okay-parse-flake-ref.exp | 0 .../lang/eval-okay-parse-flake-ref.nix | 0 .../lang/eval-okay-partition.exp | 0 .../lang/eval-okay-partition.nix | 0 .../eval-okay-path-string-interpolation.exp | 0 .../eval-okay-path-string-interpolation.nix | 0 .../{ => functional}/lang/eval-okay-path.exp | 0 .../{ => functional}/lang/eval-okay-path.nix | 0 .../lang/eval-okay-pathexists.exp | 0 .../lang/eval-okay-pathexists.nix | 0 .../lang/eval-okay-patterns.exp | 0 .../lang/eval-okay-patterns.nix | 0 .../lang/eval-okay-print.err.exp | 0 .../{ => functional}/lang/eval-okay-print.exp | 0 .../{ => functional}/lang/eval-okay-print.nix | 0 .../lang/eval-okay-readDir.exp | 0 .../lang/eval-okay-readDir.nix | 0 .../lang/eval-okay-readFileType.exp | 0 .../lang/eval-okay-readFileType.nix | 0 .../lang/eval-okay-readfile.exp | 0 .../lang/eval-okay-readfile.nix | 0 .../lang/eval-okay-redefine-builtin.exp | 0 .../lang/eval-okay-redefine-builtin.nix | 0 .../lang/eval-okay-regex-match.exp | 0 .../lang/eval-okay-regex-match.nix | 0 .../lang/eval-okay-regex-split.exp | 0 .../lang/eval-okay-regex-split.nix | 0 .../lang/eval-okay-regression-20220122.exp | 0 .../lang/eval-okay-regression-20220122.nix | 0 .../lang/eval-okay-regression-20220125.exp | 0 .../lang/eval-okay-regression-20220125.nix | 0 .../lang/eval-okay-remove.exp | 0 .../lang/eval-okay-remove.nix | 0 .../lang/eval-okay-replacestrings.exp | 0 .../lang/eval-okay-replacestrings.nix | 0 .../lang/eval-okay-scope-1.exp | 0 .../lang/eval-okay-scope-1.nix | 0 .../lang/eval-okay-scope-2.exp | 0 .../lang/eval-okay-scope-2.nix | 0 .../lang/eval-okay-scope-3.exp | 0 .../lang/eval-okay-scope-3.nix | 0 .../lang/eval-okay-scope-4.exp | 0 .../lang/eval-okay-scope-4.nix | 0 .../lang/eval-okay-scope-6.exp | 0 .../lang/eval-okay-scope-6.nix | 0 .../lang/eval-okay-scope-7.exp | 0 .../lang/eval-okay-scope-7.nix | 0 .../lang/eval-okay-search-path.exp | 0 .../lang/eval-okay-search-path.flags | 0 .../lang/eval-okay-search-path.nix | 0 tests/{ => functional}/lang/eval-okay-seq.exp | 0 tests/{ => functional}/lang/eval-okay-seq.nix | 0 .../{ => functional}/lang/eval-okay-sort.exp | 0 .../{ => functional}/lang/eval-okay-sort.nix | 0 .../lang/eval-okay-splitversion.exp | 0 .../lang/eval-okay-splitversion.nix | 0 .../lang/eval-okay-string.exp | 0 .../lang/eval-okay-string.nix | 0 .../lang/eval-okay-strings-as-attrs-names.exp | 0 .../lang/eval-okay-strings-as-attrs-names.nix | 0 .../lang/eval-okay-substring.exp | 0 .../lang/eval-okay-substring.nix | 0 .../lang/eval-okay-tail-call-1.exp-disabled | 0 .../lang/eval-okay-tail-call-1.nix | 0 .../lang/eval-okay-tojson.exp | 0 .../lang/eval-okay-tojson.nix | 0 .../{ => functional}/lang/eval-okay-toxml.exp | 0 .../{ => functional}/lang/eval-okay-toxml.nix | 0 .../lang/eval-okay-toxml2.exp | 0 .../lang/eval-okay-toxml2.nix | 0 .../lang/eval-okay-tryeval.exp | 0 .../lang/eval-okay-tryeval.nix | 0 .../{ => functional}/lang/eval-okay-types.exp | 0 .../{ => functional}/lang/eval-okay-types.nix | 0 .../lang/eval-okay-versions.exp | 0 .../lang/eval-okay-versions.nix | 0 .../{ => functional}/lang/eval-okay-with.exp | 0 .../{ => functional}/lang/eval-okay-with.nix | 0 .../lang/eval-okay-xml.exp.xml | 0 tests/{ => functional}/lang/eval-okay-xml.nix | 0 .../lang/eval-okay-zipAttrsWith.exp | 0 .../lang/eval-okay-zipAttrsWith.nix | 0 tests/{ => functional}/lang/framework.sh | 0 tests/{ => functional}/lang/imported.nix | 0 tests/{ => functional}/lang/imported2.nix | 0 tests/{ => functional}/lang/lib.nix | 0 .../lang/parse-fail-dup-attrs-1.err.exp | 0 .../lang/parse-fail-dup-attrs-1.nix | 0 .../lang/parse-fail-dup-attrs-2.err.exp | 0 .../lang/parse-fail-dup-attrs-2.nix | 0 .../lang/parse-fail-dup-attrs-3.err.exp | 0 .../lang/parse-fail-dup-attrs-3.nix | 0 .../lang/parse-fail-dup-attrs-4.err.exp | 0 .../lang/parse-fail-dup-attrs-4.nix | 0 .../lang/parse-fail-dup-attrs-6.err.exp | 0 .../lang/parse-fail-dup-attrs-7.err.exp | 0 .../lang/parse-fail-dup-attrs-7.nix | 0 .../lang/parse-fail-dup-formals.err.exp | 0 .../lang/parse-fail-dup-formals.nix | 0 .../lang/parse-fail-eof-in-string.err.exp | 0 .../lang/parse-fail-eof-in-string.nix | 0 .../parse-fail-mixed-nested-attrs1.err.exp | 0 .../lang/parse-fail-mixed-nested-attrs1.nix | 0 .../parse-fail-mixed-nested-attrs2.err.exp | 0 .../lang/parse-fail-mixed-nested-attrs2.nix | 0 .../lang/parse-fail-patterns-1.err.exp | 0 .../lang/parse-fail-patterns-1.nix | 0 .../parse-fail-regression-20060610.err.exp | 0 .../lang/parse-fail-regression-20060610.nix | 0 .../lang/parse-fail-undef-var-2.err.exp | 0 .../lang/parse-fail-undef-var-2.nix | 0 .../lang/parse-fail-undef-var.err.exp | 0 .../lang/parse-fail-undef-var.nix | 0 .../lang/parse-fail-utf8.err.exp | 0 .../{ => functional}/lang/parse-fail-utf8.nix | 0 tests/{ => functional}/lang/parse-okay-1.exp | 0 tests/{ => functional}/lang/parse-okay-1.nix | 0 .../{ => functional}/lang/parse-okay-crlf.exp | 0 .../{ => functional}/lang/parse-okay-crlf.nix | 0 .../lang/parse-okay-dup-attrs-5.exp | 0 .../lang/parse-okay-dup-attrs-5.nix | 0 .../lang/parse-okay-dup-attrs-6.exp | 0 .../lang/parse-okay-dup-attrs-6.nix | 0 .../lang/parse-okay-mixed-nested-attrs-1.exp | 0 .../lang/parse-okay-mixed-nested-attrs-1.nix | 0 .../lang/parse-okay-mixed-nested-attrs-2.exp | 0 .../lang/parse-okay-mixed-nested-attrs-2.nix | 0 .../lang/parse-okay-mixed-nested-attrs-3.exp | 0 .../lang/parse-okay-mixed-nested-attrs-3.nix | 0 .../lang/parse-okay-regression-20041027.exp | 0 .../lang/parse-okay-regression-20041027.nix | 0 .../lang/parse-okay-regression-751.exp | 0 .../lang/parse-okay-regression-751.nix | 0 .../lang/parse-okay-subversion.exp | 0 .../lang/parse-okay-subversion.nix | 0 .../{ => functional}/lang/parse-okay-url.exp | 0 .../{ => functional}/lang/parse-okay-url.nix | 0 tests/{ => functional}/lang/readDir/bar | 0 .../lang/readDir/foo/git-hates-directories | 0 tests/{ => functional}/lang/readDir/ldir | 0 tests/{ => functional}/lang/readDir/linked | 0 tests/{ => functional}/legacy-ssh-store.sh | 0 .../linux-sandbox-cert-test.nix | 0 tests/{ => functional}/linux-sandbox.sh | 0 tests/{ => functional}/local-store.sh | 0 tests/{ => functional}/local.mk | 13 +++-- tests/{ => functional}/logging.sh | 0 tests/{ => functional}/misc.sh | 0 tests/{ => functional}/multiple-outputs.nix | 0 tests/{ => functional}/multiple-outputs.sh | 0 tests/{ => functional}/nar-access.nix | 0 tests/{ => functional}/nar-access.sh | 0 tests/{ => functional}/nested-sandboxing.sh | 2 +- .../nested-sandboxing/command.sh | 0 .../nested-sandboxing/runner.nix | 0 tests/{ => functional}/nix-build-examples.nix | 0 tests/{ => functional}/nix-build.sh | 0 tests/{ => functional}/nix-channel.sh | 0 .../{ => functional}/nix-collect-garbage-d.sh | 0 tests/{ => functional}/nix-copy-ssh-ng.sh | 0 tests/{ => functional}/nix-copy-ssh.sh | 0 .../{ => functional}/nix-daemon-untrusting.sh | 0 tests/{ => functional}/nix-profile.sh | 0 tests/{ => functional}/nix-shell.sh | 0 tests/{ => functional}/nix_path.sh | 0 tests/{ => functional}/optimise-store.sh | 0 .../{ => functional}/output-normalization.sh | 0 tests/{ => functional}/parallel.builder.sh | 0 tests/{ => functional}/parallel.nix | 0 tests/{ => functional}/parallel.sh | 0 tests/{ => functional}/pass-as-file.sh | 0 tests/{ => functional}/path-from-hash-part.sh | 0 tests/{ => functional}/path.nix | 0 tests/{ => functional}/placeholders.sh | 0 tests/{ => functional}/plugins.sh | 0 tests/{ => functional}/plugins/local.mk | 0 tests/{ => functional}/plugins/plugintest.cc | 0 tests/{ => functional}/post-hook.sh | 0 tests/{ => functional}/pure-eval.nix | 0 tests/{ => functional}/pure-eval.sh | 0 tests/{ => functional}/push-to-store-old.sh | 0 tests/{ => functional}/push-to-store.sh | 0 tests/{ => functional}/read-only-store.sh | 0 tests/{ => functional}/readfile-context.nix | 0 tests/{ => functional}/readfile-context.sh | 0 tests/{ => functional}/recursive.nix | 0 tests/{ => functional}/recursive.sh | 0 tests/{ => functional}/referrers.sh | 0 tests/{ => functional}/remote-store.sh | 0 tests/{ => functional}/repair.sh | 0 tests/{ => functional}/repl.sh | 0 tests/{ => functional}/restricted.nix | 0 tests/{ => functional}/restricted.sh | 6 +- tests/{ => functional}/search.nix | 0 tests/{ => functional}/search.sh | 0 tests/{ => functional}/secure-drv-outputs.nix | 0 tests/{ => functional}/secure-drv-outputs.sh | 0 tests/{ => functional}/selfref-gc.sh | 0 tests/{ => functional}/shell-hello.nix | 0 tests/{ => functional}/shell.nix | 0 tests/{ => functional}/shell.sh | 0 tests/{ => functional}/shell.shebang.rb | 0 tests/{ => functional}/shell.shebang.sh | 0 tests/{ => functional}/signing.sh | 0 tests/{ => functional}/simple-failing.nix | 0 tests/{ => functional}/simple.builder.sh | 0 tests/{ => functional}/simple.nix | 0 tests/{ => functional}/simple.sh | 0 tests/{ => functional}/ssh-relay.sh | 0 tests/{ => functional}/store-ping.sh | 0 .../structured-attrs-shell.nix | 0 tests/{ => functional}/structured-attrs.nix | 0 tests/{ => functional}/structured-attrs.sh | 0 .../substitute-with-invalid-ca.sh | 0 tests/{ => functional}/suggestions.sh | 0 .../{ => functional}/supplementary-groups.sh | 0 tests/{ => functional}/tarball.sh | 0 tests/{ => functional}/test-infra.sh | 0 .../{ => functional}/test-libstoreconsumer.sh | 0 .../test-libstoreconsumer/README.md | 0 .../test-libstoreconsumer/local.mk | 0 .../test-libstoreconsumer/main.cc | 0 tests/{ => functional}/timeout.nix | 0 tests/{ => functional}/timeout.sh | 0 tests/{ => functional}/toString-path.sh | 0 tests/{ => functional}/undefined-variable.nix | 0 tests/{ => functional}/user-envs-migration.sh | 0 tests/{ => functional}/user-envs.builder.sh | 0 tests/{ => functional}/user-envs.nix | 0 tests/{ => functional}/user-envs.sh | 0 tests/{ => functional}/why-depends.sh | 0 tests/{ => functional}/zstd.sh | 0 599 files changed, 84 insertions(+), 87 deletions(-) rename tests/{ => functional}/add.sh (100%) rename tests/{ => functional}/bad.tar.xz (100%) rename tests/{ => functional}/bash-profile.sh (78%) rename tests/{ => functional}/big-derivation-attr.nix (100%) rename tests/{ => functional}/binary-cache-build-remote.sh (100%) rename tests/{ => functional}/binary-cache.sh (100%) rename tests/{ => functional}/brotli.sh (100%) rename tests/{ => functional}/build-delete.sh (100%) rename tests/{ => functional}/build-dry.sh (100%) rename tests/{ => functional}/build-hook-ca-fixed.nix (100%) rename tests/{ => functional}/build-hook-ca-floating.nix (100%) rename tests/{ => functional}/build-hook.nix (100%) rename tests/{ => functional}/build-remote-content-addressed-fixed.sh (100%) rename tests/{ => functional}/build-remote-content-addressed-floating.sh (100%) rename tests/{ => functional}/build-remote-input-addressed.sh (100%) rename tests/{ => functional}/build-remote-trustless-after.sh (100%) rename tests/{ => functional}/build-remote-trustless-should-fail-0.sh (100%) rename tests/{ => functional}/build-remote-trustless-should-pass-0.sh (100%) rename tests/{ => functional}/build-remote-trustless-should-pass-1.sh (100%) rename tests/{ => functional}/build-remote-trustless-should-pass-2.sh (100%) rename tests/{ => functional}/build-remote-trustless-should-pass-3.sh (100%) rename tests/{ => functional}/build-remote-trustless.sh (82%) rename tests/{ => functional}/build-remote.sh (100%) rename tests/{ => functional}/build.sh (100%) rename tests/{ => functional}/ca-shell.nix (100%) rename tests/{ => functional}/ca/build-cache.sh (100%) rename tests/{ => functional}/ca/build-dry.sh (100%) rename tests/{ => functional}/ca/build-with-garbage-path.sh (100%) rename tests/{ => functional}/ca/build.sh (100%) rename tests/{ => functional}/ca/common.sh (100%) rename tests/{ => functional}/ca/concurrent-builds.sh (100%) rename tests/{ => functional}/ca/config.nix.in (100%) rename tests/{ => functional}/ca/content-addressed.nix (100%) rename tests/{ => functional}/ca/derivation-json.sh (100%) rename tests/{ => functional}/ca/duplicate-realisation-in-closure.sh (100%) rename tests/{ => functional}/ca/flake.nix (100%) rename tests/{ => functional}/ca/gc.sh (100%) rename tests/{ => functional}/ca/import-derivation.sh (100%) rename tests/{ => functional}/ca/local.mk (94%) rename tests/{ => functional}/ca/new-build-cmd.sh (100%) rename tests/{ => functional}/ca/nix-copy.sh (100%) rename tests/{ => functional}/ca/nix-run.sh (100%) rename tests/{ => functional}/ca/nix-shell.sh (100%) rename tests/{ => functional}/ca/nondeterministic.nix (100%) rename tests/{ => functional}/ca/post-hook.sh (100%) rename tests/{ => functional}/ca/racy.nix (100%) rename tests/{ => functional}/ca/recursive.sh (100%) rename tests/{ => functional}/ca/repl.sh (100%) rename tests/{ => functional}/ca/selfref-gc.sh (100%) rename tests/{ => functional}/ca/signatures.sh (100%) rename tests/{ => functional}/ca/substitute.sh (100%) rename tests/{ => functional}/ca/why-depends.sh (100%) rename tests/{ => functional}/case-hack.sh (100%) rename tests/{ => functional}/case.nar (100%) rename tests/{ => functional}/check-refs.nix (100%) rename tests/{ => functional}/check-refs.sh (100%) rename tests/{ => functional}/check-reqs.nix (100%) rename tests/{ => functional}/check-reqs.sh (100%) rename tests/{ => functional}/check.nix (100%) rename tests/{ => functional}/check.sh (100%) rename tests/{ => functional}/common.sh (100%) rename tests/{ => functional}/common/vars-and-functions.sh.in (99%) rename tests/{ => functional}/completions.sh (100%) rename tests/{ => functional}/compression-levels.sh (100%) rename tests/{ => functional}/compute-levels.sh (100%) rename tests/{ => functional}/config.nix.in (100%) rename tests/{ => functional}/config.sh (100%) rename tests/{ => functional}/config/nix-with-substituters.conf (100%) rename tests/{ => functional}/db-migration.sh (100%) rename tests/{ => functional}/dependencies.builder0.sh (100%) rename tests/{ => functional}/dependencies.nix (100%) rename tests/{ => functional}/dependencies.sh (100%) rename tests/{ => functional}/derivation-json.sh (100%) rename tests/{ => functional}/dummy (100%) rename tests/{ => functional}/dump-db.sh (100%) rename tests/{ => functional}/dyn-drv/build-built-drv.sh (100%) rename tests/{ => functional}/dyn-drv/common.sh (100%) rename tests/{ => functional}/dyn-drv/config.nix.in (100%) rename tests/{ => functional}/dyn-drv/dep-built-drv.sh (100%) rename tests/{ => functional}/dyn-drv/eval-outputOf.sh (100%) rename tests/{ => functional}/dyn-drv/local.mk (87%) rename tests/{ => functional}/dyn-drv/old-daemon-error-hack.nix (100%) rename tests/{ => functional}/dyn-drv/old-daemon-error-hack.sh (100%) rename tests/{ => functional}/dyn-drv/recursive-mod-json.nix (100%) rename tests/{ => functional}/dyn-drv/recursive-mod-json.sh (100%) rename tests/{ => functional}/dyn-drv/text-hashed-output.nix (100%) rename tests/{ => functional}/dyn-drv/text-hashed-output.sh (100%) rename tests/{ => functional}/eval-store.sh (100%) rename tests/{ => functional}/eval.nix (100%) rename tests/{ => functional}/eval.sh (100%) rename tests/{ => functional}/experimental-features.sh (100%) rename tests/{ => functional}/export-graph.nix (100%) rename tests/{ => functional}/export-graph.sh (100%) rename tests/{ => functional}/export.sh (100%) rename tests/{ => functional}/failing.nix (100%) rename tests/{ => functional}/fetchClosure.sh (100%) rename tests/{ => functional}/fetchGit.sh (100%) rename tests/{ => functional}/fetchGitRefs.sh (100%) rename tests/{ => functional}/fetchGitSubmodules.sh (100%) rename tests/{ => functional}/fetchMercurial.sh (100%) rename tests/{ => functional}/fetchPath.sh (100%) rename tests/{ => functional}/fetchTree-file.sh (100%) rename tests/{ => functional}/fetchurl.sh (100%) rename tests/{ => functional}/filter-source.nix (100%) rename tests/{ => functional}/filter-source.sh (100%) rename tests/{ => functional}/fixed.builder1.sh (100%) rename tests/{ => functional}/fixed.builder2.sh (100%) rename tests/{ => functional}/fixed.nix (100%) rename tests/{ => functional}/fixed.sh (100%) rename tests/{ => functional}/flakes/absolute-attr-paths.sh (100%) rename tests/{ => functional}/flakes/absolute-paths.sh (100%) rename tests/{ => functional}/flakes/build-paths.sh (100%) rename tests/{ => functional}/flakes/bundle.sh (100%) rename tests/{ => functional}/flakes/check.sh (100%) rename tests/{ => functional}/flakes/circular.sh (100%) rename tests/{ => functional}/flakes/common.sh (100%) rename tests/{ => functional}/flakes/config.sh (100%) rename tests/{ => functional}/flakes/flake-in-submodule.sh (100%) rename tests/{ => functional}/flakes/flakes.sh (100%) rename tests/{ => functional}/flakes/follow-paths.sh (100%) rename tests/{ => functional}/flakes/init.sh (100%) rename tests/{ => functional}/flakes/inputs.sh (100%) rename tests/{ => functional}/flakes/mercurial.sh (100%) rename tests/{ => functional}/flakes/run.sh (100%) rename tests/{ => functional}/flakes/search-root.sh (100%) rename tests/{ => functional}/flakes/show.sh (100%) rename tests/{ => functional}/flakes/unlocked-override.sh (100%) rename tests/{ => functional}/fmt.sh (100%) rename tests/{ => functional}/fmt.simple.sh (100%) rename tests/{ => functional}/function-trace.sh (100%) rename tests/{ => functional}/gc-auto.sh (100%) rename tests/{ => functional}/gc-concurrent.builder.sh (100%) rename tests/{ => functional}/gc-concurrent.nix (100%) rename tests/{ => functional}/gc-concurrent.sh (100%) rename tests/{ => functional}/gc-concurrent2.builder.sh (100%) rename tests/{ => functional}/gc-non-blocking.sh (100%) rename tests/{ => functional}/gc-runtime.nix (100%) rename tests/{ => functional}/gc-runtime.sh (100%) rename tests/{ => functional}/gc.sh (100%) rename tests/{ => functional}/hash-check.nix (100%) rename tests/{ => functional}/hash.sh (100%) rename tests/{ => functional}/hermetic.nix (100%) rename tests/{ => functional}/import-derivation.nix (100%) rename tests/{ => functional}/import-derivation.sh (100%) rename tests/{ => functional}/impure-derivations.nix (100%) rename tests/{ => functional}/impure-derivations.sh (100%) rename tests/{ => functional}/init.sh (100%) rename tests/{ => functional}/install-darwin.sh (100%) rename tests/{ => functional}/lang-test-infra.sh (100%) rename tests/{ => functional}/lang.sh (98%) rename tests/{ => functional}/lang/binary-data (100%) rename tests/{ => functional}/lang/data (100%) rename tests/{ => functional}/lang/dir1/a.nix (100%) rename tests/{ => functional}/lang/dir2/a.nix (100%) rename tests/{ => functional}/lang/dir2/b.nix (100%) rename tests/{ => functional}/lang/dir3/a.nix (100%) rename tests/{ => functional}/lang/dir3/b.nix (100%) rename tests/{ => functional}/lang/dir3/c.nix (100%) rename tests/{ => functional}/lang/dir4/a.nix (100%) rename tests/{ => functional}/lang/dir4/c.nix (100%) rename tests/{ => functional}/lang/empty.exp (100%) rename tests/{ => functional}/lang/eval-fail-abort.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-abort.nix (100%) rename tests/{ => functional}/lang/eval-fail-antiquoted-path.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-assert.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-assert.nix (100%) rename tests/{ => functional}/lang/eval-fail-bad-antiquote-1.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-bad-antiquote-2.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-bad-antiquote-3.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-bad-string-interpolation-1.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-bad-string-interpolation-1.nix (100%) rename tests/{ => functional}/lang/eval-fail-bad-string-interpolation-2.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-bad-string-interpolation-2.nix (100%) rename tests/{ => functional}/lang/eval-fail-bad-string-interpolation-3.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-bad-string-interpolation-3.nix (100%) rename tests/{ => functional}/lang/eval-fail-blackhole.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-blackhole.nix (100%) rename tests/{ => functional}/lang/eval-fail-deepseq.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-deepseq.nix (100%) rename tests/{ => functional}/lang/eval-fail-dup-dynamic-attrs.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-dup-dynamic-attrs.nix (100%) rename tests/{ => functional}/lang/eval-fail-foldlStrict-strict-op-application.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-foldlStrict-strict-op-application.nix (100%) rename tests/{ => functional}/lang/eval-fail-fromTOML-timestamps.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-fromTOML-timestamps.nix (100%) rename tests/{ => functional}/lang/eval-fail-hashfile-missing.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-hashfile-missing.nix (100%) rename tests/{ => functional}/lang/eval-fail-list.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-list.nix (100%) rename tests/{ => functional}/lang/eval-fail-missing-arg.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-missing-arg.nix (100%) rename tests/{ => functional}/lang/eval-fail-nonexist-path.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-nonexist-path.nix (100%) rename tests/{ => functional}/lang/eval-fail-path-slash.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-path-slash.nix (100%) rename tests/{ => functional}/lang/eval-fail-recursion.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-recursion.nix (100%) rename tests/{ => functional}/lang/eval-fail-remove.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-remove.nix (100%) rename tests/{ => functional}/lang/eval-fail-scope-5.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-scope-5.nix (100%) rename tests/{ => functional}/lang/eval-fail-seq.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-seq.nix (100%) rename tests/{ => functional}/lang/eval-fail-set-override.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-set-override.nix (100%) rename tests/{ => functional}/lang/eval-fail-set.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-set.nix (100%) rename tests/{ => functional}/lang/eval-fail-substring.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-substring.nix (100%) rename tests/{ => functional}/lang/eval-fail-to-path.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-to-path.nix (100%) rename tests/{ => functional}/lang/eval-fail-toJSON.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-toJSON.nix (100%) rename tests/{ => functional}/lang/eval-fail-undeclared-arg.err.exp (100%) rename tests/{ => functional}/lang/eval-fail-undeclared-arg.nix (100%) rename tests/{ => functional}/lang/eval-okay-any-all.exp (100%) rename tests/{ => functional}/lang/eval-okay-any-all.nix (100%) rename tests/{ => functional}/lang/eval-okay-arithmetic.exp (100%) rename tests/{ => functional}/lang/eval-okay-arithmetic.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrnames.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrnames.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrs2.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrs2.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrs3.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrs3.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrs4.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrs4.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrs5.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrs5.nix (100%) rename tests/{ => functional}/lang/eval-okay-attrs6.exp (100%) rename tests/{ => functional}/lang/eval-okay-attrs6.nix (100%) rename tests/{ => functional}/lang/eval-okay-autoargs.exp (100%) rename tests/{ => functional}/lang/eval-okay-autoargs.flags (100%) rename tests/{ => functional}/lang/eval-okay-autoargs.nix (100%) rename tests/{ => functional}/lang/eval-okay-backslash-newline-1.exp (100%) rename tests/{ => functional}/lang/eval-okay-backslash-newline-1.nix (100%) rename tests/{ => functional}/lang/eval-okay-backslash-newline-2.exp (100%) rename tests/{ => functional}/lang/eval-okay-backslash-newline-2.nix (100%) rename tests/{ => functional}/lang/eval-okay-builtins-add.exp (100%) rename tests/{ => functional}/lang/eval-okay-builtins-add.nix (100%) rename tests/{ => functional}/lang/eval-okay-builtins.exp (100%) rename tests/{ => functional}/lang/eval-okay-builtins.nix (100%) rename tests/{ => functional}/lang/eval-okay-callable-attrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-callable-attrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-catattrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-catattrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-closure.exp (100%) rename tests/{ => functional}/lang/eval-okay-closure.exp.xml (100%) rename tests/{ => functional}/lang/eval-okay-closure.nix (100%) rename tests/{ => functional}/lang/eval-okay-comments.exp (100%) rename tests/{ => functional}/lang/eval-okay-comments.nix (100%) rename tests/{ => functional}/lang/eval-okay-concat.exp (100%) rename tests/{ => functional}/lang/eval-okay-concat.nix (100%) rename tests/{ => functional}/lang/eval-okay-concatmap.exp (100%) rename tests/{ => functional}/lang/eval-okay-concatmap.nix (100%) rename tests/{ => functional}/lang/eval-okay-concatstringssep.exp (100%) rename tests/{ => functional}/lang/eval-okay-concatstringssep.nix (100%) rename tests/{ => functional}/lang/eval-okay-context-introspection.exp (100%) rename tests/{ => functional}/lang/eval-okay-context-introspection.nix (100%) rename tests/{ => functional}/lang/eval-okay-context.exp (100%) rename tests/{ => functional}/lang/eval-okay-context.nix (100%) rename tests/{ => functional}/lang/eval-okay-curpos.exp (100%) rename tests/{ => functional}/lang/eval-okay-curpos.nix (100%) rename tests/{ => functional}/lang/eval-okay-deepseq.exp (100%) rename tests/{ => functional}/lang/eval-okay-deepseq.nix (100%) rename tests/{ => functional}/lang/eval-okay-delayed-with-inherit.exp (100%) rename tests/{ => functional}/lang/eval-okay-delayed-with-inherit.nix (100%) rename tests/{ => functional}/lang/eval-okay-delayed-with.exp (100%) rename tests/{ => functional}/lang/eval-okay-delayed-with.nix (100%) rename tests/{ => functional}/lang/eval-okay-dynamic-attrs-2.exp (100%) rename tests/{ => functional}/lang/eval-okay-dynamic-attrs-2.nix (100%) rename tests/{ => functional}/lang/eval-okay-dynamic-attrs-bare.exp (100%) rename tests/{ => functional}/lang/eval-okay-dynamic-attrs-bare.nix (100%) rename tests/{ => functional}/lang/eval-okay-dynamic-attrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-dynamic-attrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-elem.exp (100%) rename tests/{ => functional}/lang/eval-okay-elem.nix (100%) rename tests/{ => functional}/lang/eval-okay-empty-args.exp (100%) rename tests/{ => functional}/lang/eval-okay-empty-args.nix (100%) rename tests/{ => functional}/lang/eval-okay-eq-derivations.exp (100%) rename tests/{ => functional}/lang/eval-okay-eq-derivations.nix (100%) rename tests/{ => functional}/lang/eval-okay-eq.exp (100%) rename tests/{ => functional}/lang/eval-okay-eq.nix (100%) rename tests/{ => functional}/lang/eval-okay-filter.exp (100%) rename tests/{ => functional}/lang/eval-okay-filter.nix (100%) rename tests/{ => functional}/lang/eval-okay-flake-ref-to-string.exp (100%) rename tests/{ => functional}/lang/eval-okay-flake-ref-to-string.nix (100%) rename tests/{ => functional}/lang/eval-okay-flatten.exp (100%) rename tests/{ => functional}/lang/eval-okay-flatten.nix (100%) rename tests/{ => functional}/lang/eval-okay-float.exp (100%) rename tests/{ => functional}/lang/eval-okay-float.nix (100%) rename tests/{ => functional}/lang/eval-okay-floor-ceil.exp (100%) rename tests/{ => functional}/lang/eval-okay-floor-ceil.nix (100%) rename tests/{ => functional}/lang/eval-okay-foldlStrict-lazy-elements.exp (100%) rename tests/{ => functional}/lang/eval-okay-foldlStrict-lazy-elements.nix (100%) rename tests/{ => functional}/lang/eval-okay-foldlStrict-lazy-initial-accumulator.exp (100%) rename tests/{ => functional}/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix (100%) rename tests/{ => functional}/lang/eval-okay-foldlStrict.exp (100%) rename tests/{ => functional}/lang/eval-okay-foldlStrict.nix (100%) rename tests/{ => functional}/lang/eval-okay-fromTOML-timestamps.exp (100%) rename tests/{ => functional}/lang/eval-okay-fromTOML-timestamps.flags (100%) rename tests/{ => functional}/lang/eval-okay-fromTOML-timestamps.nix (100%) rename tests/{ => functional}/lang/eval-okay-fromTOML.exp (100%) rename tests/{ => functional}/lang/eval-okay-fromTOML.nix (100%) rename tests/{ => functional}/lang/eval-okay-fromjson-escapes.exp (100%) rename tests/{ => functional}/lang/eval-okay-fromjson-escapes.nix (100%) rename tests/{ => functional}/lang/eval-okay-fromjson.exp (100%) rename tests/{ => functional}/lang/eval-okay-fromjson.nix (100%) rename tests/{ => functional}/lang/eval-okay-functionargs.exp (100%) rename tests/{ => functional}/lang/eval-okay-functionargs.exp.xml (100%) rename tests/{ => functional}/lang/eval-okay-functionargs.nix (100%) rename tests/{ => functional}/lang/eval-okay-getattrpos-functionargs.exp (100%) rename tests/{ => functional}/lang/eval-okay-getattrpos-functionargs.nix (100%) rename tests/{ => functional}/lang/eval-okay-getattrpos-undefined.exp (100%) rename tests/{ => functional}/lang/eval-okay-getattrpos-undefined.nix (100%) rename tests/{ => functional}/lang/eval-okay-getattrpos.exp (100%) rename tests/{ => functional}/lang/eval-okay-getattrpos.nix (100%) rename tests/{ => functional}/lang/eval-okay-getenv.exp (100%) rename tests/{ => functional}/lang/eval-okay-getenv.nix (100%) rename tests/{ => functional}/lang/eval-okay-groupBy.exp (100%) rename tests/{ => functional}/lang/eval-okay-groupBy.nix (100%) rename tests/{ => functional}/lang/eval-okay-hash.exp (100%) rename tests/{ => functional}/lang/eval-okay-hashfile.exp (100%) rename tests/{ => functional}/lang/eval-okay-hashfile.nix (100%) rename tests/{ => functional}/lang/eval-okay-hashstring.exp (100%) rename tests/{ => functional}/lang/eval-okay-hashstring.nix (100%) rename tests/{ => functional}/lang/eval-okay-if.exp (100%) rename tests/{ => functional}/lang/eval-okay-if.nix (100%) rename tests/{ => functional}/lang/eval-okay-import.exp (100%) rename tests/{ => functional}/lang/eval-okay-import.nix (100%) rename tests/{ => functional}/lang/eval-okay-ind-string.exp (100%) rename tests/{ => functional}/lang/eval-okay-ind-string.nix (100%) rename tests/{ => functional}/lang/eval-okay-intersectAttrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-intersectAttrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-let.exp (100%) rename tests/{ => functional}/lang/eval-okay-let.nix (100%) rename tests/{ => functional}/lang/eval-okay-list.exp (100%) rename tests/{ => functional}/lang/eval-okay-list.nix (100%) rename tests/{ => functional}/lang/eval-okay-listtoattrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-listtoattrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-logic.exp (100%) rename tests/{ => functional}/lang/eval-okay-logic.nix (100%) rename tests/{ => functional}/lang/eval-okay-map.exp (100%) rename tests/{ => functional}/lang/eval-okay-map.nix (100%) rename tests/{ => functional}/lang/eval-okay-mapattrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-mapattrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-merge-dynamic-attrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-merge-dynamic-attrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-nested-with.exp (100%) rename tests/{ => functional}/lang/eval-okay-nested-with.nix (100%) rename tests/{ => functional}/lang/eval-okay-new-let.exp (100%) rename tests/{ => functional}/lang/eval-okay-new-let.nix (100%) rename tests/{ => functional}/lang/eval-okay-null-dynamic-attrs.exp (100%) rename tests/{ => functional}/lang/eval-okay-null-dynamic-attrs.nix (100%) rename tests/{ => functional}/lang/eval-okay-overrides.exp (100%) rename tests/{ => functional}/lang/eval-okay-overrides.nix (100%) rename tests/{ => functional}/lang/eval-okay-parse-flake-ref.exp (100%) rename tests/{ => functional}/lang/eval-okay-parse-flake-ref.nix (100%) rename tests/{ => functional}/lang/eval-okay-partition.exp (100%) rename tests/{ => functional}/lang/eval-okay-partition.nix (100%) rename tests/{ => functional}/lang/eval-okay-path-string-interpolation.exp (100%) rename tests/{ => functional}/lang/eval-okay-path-string-interpolation.nix (100%) rename tests/{ => functional}/lang/eval-okay-path.exp (100%) rename tests/{ => functional}/lang/eval-okay-path.nix (100%) rename tests/{ => functional}/lang/eval-okay-pathexists.exp (100%) rename tests/{ => functional}/lang/eval-okay-pathexists.nix (100%) rename tests/{ => functional}/lang/eval-okay-patterns.exp (100%) rename tests/{ => functional}/lang/eval-okay-patterns.nix (100%) rename tests/{ => functional}/lang/eval-okay-print.err.exp (100%) rename tests/{ => functional}/lang/eval-okay-print.exp (100%) rename tests/{ => functional}/lang/eval-okay-print.nix (100%) rename tests/{ => functional}/lang/eval-okay-readDir.exp (100%) rename tests/{ => functional}/lang/eval-okay-readDir.nix (100%) rename tests/{ => functional}/lang/eval-okay-readFileType.exp (100%) rename tests/{ => functional}/lang/eval-okay-readFileType.nix (100%) rename tests/{ => functional}/lang/eval-okay-readfile.exp (100%) rename tests/{ => functional}/lang/eval-okay-readfile.nix (100%) rename tests/{ => functional}/lang/eval-okay-redefine-builtin.exp (100%) rename tests/{ => functional}/lang/eval-okay-redefine-builtin.nix (100%) rename tests/{ => functional}/lang/eval-okay-regex-match.exp (100%) rename tests/{ => functional}/lang/eval-okay-regex-match.nix (100%) rename tests/{ => functional}/lang/eval-okay-regex-split.exp (100%) rename tests/{ => functional}/lang/eval-okay-regex-split.nix (100%) rename tests/{ => functional}/lang/eval-okay-regression-20220122.exp (100%) rename tests/{ => functional}/lang/eval-okay-regression-20220122.nix (100%) rename tests/{ => functional}/lang/eval-okay-regression-20220125.exp (100%) rename tests/{ => functional}/lang/eval-okay-regression-20220125.nix (100%) rename tests/{ => functional}/lang/eval-okay-remove.exp (100%) rename tests/{ => functional}/lang/eval-okay-remove.nix (100%) rename tests/{ => functional}/lang/eval-okay-replacestrings.exp (100%) rename tests/{ => functional}/lang/eval-okay-replacestrings.nix (100%) rename tests/{ => functional}/lang/eval-okay-scope-1.exp (100%) rename tests/{ => functional}/lang/eval-okay-scope-1.nix (100%) rename tests/{ => functional}/lang/eval-okay-scope-2.exp (100%) rename tests/{ => functional}/lang/eval-okay-scope-2.nix (100%) rename tests/{ => functional}/lang/eval-okay-scope-3.exp (100%) rename tests/{ => functional}/lang/eval-okay-scope-3.nix (100%) rename tests/{ => functional}/lang/eval-okay-scope-4.exp (100%) rename tests/{ => functional}/lang/eval-okay-scope-4.nix (100%) rename tests/{ => functional}/lang/eval-okay-scope-6.exp (100%) rename tests/{ => functional}/lang/eval-okay-scope-6.nix (100%) rename tests/{ => functional}/lang/eval-okay-scope-7.exp (100%) rename tests/{ => functional}/lang/eval-okay-scope-7.nix (100%) rename tests/{ => functional}/lang/eval-okay-search-path.exp (100%) rename tests/{ => functional}/lang/eval-okay-search-path.flags (100%) rename tests/{ => functional}/lang/eval-okay-search-path.nix (100%) rename tests/{ => functional}/lang/eval-okay-seq.exp (100%) rename tests/{ => functional}/lang/eval-okay-seq.nix (100%) rename tests/{ => functional}/lang/eval-okay-sort.exp (100%) rename tests/{ => functional}/lang/eval-okay-sort.nix (100%) rename tests/{ => functional}/lang/eval-okay-splitversion.exp (100%) rename tests/{ => functional}/lang/eval-okay-splitversion.nix (100%) rename tests/{ => functional}/lang/eval-okay-string.exp (100%) rename tests/{ => functional}/lang/eval-okay-string.nix (100%) rename tests/{ => functional}/lang/eval-okay-strings-as-attrs-names.exp (100%) rename tests/{ => functional}/lang/eval-okay-strings-as-attrs-names.nix (100%) rename tests/{ => functional}/lang/eval-okay-substring.exp (100%) rename tests/{ => functional}/lang/eval-okay-substring.nix (100%) rename tests/{ => functional}/lang/eval-okay-tail-call-1.exp-disabled (100%) rename tests/{ => functional}/lang/eval-okay-tail-call-1.nix (100%) rename tests/{ => functional}/lang/eval-okay-tojson.exp (100%) rename tests/{ => functional}/lang/eval-okay-tojson.nix (100%) rename tests/{ => functional}/lang/eval-okay-toxml.exp (100%) rename tests/{ => functional}/lang/eval-okay-toxml.nix (100%) rename tests/{ => functional}/lang/eval-okay-toxml2.exp (100%) rename tests/{ => functional}/lang/eval-okay-toxml2.nix (100%) rename tests/{ => functional}/lang/eval-okay-tryeval.exp (100%) rename tests/{ => functional}/lang/eval-okay-tryeval.nix (100%) rename tests/{ => functional}/lang/eval-okay-types.exp (100%) rename tests/{ => functional}/lang/eval-okay-types.nix (100%) rename tests/{ => functional}/lang/eval-okay-versions.exp (100%) rename tests/{ => functional}/lang/eval-okay-versions.nix (100%) rename tests/{ => functional}/lang/eval-okay-with.exp (100%) rename tests/{ => functional}/lang/eval-okay-with.nix (100%) rename tests/{ => functional}/lang/eval-okay-xml.exp.xml (100%) rename tests/{ => functional}/lang/eval-okay-xml.nix (100%) rename tests/{ => functional}/lang/eval-okay-zipAttrsWith.exp (100%) rename tests/{ => functional}/lang/eval-okay-zipAttrsWith.nix (100%) rename tests/{ => functional}/lang/framework.sh (100%) rename tests/{ => functional}/lang/imported.nix (100%) rename tests/{ => functional}/lang/imported2.nix (100%) rename tests/{ => functional}/lang/lib.nix (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-1.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-1.nix (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-2.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-2.nix (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-3.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-3.nix (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-4.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-4.nix (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-6.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-7.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-attrs-7.nix (100%) rename tests/{ => functional}/lang/parse-fail-dup-formals.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-dup-formals.nix (100%) rename tests/{ => functional}/lang/parse-fail-eof-in-string.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-eof-in-string.nix (100%) rename tests/{ => functional}/lang/parse-fail-mixed-nested-attrs1.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-mixed-nested-attrs1.nix (100%) rename tests/{ => functional}/lang/parse-fail-mixed-nested-attrs2.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-mixed-nested-attrs2.nix (100%) rename tests/{ => functional}/lang/parse-fail-patterns-1.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-patterns-1.nix (100%) rename tests/{ => functional}/lang/parse-fail-regression-20060610.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-regression-20060610.nix (100%) rename tests/{ => functional}/lang/parse-fail-undef-var-2.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-undef-var-2.nix (100%) rename tests/{ => functional}/lang/parse-fail-undef-var.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-undef-var.nix (100%) rename tests/{ => functional}/lang/parse-fail-utf8.err.exp (100%) rename tests/{ => functional}/lang/parse-fail-utf8.nix (100%) rename tests/{ => functional}/lang/parse-okay-1.exp (100%) rename tests/{ => functional}/lang/parse-okay-1.nix (100%) rename tests/{ => functional}/lang/parse-okay-crlf.exp (100%) rename tests/{ => functional}/lang/parse-okay-crlf.nix (100%) rename tests/{ => functional}/lang/parse-okay-dup-attrs-5.exp (100%) rename tests/{ => functional}/lang/parse-okay-dup-attrs-5.nix (100%) rename tests/{ => functional}/lang/parse-okay-dup-attrs-6.exp (100%) rename tests/{ => functional}/lang/parse-okay-dup-attrs-6.nix (100%) rename tests/{ => functional}/lang/parse-okay-mixed-nested-attrs-1.exp (100%) rename tests/{ => functional}/lang/parse-okay-mixed-nested-attrs-1.nix (100%) rename tests/{ => functional}/lang/parse-okay-mixed-nested-attrs-2.exp (100%) rename tests/{ => functional}/lang/parse-okay-mixed-nested-attrs-2.nix (100%) rename tests/{ => functional}/lang/parse-okay-mixed-nested-attrs-3.exp (100%) rename tests/{ => functional}/lang/parse-okay-mixed-nested-attrs-3.nix (100%) rename tests/{ => functional}/lang/parse-okay-regression-20041027.exp (100%) rename tests/{ => functional}/lang/parse-okay-regression-20041027.nix (100%) rename tests/{ => functional}/lang/parse-okay-regression-751.exp (100%) rename tests/{ => functional}/lang/parse-okay-regression-751.nix (100%) rename tests/{ => functional}/lang/parse-okay-subversion.exp (100%) rename tests/{ => functional}/lang/parse-okay-subversion.nix (100%) rename tests/{ => functional}/lang/parse-okay-url.exp (100%) rename tests/{ => functional}/lang/parse-okay-url.nix (100%) rename tests/{ => functional}/lang/readDir/bar (100%) rename tests/{ => functional}/lang/readDir/foo/git-hates-directories (100%) rename tests/{ => functional}/lang/readDir/ldir (100%) rename tests/{ => functional}/lang/readDir/linked (100%) rename tests/{ => functional}/legacy-ssh-store.sh (100%) rename tests/{ => functional}/linux-sandbox-cert-test.nix (100%) rename tests/{ => functional}/linux-sandbox.sh (100%) rename tests/{ => functional}/local-store.sh (100%) rename tests/{ => functional}/local.mk (90%) rename tests/{ => functional}/logging.sh (100%) rename tests/{ => functional}/misc.sh (100%) rename tests/{ => functional}/multiple-outputs.nix (100%) rename tests/{ => functional}/multiple-outputs.sh (100%) rename tests/{ => functional}/nar-access.nix (100%) rename tests/{ => functional}/nar-access.sh (100%) rename tests/{ => functional}/nested-sandboxing.sh (75%) rename tests/{ => functional}/nested-sandboxing/command.sh (100%) rename tests/{ => functional}/nested-sandboxing/runner.nix (100%) rename tests/{ => functional}/nix-build-examples.nix (100%) rename tests/{ => functional}/nix-build.sh (100%) rename tests/{ => functional}/nix-channel.sh (100%) rename tests/{ => functional}/nix-collect-garbage-d.sh (100%) rename tests/{ => functional}/nix-copy-ssh-ng.sh (100%) rename tests/{ => functional}/nix-copy-ssh.sh (100%) rename tests/{ => functional}/nix-daemon-untrusting.sh (100%) rename tests/{ => functional}/nix-profile.sh (100%) rename tests/{ => functional}/nix-shell.sh (100%) rename tests/{ => functional}/nix_path.sh (100%) rename tests/{ => functional}/optimise-store.sh (100%) rename tests/{ => functional}/output-normalization.sh (100%) rename tests/{ => functional}/parallel.builder.sh (100%) rename tests/{ => functional}/parallel.nix (100%) rename tests/{ => functional}/parallel.sh (100%) rename tests/{ => functional}/pass-as-file.sh (100%) rename tests/{ => functional}/path-from-hash-part.sh (100%) rename tests/{ => functional}/path.nix (100%) rename tests/{ => functional}/placeholders.sh (100%) rename tests/{ => functional}/plugins.sh (100%) rename tests/{ => functional}/plugins/local.mk (100%) rename tests/{ => functional}/plugins/plugintest.cc (100%) rename tests/{ => functional}/post-hook.sh (100%) rename tests/{ => functional}/pure-eval.nix (100%) rename tests/{ => functional}/pure-eval.sh (100%) rename tests/{ => functional}/push-to-store-old.sh (100%) rename tests/{ => functional}/push-to-store.sh (100%) rename tests/{ => functional}/read-only-store.sh (100%) rename tests/{ => functional}/readfile-context.nix (100%) rename tests/{ => functional}/readfile-context.sh (100%) rename tests/{ => functional}/recursive.nix (100%) rename tests/{ => functional}/recursive.sh (100%) rename tests/{ => functional}/referrers.sh (100%) rename tests/{ => functional}/remote-store.sh (100%) rename tests/{ => functional}/repair.sh (100%) rename tests/{ => functional}/repl.sh (100%) rename tests/{ => functional}/restricted.nix (100%) rename tests/{ => functional}/restricted.sh (95%) rename tests/{ => functional}/search.nix (100%) rename tests/{ => functional}/search.sh (100%) rename tests/{ => functional}/secure-drv-outputs.nix (100%) rename tests/{ => functional}/secure-drv-outputs.sh (100%) rename tests/{ => functional}/selfref-gc.sh (100%) rename tests/{ => functional}/shell-hello.nix (100%) rename tests/{ => functional}/shell.nix (100%) rename tests/{ => functional}/shell.sh (100%) rename tests/{ => functional}/shell.shebang.rb (100%) rename tests/{ => functional}/shell.shebang.sh (100%) rename tests/{ => functional}/signing.sh (100%) rename tests/{ => functional}/simple-failing.nix (100%) rename tests/{ => functional}/simple.builder.sh (100%) rename tests/{ => functional}/simple.nix (100%) rename tests/{ => functional}/simple.sh (100%) rename tests/{ => functional}/ssh-relay.sh (100%) rename tests/{ => functional}/store-ping.sh (100%) rename tests/{ => functional}/structured-attrs-shell.nix (100%) rename tests/{ => functional}/structured-attrs.nix (100%) rename tests/{ => functional}/structured-attrs.sh (100%) rename tests/{ => functional}/substitute-with-invalid-ca.sh (100%) rename tests/{ => functional}/suggestions.sh (100%) rename tests/{ => functional}/supplementary-groups.sh (100%) rename tests/{ => functional}/tarball.sh (100%) rename tests/{ => functional}/test-infra.sh (100%) rename tests/{ => functional}/test-libstoreconsumer.sh (100%) rename tests/{ => functional}/test-libstoreconsumer/README.md (100%) rename tests/{ => functional}/test-libstoreconsumer/local.mk (100%) rename tests/{ => functional}/test-libstoreconsumer/main.cc (100%) rename tests/{ => functional}/timeout.nix (100%) rename tests/{ => functional}/timeout.sh (100%) rename tests/{ => functional}/toString-path.sh (100%) rename tests/{ => functional}/undefined-variable.nix (100%) rename tests/{ => functional}/user-envs-migration.sh (100%) rename tests/{ => functional}/user-envs.builder.sh (100%) rename tests/{ => functional}/user-envs.nix (100%) rename tests/{ => functional}/user-envs.sh (100%) rename tests/{ => functional}/why-depends.sh (100%) rename tests/{ => functional}/zstd.sh (100%) diff --git a/.github/labeler.yml b/.github/labeler.yml index 12120bdb3..7544f07a6 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -20,4 +20,4 @@ # Unit tests - src/*/tests/**/* # Functional and integration tests - - tests/**/* + - tests/functional/**/* diff --git a/.gitignore b/.gitignore index bf4f33564..2d3314015 100644 --- a/.gitignore +++ b/.gitignore @@ -79,24 +79,24 @@ perl/Makefile.config /src/build-remote/build-remote -# /tests/ -/tests/test-tmp -/tests/common/vars-and-functions.sh -/tests/result* -/tests/restricted-innocent -/tests/shell -/tests/shell.drv -/tests/config.nix -/tests/ca/config.nix -/tests/dyn-drv/config.nix -/tests/repl-result-out -/tests/test-libstoreconsumer/test-libstoreconsumer +# /tests/functional/ +/tests/functional/test-tmp +/tests/functional/common/vars-and-functions.sh +/tests/functional/result* +/tests/functional/restricted-innocent +/tests/functional/shell +/tests/functional/shell.drv +/tests/functional/config.nix +/tests/functional/ca/config.nix +/tests/functional/dyn-drv/config.nix +/tests/functional/repl-result-out +/tests/functional/test-libstoreconsumer/test-libstoreconsumer -# /tests/lang/ -/tests/lang/*.out -/tests/lang/*.out.xml -/tests/lang/*.err -/tests/lang/*.ast +# /tests/functional/lang/ +/tests/functional/lang/*.out +/tests/functional/lang/*.out.xml +/tests/functional/lang/*.err +/tests/functional/lang/*.ast /perl/lib/Nix/Config.pm /perl/lib/Nix/Store.cc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a9cff3b2..73210b303 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). - [ ] Fixes an [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) issue - [ ] Tests, as appropriate: - - Functional tests – [`tests/**.sh`](./tests) + - Functional tests – [`tests/functional/**.sh`](./tests/functional) - Unit tests – [`src/*/tests`](./src/) - Integration tests – [`tests/nixos/*`](./tests/nixos) - [ ] User documentation in the [manual](..doc/manual/src) diff --git a/Makefile b/Makefile index 2eabeb077..6658e3490 100644 --- a/Makefile +++ b/Makefile @@ -27,11 +27,11 @@ makefiles += \ src/libutil/tests/local.mk \ src/libstore/tests/local.mk \ src/libexpr/tests/local.mk \ - tests/local.mk \ - tests/ca/local.mk \ - tests/dyn-drv/local.mk \ - tests/test-libstoreconsumer/local.mk \ - tests/plugins/local.mk + tests/functional/local.mk \ + tests/functional/ca/local.mk \ + tests/functional/dyn-drv/local.mk \ + tests/functional/test-libstoreconsumer/local.mk \ + tests/functional/plugins/local.mk else makefiles += \ mk/disable-tests.mk diff --git a/doc/manual/src/contributing/testing.md b/doc/manual/src/contributing/testing.md index 04bb4343d..1a6388e40 100644 --- a/doc/manual/src/contributing/testing.md +++ b/doc/manual/src/contributing/testing.md @@ -82,7 +82,7 @@ The characterization tests will mark themselves "skipped" since they regenerated ## Functional tests -The functional tests reside under the `tests` directory and are listed in `tests/local.mk`. +The functional tests reside under the `tests/functional` directory and are listed in `tests/functional/local.mk`. Each test is a bash script. ### Running the whole test suite @@ -91,8 +91,8 @@ The whole test suite can be run with: ```shell-session $ make install && make installcheck -ran test tests/foo.sh... [PASS] -ran test tests/bar.sh... [PASS] +ran test tests/functional/foo.sh... [PASS] +ran test tests/functional/bar.sh... [PASS] ... ``` @@ -100,14 +100,14 @@ ran test tests/bar.sh... [PASS] Sometimes it is useful to group related tests so they can be easily run together without running the entire test suite. Each test group is in a subdirectory of `tests`. -For example, `tests/ca/local.mk` defines a `ca` test group for content-addressed derivation outputs. +For example, `tests/functional/ca/local.mk` defines a `ca` test group for content-addressed derivation outputs. That test group can be run like this: ```shell-session $ make ca.test-group -j50 -ran test tests/ca/nix-run.sh... [PASS] -ran test tests/ca/import-derivation.sh... [PASS] +ran test tests/functional/ca/nix-run.sh... [PASS] +ran test tests/functional/ca/import-derivation.sh... [PASS] ... ``` @@ -126,21 +126,21 @@ install-tests-groups += $(test-group-name) Individual tests can be run with `make`: ```shell-session -$ make tests/${testName}.sh.test -ran test tests/${testName}.sh... [PASS] +$ make tests/functional/${testName}.sh.test +ran test tests/functional/${testName}.sh... [PASS] ``` or without `make`: ```shell-session -$ ./mk/run-test.sh tests/${testName}.sh -ran test tests/${testName}.sh... [PASS] +$ ./mk/run-test.sh tests/functional/${testName}.sh +ran test tests/functional/${testName}.sh... [PASS] ``` To see the complete output, one can also run: ```shell-session -$ ./mk/debug-test.sh tests/${testName}.sh +$ ./mk/debug-test.sh tests/functional/${testName}.sh + foo output from foo + bar @@ -175,7 +175,7 @@ edit it like so: Then, running the test with `./mk/debug-test.sh` will drop you into GDB once the script reaches that point: ```shell-session -$ ./mk/debug-test.sh tests/${testName}.sh +$ ./mk/debug-test.sh tests/functional/${testName}.sh ... + gdb blash blub GNU gdb (GDB) 12.1 @@ -206,7 +206,7 @@ It is frequently useful to regenerate the expected output. To do that, rerun the failed test(s) with `_NIX_TEST_ACCEPT=1`. For example: ```bash -_NIX_TEST_ACCEPT=1 make tests/lang.sh.test +_NIX_TEST_ACCEPT=1 make tests/functional/lang.sh.test ``` This convention is shared with the [characterization unit tests](#characterization-testing-1) too. diff --git a/flake.nix b/flake.nix index 4de155362..c331b2651 100644 --- a/flake.nix +++ b/flake.nix @@ -61,36 +61,28 @@ nixSrc = fileset.toSource { root = ./.; - fileset = fileset.intersect baseFiles ( - fileset.difference - (fileset.unions [ - ./.version - ./boehmgc-coroutine-sp-fallback.diff - ./bootstrap.sh - ./configure.ac - ./doc - ./local.mk - ./m4 - ./Makefile - ./Makefile.config.in - ./misc - ./mk - ./precompiled-headers.h - ./src - ./tests - ./unit-test-data - ./COPYING - ./scripts/local.mk - (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) - # TODO: do we really need README.md? It doesn't seem used in the build. - ./README.md - ]) - (fileset.unions [ - # Removed file sets - ./tests/nixos - ./tests/installer - ]) - ); + fileset = fileset.intersect baseFiles (fileset.unions [ + ./.version + ./boehmgc-coroutine-sp-fallback.diff + ./bootstrap.sh + ./configure.ac + ./doc + ./local.mk + ./m4 + ./Makefile + ./Makefile.config.in + ./misc + ./mk + ./precompiled-headers.h + ./src + ./tests/functional + ./unit-test-data + ./COPYING + ./scripts/local.mk + (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) + # TODO: do we really need README.md? It doesn't seem used in the build. + ./README.md + ]); }; # Memoize nixpkgs for different platforms for efficiency. diff --git a/mk/common-test.sh b/mk/common-test.sh index 0a2e4c1c2..7ab25febf 100644 --- a/mk/common-test.sh +++ b/mk/common-test.sh @@ -1,11 +1,15 @@ +test_dir=tests/functional + +test=$(echo -n "$test" | sed -e "s|^$test_dir/||") + TESTS_ENVIRONMENT=("TEST_NAME=${test%.*}" 'NIX_REMOTE=') : ${BASH:=/usr/bin/env bash} init_test () { - cd tests && env "${TESTS_ENVIRONMENT[@]}" $BASH -e init.sh 2>/dev/null > /dev/null + cd "$test_dir" && env "${TESTS_ENVIRONMENT[@]}" $BASH -e init.sh 2>/dev/null > /dev/null } run_test_proper () { - cd $(dirname $test) && env "${TESTS_ENVIRONMENT[@]}" $BASH -e $(basename $test) + cd "$test_dir/$(dirname $test)" && env "${TESTS_ENVIRONMENT[@]}" $BASH -e $(basename $test) } diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index 26c87391c..516cbef83 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -776,7 +776,7 @@ void LocalStore::collectGarbage(const GCOptions & options, GCResults & results) } }; - /* Synchronisation point for testing, see tests/gc-concurrent.sh. */ + /* Synchronisation point for testing, see tests/functional/gc-concurrent.sh. */ if (auto p = getEnv("_NIX_TEST_GC_SYNC")) readFile(*p); diff --git a/src/libutil/url-parts.hh b/src/libutil/url-parts.hh index 4712c0c91..5c5a30dc2 100644 --- a/src/libutil/url-parts.hh +++ b/src/libutil/url-parts.hh @@ -30,7 +30,7 @@ extern std::regex refRegex; /// Instead of defining what a good Git Ref is, we define what a bad Git Ref is /// This is because of the definition of a ref in refs.c in https://github.com/git/git -/// See tests/fetchGitRefs.sh for the full definition +/// See tests/functional/fetchGitRefs.sh for the full definition const static std::string badGitRefRegexS = "//|^[./]|/\\.|\\.\\.|[[:cntrl:][:space:]:?^~\[]|\\\\|\\*|\\.lock$|\\.lock/|@\\{|[/.]$|^@$|^$"; extern std::regex badGitRefRegex; diff --git a/tests/add.sh b/tests/functional/add.sh similarity index 100% rename from tests/add.sh rename to tests/functional/add.sh diff --git a/tests/bad.tar.xz b/tests/functional/bad.tar.xz similarity index 100% rename from tests/bad.tar.xz rename to tests/functional/bad.tar.xz diff --git a/tests/bash-profile.sh b/tests/functional/bash-profile.sh similarity index 78% rename from tests/bash-profile.sh rename to tests/functional/bash-profile.sh index e2e0d1090..3faeaaba1 100644 --- a/tests/bash-profile.sh +++ b/tests/functional/bash-profile.sh @@ -1,6 +1,6 @@ source common.sh -sed -e "s|@localstatedir@|$TEST_ROOT/profile-var|g" -e "s|@coreutils@|$coreutils|g" < ../scripts/nix-profile.sh.in > $TEST_ROOT/nix-profile.sh +sed -e "s|@localstatedir@|$TEST_ROOT/profile-var|g" -e "s|@coreutils@|$coreutils|g" < ../../scripts/nix-profile.sh.in > $TEST_ROOT/nix-profile.sh user=$(whoami) rm -rf $TEST_HOME $TEST_ROOT/profile-var diff --git a/tests/big-derivation-attr.nix b/tests/functional/big-derivation-attr.nix similarity index 100% rename from tests/big-derivation-attr.nix rename to tests/functional/big-derivation-attr.nix diff --git a/tests/binary-cache-build-remote.sh b/tests/functional/binary-cache-build-remote.sh similarity index 100% rename from tests/binary-cache-build-remote.sh rename to tests/functional/binary-cache-build-remote.sh diff --git a/tests/binary-cache.sh b/tests/functional/binary-cache.sh similarity index 100% rename from tests/binary-cache.sh rename to tests/functional/binary-cache.sh diff --git a/tests/brotli.sh b/tests/functional/brotli.sh similarity index 100% rename from tests/brotli.sh rename to tests/functional/brotli.sh diff --git a/tests/build-delete.sh b/tests/functional/build-delete.sh similarity index 100% rename from tests/build-delete.sh rename to tests/functional/build-delete.sh diff --git a/tests/build-dry.sh b/tests/functional/build-dry.sh similarity index 100% rename from tests/build-dry.sh rename to tests/functional/build-dry.sh diff --git a/tests/build-hook-ca-fixed.nix b/tests/functional/build-hook-ca-fixed.nix similarity index 100% rename from tests/build-hook-ca-fixed.nix rename to tests/functional/build-hook-ca-fixed.nix diff --git a/tests/build-hook-ca-floating.nix b/tests/functional/build-hook-ca-floating.nix similarity index 100% rename from tests/build-hook-ca-floating.nix rename to tests/functional/build-hook-ca-floating.nix diff --git a/tests/build-hook.nix b/tests/functional/build-hook.nix similarity index 100% rename from tests/build-hook.nix rename to tests/functional/build-hook.nix diff --git a/tests/build-remote-content-addressed-fixed.sh b/tests/functional/build-remote-content-addressed-fixed.sh similarity index 100% rename from tests/build-remote-content-addressed-fixed.sh rename to tests/functional/build-remote-content-addressed-fixed.sh diff --git a/tests/build-remote-content-addressed-floating.sh b/tests/functional/build-remote-content-addressed-floating.sh similarity index 100% rename from tests/build-remote-content-addressed-floating.sh rename to tests/functional/build-remote-content-addressed-floating.sh diff --git a/tests/build-remote-input-addressed.sh b/tests/functional/build-remote-input-addressed.sh similarity index 100% rename from tests/build-remote-input-addressed.sh rename to tests/functional/build-remote-input-addressed.sh diff --git a/tests/build-remote-trustless-after.sh b/tests/functional/build-remote-trustless-after.sh similarity index 100% rename from tests/build-remote-trustless-after.sh rename to tests/functional/build-remote-trustless-after.sh diff --git a/tests/build-remote-trustless-should-fail-0.sh b/tests/functional/build-remote-trustless-should-fail-0.sh similarity index 100% rename from tests/build-remote-trustless-should-fail-0.sh rename to tests/functional/build-remote-trustless-should-fail-0.sh diff --git a/tests/build-remote-trustless-should-pass-0.sh b/tests/functional/build-remote-trustless-should-pass-0.sh similarity index 100% rename from tests/build-remote-trustless-should-pass-0.sh rename to tests/functional/build-remote-trustless-should-pass-0.sh diff --git a/tests/build-remote-trustless-should-pass-1.sh b/tests/functional/build-remote-trustless-should-pass-1.sh similarity index 100% rename from tests/build-remote-trustless-should-pass-1.sh rename to tests/functional/build-remote-trustless-should-pass-1.sh diff --git a/tests/build-remote-trustless-should-pass-2.sh b/tests/functional/build-remote-trustless-should-pass-2.sh similarity index 100% rename from tests/build-remote-trustless-should-pass-2.sh rename to tests/functional/build-remote-trustless-should-pass-2.sh diff --git a/tests/build-remote-trustless-should-pass-3.sh b/tests/functional/build-remote-trustless-should-pass-3.sh similarity index 100% rename from tests/build-remote-trustless-should-pass-3.sh rename to tests/functional/build-remote-trustless-should-pass-3.sh diff --git a/tests/build-remote-trustless.sh b/tests/functional/build-remote-trustless.sh similarity index 82% rename from tests/build-remote-trustless.sh rename to tests/functional/build-remote-trustless.sh index 9df44e0c5..81e5253bf 100644 --- a/tests/build-remote-trustless.sh +++ b/tests/functional/build-remote-trustless.sh @@ -6,7 +6,7 @@ unset NIX_STATE_DIR remoteDir=$TEST_ROOT/remote -# Note: ssh{-ng}://localhost bypasses ssh. See tests/build-remote.sh for +# Note: ssh{-ng}://localhost bypasses ssh. See tests/functional/build-remote.sh for # more details. nix-build $file -o $TEST_ROOT/result --max-jobs 0 \ --arg busybox $busybox \ diff --git a/tests/build-remote.sh b/tests/functional/build-remote.sh similarity index 100% rename from tests/build-remote.sh rename to tests/functional/build-remote.sh diff --git a/tests/build.sh b/tests/functional/build.sh similarity index 100% rename from tests/build.sh rename to tests/functional/build.sh diff --git a/tests/ca-shell.nix b/tests/functional/ca-shell.nix similarity index 100% rename from tests/ca-shell.nix rename to tests/functional/ca-shell.nix diff --git a/tests/ca/build-cache.sh b/tests/functional/ca/build-cache.sh similarity index 100% rename from tests/ca/build-cache.sh rename to tests/functional/ca/build-cache.sh diff --git a/tests/ca/build-dry.sh b/tests/functional/ca/build-dry.sh similarity index 100% rename from tests/ca/build-dry.sh rename to tests/functional/ca/build-dry.sh diff --git a/tests/ca/build-with-garbage-path.sh b/tests/functional/ca/build-with-garbage-path.sh similarity index 100% rename from tests/ca/build-with-garbage-path.sh rename to tests/functional/ca/build-with-garbage-path.sh diff --git a/tests/ca/build.sh b/tests/functional/ca/build.sh similarity index 100% rename from tests/ca/build.sh rename to tests/functional/ca/build.sh diff --git a/tests/ca/common.sh b/tests/functional/ca/common.sh similarity index 100% rename from tests/ca/common.sh rename to tests/functional/ca/common.sh diff --git a/tests/ca/concurrent-builds.sh b/tests/functional/ca/concurrent-builds.sh similarity index 100% rename from tests/ca/concurrent-builds.sh rename to tests/functional/ca/concurrent-builds.sh diff --git a/tests/ca/config.nix.in b/tests/functional/ca/config.nix.in similarity index 100% rename from tests/ca/config.nix.in rename to tests/functional/ca/config.nix.in diff --git a/tests/ca/content-addressed.nix b/tests/functional/ca/content-addressed.nix similarity index 100% rename from tests/ca/content-addressed.nix rename to tests/functional/ca/content-addressed.nix diff --git a/tests/ca/derivation-json.sh b/tests/functional/ca/derivation-json.sh similarity index 100% rename from tests/ca/derivation-json.sh rename to tests/functional/ca/derivation-json.sh diff --git a/tests/ca/duplicate-realisation-in-closure.sh b/tests/functional/ca/duplicate-realisation-in-closure.sh similarity index 100% rename from tests/ca/duplicate-realisation-in-closure.sh rename to tests/functional/ca/duplicate-realisation-in-closure.sh diff --git a/tests/ca/flake.nix b/tests/functional/ca/flake.nix similarity index 100% rename from tests/ca/flake.nix rename to tests/functional/ca/flake.nix diff --git a/tests/ca/gc.sh b/tests/functional/ca/gc.sh similarity index 100% rename from tests/ca/gc.sh rename to tests/functional/ca/gc.sh diff --git a/tests/ca/import-derivation.sh b/tests/functional/ca/import-derivation.sh similarity index 100% rename from tests/ca/import-derivation.sh rename to tests/functional/ca/import-derivation.sh diff --git a/tests/ca/local.mk b/tests/functional/ca/local.mk similarity index 94% rename from tests/ca/local.mk rename to tests/functional/ca/local.mk index 0852e592e..fd87b8d1f 100644 --- a/tests/ca/local.mk +++ b/tests/functional/ca/local.mk @@ -25,4 +25,4 @@ clean-files += \ $(d)/config.nix test-deps += \ - tests/ca/config.nix + tests/functional/ca/config.nix diff --git a/tests/ca/new-build-cmd.sh b/tests/functional/ca/new-build-cmd.sh similarity index 100% rename from tests/ca/new-build-cmd.sh rename to tests/functional/ca/new-build-cmd.sh diff --git a/tests/ca/nix-copy.sh b/tests/functional/ca/nix-copy.sh similarity index 100% rename from tests/ca/nix-copy.sh rename to tests/functional/ca/nix-copy.sh diff --git a/tests/ca/nix-run.sh b/tests/functional/ca/nix-run.sh similarity index 100% rename from tests/ca/nix-run.sh rename to tests/functional/ca/nix-run.sh diff --git a/tests/ca/nix-shell.sh b/tests/functional/ca/nix-shell.sh similarity index 100% rename from tests/ca/nix-shell.sh rename to tests/functional/ca/nix-shell.sh diff --git a/tests/ca/nondeterministic.nix b/tests/functional/ca/nondeterministic.nix similarity index 100% rename from tests/ca/nondeterministic.nix rename to tests/functional/ca/nondeterministic.nix diff --git a/tests/ca/post-hook.sh b/tests/functional/ca/post-hook.sh similarity index 100% rename from tests/ca/post-hook.sh rename to tests/functional/ca/post-hook.sh diff --git a/tests/ca/racy.nix b/tests/functional/ca/racy.nix similarity index 100% rename from tests/ca/racy.nix rename to tests/functional/ca/racy.nix diff --git a/tests/ca/recursive.sh b/tests/functional/ca/recursive.sh similarity index 100% rename from tests/ca/recursive.sh rename to tests/functional/ca/recursive.sh diff --git a/tests/ca/repl.sh b/tests/functional/ca/repl.sh similarity index 100% rename from tests/ca/repl.sh rename to tests/functional/ca/repl.sh diff --git a/tests/ca/selfref-gc.sh b/tests/functional/ca/selfref-gc.sh similarity index 100% rename from tests/ca/selfref-gc.sh rename to tests/functional/ca/selfref-gc.sh diff --git a/tests/ca/signatures.sh b/tests/functional/ca/signatures.sh similarity index 100% rename from tests/ca/signatures.sh rename to tests/functional/ca/signatures.sh diff --git a/tests/ca/substitute.sh b/tests/functional/ca/substitute.sh similarity index 100% rename from tests/ca/substitute.sh rename to tests/functional/ca/substitute.sh diff --git a/tests/ca/why-depends.sh b/tests/functional/ca/why-depends.sh similarity index 100% rename from tests/ca/why-depends.sh rename to tests/functional/ca/why-depends.sh diff --git a/tests/case-hack.sh b/tests/functional/case-hack.sh similarity index 100% rename from tests/case-hack.sh rename to tests/functional/case-hack.sh diff --git a/tests/case.nar b/tests/functional/case.nar similarity index 100% rename from tests/case.nar rename to tests/functional/case.nar diff --git a/tests/check-refs.nix b/tests/functional/check-refs.nix similarity index 100% rename from tests/check-refs.nix rename to tests/functional/check-refs.nix diff --git a/tests/check-refs.sh b/tests/functional/check-refs.sh similarity index 100% rename from tests/check-refs.sh rename to tests/functional/check-refs.sh diff --git a/tests/check-reqs.nix b/tests/functional/check-reqs.nix similarity index 100% rename from tests/check-reqs.nix rename to tests/functional/check-reqs.nix diff --git a/tests/check-reqs.sh b/tests/functional/check-reqs.sh similarity index 100% rename from tests/check-reqs.sh rename to tests/functional/check-reqs.sh diff --git a/tests/check.nix b/tests/functional/check.nix similarity index 100% rename from tests/check.nix rename to tests/functional/check.nix diff --git a/tests/check.sh b/tests/functional/check.sh similarity index 100% rename from tests/check.sh rename to tests/functional/check.sh diff --git a/tests/common.sh b/tests/functional/common.sh similarity index 100% rename from tests/common.sh rename to tests/functional/common.sh diff --git a/tests/common/vars-and-functions.sh.in b/tests/functional/common/vars-and-functions.sh.in similarity index 99% rename from tests/common/vars-and-functions.sh.in rename to tests/functional/common/vars-and-functions.sh.in index 8f9ec4b1a..967d6be54 100644 --- a/tests/common/vars-and-functions.sh.in +++ b/tests/functional/common/vars-and-functions.sh.in @@ -6,7 +6,7 @@ COMMON_VARS_AND_FUNCTIONS_SH_SOURCED=1 export PS4='+(${BASH_SOURCE[0]-$0}:$LINENO) ' -export TEST_ROOT=$(realpath ${TMPDIR:-/tmp}/nix-test)/${TEST_NAME:-default} +export TEST_ROOT=$(realpath ${TMPDIR:-/tmp}/nix-test)/${TEST_NAME:-default/tests\/functional//} export NIX_STORE_DIR if ! NIX_STORE_DIR=$(readlink -f $TEST_ROOT/store 2> /dev/null); then # Maybe the build directory is symlinked. diff --git a/tests/completions.sh b/tests/functional/completions.sh similarity index 100% rename from tests/completions.sh rename to tests/functional/completions.sh diff --git a/tests/compression-levels.sh b/tests/functional/compression-levels.sh similarity index 100% rename from tests/compression-levels.sh rename to tests/functional/compression-levels.sh diff --git a/tests/compute-levels.sh b/tests/functional/compute-levels.sh similarity index 100% rename from tests/compute-levels.sh rename to tests/functional/compute-levels.sh diff --git a/tests/config.nix.in b/tests/functional/config.nix.in similarity index 100% rename from tests/config.nix.in rename to tests/functional/config.nix.in diff --git a/tests/config.sh b/tests/functional/config.sh similarity index 100% rename from tests/config.sh rename to tests/functional/config.sh diff --git a/tests/config/nix-with-substituters.conf b/tests/functional/config/nix-with-substituters.conf similarity index 100% rename from tests/config/nix-with-substituters.conf rename to tests/functional/config/nix-with-substituters.conf diff --git a/tests/db-migration.sh b/tests/functional/db-migration.sh similarity index 100% rename from tests/db-migration.sh rename to tests/functional/db-migration.sh diff --git a/tests/dependencies.builder0.sh b/tests/functional/dependencies.builder0.sh similarity index 100% rename from tests/dependencies.builder0.sh rename to tests/functional/dependencies.builder0.sh diff --git a/tests/dependencies.nix b/tests/functional/dependencies.nix similarity index 100% rename from tests/dependencies.nix rename to tests/functional/dependencies.nix diff --git a/tests/dependencies.sh b/tests/functional/dependencies.sh similarity index 100% rename from tests/dependencies.sh rename to tests/functional/dependencies.sh diff --git a/tests/derivation-json.sh b/tests/functional/derivation-json.sh similarity index 100% rename from tests/derivation-json.sh rename to tests/functional/derivation-json.sh diff --git a/tests/dummy b/tests/functional/dummy similarity index 100% rename from tests/dummy rename to tests/functional/dummy diff --git a/tests/dump-db.sh b/tests/functional/dump-db.sh similarity index 100% rename from tests/dump-db.sh rename to tests/functional/dump-db.sh diff --git a/tests/dyn-drv/build-built-drv.sh b/tests/functional/dyn-drv/build-built-drv.sh similarity index 100% rename from tests/dyn-drv/build-built-drv.sh rename to tests/functional/dyn-drv/build-built-drv.sh diff --git a/tests/dyn-drv/common.sh b/tests/functional/dyn-drv/common.sh similarity index 100% rename from tests/dyn-drv/common.sh rename to tests/functional/dyn-drv/common.sh diff --git a/tests/dyn-drv/config.nix.in b/tests/functional/dyn-drv/config.nix.in similarity index 100% rename from tests/dyn-drv/config.nix.in rename to tests/functional/dyn-drv/config.nix.in diff --git a/tests/dyn-drv/dep-built-drv.sh b/tests/functional/dyn-drv/dep-built-drv.sh similarity index 100% rename from tests/dyn-drv/dep-built-drv.sh rename to tests/functional/dyn-drv/dep-built-drv.sh diff --git a/tests/dyn-drv/eval-outputOf.sh b/tests/functional/dyn-drv/eval-outputOf.sh similarity index 100% rename from tests/dyn-drv/eval-outputOf.sh rename to tests/functional/dyn-drv/eval-outputOf.sh diff --git a/tests/dyn-drv/local.mk b/tests/functional/dyn-drv/local.mk similarity index 87% rename from tests/dyn-drv/local.mk rename to tests/functional/dyn-drv/local.mk index 6b435499b..c87534944 100644 --- a/tests/dyn-drv/local.mk +++ b/tests/functional/dyn-drv/local.mk @@ -12,4 +12,4 @@ clean-files += \ $(d)/config.nix test-deps += \ - tests/dyn-drv/config.nix + tests/functional/dyn-drv/config.nix diff --git a/tests/dyn-drv/old-daemon-error-hack.nix b/tests/functional/dyn-drv/old-daemon-error-hack.nix similarity index 100% rename from tests/dyn-drv/old-daemon-error-hack.nix rename to tests/functional/dyn-drv/old-daemon-error-hack.nix diff --git a/tests/dyn-drv/old-daemon-error-hack.sh b/tests/functional/dyn-drv/old-daemon-error-hack.sh similarity index 100% rename from tests/dyn-drv/old-daemon-error-hack.sh rename to tests/functional/dyn-drv/old-daemon-error-hack.sh diff --git a/tests/dyn-drv/recursive-mod-json.nix b/tests/functional/dyn-drv/recursive-mod-json.nix similarity index 100% rename from tests/dyn-drv/recursive-mod-json.nix rename to tests/functional/dyn-drv/recursive-mod-json.nix diff --git a/tests/dyn-drv/recursive-mod-json.sh b/tests/functional/dyn-drv/recursive-mod-json.sh similarity index 100% rename from tests/dyn-drv/recursive-mod-json.sh rename to tests/functional/dyn-drv/recursive-mod-json.sh diff --git a/tests/dyn-drv/text-hashed-output.nix b/tests/functional/dyn-drv/text-hashed-output.nix similarity index 100% rename from tests/dyn-drv/text-hashed-output.nix rename to tests/functional/dyn-drv/text-hashed-output.nix diff --git a/tests/dyn-drv/text-hashed-output.sh b/tests/functional/dyn-drv/text-hashed-output.sh similarity index 100% rename from tests/dyn-drv/text-hashed-output.sh rename to tests/functional/dyn-drv/text-hashed-output.sh diff --git a/tests/eval-store.sh b/tests/functional/eval-store.sh similarity index 100% rename from tests/eval-store.sh rename to tests/functional/eval-store.sh diff --git a/tests/eval.nix b/tests/functional/eval.nix similarity index 100% rename from tests/eval.nix rename to tests/functional/eval.nix diff --git a/tests/eval.sh b/tests/functional/eval.sh similarity index 100% rename from tests/eval.sh rename to tests/functional/eval.sh diff --git a/tests/experimental-features.sh b/tests/functional/experimental-features.sh similarity index 100% rename from tests/experimental-features.sh rename to tests/functional/experimental-features.sh diff --git a/tests/export-graph.nix b/tests/functional/export-graph.nix similarity index 100% rename from tests/export-graph.nix rename to tests/functional/export-graph.nix diff --git a/tests/export-graph.sh b/tests/functional/export-graph.sh similarity index 100% rename from tests/export-graph.sh rename to tests/functional/export-graph.sh diff --git a/tests/export.sh b/tests/functional/export.sh similarity index 100% rename from tests/export.sh rename to tests/functional/export.sh diff --git a/tests/failing.nix b/tests/functional/failing.nix similarity index 100% rename from tests/failing.nix rename to tests/functional/failing.nix diff --git a/tests/fetchClosure.sh b/tests/functional/fetchClosure.sh similarity index 100% rename from tests/fetchClosure.sh rename to tests/functional/fetchClosure.sh diff --git a/tests/fetchGit.sh b/tests/functional/fetchGit.sh similarity index 100% rename from tests/fetchGit.sh rename to tests/functional/fetchGit.sh diff --git a/tests/fetchGitRefs.sh b/tests/functional/fetchGitRefs.sh similarity index 100% rename from tests/fetchGitRefs.sh rename to tests/functional/fetchGitRefs.sh diff --git a/tests/fetchGitSubmodules.sh b/tests/functional/fetchGitSubmodules.sh similarity index 100% rename from tests/fetchGitSubmodules.sh rename to tests/functional/fetchGitSubmodules.sh diff --git a/tests/fetchMercurial.sh b/tests/functional/fetchMercurial.sh similarity index 100% rename from tests/fetchMercurial.sh rename to tests/functional/fetchMercurial.sh diff --git a/tests/fetchPath.sh b/tests/functional/fetchPath.sh similarity index 100% rename from tests/fetchPath.sh rename to tests/functional/fetchPath.sh diff --git a/tests/fetchTree-file.sh b/tests/functional/fetchTree-file.sh similarity index 100% rename from tests/fetchTree-file.sh rename to tests/functional/fetchTree-file.sh diff --git a/tests/fetchurl.sh b/tests/functional/fetchurl.sh similarity index 100% rename from tests/fetchurl.sh rename to tests/functional/fetchurl.sh diff --git a/tests/filter-source.nix b/tests/functional/filter-source.nix similarity index 100% rename from tests/filter-source.nix rename to tests/functional/filter-source.nix diff --git a/tests/filter-source.sh b/tests/functional/filter-source.sh similarity index 100% rename from tests/filter-source.sh rename to tests/functional/filter-source.sh diff --git a/tests/fixed.builder1.sh b/tests/functional/fixed.builder1.sh similarity index 100% rename from tests/fixed.builder1.sh rename to tests/functional/fixed.builder1.sh diff --git a/tests/fixed.builder2.sh b/tests/functional/fixed.builder2.sh similarity index 100% rename from tests/fixed.builder2.sh rename to tests/functional/fixed.builder2.sh diff --git a/tests/fixed.nix b/tests/functional/fixed.nix similarity index 100% rename from tests/fixed.nix rename to tests/functional/fixed.nix diff --git a/tests/fixed.sh b/tests/functional/fixed.sh similarity index 100% rename from tests/fixed.sh rename to tests/functional/fixed.sh diff --git a/tests/flakes/absolute-attr-paths.sh b/tests/functional/flakes/absolute-attr-paths.sh similarity index 100% rename from tests/flakes/absolute-attr-paths.sh rename to tests/functional/flakes/absolute-attr-paths.sh diff --git a/tests/flakes/absolute-paths.sh b/tests/functional/flakes/absolute-paths.sh similarity index 100% rename from tests/flakes/absolute-paths.sh rename to tests/functional/flakes/absolute-paths.sh diff --git a/tests/flakes/build-paths.sh b/tests/functional/flakes/build-paths.sh similarity index 100% rename from tests/flakes/build-paths.sh rename to tests/functional/flakes/build-paths.sh diff --git a/tests/flakes/bundle.sh b/tests/functional/flakes/bundle.sh similarity index 100% rename from tests/flakes/bundle.sh rename to tests/functional/flakes/bundle.sh diff --git a/tests/flakes/check.sh b/tests/functional/flakes/check.sh similarity index 100% rename from tests/flakes/check.sh rename to tests/functional/flakes/check.sh diff --git a/tests/flakes/circular.sh b/tests/functional/flakes/circular.sh similarity index 100% rename from tests/flakes/circular.sh rename to tests/functional/flakes/circular.sh diff --git a/tests/flakes/common.sh b/tests/functional/flakes/common.sh similarity index 100% rename from tests/flakes/common.sh rename to tests/functional/flakes/common.sh diff --git a/tests/flakes/config.sh b/tests/functional/flakes/config.sh similarity index 100% rename from tests/flakes/config.sh rename to tests/functional/flakes/config.sh diff --git a/tests/flakes/flake-in-submodule.sh b/tests/functional/flakes/flake-in-submodule.sh similarity index 100% rename from tests/flakes/flake-in-submodule.sh rename to tests/functional/flakes/flake-in-submodule.sh diff --git a/tests/flakes/flakes.sh b/tests/functional/flakes/flakes.sh similarity index 100% rename from tests/flakes/flakes.sh rename to tests/functional/flakes/flakes.sh diff --git a/tests/flakes/follow-paths.sh b/tests/functional/flakes/follow-paths.sh similarity index 100% rename from tests/flakes/follow-paths.sh rename to tests/functional/flakes/follow-paths.sh diff --git a/tests/flakes/init.sh b/tests/functional/flakes/init.sh similarity index 100% rename from tests/flakes/init.sh rename to tests/functional/flakes/init.sh diff --git a/tests/flakes/inputs.sh b/tests/functional/flakes/inputs.sh similarity index 100% rename from tests/flakes/inputs.sh rename to tests/functional/flakes/inputs.sh diff --git a/tests/flakes/mercurial.sh b/tests/functional/flakes/mercurial.sh similarity index 100% rename from tests/flakes/mercurial.sh rename to tests/functional/flakes/mercurial.sh diff --git a/tests/flakes/run.sh b/tests/functional/flakes/run.sh similarity index 100% rename from tests/flakes/run.sh rename to tests/functional/flakes/run.sh diff --git a/tests/flakes/search-root.sh b/tests/functional/flakes/search-root.sh similarity index 100% rename from tests/flakes/search-root.sh rename to tests/functional/flakes/search-root.sh diff --git a/tests/flakes/show.sh b/tests/functional/flakes/show.sh similarity index 100% rename from tests/flakes/show.sh rename to tests/functional/flakes/show.sh diff --git a/tests/flakes/unlocked-override.sh b/tests/functional/flakes/unlocked-override.sh similarity index 100% rename from tests/flakes/unlocked-override.sh rename to tests/functional/flakes/unlocked-override.sh diff --git a/tests/fmt.sh b/tests/functional/fmt.sh similarity index 100% rename from tests/fmt.sh rename to tests/functional/fmt.sh diff --git a/tests/fmt.simple.sh b/tests/functional/fmt.simple.sh similarity index 100% rename from tests/fmt.simple.sh rename to tests/functional/fmt.simple.sh diff --git a/tests/function-trace.sh b/tests/functional/function-trace.sh similarity index 100% rename from tests/function-trace.sh rename to tests/functional/function-trace.sh diff --git a/tests/gc-auto.sh b/tests/functional/gc-auto.sh similarity index 100% rename from tests/gc-auto.sh rename to tests/functional/gc-auto.sh diff --git a/tests/gc-concurrent.builder.sh b/tests/functional/gc-concurrent.builder.sh similarity index 100% rename from tests/gc-concurrent.builder.sh rename to tests/functional/gc-concurrent.builder.sh diff --git a/tests/gc-concurrent.nix b/tests/functional/gc-concurrent.nix similarity index 100% rename from tests/gc-concurrent.nix rename to tests/functional/gc-concurrent.nix diff --git a/tests/gc-concurrent.sh b/tests/functional/gc-concurrent.sh similarity index 100% rename from tests/gc-concurrent.sh rename to tests/functional/gc-concurrent.sh diff --git a/tests/gc-concurrent2.builder.sh b/tests/functional/gc-concurrent2.builder.sh similarity index 100% rename from tests/gc-concurrent2.builder.sh rename to tests/functional/gc-concurrent2.builder.sh diff --git a/tests/gc-non-blocking.sh b/tests/functional/gc-non-blocking.sh similarity index 100% rename from tests/gc-non-blocking.sh rename to tests/functional/gc-non-blocking.sh diff --git a/tests/gc-runtime.nix b/tests/functional/gc-runtime.nix similarity index 100% rename from tests/gc-runtime.nix rename to tests/functional/gc-runtime.nix diff --git a/tests/gc-runtime.sh b/tests/functional/gc-runtime.sh similarity index 100% rename from tests/gc-runtime.sh rename to tests/functional/gc-runtime.sh diff --git a/tests/gc.sh b/tests/functional/gc.sh similarity index 100% rename from tests/gc.sh rename to tests/functional/gc.sh diff --git a/tests/hash-check.nix b/tests/functional/hash-check.nix similarity index 100% rename from tests/hash-check.nix rename to tests/functional/hash-check.nix diff --git a/tests/hash.sh b/tests/functional/hash.sh similarity index 100% rename from tests/hash.sh rename to tests/functional/hash.sh diff --git a/tests/hermetic.nix b/tests/functional/hermetic.nix similarity index 100% rename from tests/hermetic.nix rename to tests/functional/hermetic.nix diff --git a/tests/import-derivation.nix b/tests/functional/import-derivation.nix similarity index 100% rename from tests/import-derivation.nix rename to tests/functional/import-derivation.nix diff --git a/tests/import-derivation.sh b/tests/functional/import-derivation.sh similarity index 100% rename from tests/import-derivation.sh rename to tests/functional/import-derivation.sh diff --git a/tests/impure-derivations.nix b/tests/functional/impure-derivations.nix similarity index 100% rename from tests/impure-derivations.nix rename to tests/functional/impure-derivations.nix diff --git a/tests/impure-derivations.sh b/tests/functional/impure-derivations.sh similarity index 100% rename from tests/impure-derivations.sh rename to tests/functional/impure-derivations.sh diff --git a/tests/init.sh b/tests/functional/init.sh similarity index 100% rename from tests/init.sh rename to tests/functional/init.sh diff --git a/tests/install-darwin.sh b/tests/functional/install-darwin.sh similarity index 100% rename from tests/install-darwin.sh rename to tests/functional/install-darwin.sh diff --git a/tests/lang-test-infra.sh b/tests/functional/lang-test-infra.sh similarity index 100% rename from tests/lang-test-infra.sh rename to tests/functional/lang-test-infra.sh diff --git a/tests/lang.sh b/tests/functional/lang.sh similarity index 98% rename from tests/lang.sh rename to tests/functional/lang.sh index 75dbbc38e..f4760eced 100755 --- a/tests/lang.sh +++ b/tests/functional/lang.sh @@ -134,7 +134,7 @@ else echo '' echo 'You can rerun this test with:' echo '' - echo ' _NIX_TEST_ACCEPT=1 make tests/lang.sh.test' + echo ' _NIX_TEST_ACCEPT=1 make tests/functional/lang.sh.test' echo '' echo 'to regenerate the files containing the expected output,' echo 'and then view the git diff to decide whether a change is' diff --git a/tests/lang/binary-data b/tests/functional/lang/binary-data similarity index 100% rename from tests/lang/binary-data rename to tests/functional/lang/binary-data diff --git a/tests/lang/data b/tests/functional/lang/data similarity index 100% rename from tests/lang/data rename to tests/functional/lang/data diff --git a/tests/lang/dir1/a.nix b/tests/functional/lang/dir1/a.nix similarity index 100% rename from tests/lang/dir1/a.nix rename to tests/functional/lang/dir1/a.nix diff --git a/tests/lang/dir2/a.nix b/tests/functional/lang/dir2/a.nix similarity index 100% rename from tests/lang/dir2/a.nix rename to tests/functional/lang/dir2/a.nix diff --git a/tests/lang/dir2/b.nix b/tests/functional/lang/dir2/b.nix similarity index 100% rename from tests/lang/dir2/b.nix rename to tests/functional/lang/dir2/b.nix diff --git a/tests/lang/dir3/a.nix b/tests/functional/lang/dir3/a.nix similarity index 100% rename from tests/lang/dir3/a.nix rename to tests/functional/lang/dir3/a.nix diff --git a/tests/lang/dir3/b.nix b/tests/functional/lang/dir3/b.nix similarity index 100% rename from tests/lang/dir3/b.nix rename to tests/functional/lang/dir3/b.nix diff --git a/tests/lang/dir3/c.nix b/tests/functional/lang/dir3/c.nix similarity index 100% rename from tests/lang/dir3/c.nix rename to tests/functional/lang/dir3/c.nix diff --git a/tests/lang/dir4/a.nix b/tests/functional/lang/dir4/a.nix similarity index 100% rename from tests/lang/dir4/a.nix rename to tests/functional/lang/dir4/a.nix diff --git a/tests/lang/dir4/c.nix b/tests/functional/lang/dir4/c.nix similarity index 100% rename from tests/lang/dir4/c.nix rename to tests/functional/lang/dir4/c.nix diff --git a/tests/lang/empty.exp b/tests/functional/lang/empty.exp similarity index 100% rename from tests/lang/empty.exp rename to tests/functional/lang/empty.exp diff --git a/tests/lang/eval-fail-abort.err.exp b/tests/functional/lang/eval-fail-abort.err.exp similarity index 100% rename from tests/lang/eval-fail-abort.err.exp rename to tests/functional/lang/eval-fail-abort.err.exp diff --git a/tests/lang/eval-fail-abort.nix b/tests/functional/lang/eval-fail-abort.nix similarity index 100% rename from tests/lang/eval-fail-abort.nix rename to tests/functional/lang/eval-fail-abort.nix diff --git a/tests/lang/eval-fail-antiquoted-path.err.exp b/tests/functional/lang/eval-fail-antiquoted-path.err.exp similarity index 100% rename from tests/lang/eval-fail-antiquoted-path.err.exp rename to tests/functional/lang/eval-fail-antiquoted-path.err.exp diff --git a/tests/lang/eval-fail-assert.err.exp b/tests/functional/lang/eval-fail-assert.err.exp similarity index 100% rename from tests/lang/eval-fail-assert.err.exp rename to tests/functional/lang/eval-fail-assert.err.exp diff --git a/tests/lang/eval-fail-assert.nix b/tests/functional/lang/eval-fail-assert.nix similarity index 100% rename from tests/lang/eval-fail-assert.nix rename to tests/functional/lang/eval-fail-assert.nix diff --git a/tests/lang/eval-fail-bad-antiquote-1.err.exp b/tests/functional/lang/eval-fail-bad-antiquote-1.err.exp similarity index 100% rename from tests/lang/eval-fail-bad-antiquote-1.err.exp rename to tests/functional/lang/eval-fail-bad-antiquote-1.err.exp diff --git a/tests/lang/eval-fail-bad-antiquote-2.err.exp b/tests/functional/lang/eval-fail-bad-antiquote-2.err.exp similarity index 100% rename from tests/lang/eval-fail-bad-antiquote-2.err.exp rename to tests/functional/lang/eval-fail-bad-antiquote-2.err.exp diff --git a/tests/lang/eval-fail-bad-antiquote-3.err.exp b/tests/functional/lang/eval-fail-bad-antiquote-3.err.exp similarity index 100% rename from tests/lang/eval-fail-bad-antiquote-3.err.exp rename to tests/functional/lang/eval-fail-bad-antiquote-3.err.exp diff --git a/tests/lang/eval-fail-bad-string-interpolation-1.err.exp b/tests/functional/lang/eval-fail-bad-string-interpolation-1.err.exp similarity index 100% rename from tests/lang/eval-fail-bad-string-interpolation-1.err.exp rename to tests/functional/lang/eval-fail-bad-string-interpolation-1.err.exp diff --git a/tests/lang/eval-fail-bad-string-interpolation-1.nix b/tests/functional/lang/eval-fail-bad-string-interpolation-1.nix similarity index 100% rename from tests/lang/eval-fail-bad-string-interpolation-1.nix rename to tests/functional/lang/eval-fail-bad-string-interpolation-1.nix diff --git a/tests/lang/eval-fail-bad-string-interpolation-2.err.exp b/tests/functional/lang/eval-fail-bad-string-interpolation-2.err.exp similarity index 100% rename from tests/lang/eval-fail-bad-string-interpolation-2.err.exp rename to tests/functional/lang/eval-fail-bad-string-interpolation-2.err.exp diff --git a/tests/lang/eval-fail-bad-string-interpolation-2.nix b/tests/functional/lang/eval-fail-bad-string-interpolation-2.nix similarity index 100% rename from tests/lang/eval-fail-bad-string-interpolation-2.nix rename to tests/functional/lang/eval-fail-bad-string-interpolation-2.nix diff --git a/tests/lang/eval-fail-bad-string-interpolation-3.err.exp b/tests/functional/lang/eval-fail-bad-string-interpolation-3.err.exp similarity index 100% rename from tests/lang/eval-fail-bad-string-interpolation-3.err.exp rename to tests/functional/lang/eval-fail-bad-string-interpolation-3.err.exp diff --git a/tests/lang/eval-fail-bad-string-interpolation-3.nix b/tests/functional/lang/eval-fail-bad-string-interpolation-3.nix similarity index 100% rename from tests/lang/eval-fail-bad-string-interpolation-3.nix rename to tests/functional/lang/eval-fail-bad-string-interpolation-3.nix diff --git a/tests/lang/eval-fail-blackhole.err.exp b/tests/functional/lang/eval-fail-blackhole.err.exp similarity index 100% rename from tests/lang/eval-fail-blackhole.err.exp rename to tests/functional/lang/eval-fail-blackhole.err.exp diff --git a/tests/lang/eval-fail-blackhole.nix b/tests/functional/lang/eval-fail-blackhole.nix similarity index 100% rename from tests/lang/eval-fail-blackhole.nix rename to tests/functional/lang/eval-fail-blackhole.nix diff --git a/tests/lang/eval-fail-deepseq.err.exp b/tests/functional/lang/eval-fail-deepseq.err.exp similarity index 100% rename from tests/lang/eval-fail-deepseq.err.exp rename to tests/functional/lang/eval-fail-deepseq.err.exp diff --git a/tests/lang/eval-fail-deepseq.nix b/tests/functional/lang/eval-fail-deepseq.nix similarity index 100% rename from tests/lang/eval-fail-deepseq.nix rename to tests/functional/lang/eval-fail-deepseq.nix diff --git a/tests/lang/eval-fail-dup-dynamic-attrs.err.exp b/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp similarity index 100% rename from tests/lang/eval-fail-dup-dynamic-attrs.err.exp rename to tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp diff --git a/tests/lang/eval-fail-dup-dynamic-attrs.nix b/tests/functional/lang/eval-fail-dup-dynamic-attrs.nix similarity index 100% rename from tests/lang/eval-fail-dup-dynamic-attrs.nix rename to tests/functional/lang/eval-fail-dup-dynamic-attrs.nix diff --git a/tests/lang/eval-fail-foldlStrict-strict-op-application.err.exp b/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.err.exp similarity index 100% rename from tests/lang/eval-fail-foldlStrict-strict-op-application.err.exp rename to tests/functional/lang/eval-fail-foldlStrict-strict-op-application.err.exp diff --git a/tests/lang/eval-fail-foldlStrict-strict-op-application.nix b/tests/functional/lang/eval-fail-foldlStrict-strict-op-application.nix similarity index 100% rename from tests/lang/eval-fail-foldlStrict-strict-op-application.nix rename to tests/functional/lang/eval-fail-foldlStrict-strict-op-application.nix diff --git a/tests/lang/eval-fail-fromTOML-timestamps.err.exp b/tests/functional/lang/eval-fail-fromTOML-timestamps.err.exp similarity index 100% rename from tests/lang/eval-fail-fromTOML-timestamps.err.exp rename to tests/functional/lang/eval-fail-fromTOML-timestamps.err.exp diff --git a/tests/lang/eval-fail-fromTOML-timestamps.nix b/tests/functional/lang/eval-fail-fromTOML-timestamps.nix similarity index 100% rename from tests/lang/eval-fail-fromTOML-timestamps.nix rename to tests/functional/lang/eval-fail-fromTOML-timestamps.nix diff --git a/tests/lang/eval-fail-hashfile-missing.err.exp b/tests/functional/lang/eval-fail-hashfile-missing.err.exp similarity index 100% rename from tests/lang/eval-fail-hashfile-missing.err.exp rename to tests/functional/lang/eval-fail-hashfile-missing.err.exp diff --git a/tests/lang/eval-fail-hashfile-missing.nix b/tests/functional/lang/eval-fail-hashfile-missing.nix similarity index 100% rename from tests/lang/eval-fail-hashfile-missing.nix rename to tests/functional/lang/eval-fail-hashfile-missing.nix diff --git a/tests/lang/eval-fail-list.err.exp b/tests/functional/lang/eval-fail-list.err.exp similarity index 100% rename from tests/lang/eval-fail-list.err.exp rename to tests/functional/lang/eval-fail-list.err.exp diff --git a/tests/lang/eval-fail-list.nix b/tests/functional/lang/eval-fail-list.nix similarity index 100% rename from tests/lang/eval-fail-list.nix rename to tests/functional/lang/eval-fail-list.nix diff --git a/tests/lang/eval-fail-missing-arg.err.exp b/tests/functional/lang/eval-fail-missing-arg.err.exp similarity index 100% rename from tests/lang/eval-fail-missing-arg.err.exp rename to tests/functional/lang/eval-fail-missing-arg.err.exp diff --git a/tests/lang/eval-fail-missing-arg.nix b/tests/functional/lang/eval-fail-missing-arg.nix similarity index 100% rename from tests/lang/eval-fail-missing-arg.nix rename to tests/functional/lang/eval-fail-missing-arg.nix diff --git a/tests/lang/eval-fail-nonexist-path.err.exp b/tests/functional/lang/eval-fail-nonexist-path.err.exp similarity index 100% rename from tests/lang/eval-fail-nonexist-path.err.exp rename to tests/functional/lang/eval-fail-nonexist-path.err.exp diff --git a/tests/lang/eval-fail-nonexist-path.nix b/tests/functional/lang/eval-fail-nonexist-path.nix similarity index 100% rename from tests/lang/eval-fail-nonexist-path.nix rename to tests/functional/lang/eval-fail-nonexist-path.nix diff --git a/tests/lang/eval-fail-path-slash.err.exp b/tests/functional/lang/eval-fail-path-slash.err.exp similarity index 100% rename from tests/lang/eval-fail-path-slash.err.exp rename to tests/functional/lang/eval-fail-path-slash.err.exp diff --git a/tests/lang/eval-fail-path-slash.nix b/tests/functional/lang/eval-fail-path-slash.nix similarity index 100% rename from tests/lang/eval-fail-path-slash.nix rename to tests/functional/lang/eval-fail-path-slash.nix diff --git a/tests/lang/eval-fail-recursion.err.exp b/tests/functional/lang/eval-fail-recursion.err.exp similarity index 100% rename from tests/lang/eval-fail-recursion.err.exp rename to tests/functional/lang/eval-fail-recursion.err.exp diff --git a/tests/lang/eval-fail-recursion.nix b/tests/functional/lang/eval-fail-recursion.nix similarity index 100% rename from tests/lang/eval-fail-recursion.nix rename to tests/functional/lang/eval-fail-recursion.nix diff --git a/tests/lang/eval-fail-remove.err.exp b/tests/functional/lang/eval-fail-remove.err.exp similarity index 100% rename from tests/lang/eval-fail-remove.err.exp rename to tests/functional/lang/eval-fail-remove.err.exp diff --git a/tests/lang/eval-fail-remove.nix b/tests/functional/lang/eval-fail-remove.nix similarity index 100% rename from tests/lang/eval-fail-remove.nix rename to tests/functional/lang/eval-fail-remove.nix diff --git a/tests/lang/eval-fail-scope-5.err.exp b/tests/functional/lang/eval-fail-scope-5.err.exp similarity index 100% rename from tests/lang/eval-fail-scope-5.err.exp rename to tests/functional/lang/eval-fail-scope-5.err.exp diff --git a/tests/lang/eval-fail-scope-5.nix b/tests/functional/lang/eval-fail-scope-5.nix similarity index 100% rename from tests/lang/eval-fail-scope-5.nix rename to tests/functional/lang/eval-fail-scope-5.nix diff --git a/tests/lang/eval-fail-seq.err.exp b/tests/functional/lang/eval-fail-seq.err.exp similarity index 100% rename from tests/lang/eval-fail-seq.err.exp rename to tests/functional/lang/eval-fail-seq.err.exp diff --git a/tests/lang/eval-fail-seq.nix b/tests/functional/lang/eval-fail-seq.nix similarity index 100% rename from tests/lang/eval-fail-seq.nix rename to tests/functional/lang/eval-fail-seq.nix diff --git a/tests/lang/eval-fail-set-override.err.exp b/tests/functional/lang/eval-fail-set-override.err.exp similarity index 100% rename from tests/lang/eval-fail-set-override.err.exp rename to tests/functional/lang/eval-fail-set-override.err.exp diff --git a/tests/lang/eval-fail-set-override.nix b/tests/functional/lang/eval-fail-set-override.nix similarity index 100% rename from tests/lang/eval-fail-set-override.nix rename to tests/functional/lang/eval-fail-set-override.nix diff --git a/tests/lang/eval-fail-set.err.exp b/tests/functional/lang/eval-fail-set.err.exp similarity index 100% rename from tests/lang/eval-fail-set.err.exp rename to tests/functional/lang/eval-fail-set.err.exp diff --git a/tests/lang/eval-fail-set.nix b/tests/functional/lang/eval-fail-set.nix similarity index 100% rename from tests/lang/eval-fail-set.nix rename to tests/functional/lang/eval-fail-set.nix diff --git a/tests/lang/eval-fail-substring.err.exp b/tests/functional/lang/eval-fail-substring.err.exp similarity index 100% rename from tests/lang/eval-fail-substring.err.exp rename to tests/functional/lang/eval-fail-substring.err.exp diff --git a/tests/lang/eval-fail-substring.nix b/tests/functional/lang/eval-fail-substring.nix similarity index 100% rename from tests/lang/eval-fail-substring.nix rename to tests/functional/lang/eval-fail-substring.nix diff --git a/tests/lang/eval-fail-to-path.err.exp b/tests/functional/lang/eval-fail-to-path.err.exp similarity index 100% rename from tests/lang/eval-fail-to-path.err.exp rename to tests/functional/lang/eval-fail-to-path.err.exp diff --git a/tests/lang/eval-fail-to-path.nix b/tests/functional/lang/eval-fail-to-path.nix similarity index 100% rename from tests/lang/eval-fail-to-path.nix rename to tests/functional/lang/eval-fail-to-path.nix diff --git a/tests/lang/eval-fail-toJSON.err.exp b/tests/functional/lang/eval-fail-toJSON.err.exp similarity index 100% rename from tests/lang/eval-fail-toJSON.err.exp rename to tests/functional/lang/eval-fail-toJSON.err.exp diff --git a/tests/lang/eval-fail-toJSON.nix b/tests/functional/lang/eval-fail-toJSON.nix similarity index 100% rename from tests/lang/eval-fail-toJSON.nix rename to tests/functional/lang/eval-fail-toJSON.nix diff --git a/tests/lang/eval-fail-undeclared-arg.err.exp b/tests/functional/lang/eval-fail-undeclared-arg.err.exp similarity index 100% rename from tests/lang/eval-fail-undeclared-arg.err.exp rename to tests/functional/lang/eval-fail-undeclared-arg.err.exp diff --git a/tests/lang/eval-fail-undeclared-arg.nix b/tests/functional/lang/eval-fail-undeclared-arg.nix similarity index 100% rename from tests/lang/eval-fail-undeclared-arg.nix rename to tests/functional/lang/eval-fail-undeclared-arg.nix diff --git a/tests/lang/eval-okay-any-all.exp b/tests/functional/lang/eval-okay-any-all.exp similarity index 100% rename from tests/lang/eval-okay-any-all.exp rename to tests/functional/lang/eval-okay-any-all.exp diff --git a/tests/lang/eval-okay-any-all.nix b/tests/functional/lang/eval-okay-any-all.nix similarity index 100% rename from tests/lang/eval-okay-any-all.nix rename to tests/functional/lang/eval-okay-any-all.nix diff --git a/tests/lang/eval-okay-arithmetic.exp b/tests/functional/lang/eval-okay-arithmetic.exp similarity index 100% rename from tests/lang/eval-okay-arithmetic.exp rename to tests/functional/lang/eval-okay-arithmetic.exp diff --git a/tests/lang/eval-okay-arithmetic.nix b/tests/functional/lang/eval-okay-arithmetic.nix similarity index 100% rename from tests/lang/eval-okay-arithmetic.nix rename to tests/functional/lang/eval-okay-arithmetic.nix diff --git a/tests/lang/eval-okay-attrnames.exp b/tests/functional/lang/eval-okay-attrnames.exp similarity index 100% rename from tests/lang/eval-okay-attrnames.exp rename to tests/functional/lang/eval-okay-attrnames.exp diff --git a/tests/lang/eval-okay-attrnames.nix b/tests/functional/lang/eval-okay-attrnames.nix similarity index 100% rename from tests/lang/eval-okay-attrnames.nix rename to tests/functional/lang/eval-okay-attrnames.nix diff --git a/tests/lang/eval-okay-attrs.exp b/tests/functional/lang/eval-okay-attrs.exp similarity index 100% rename from tests/lang/eval-okay-attrs.exp rename to tests/functional/lang/eval-okay-attrs.exp diff --git a/tests/lang/eval-okay-attrs.nix b/tests/functional/lang/eval-okay-attrs.nix similarity index 100% rename from tests/lang/eval-okay-attrs.nix rename to tests/functional/lang/eval-okay-attrs.nix diff --git a/tests/lang/eval-okay-attrs2.exp b/tests/functional/lang/eval-okay-attrs2.exp similarity index 100% rename from tests/lang/eval-okay-attrs2.exp rename to tests/functional/lang/eval-okay-attrs2.exp diff --git a/tests/lang/eval-okay-attrs2.nix b/tests/functional/lang/eval-okay-attrs2.nix similarity index 100% rename from tests/lang/eval-okay-attrs2.nix rename to tests/functional/lang/eval-okay-attrs2.nix diff --git a/tests/lang/eval-okay-attrs3.exp b/tests/functional/lang/eval-okay-attrs3.exp similarity index 100% rename from tests/lang/eval-okay-attrs3.exp rename to tests/functional/lang/eval-okay-attrs3.exp diff --git a/tests/lang/eval-okay-attrs3.nix b/tests/functional/lang/eval-okay-attrs3.nix similarity index 100% rename from tests/lang/eval-okay-attrs3.nix rename to tests/functional/lang/eval-okay-attrs3.nix diff --git a/tests/lang/eval-okay-attrs4.exp b/tests/functional/lang/eval-okay-attrs4.exp similarity index 100% rename from tests/lang/eval-okay-attrs4.exp rename to tests/functional/lang/eval-okay-attrs4.exp diff --git a/tests/lang/eval-okay-attrs4.nix b/tests/functional/lang/eval-okay-attrs4.nix similarity index 100% rename from tests/lang/eval-okay-attrs4.nix rename to tests/functional/lang/eval-okay-attrs4.nix diff --git a/tests/lang/eval-okay-attrs5.exp b/tests/functional/lang/eval-okay-attrs5.exp similarity index 100% rename from tests/lang/eval-okay-attrs5.exp rename to tests/functional/lang/eval-okay-attrs5.exp diff --git a/tests/lang/eval-okay-attrs5.nix b/tests/functional/lang/eval-okay-attrs5.nix similarity index 100% rename from tests/lang/eval-okay-attrs5.nix rename to tests/functional/lang/eval-okay-attrs5.nix diff --git a/tests/lang/eval-okay-attrs6.exp b/tests/functional/lang/eval-okay-attrs6.exp similarity index 100% rename from tests/lang/eval-okay-attrs6.exp rename to tests/functional/lang/eval-okay-attrs6.exp diff --git a/tests/lang/eval-okay-attrs6.nix b/tests/functional/lang/eval-okay-attrs6.nix similarity index 100% rename from tests/lang/eval-okay-attrs6.nix rename to tests/functional/lang/eval-okay-attrs6.nix diff --git a/tests/lang/eval-okay-autoargs.exp b/tests/functional/lang/eval-okay-autoargs.exp similarity index 100% rename from tests/lang/eval-okay-autoargs.exp rename to tests/functional/lang/eval-okay-autoargs.exp diff --git a/tests/lang/eval-okay-autoargs.flags b/tests/functional/lang/eval-okay-autoargs.flags similarity index 100% rename from tests/lang/eval-okay-autoargs.flags rename to tests/functional/lang/eval-okay-autoargs.flags diff --git a/tests/lang/eval-okay-autoargs.nix b/tests/functional/lang/eval-okay-autoargs.nix similarity index 100% rename from tests/lang/eval-okay-autoargs.nix rename to tests/functional/lang/eval-okay-autoargs.nix diff --git a/tests/lang/eval-okay-backslash-newline-1.exp b/tests/functional/lang/eval-okay-backslash-newline-1.exp similarity index 100% rename from tests/lang/eval-okay-backslash-newline-1.exp rename to tests/functional/lang/eval-okay-backslash-newline-1.exp diff --git a/tests/lang/eval-okay-backslash-newline-1.nix b/tests/functional/lang/eval-okay-backslash-newline-1.nix similarity index 100% rename from tests/lang/eval-okay-backslash-newline-1.nix rename to tests/functional/lang/eval-okay-backslash-newline-1.nix diff --git a/tests/lang/eval-okay-backslash-newline-2.exp b/tests/functional/lang/eval-okay-backslash-newline-2.exp similarity index 100% rename from tests/lang/eval-okay-backslash-newline-2.exp rename to tests/functional/lang/eval-okay-backslash-newline-2.exp diff --git a/tests/lang/eval-okay-backslash-newline-2.nix b/tests/functional/lang/eval-okay-backslash-newline-2.nix similarity index 100% rename from tests/lang/eval-okay-backslash-newline-2.nix rename to tests/functional/lang/eval-okay-backslash-newline-2.nix diff --git a/tests/lang/eval-okay-builtins-add.exp b/tests/functional/lang/eval-okay-builtins-add.exp similarity index 100% rename from tests/lang/eval-okay-builtins-add.exp rename to tests/functional/lang/eval-okay-builtins-add.exp diff --git a/tests/lang/eval-okay-builtins-add.nix b/tests/functional/lang/eval-okay-builtins-add.nix similarity index 100% rename from tests/lang/eval-okay-builtins-add.nix rename to tests/functional/lang/eval-okay-builtins-add.nix diff --git a/tests/lang/eval-okay-builtins.exp b/tests/functional/lang/eval-okay-builtins.exp similarity index 100% rename from tests/lang/eval-okay-builtins.exp rename to tests/functional/lang/eval-okay-builtins.exp diff --git a/tests/lang/eval-okay-builtins.nix b/tests/functional/lang/eval-okay-builtins.nix similarity index 100% rename from tests/lang/eval-okay-builtins.nix rename to tests/functional/lang/eval-okay-builtins.nix diff --git a/tests/lang/eval-okay-callable-attrs.exp b/tests/functional/lang/eval-okay-callable-attrs.exp similarity index 100% rename from tests/lang/eval-okay-callable-attrs.exp rename to tests/functional/lang/eval-okay-callable-attrs.exp diff --git a/tests/lang/eval-okay-callable-attrs.nix b/tests/functional/lang/eval-okay-callable-attrs.nix similarity index 100% rename from tests/lang/eval-okay-callable-attrs.nix rename to tests/functional/lang/eval-okay-callable-attrs.nix diff --git a/tests/lang/eval-okay-catattrs.exp b/tests/functional/lang/eval-okay-catattrs.exp similarity index 100% rename from tests/lang/eval-okay-catattrs.exp rename to tests/functional/lang/eval-okay-catattrs.exp diff --git a/tests/lang/eval-okay-catattrs.nix b/tests/functional/lang/eval-okay-catattrs.nix similarity index 100% rename from tests/lang/eval-okay-catattrs.nix rename to tests/functional/lang/eval-okay-catattrs.nix diff --git a/tests/lang/eval-okay-closure.exp b/tests/functional/lang/eval-okay-closure.exp similarity index 100% rename from tests/lang/eval-okay-closure.exp rename to tests/functional/lang/eval-okay-closure.exp diff --git a/tests/lang/eval-okay-closure.exp.xml b/tests/functional/lang/eval-okay-closure.exp.xml similarity index 100% rename from tests/lang/eval-okay-closure.exp.xml rename to tests/functional/lang/eval-okay-closure.exp.xml diff --git a/tests/lang/eval-okay-closure.nix b/tests/functional/lang/eval-okay-closure.nix similarity index 100% rename from tests/lang/eval-okay-closure.nix rename to tests/functional/lang/eval-okay-closure.nix diff --git a/tests/lang/eval-okay-comments.exp b/tests/functional/lang/eval-okay-comments.exp similarity index 100% rename from tests/lang/eval-okay-comments.exp rename to tests/functional/lang/eval-okay-comments.exp diff --git a/tests/lang/eval-okay-comments.nix b/tests/functional/lang/eval-okay-comments.nix similarity index 100% rename from tests/lang/eval-okay-comments.nix rename to tests/functional/lang/eval-okay-comments.nix diff --git a/tests/lang/eval-okay-concat.exp b/tests/functional/lang/eval-okay-concat.exp similarity index 100% rename from tests/lang/eval-okay-concat.exp rename to tests/functional/lang/eval-okay-concat.exp diff --git a/tests/lang/eval-okay-concat.nix b/tests/functional/lang/eval-okay-concat.nix similarity index 100% rename from tests/lang/eval-okay-concat.nix rename to tests/functional/lang/eval-okay-concat.nix diff --git a/tests/lang/eval-okay-concatmap.exp b/tests/functional/lang/eval-okay-concatmap.exp similarity index 100% rename from tests/lang/eval-okay-concatmap.exp rename to tests/functional/lang/eval-okay-concatmap.exp diff --git a/tests/lang/eval-okay-concatmap.nix b/tests/functional/lang/eval-okay-concatmap.nix similarity index 100% rename from tests/lang/eval-okay-concatmap.nix rename to tests/functional/lang/eval-okay-concatmap.nix diff --git a/tests/lang/eval-okay-concatstringssep.exp b/tests/functional/lang/eval-okay-concatstringssep.exp similarity index 100% rename from tests/lang/eval-okay-concatstringssep.exp rename to tests/functional/lang/eval-okay-concatstringssep.exp diff --git a/tests/lang/eval-okay-concatstringssep.nix b/tests/functional/lang/eval-okay-concatstringssep.nix similarity index 100% rename from tests/lang/eval-okay-concatstringssep.nix rename to tests/functional/lang/eval-okay-concatstringssep.nix diff --git a/tests/lang/eval-okay-context-introspection.exp b/tests/functional/lang/eval-okay-context-introspection.exp similarity index 100% rename from tests/lang/eval-okay-context-introspection.exp rename to tests/functional/lang/eval-okay-context-introspection.exp diff --git a/tests/lang/eval-okay-context-introspection.nix b/tests/functional/lang/eval-okay-context-introspection.nix similarity index 100% rename from tests/lang/eval-okay-context-introspection.nix rename to tests/functional/lang/eval-okay-context-introspection.nix diff --git a/tests/lang/eval-okay-context.exp b/tests/functional/lang/eval-okay-context.exp similarity index 100% rename from tests/lang/eval-okay-context.exp rename to tests/functional/lang/eval-okay-context.exp diff --git a/tests/lang/eval-okay-context.nix b/tests/functional/lang/eval-okay-context.nix similarity index 100% rename from tests/lang/eval-okay-context.nix rename to tests/functional/lang/eval-okay-context.nix diff --git a/tests/lang/eval-okay-curpos.exp b/tests/functional/lang/eval-okay-curpos.exp similarity index 100% rename from tests/lang/eval-okay-curpos.exp rename to tests/functional/lang/eval-okay-curpos.exp diff --git a/tests/lang/eval-okay-curpos.nix b/tests/functional/lang/eval-okay-curpos.nix similarity index 100% rename from tests/lang/eval-okay-curpos.nix rename to tests/functional/lang/eval-okay-curpos.nix diff --git a/tests/lang/eval-okay-deepseq.exp b/tests/functional/lang/eval-okay-deepseq.exp similarity index 100% rename from tests/lang/eval-okay-deepseq.exp rename to tests/functional/lang/eval-okay-deepseq.exp diff --git a/tests/lang/eval-okay-deepseq.nix b/tests/functional/lang/eval-okay-deepseq.nix similarity index 100% rename from tests/lang/eval-okay-deepseq.nix rename to tests/functional/lang/eval-okay-deepseq.nix diff --git a/tests/lang/eval-okay-delayed-with-inherit.exp b/tests/functional/lang/eval-okay-delayed-with-inherit.exp similarity index 100% rename from tests/lang/eval-okay-delayed-with-inherit.exp rename to tests/functional/lang/eval-okay-delayed-with-inherit.exp diff --git a/tests/lang/eval-okay-delayed-with-inherit.nix b/tests/functional/lang/eval-okay-delayed-with-inherit.nix similarity index 100% rename from tests/lang/eval-okay-delayed-with-inherit.nix rename to tests/functional/lang/eval-okay-delayed-with-inherit.nix diff --git a/tests/lang/eval-okay-delayed-with.exp b/tests/functional/lang/eval-okay-delayed-with.exp similarity index 100% rename from tests/lang/eval-okay-delayed-with.exp rename to tests/functional/lang/eval-okay-delayed-with.exp diff --git a/tests/lang/eval-okay-delayed-with.nix b/tests/functional/lang/eval-okay-delayed-with.nix similarity index 100% rename from tests/lang/eval-okay-delayed-with.nix rename to tests/functional/lang/eval-okay-delayed-with.nix diff --git a/tests/lang/eval-okay-dynamic-attrs-2.exp b/tests/functional/lang/eval-okay-dynamic-attrs-2.exp similarity index 100% rename from tests/lang/eval-okay-dynamic-attrs-2.exp rename to tests/functional/lang/eval-okay-dynamic-attrs-2.exp diff --git a/tests/lang/eval-okay-dynamic-attrs-2.nix b/tests/functional/lang/eval-okay-dynamic-attrs-2.nix similarity index 100% rename from tests/lang/eval-okay-dynamic-attrs-2.nix rename to tests/functional/lang/eval-okay-dynamic-attrs-2.nix diff --git a/tests/lang/eval-okay-dynamic-attrs-bare.exp b/tests/functional/lang/eval-okay-dynamic-attrs-bare.exp similarity index 100% rename from tests/lang/eval-okay-dynamic-attrs-bare.exp rename to tests/functional/lang/eval-okay-dynamic-attrs-bare.exp diff --git a/tests/lang/eval-okay-dynamic-attrs-bare.nix b/tests/functional/lang/eval-okay-dynamic-attrs-bare.nix similarity index 100% rename from tests/lang/eval-okay-dynamic-attrs-bare.nix rename to tests/functional/lang/eval-okay-dynamic-attrs-bare.nix diff --git a/tests/lang/eval-okay-dynamic-attrs.exp b/tests/functional/lang/eval-okay-dynamic-attrs.exp similarity index 100% rename from tests/lang/eval-okay-dynamic-attrs.exp rename to tests/functional/lang/eval-okay-dynamic-attrs.exp diff --git a/tests/lang/eval-okay-dynamic-attrs.nix b/tests/functional/lang/eval-okay-dynamic-attrs.nix similarity index 100% rename from tests/lang/eval-okay-dynamic-attrs.nix rename to tests/functional/lang/eval-okay-dynamic-attrs.nix diff --git a/tests/lang/eval-okay-elem.exp b/tests/functional/lang/eval-okay-elem.exp similarity index 100% rename from tests/lang/eval-okay-elem.exp rename to tests/functional/lang/eval-okay-elem.exp diff --git a/tests/lang/eval-okay-elem.nix b/tests/functional/lang/eval-okay-elem.nix similarity index 100% rename from tests/lang/eval-okay-elem.nix rename to tests/functional/lang/eval-okay-elem.nix diff --git a/tests/lang/eval-okay-empty-args.exp b/tests/functional/lang/eval-okay-empty-args.exp similarity index 100% rename from tests/lang/eval-okay-empty-args.exp rename to tests/functional/lang/eval-okay-empty-args.exp diff --git a/tests/lang/eval-okay-empty-args.nix b/tests/functional/lang/eval-okay-empty-args.nix similarity index 100% rename from tests/lang/eval-okay-empty-args.nix rename to tests/functional/lang/eval-okay-empty-args.nix diff --git a/tests/lang/eval-okay-eq-derivations.exp b/tests/functional/lang/eval-okay-eq-derivations.exp similarity index 100% rename from tests/lang/eval-okay-eq-derivations.exp rename to tests/functional/lang/eval-okay-eq-derivations.exp diff --git a/tests/lang/eval-okay-eq-derivations.nix b/tests/functional/lang/eval-okay-eq-derivations.nix similarity index 100% rename from tests/lang/eval-okay-eq-derivations.nix rename to tests/functional/lang/eval-okay-eq-derivations.nix diff --git a/tests/lang/eval-okay-eq.exp b/tests/functional/lang/eval-okay-eq.exp similarity index 100% rename from tests/lang/eval-okay-eq.exp rename to tests/functional/lang/eval-okay-eq.exp diff --git a/tests/lang/eval-okay-eq.nix b/tests/functional/lang/eval-okay-eq.nix similarity index 100% rename from tests/lang/eval-okay-eq.nix rename to tests/functional/lang/eval-okay-eq.nix diff --git a/tests/lang/eval-okay-filter.exp b/tests/functional/lang/eval-okay-filter.exp similarity index 100% rename from tests/lang/eval-okay-filter.exp rename to tests/functional/lang/eval-okay-filter.exp diff --git a/tests/lang/eval-okay-filter.nix b/tests/functional/lang/eval-okay-filter.nix similarity index 100% rename from tests/lang/eval-okay-filter.nix rename to tests/functional/lang/eval-okay-filter.nix diff --git a/tests/lang/eval-okay-flake-ref-to-string.exp b/tests/functional/lang/eval-okay-flake-ref-to-string.exp similarity index 100% rename from tests/lang/eval-okay-flake-ref-to-string.exp rename to tests/functional/lang/eval-okay-flake-ref-to-string.exp diff --git a/tests/lang/eval-okay-flake-ref-to-string.nix b/tests/functional/lang/eval-okay-flake-ref-to-string.nix similarity index 100% rename from tests/lang/eval-okay-flake-ref-to-string.nix rename to tests/functional/lang/eval-okay-flake-ref-to-string.nix diff --git a/tests/lang/eval-okay-flatten.exp b/tests/functional/lang/eval-okay-flatten.exp similarity index 100% rename from tests/lang/eval-okay-flatten.exp rename to tests/functional/lang/eval-okay-flatten.exp diff --git a/tests/lang/eval-okay-flatten.nix b/tests/functional/lang/eval-okay-flatten.nix similarity index 100% rename from tests/lang/eval-okay-flatten.nix rename to tests/functional/lang/eval-okay-flatten.nix diff --git a/tests/lang/eval-okay-float.exp b/tests/functional/lang/eval-okay-float.exp similarity index 100% rename from tests/lang/eval-okay-float.exp rename to tests/functional/lang/eval-okay-float.exp diff --git a/tests/lang/eval-okay-float.nix b/tests/functional/lang/eval-okay-float.nix similarity index 100% rename from tests/lang/eval-okay-float.nix rename to tests/functional/lang/eval-okay-float.nix diff --git a/tests/lang/eval-okay-floor-ceil.exp b/tests/functional/lang/eval-okay-floor-ceil.exp similarity index 100% rename from tests/lang/eval-okay-floor-ceil.exp rename to tests/functional/lang/eval-okay-floor-ceil.exp diff --git a/tests/lang/eval-okay-floor-ceil.nix b/tests/functional/lang/eval-okay-floor-ceil.nix similarity index 100% rename from tests/lang/eval-okay-floor-ceil.nix rename to tests/functional/lang/eval-okay-floor-ceil.nix diff --git a/tests/lang/eval-okay-foldlStrict-lazy-elements.exp b/tests/functional/lang/eval-okay-foldlStrict-lazy-elements.exp similarity index 100% rename from tests/lang/eval-okay-foldlStrict-lazy-elements.exp rename to tests/functional/lang/eval-okay-foldlStrict-lazy-elements.exp diff --git a/tests/lang/eval-okay-foldlStrict-lazy-elements.nix b/tests/functional/lang/eval-okay-foldlStrict-lazy-elements.nix similarity index 100% rename from tests/lang/eval-okay-foldlStrict-lazy-elements.nix rename to tests/functional/lang/eval-okay-foldlStrict-lazy-elements.nix diff --git a/tests/lang/eval-okay-foldlStrict-lazy-initial-accumulator.exp b/tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.exp similarity index 100% rename from tests/lang/eval-okay-foldlStrict-lazy-initial-accumulator.exp rename to tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.exp diff --git a/tests/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix b/tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix similarity index 100% rename from tests/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix rename to tests/functional/lang/eval-okay-foldlStrict-lazy-initial-accumulator.nix diff --git a/tests/lang/eval-okay-foldlStrict.exp b/tests/functional/lang/eval-okay-foldlStrict.exp similarity index 100% rename from tests/lang/eval-okay-foldlStrict.exp rename to tests/functional/lang/eval-okay-foldlStrict.exp diff --git a/tests/lang/eval-okay-foldlStrict.nix b/tests/functional/lang/eval-okay-foldlStrict.nix similarity index 100% rename from tests/lang/eval-okay-foldlStrict.nix rename to tests/functional/lang/eval-okay-foldlStrict.nix diff --git a/tests/lang/eval-okay-fromTOML-timestamps.exp b/tests/functional/lang/eval-okay-fromTOML-timestamps.exp similarity index 100% rename from tests/lang/eval-okay-fromTOML-timestamps.exp rename to tests/functional/lang/eval-okay-fromTOML-timestamps.exp diff --git a/tests/lang/eval-okay-fromTOML-timestamps.flags b/tests/functional/lang/eval-okay-fromTOML-timestamps.flags similarity index 100% rename from tests/lang/eval-okay-fromTOML-timestamps.flags rename to tests/functional/lang/eval-okay-fromTOML-timestamps.flags diff --git a/tests/lang/eval-okay-fromTOML-timestamps.nix b/tests/functional/lang/eval-okay-fromTOML-timestamps.nix similarity index 100% rename from tests/lang/eval-okay-fromTOML-timestamps.nix rename to tests/functional/lang/eval-okay-fromTOML-timestamps.nix diff --git a/tests/lang/eval-okay-fromTOML.exp b/tests/functional/lang/eval-okay-fromTOML.exp similarity index 100% rename from tests/lang/eval-okay-fromTOML.exp rename to tests/functional/lang/eval-okay-fromTOML.exp diff --git a/tests/lang/eval-okay-fromTOML.nix b/tests/functional/lang/eval-okay-fromTOML.nix similarity index 100% rename from tests/lang/eval-okay-fromTOML.nix rename to tests/functional/lang/eval-okay-fromTOML.nix diff --git a/tests/lang/eval-okay-fromjson-escapes.exp b/tests/functional/lang/eval-okay-fromjson-escapes.exp similarity index 100% rename from tests/lang/eval-okay-fromjson-escapes.exp rename to tests/functional/lang/eval-okay-fromjson-escapes.exp diff --git a/tests/lang/eval-okay-fromjson-escapes.nix b/tests/functional/lang/eval-okay-fromjson-escapes.nix similarity index 100% rename from tests/lang/eval-okay-fromjson-escapes.nix rename to tests/functional/lang/eval-okay-fromjson-escapes.nix diff --git a/tests/lang/eval-okay-fromjson.exp b/tests/functional/lang/eval-okay-fromjson.exp similarity index 100% rename from tests/lang/eval-okay-fromjson.exp rename to tests/functional/lang/eval-okay-fromjson.exp diff --git a/tests/lang/eval-okay-fromjson.nix b/tests/functional/lang/eval-okay-fromjson.nix similarity index 100% rename from tests/lang/eval-okay-fromjson.nix rename to tests/functional/lang/eval-okay-fromjson.nix diff --git a/tests/lang/eval-okay-functionargs.exp b/tests/functional/lang/eval-okay-functionargs.exp similarity index 100% rename from tests/lang/eval-okay-functionargs.exp rename to tests/functional/lang/eval-okay-functionargs.exp diff --git a/tests/lang/eval-okay-functionargs.exp.xml b/tests/functional/lang/eval-okay-functionargs.exp.xml similarity index 100% rename from tests/lang/eval-okay-functionargs.exp.xml rename to tests/functional/lang/eval-okay-functionargs.exp.xml diff --git a/tests/lang/eval-okay-functionargs.nix b/tests/functional/lang/eval-okay-functionargs.nix similarity index 100% rename from tests/lang/eval-okay-functionargs.nix rename to tests/functional/lang/eval-okay-functionargs.nix diff --git a/tests/lang/eval-okay-getattrpos-functionargs.exp b/tests/functional/lang/eval-okay-getattrpos-functionargs.exp similarity index 100% rename from tests/lang/eval-okay-getattrpos-functionargs.exp rename to tests/functional/lang/eval-okay-getattrpos-functionargs.exp diff --git a/tests/lang/eval-okay-getattrpos-functionargs.nix b/tests/functional/lang/eval-okay-getattrpos-functionargs.nix similarity index 100% rename from tests/lang/eval-okay-getattrpos-functionargs.nix rename to tests/functional/lang/eval-okay-getattrpos-functionargs.nix diff --git a/tests/lang/eval-okay-getattrpos-undefined.exp b/tests/functional/lang/eval-okay-getattrpos-undefined.exp similarity index 100% rename from tests/lang/eval-okay-getattrpos-undefined.exp rename to tests/functional/lang/eval-okay-getattrpos-undefined.exp diff --git a/tests/lang/eval-okay-getattrpos-undefined.nix b/tests/functional/lang/eval-okay-getattrpos-undefined.nix similarity index 100% rename from tests/lang/eval-okay-getattrpos-undefined.nix rename to tests/functional/lang/eval-okay-getattrpos-undefined.nix diff --git a/tests/lang/eval-okay-getattrpos.exp b/tests/functional/lang/eval-okay-getattrpos.exp similarity index 100% rename from tests/lang/eval-okay-getattrpos.exp rename to tests/functional/lang/eval-okay-getattrpos.exp diff --git a/tests/lang/eval-okay-getattrpos.nix b/tests/functional/lang/eval-okay-getattrpos.nix similarity index 100% rename from tests/lang/eval-okay-getattrpos.nix rename to tests/functional/lang/eval-okay-getattrpos.nix diff --git a/tests/lang/eval-okay-getenv.exp b/tests/functional/lang/eval-okay-getenv.exp similarity index 100% rename from tests/lang/eval-okay-getenv.exp rename to tests/functional/lang/eval-okay-getenv.exp diff --git a/tests/lang/eval-okay-getenv.nix b/tests/functional/lang/eval-okay-getenv.nix similarity index 100% rename from tests/lang/eval-okay-getenv.nix rename to tests/functional/lang/eval-okay-getenv.nix diff --git a/tests/lang/eval-okay-groupBy.exp b/tests/functional/lang/eval-okay-groupBy.exp similarity index 100% rename from tests/lang/eval-okay-groupBy.exp rename to tests/functional/lang/eval-okay-groupBy.exp diff --git a/tests/lang/eval-okay-groupBy.nix b/tests/functional/lang/eval-okay-groupBy.nix similarity index 100% rename from tests/lang/eval-okay-groupBy.nix rename to tests/functional/lang/eval-okay-groupBy.nix diff --git a/tests/lang/eval-okay-hash.exp b/tests/functional/lang/eval-okay-hash.exp similarity index 100% rename from tests/lang/eval-okay-hash.exp rename to tests/functional/lang/eval-okay-hash.exp diff --git a/tests/lang/eval-okay-hashfile.exp b/tests/functional/lang/eval-okay-hashfile.exp similarity index 100% rename from tests/lang/eval-okay-hashfile.exp rename to tests/functional/lang/eval-okay-hashfile.exp diff --git a/tests/lang/eval-okay-hashfile.nix b/tests/functional/lang/eval-okay-hashfile.nix similarity index 100% rename from tests/lang/eval-okay-hashfile.nix rename to tests/functional/lang/eval-okay-hashfile.nix diff --git a/tests/lang/eval-okay-hashstring.exp b/tests/functional/lang/eval-okay-hashstring.exp similarity index 100% rename from tests/lang/eval-okay-hashstring.exp rename to tests/functional/lang/eval-okay-hashstring.exp diff --git a/tests/lang/eval-okay-hashstring.nix b/tests/functional/lang/eval-okay-hashstring.nix similarity index 100% rename from tests/lang/eval-okay-hashstring.nix rename to tests/functional/lang/eval-okay-hashstring.nix diff --git a/tests/lang/eval-okay-if.exp b/tests/functional/lang/eval-okay-if.exp similarity index 100% rename from tests/lang/eval-okay-if.exp rename to tests/functional/lang/eval-okay-if.exp diff --git a/tests/lang/eval-okay-if.nix b/tests/functional/lang/eval-okay-if.nix similarity index 100% rename from tests/lang/eval-okay-if.nix rename to tests/functional/lang/eval-okay-if.nix diff --git a/tests/lang/eval-okay-import.exp b/tests/functional/lang/eval-okay-import.exp similarity index 100% rename from tests/lang/eval-okay-import.exp rename to tests/functional/lang/eval-okay-import.exp diff --git a/tests/lang/eval-okay-import.nix b/tests/functional/lang/eval-okay-import.nix similarity index 100% rename from tests/lang/eval-okay-import.nix rename to tests/functional/lang/eval-okay-import.nix diff --git a/tests/lang/eval-okay-ind-string.exp b/tests/functional/lang/eval-okay-ind-string.exp similarity index 100% rename from tests/lang/eval-okay-ind-string.exp rename to tests/functional/lang/eval-okay-ind-string.exp diff --git a/tests/lang/eval-okay-ind-string.nix b/tests/functional/lang/eval-okay-ind-string.nix similarity index 100% rename from tests/lang/eval-okay-ind-string.nix rename to tests/functional/lang/eval-okay-ind-string.nix diff --git a/tests/lang/eval-okay-intersectAttrs.exp b/tests/functional/lang/eval-okay-intersectAttrs.exp similarity index 100% rename from tests/lang/eval-okay-intersectAttrs.exp rename to tests/functional/lang/eval-okay-intersectAttrs.exp diff --git a/tests/lang/eval-okay-intersectAttrs.nix b/tests/functional/lang/eval-okay-intersectAttrs.nix similarity index 100% rename from tests/lang/eval-okay-intersectAttrs.nix rename to tests/functional/lang/eval-okay-intersectAttrs.nix diff --git a/tests/lang/eval-okay-let.exp b/tests/functional/lang/eval-okay-let.exp similarity index 100% rename from tests/lang/eval-okay-let.exp rename to tests/functional/lang/eval-okay-let.exp diff --git a/tests/lang/eval-okay-let.nix b/tests/functional/lang/eval-okay-let.nix similarity index 100% rename from tests/lang/eval-okay-let.nix rename to tests/functional/lang/eval-okay-let.nix diff --git a/tests/lang/eval-okay-list.exp b/tests/functional/lang/eval-okay-list.exp similarity index 100% rename from tests/lang/eval-okay-list.exp rename to tests/functional/lang/eval-okay-list.exp diff --git a/tests/lang/eval-okay-list.nix b/tests/functional/lang/eval-okay-list.nix similarity index 100% rename from tests/lang/eval-okay-list.nix rename to tests/functional/lang/eval-okay-list.nix diff --git a/tests/lang/eval-okay-listtoattrs.exp b/tests/functional/lang/eval-okay-listtoattrs.exp similarity index 100% rename from tests/lang/eval-okay-listtoattrs.exp rename to tests/functional/lang/eval-okay-listtoattrs.exp diff --git a/tests/lang/eval-okay-listtoattrs.nix b/tests/functional/lang/eval-okay-listtoattrs.nix similarity index 100% rename from tests/lang/eval-okay-listtoattrs.nix rename to tests/functional/lang/eval-okay-listtoattrs.nix diff --git a/tests/lang/eval-okay-logic.exp b/tests/functional/lang/eval-okay-logic.exp similarity index 100% rename from tests/lang/eval-okay-logic.exp rename to tests/functional/lang/eval-okay-logic.exp diff --git a/tests/lang/eval-okay-logic.nix b/tests/functional/lang/eval-okay-logic.nix similarity index 100% rename from tests/lang/eval-okay-logic.nix rename to tests/functional/lang/eval-okay-logic.nix diff --git a/tests/lang/eval-okay-map.exp b/tests/functional/lang/eval-okay-map.exp similarity index 100% rename from tests/lang/eval-okay-map.exp rename to tests/functional/lang/eval-okay-map.exp diff --git a/tests/lang/eval-okay-map.nix b/tests/functional/lang/eval-okay-map.nix similarity index 100% rename from tests/lang/eval-okay-map.nix rename to tests/functional/lang/eval-okay-map.nix diff --git a/tests/lang/eval-okay-mapattrs.exp b/tests/functional/lang/eval-okay-mapattrs.exp similarity index 100% rename from tests/lang/eval-okay-mapattrs.exp rename to tests/functional/lang/eval-okay-mapattrs.exp diff --git a/tests/lang/eval-okay-mapattrs.nix b/tests/functional/lang/eval-okay-mapattrs.nix similarity index 100% rename from tests/lang/eval-okay-mapattrs.nix rename to tests/functional/lang/eval-okay-mapattrs.nix diff --git a/tests/lang/eval-okay-merge-dynamic-attrs.exp b/tests/functional/lang/eval-okay-merge-dynamic-attrs.exp similarity index 100% rename from tests/lang/eval-okay-merge-dynamic-attrs.exp rename to tests/functional/lang/eval-okay-merge-dynamic-attrs.exp diff --git a/tests/lang/eval-okay-merge-dynamic-attrs.nix b/tests/functional/lang/eval-okay-merge-dynamic-attrs.nix similarity index 100% rename from tests/lang/eval-okay-merge-dynamic-attrs.nix rename to tests/functional/lang/eval-okay-merge-dynamic-attrs.nix diff --git a/tests/lang/eval-okay-nested-with.exp b/tests/functional/lang/eval-okay-nested-with.exp similarity index 100% rename from tests/lang/eval-okay-nested-with.exp rename to tests/functional/lang/eval-okay-nested-with.exp diff --git a/tests/lang/eval-okay-nested-with.nix b/tests/functional/lang/eval-okay-nested-with.nix similarity index 100% rename from tests/lang/eval-okay-nested-with.nix rename to tests/functional/lang/eval-okay-nested-with.nix diff --git a/tests/lang/eval-okay-new-let.exp b/tests/functional/lang/eval-okay-new-let.exp similarity index 100% rename from tests/lang/eval-okay-new-let.exp rename to tests/functional/lang/eval-okay-new-let.exp diff --git a/tests/lang/eval-okay-new-let.nix b/tests/functional/lang/eval-okay-new-let.nix similarity index 100% rename from tests/lang/eval-okay-new-let.nix rename to tests/functional/lang/eval-okay-new-let.nix diff --git a/tests/lang/eval-okay-null-dynamic-attrs.exp b/tests/functional/lang/eval-okay-null-dynamic-attrs.exp similarity index 100% rename from tests/lang/eval-okay-null-dynamic-attrs.exp rename to tests/functional/lang/eval-okay-null-dynamic-attrs.exp diff --git a/tests/lang/eval-okay-null-dynamic-attrs.nix b/tests/functional/lang/eval-okay-null-dynamic-attrs.nix similarity index 100% rename from tests/lang/eval-okay-null-dynamic-attrs.nix rename to tests/functional/lang/eval-okay-null-dynamic-attrs.nix diff --git a/tests/lang/eval-okay-overrides.exp b/tests/functional/lang/eval-okay-overrides.exp similarity index 100% rename from tests/lang/eval-okay-overrides.exp rename to tests/functional/lang/eval-okay-overrides.exp diff --git a/tests/lang/eval-okay-overrides.nix b/tests/functional/lang/eval-okay-overrides.nix similarity index 100% rename from tests/lang/eval-okay-overrides.nix rename to tests/functional/lang/eval-okay-overrides.nix diff --git a/tests/lang/eval-okay-parse-flake-ref.exp b/tests/functional/lang/eval-okay-parse-flake-ref.exp similarity index 100% rename from tests/lang/eval-okay-parse-flake-ref.exp rename to tests/functional/lang/eval-okay-parse-flake-ref.exp diff --git a/tests/lang/eval-okay-parse-flake-ref.nix b/tests/functional/lang/eval-okay-parse-flake-ref.nix similarity index 100% rename from tests/lang/eval-okay-parse-flake-ref.nix rename to tests/functional/lang/eval-okay-parse-flake-ref.nix diff --git a/tests/lang/eval-okay-partition.exp b/tests/functional/lang/eval-okay-partition.exp similarity index 100% rename from tests/lang/eval-okay-partition.exp rename to tests/functional/lang/eval-okay-partition.exp diff --git a/tests/lang/eval-okay-partition.nix b/tests/functional/lang/eval-okay-partition.nix similarity index 100% rename from tests/lang/eval-okay-partition.nix rename to tests/functional/lang/eval-okay-partition.nix diff --git a/tests/lang/eval-okay-path-string-interpolation.exp b/tests/functional/lang/eval-okay-path-string-interpolation.exp similarity index 100% rename from tests/lang/eval-okay-path-string-interpolation.exp rename to tests/functional/lang/eval-okay-path-string-interpolation.exp diff --git a/tests/lang/eval-okay-path-string-interpolation.nix b/tests/functional/lang/eval-okay-path-string-interpolation.nix similarity index 100% rename from tests/lang/eval-okay-path-string-interpolation.nix rename to tests/functional/lang/eval-okay-path-string-interpolation.nix diff --git a/tests/lang/eval-okay-path.exp b/tests/functional/lang/eval-okay-path.exp similarity index 100% rename from tests/lang/eval-okay-path.exp rename to tests/functional/lang/eval-okay-path.exp diff --git a/tests/lang/eval-okay-path.nix b/tests/functional/lang/eval-okay-path.nix similarity index 100% rename from tests/lang/eval-okay-path.nix rename to tests/functional/lang/eval-okay-path.nix diff --git a/tests/lang/eval-okay-pathexists.exp b/tests/functional/lang/eval-okay-pathexists.exp similarity index 100% rename from tests/lang/eval-okay-pathexists.exp rename to tests/functional/lang/eval-okay-pathexists.exp diff --git a/tests/lang/eval-okay-pathexists.nix b/tests/functional/lang/eval-okay-pathexists.nix similarity index 100% rename from tests/lang/eval-okay-pathexists.nix rename to tests/functional/lang/eval-okay-pathexists.nix diff --git a/tests/lang/eval-okay-patterns.exp b/tests/functional/lang/eval-okay-patterns.exp similarity index 100% rename from tests/lang/eval-okay-patterns.exp rename to tests/functional/lang/eval-okay-patterns.exp diff --git a/tests/lang/eval-okay-patterns.nix b/tests/functional/lang/eval-okay-patterns.nix similarity index 100% rename from tests/lang/eval-okay-patterns.nix rename to tests/functional/lang/eval-okay-patterns.nix diff --git a/tests/lang/eval-okay-print.err.exp b/tests/functional/lang/eval-okay-print.err.exp similarity index 100% rename from tests/lang/eval-okay-print.err.exp rename to tests/functional/lang/eval-okay-print.err.exp diff --git a/tests/lang/eval-okay-print.exp b/tests/functional/lang/eval-okay-print.exp similarity index 100% rename from tests/lang/eval-okay-print.exp rename to tests/functional/lang/eval-okay-print.exp diff --git a/tests/lang/eval-okay-print.nix b/tests/functional/lang/eval-okay-print.nix similarity index 100% rename from tests/lang/eval-okay-print.nix rename to tests/functional/lang/eval-okay-print.nix diff --git a/tests/lang/eval-okay-readDir.exp b/tests/functional/lang/eval-okay-readDir.exp similarity index 100% rename from tests/lang/eval-okay-readDir.exp rename to tests/functional/lang/eval-okay-readDir.exp diff --git a/tests/lang/eval-okay-readDir.nix b/tests/functional/lang/eval-okay-readDir.nix similarity index 100% rename from tests/lang/eval-okay-readDir.nix rename to tests/functional/lang/eval-okay-readDir.nix diff --git a/tests/lang/eval-okay-readFileType.exp b/tests/functional/lang/eval-okay-readFileType.exp similarity index 100% rename from tests/lang/eval-okay-readFileType.exp rename to tests/functional/lang/eval-okay-readFileType.exp diff --git a/tests/lang/eval-okay-readFileType.nix b/tests/functional/lang/eval-okay-readFileType.nix similarity index 100% rename from tests/lang/eval-okay-readFileType.nix rename to tests/functional/lang/eval-okay-readFileType.nix diff --git a/tests/lang/eval-okay-readfile.exp b/tests/functional/lang/eval-okay-readfile.exp similarity index 100% rename from tests/lang/eval-okay-readfile.exp rename to tests/functional/lang/eval-okay-readfile.exp diff --git a/tests/lang/eval-okay-readfile.nix b/tests/functional/lang/eval-okay-readfile.nix similarity index 100% rename from tests/lang/eval-okay-readfile.nix rename to tests/functional/lang/eval-okay-readfile.nix diff --git a/tests/lang/eval-okay-redefine-builtin.exp b/tests/functional/lang/eval-okay-redefine-builtin.exp similarity index 100% rename from tests/lang/eval-okay-redefine-builtin.exp rename to tests/functional/lang/eval-okay-redefine-builtin.exp diff --git a/tests/lang/eval-okay-redefine-builtin.nix b/tests/functional/lang/eval-okay-redefine-builtin.nix similarity index 100% rename from tests/lang/eval-okay-redefine-builtin.nix rename to tests/functional/lang/eval-okay-redefine-builtin.nix diff --git a/tests/lang/eval-okay-regex-match.exp b/tests/functional/lang/eval-okay-regex-match.exp similarity index 100% rename from tests/lang/eval-okay-regex-match.exp rename to tests/functional/lang/eval-okay-regex-match.exp diff --git a/tests/lang/eval-okay-regex-match.nix b/tests/functional/lang/eval-okay-regex-match.nix similarity index 100% rename from tests/lang/eval-okay-regex-match.nix rename to tests/functional/lang/eval-okay-regex-match.nix diff --git a/tests/lang/eval-okay-regex-split.exp b/tests/functional/lang/eval-okay-regex-split.exp similarity index 100% rename from tests/lang/eval-okay-regex-split.exp rename to tests/functional/lang/eval-okay-regex-split.exp diff --git a/tests/lang/eval-okay-regex-split.nix b/tests/functional/lang/eval-okay-regex-split.nix similarity index 100% rename from tests/lang/eval-okay-regex-split.nix rename to tests/functional/lang/eval-okay-regex-split.nix diff --git a/tests/lang/eval-okay-regression-20220122.exp b/tests/functional/lang/eval-okay-regression-20220122.exp similarity index 100% rename from tests/lang/eval-okay-regression-20220122.exp rename to tests/functional/lang/eval-okay-regression-20220122.exp diff --git a/tests/lang/eval-okay-regression-20220122.nix b/tests/functional/lang/eval-okay-regression-20220122.nix similarity index 100% rename from tests/lang/eval-okay-regression-20220122.nix rename to tests/functional/lang/eval-okay-regression-20220122.nix diff --git a/tests/lang/eval-okay-regression-20220125.exp b/tests/functional/lang/eval-okay-regression-20220125.exp similarity index 100% rename from tests/lang/eval-okay-regression-20220125.exp rename to tests/functional/lang/eval-okay-regression-20220125.exp diff --git a/tests/lang/eval-okay-regression-20220125.nix b/tests/functional/lang/eval-okay-regression-20220125.nix similarity index 100% rename from tests/lang/eval-okay-regression-20220125.nix rename to tests/functional/lang/eval-okay-regression-20220125.nix diff --git a/tests/lang/eval-okay-remove.exp b/tests/functional/lang/eval-okay-remove.exp similarity index 100% rename from tests/lang/eval-okay-remove.exp rename to tests/functional/lang/eval-okay-remove.exp diff --git a/tests/lang/eval-okay-remove.nix b/tests/functional/lang/eval-okay-remove.nix similarity index 100% rename from tests/lang/eval-okay-remove.nix rename to tests/functional/lang/eval-okay-remove.nix diff --git a/tests/lang/eval-okay-replacestrings.exp b/tests/functional/lang/eval-okay-replacestrings.exp similarity index 100% rename from tests/lang/eval-okay-replacestrings.exp rename to tests/functional/lang/eval-okay-replacestrings.exp diff --git a/tests/lang/eval-okay-replacestrings.nix b/tests/functional/lang/eval-okay-replacestrings.nix similarity index 100% rename from tests/lang/eval-okay-replacestrings.nix rename to tests/functional/lang/eval-okay-replacestrings.nix diff --git a/tests/lang/eval-okay-scope-1.exp b/tests/functional/lang/eval-okay-scope-1.exp similarity index 100% rename from tests/lang/eval-okay-scope-1.exp rename to tests/functional/lang/eval-okay-scope-1.exp diff --git a/tests/lang/eval-okay-scope-1.nix b/tests/functional/lang/eval-okay-scope-1.nix similarity index 100% rename from tests/lang/eval-okay-scope-1.nix rename to tests/functional/lang/eval-okay-scope-1.nix diff --git a/tests/lang/eval-okay-scope-2.exp b/tests/functional/lang/eval-okay-scope-2.exp similarity index 100% rename from tests/lang/eval-okay-scope-2.exp rename to tests/functional/lang/eval-okay-scope-2.exp diff --git a/tests/lang/eval-okay-scope-2.nix b/tests/functional/lang/eval-okay-scope-2.nix similarity index 100% rename from tests/lang/eval-okay-scope-2.nix rename to tests/functional/lang/eval-okay-scope-2.nix diff --git a/tests/lang/eval-okay-scope-3.exp b/tests/functional/lang/eval-okay-scope-3.exp similarity index 100% rename from tests/lang/eval-okay-scope-3.exp rename to tests/functional/lang/eval-okay-scope-3.exp diff --git a/tests/lang/eval-okay-scope-3.nix b/tests/functional/lang/eval-okay-scope-3.nix similarity index 100% rename from tests/lang/eval-okay-scope-3.nix rename to tests/functional/lang/eval-okay-scope-3.nix diff --git a/tests/lang/eval-okay-scope-4.exp b/tests/functional/lang/eval-okay-scope-4.exp similarity index 100% rename from tests/lang/eval-okay-scope-4.exp rename to tests/functional/lang/eval-okay-scope-4.exp diff --git a/tests/lang/eval-okay-scope-4.nix b/tests/functional/lang/eval-okay-scope-4.nix similarity index 100% rename from tests/lang/eval-okay-scope-4.nix rename to tests/functional/lang/eval-okay-scope-4.nix diff --git a/tests/lang/eval-okay-scope-6.exp b/tests/functional/lang/eval-okay-scope-6.exp similarity index 100% rename from tests/lang/eval-okay-scope-6.exp rename to tests/functional/lang/eval-okay-scope-6.exp diff --git a/tests/lang/eval-okay-scope-6.nix b/tests/functional/lang/eval-okay-scope-6.nix similarity index 100% rename from tests/lang/eval-okay-scope-6.nix rename to tests/functional/lang/eval-okay-scope-6.nix diff --git a/tests/lang/eval-okay-scope-7.exp b/tests/functional/lang/eval-okay-scope-7.exp similarity index 100% rename from tests/lang/eval-okay-scope-7.exp rename to tests/functional/lang/eval-okay-scope-7.exp diff --git a/tests/lang/eval-okay-scope-7.nix b/tests/functional/lang/eval-okay-scope-7.nix similarity index 100% rename from tests/lang/eval-okay-scope-7.nix rename to tests/functional/lang/eval-okay-scope-7.nix diff --git a/tests/lang/eval-okay-search-path.exp b/tests/functional/lang/eval-okay-search-path.exp similarity index 100% rename from tests/lang/eval-okay-search-path.exp rename to tests/functional/lang/eval-okay-search-path.exp diff --git a/tests/lang/eval-okay-search-path.flags b/tests/functional/lang/eval-okay-search-path.flags similarity index 100% rename from tests/lang/eval-okay-search-path.flags rename to tests/functional/lang/eval-okay-search-path.flags diff --git a/tests/lang/eval-okay-search-path.nix b/tests/functional/lang/eval-okay-search-path.nix similarity index 100% rename from tests/lang/eval-okay-search-path.nix rename to tests/functional/lang/eval-okay-search-path.nix diff --git a/tests/lang/eval-okay-seq.exp b/tests/functional/lang/eval-okay-seq.exp similarity index 100% rename from tests/lang/eval-okay-seq.exp rename to tests/functional/lang/eval-okay-seq.exp diff --git a/tests/lang/eval-okay-seq.nix b/tests/functional/lang/eval-okay-seq.nix similarity index 100% rename from tests/lang/eval-okay-seq.nix rename to tests/functional/lang/eval-okay-seq.nix diff --git a/tests/lang/eval-okay-sort.exp b/tests/functional/lang/eval-okay-sort.exp similarity index 100% rename from tests/lang/eval-okay-sort.exp rename to tests/functional/lang/eval-okay-sort.exp diff --git a/tests/lang/eval-okay-sort.nix b/tests/functional/lang/eval-okay-sort.nix similarity index 100% rename from tests/lang/eval-okay-sort.nix rename to tests/functional/lang/eval-okay-sort.nix diff --git a/tests/lang/eval-okay-splitversion.exp b/tests/functional/lang/eval-okay-splitversion.exp similarity index 100% rename from tests/lang/eval-okay-splitversion.exp rename to tests/functional/lang/eval-okay-splitversion.exp diff --git a/tests/lang/eval-okay-splitversion.nix b/tests/functional/lang/eval-okay-splitversion.nix similarity index 100% rename from tests/lang/eval-okay-splitversion.nix rename to tests/functional/lang/eval-okay-splitversion.nix diff --git a/tests/lang/eval-okay-string.exp b/tests/functional/lang/eval-okay-string.exp similarity index 100% rename from tests/lang/eval-okay-string.exp rename to tests/functional/lang/eval-okay-string.exp diff --git a/tests/lang/eval-okay-string.nix b/tests/functional/lang/eval-okay-string.nix similarity index 100% rename from tests/lang/eval-okay-string.nix rename to tests/functional/lang/eval-okay-string.nix diff --git a/tests/lang/eval-okay-strings-as-attrs-names.exp b/tests/functional/lang/eval-okay-strings-as-attrs-names.exp similarity index 100% rename from tests/lang/eval-okay-strings-as-attrs-names.exp rename to tests/functional/lang/eval-okay-strings-as-attrs-names.exp diff --git a/tests/lang/eval-okay-strings-as-attrs-names.nix b/tests/functional/lang/eval-okay-strings-as-attrs-names.nix similarity index 100% rename from tests/lang/eval-okay-strings-as-attrs-names.nix rename to tests/functional/lang/eval-okay-strings-as-attrs-names.nix diff --git a/tests/lang/eval-okay-substring.exp b/tests/functional/lang/eval-okay-substring.exp similarity index 100% rename from tests/lang/eval-okay-substring.exp rename to tests/functional/lang/eval-okay-substring.exp diff --git a/tests/lang/eval-okay-substring.nix b/tests/functional/lang/eval-okay-substring.nix similarity index 100% rename from tests/lang/eval-okay-substring.nix rename to tests/functional/lang/eval-okay-substring.nix diff --git a/tests/lang/eval-okay-tail-call-1.exp-disabled b/tests/functional/lang/eval-okay-tail-call-1.exp-disabled similarity index 100% rename from tests/lang/eval-okay-tail-call-1.exp-disabled rename to tests/functional/lang/eval-okay-tail-call-1.exp-disabled diff --git a/tests/lang/eval-okay-tail-call-1.nix b/tests/functional/lang/eval-okay-tail-call-1.nix similarity index 100% rename from tests/lang/eval-okay-tail-call-1.nix rename to tests/functional/lang/eval-okay-tail-call-1.nix diff --git a/tests/lang/eval-okay-tojson.exp b/tests/functional/lang/eval-okay-tojson.exp similarity index 100% rename from tests/lang/eval-okay-tojson.exp rename to tests/functional/lang/eval-okay-tojson.exp diff --git a/tests/lang/eval-okay-tojson.nix b/tests/functional/lang/eval-okay-tojson.nix similarity index 100% rename from tests/lang/eval-okay-tojson.nix rename to tests/functional/lang/eval-okay-tojson.nix diff --git a/tests/lang/eval-okay-toxml.exp b/tests/functional/lang/eval-okay-toxml.exp similarity index 100% rename from tests/lang/eval-okay-toxml.exp rename to tests/functional/lang/eval-okay-toxml.exp diff --git a/tests/lang/eval-okay-toxml.nix b/tests/functional/lang/eval-okay-toxml.nix similarity index 100% rename from tests/lang/eval-okay-toxml.nix rename to tests/functional/lang/eval-okay-toxml.nix diff --git a/tests/lang/eval-okay-toxml2.exp b/tests/functional/lang/eval-okay-toxml2.exp similarity index 100% rename from tests/lang/eval-okay-toxml2.exp rename to tests/functional/lang/eval-okay-toxml2.exp diff --git a/tests/lang/eval-okay-toxml2.nix b/tests/functional/lang/eval-okay-toxml2.nix similarity index 100% rename from tests/lang/eval-okay-toxml2.nix rename to tests/functional/lang/eval-okay-toxml2.nix diff --git a/tests/lang/eval-okay-tryeval.exp b/tests/functional/lang/eval-okay-tryeval.exp similarity index 100% rename from tests/lang/eval-okay-tryeval.exp rename to tests/functional/lang/eval-okay-tryeval.exp diff --git a/tests/lang/eval-okay-tryeval.nix b/tests/functional/lang/eval-okay-tryeval.nix similarity index 100% rename from tests/lang/eval-okay-tryeval.nix rename to tests/functional/lang/eval-okay-tryeval.nix diff --git a/tests/lang/eval-okay-types.exp b/tests/functional/lang/eval-okay-types.exp similarity index 100% rename from tests/lang/eval-okay-types.exp rename to tests/functional/lang/eval-okay-types.exp diff --git a/tests/lang/eval-okay-types.nix b/tests/functional/lang/eval-okay-types.nix similarity index 100% rename from tests/lang/eval-okay-types.nix rename to tests/functional/lang/eval-okay-types.nix diff --git a/tests/lang/eval-okay-versions.exp b/tests/functional/lang/eval-okay-versions.exp similarity index 100% rename from tests/lang/eval-okay-versions.exp rename to tests/functional/lang/eval-okay-versions.exp diff --git a/tests/lang/eval-okay-versions.nix b/tests/functional/lang/eval-okay-versions.nix similarity index 100% rename from tests/lang/eval-okay-versions.nix rename to tests/functional/lang/eval-okay-versions.nix diff --git a/tests/lang/eval-okay-with.exp b/tests/functional/lang/eval-okay-with.exp similarity index 100% rename from tests/lang/eval-okay-with.exp rename to tests/functional/lang/eval-okay-with.exp diff --git a/tests/lang/eval-okay-with.nix b/tests/functional/lang/eval-okay-with.nix similarity index 100% rename from tests/lang/eval-okay-with.nix rename to tests/functional/lang/eval-okay-with.nix diff --git a/tests/lang/eval-okay-xml.exp.xml b/tests/functional/lang/eval-okay-xml.exp.xml similarity index 100% rename from tests/lang/eval-okay-xml.exp.xml rename to tests/functional/lang/eval-okay-xml.exp.xml diff --git a/tests/lang/eval-okay-xml.nix b/tests/functional/lang/eval-okay-xml.nix similarity index 100% rename from tests/lang/eval-okay-xml.nix rename to tests/functional/lang/eval-okay-xml.nix diff --git a/tests/lang/eval-okay-zipAttrsWith.exp b/tests/functional/lang/eval-okay-zipAttrsWith.exp similarity index 100% rename from tests/lang/eval-okay-zipAttrsWith.exp rename to tests/functional/lang/eval-okay-zipAttrsWith.exp diff --git a/tests/lang/eval-okay-zipAttrsWith.nix b/tests/functional/lang/eval-okay-zipAttrsWith.nix similarity index 100% rename from tests/lang/eval-okay-zipAttrsWith.nix rename to tests/functional/lang/eval-okay-zipAttrsWith.nix diff --git a/tests/lang/framework.sh b/tests/functional/lang/framework.sh similarity index 100% rename from tests/lang/framework.sh rename to tests/functional/lang/framework.sh diff --git a/tests/lang/imported.nix b/tests/functional/lang/imported.nix similarity index 100% rename from tests/lang/imported.nix rename to tests/functional/lang/imported.nix diff --git a/tests/lang/imported2.nix b/tests/functional/lang/imported2.nix similarity index 100% rename from tests/lang/imported2.nix rename to tests/functional/lang/imported2.nix diff --git a/tests/lang/lib.nix b/tests/functional/lang/lib.nix similarity index 100% rename from tests/lang/lib.nix rename to tests/functional/lang/lib.nix diff --git a/tests/lang/parse-fail-dup-attrs-1.err.exp b/tests/functional/lang/parse-fail-dup-attrs-1.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-attrs-1.err.exp rename to tests/functional/lang/parse-fail-dup-attrs-1.err.exp diff --git a/tests/lang/parse-fail-dup-attrs-1.nix b/tests/functional/lang/parse-fail-dup-attrs-1.nix similarity index 100% rename from tests/lang/parse-fail-dup-attrs-1.nix rename to tests/functional/lang/parse-fail-dup-attrs-1.nix diff --git a/tests/lang/parse-fail-dup-attrs-2.err.exp b/tests/functional/lang/parse-fail-dup-attrs-2.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-attrs-2.err.exp rename to tests/functional/lang/parse-fail-dup-attrs-2.err.exp diff --git a/tests/lang/parse-fail-dup-attrs-2.nix b/tests/functional/lang/parse-fail-dup-attrs-2.nix similarity index 100% rename from tests/lang/parse-fail-dup-attrs-2.nix rename to tests/functional/lang/parse-fail-dup-attrs-2.nix diff --git a/tests/lang/parse-fail-dup-attrs-3.err.exp b/tests/functional/lang/parse-fail-dup-attrs-3.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-attrs-3.err.exp rename to tests/functional/lang/parse-fail-dup-attrs-3.err.exp diff --git a/tests/lang/parse-fail-dup-attrs-3.nix b/tests/functional/lang/parse-fail-dup-attrs-3.nix similarity index 100% rename from tests/lang/parse-fail-dup-attrs-3.nix rename to tests/functional/lang/parse-fail-dup-attrs-3.nix diff --git a/tests/lang/parse-fail-dup-attrs-4.err.exp b/tests/functional/lang/parse-fail-dup-attrs-4.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-attrs-4.err.exp rename to tests/functional/lang/parse-fail-dup-attrs-4.err.exp diff --git a/tests/lang/parse-fail-dup-attrs-4.nix b/tests/functional/lang/parse-fail-dup-attrs-4.nix similarity index 100% rename from tests/lang/parse-fail-dup-attrs-4.nix rename to tests/functional/lang/parse-fail-dup-attrs-4.nix diff --git a/tests/lang/parse-fail-dup-attrs-6.err.exp b/tests/functional/lang/parse-fail-dup-attrs-6.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-attrs-6.err.exp rename to tests/functional/lang/parse-fail-dup-attrs-6.err.exp diff --git a/tests/lang/parse-fail-dup-attrs-7.err.exp b/tests/functional/lang/parse-fail-dup-attrs-7.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-attrs-7.err.exp rename to tests/functional/lang/parse-fail-dup-attrs-7.err.exp diff --git a/tests/lang/parse-fail-dup-attrs-7.nix b/tests/functional/lang/parse-fail-dup-attrs-7.nix similarity index 100% rename from tests/lang/parse-fail-dup-attrs-7.nix rename to tests/functional/lang/parse-fail-dup-attrs-7.nix diff --git a/tests/lang/parse-fail-dup-formals.err.exp b/tests/functional/lang/parse-fail-dup-formals.err.exp similarity index 100% rename from tests/lang/parse-fail-dup-formals.err.exp rename to tests/functional/lang/parse-fail-dup-formals.err.exp diff --git a/tests/lang/parse-fail-dup-formals.nix b/tests/functional/lang/parse-fail-dup-formals.nix similarity index 100% rename from tests/lang/parse-fail-dup-formals.nix rename to tests/functional/lang/parse-fail-dup-formals.nix diff --git a/tests/lang/parse-fail-eof-in-string.err.exp b/tests/functional/lang/parse-fail-eof-in-string.err.exp similarity index 100% rename from tests/lang/parse-fail-eof-in-string.err.exp rename to tests/functional/lang/parse-fail-eof-in-string.err.exp diff --git a/tests/lang/parse-fail-eof-in-string.nix b/tests/functional/lang/parse-fail-eof-in-string.nix similarity index 100% rename from tests/lang/parse-fail-eof-in-string.nix rename to tests/functional/lang/parse-fail-eof-in-string.nix diff --git a/tests/lang/parse-fail-mixed-nested-attrs1.err.exp b/tests/functional/lang/parse-fail-mixed-nested-attrs1.err.exp similarity index 100% rename from tests/lang/parse-fail-mixed-nested-attrs1.err.exp rename to tests/functional/lang/parse-fail-mixed-nested-attrs1.err.exp diff --git a/tests/lang/parse-fail-mixed-nested-attrs1.nix b/tests/functional/lang/parse-fail-mixed-nested-attrs1.nix similarity index 100% rename from tests/lang/parse-fail-mixed-nested-attrs1.nix rename to tests/functional/lang/parse-fail-mixed-nested-attrs1.nix diff --git a/tests/lang/parse-fail-mixed-nested-attrs2.err.exp b/tests/functional/lang/parse-fail-mixed-nested-attrs2.err.exp similarity index 100% rename from tests/lang/parse-fail-mixed-nested-attrs2.err.exp rename to tests/functional/lang/parse-fail-mixed-nested-attrs2.err.exp diff --git a/tests/lang/parse-fail-mixed-nested-attrs2.nix b/tests/functional/lang/parse-fail-mixed-nested-attrs2.nix similarity index 100% rename from tests/lang/parse-fail-mixed-nested-attrs2.nix rename to tests/functional/lang/parse-fail-mixed-nested-attrs2.nix diff --git a/tests/lang/parse-fail-patterns-1.err.exp b/tests/functional/lang/parse-fail-patterns-1.err.exp similarity index 100% rename from tests/lang/parse-fail-patterns-1.err.exp rename to tests/functional/lang/parse-fail-patterns-1.err.exp diff --git a/tests/lang/parse-fail-patterns-1.nix b/tests/functional/lang/parse-fail-patterns-1.nix similarity index 100% rename from tests/lang/parse-fail-patterns-1.nix rename to tests/functional/lang/parse-fail-patterns-1.nix diff --git a/tests/lang/parse-fail-regression-20060610.err.exp b/tests/functional/lang/parse-fail-regression-20060610.err.exp similarity index 100% rename from tests/lang/parse-fail-regression-20060610.err.exp rename to tests/functional/lang/parse-fail-regression-20060610.err.exp diff --git a/tests/lang/parse-fail-regression-20060610.nix b/tests/functional/lang/parse-fail-regression-20060610.nix similarity index 100% rename from tests/lang/parse-fail-regression-20060610.nix rename to tests/functional/lang/parse-fail-regression-20060610.nix diff --git a/tests/lang/parse-fail-undef-var-2.err.exp b/tests/functional/lang/parse-fail-undef-var-2.err.exp similarity index 100% rename from tests/lang/parse-fail-undef-var-2.err.exp rename to tests/functional/lang/parse-fail-undef-var-2.err.exp diff --git a/tests/lang/parse-fail-undef-var-2.nix b/tests/functional/lang/parse-fail-undef-var-2.nix similarity index 100% rename from tests/lang/parse-fail-undef-var-2.nix rename to tests/functional/lang/parse-fail-undef-var-2.nix diff --git a/tests/lang/parse-fail-undef-var.err.exp b/tests/functional/lang/parse-fail-undef-var.err.exp similarity index 100% rename from tests/lang/parse-fail-undef-var.err.exp rename to tests/functional/lang/parse-fail-undef-var.err.exp diff --git a/tests/lang/parse-fail-undef-var.nix b/tests/functional/lang/parse-fail-undef-var.nix similarity index 100% rename from tests/lang/parse-fail-undef-var.nix rename to tests/functional/lang/parse-fail-undef-var.nix diff --git a/tests/lang/parse-fail-utf8.err.exp b/tests/functional/lang/parse-fail-utf8.err.exp similarity index 100% rename from tests/lang/parse-fail-utf8.err.exp rename to tests/functional/lang/parse-fail-utf8.err.exp diff --git a/tests/lang/parse-fail-utf8.nix b/tests/functional/lang/parse-fail-utf8.nix similarity index 100% rename from tests/lang/parse-fail-utf8.nix rename to tests/functional/lang/parse-fail-utf8.nix diff --git a/tests/lang/parse-okay-1.exp b/tests/functional/lang/parse-okay-1.exp similarity index 100% rename from tests/lang/parse-okay-1.exp rename to tests/functional/lang/parse-okay-1.exp diff --git a/tests/lang/parse-okay-1.nix b/tests/functional/lang/parse-okay-1.nix similarity index 100% rename from tests/lang/parse-okay-1.nix rename to tests/functional/lang/parse-okay-1.nix diff --git a/tests/lang/parse-okay-crlf.exp b/tests/functional/lang/parse-okay-crlf.exp similarity index 100% rename from tests/lang/parse-okay-crlf.exp rename to tests/functional/lang/parse-okay-crlf.exp diff --git a/tests/lang/parse-okay-crlf.nix b/tests/functional/lang/parse-okay-crlf.nix similarity index 100% rename from tests/lang/parse-okay-crlf.nix rename to tests/functional/lang/parse-okay-crlf.nix diff --git a/tests/lang/parse-okay-dup-attrs-5.exp b/tests/functional/lang/parse-okay-dup-attrs-5.exp similarity index 100% rename from tests/lang/parse-okay-dup-attrs-5.exp rename to tests/functional/lang/parse-okay-dup-attrs-5.exp diff --git a/tests/lang/parse-okay-dup-attrs-5.nix b/tests/functional/lang/parse-okay-dup-attrs-5.nix similarity index 100% rename from tests/lang/parse-okay-dup-attrs-5.nix rename to tests/functional/lang/parse-okay-dup-attrs-5.nix diff --git a/tests/lang/parse-okay-dup-attrs-6.exp b/tests/functional/lang/parse-okay-dup-attrs-6.exp similarity index 100% rename from tests/lang/parse-okay-dup-attrs-6.exp rename to tests/functional/lang/parse-okay-dup-attrs-6.exp diff --git a/tests/lang/parse-okay-dup-attrs-6.nix b/tests/functional/lang/parse-okay-dup-attrs-6.nix similarity index 100% rename from tests/lang/parse-okay-dup-attrs-6.nix rename to tests/functional/lang/parse-okay-dup-attrs-6.nix diff --git a/tests/lang/parse-okay-mixed-nested-attrs-1.exp b/tests/functional/lang/parse-okay-mixed-nested-attrs-1.exp similarity index 100% rename from tests/lang/parse-okay-mixed-nested-attrs-1.exp rename to tests/functional/lang/parse-okay-mixed-nested-attrs-1.exp diff --git a/tests/lang/parse-okay-mixed-nested-attrs-1.nix b/tests/functional/lang/parse-okay-mixed-nested-attrs-1.nix similarity index 100% rename from tests/lang/parse-okay-mixed-nested-attrs-1.nix rename to tests/functional/lang/parse-okay-mixed-nested-attrs-1.nix diff --git a/tests/lang/parse-okay-mixed-nested-attrs-2.exp b/tests/functional/lang/parse-okay-mixed-nested-attrs-2.exp similarity index 100% rename from tests/lang/parse-okay-mixed-nested-attrs-2.exp rename to tests/functional/lang/parse-okay-mixed-nested-attrs-2.exp diff --git a/tests/lang/parse-okay-mixed-nested-attrs-2.nix b/tests/functional/lang/parse-okay-mixed-nested-attrs-2.nix similarity index 100% rename from tests/lang/parse-okay-mixed-nested-attrs-2.nix rename to tests/functional/lang/parse-okay-mixed-nested-attrs-2.nix diff --git a/tests/lang/parse-okay-mixed-nested-attrs-3.exp b/tests/functional/lang/parse-okay-mixed-nested-attrs-3.exp similarity index 100% rename from tests/lang/parse-okay-mixed-nested-attrs-3.exp rename to tests/functional/lang/parse-okay-mixed-nested-attrs-3.exp diff --git a/tests/lang/parse-okay-mixed-nested-attrs-3.nix b/tests/functional/lang/parse-okay-mixed-nested-attrs-3.nix similarity index 100% rename from tests/lang/parse-okay-mixed-nested-attrs-3.nix rename to tests/functional/lang/parse-okay-mixed-nested-attrs-3.nix diff --git a/tests/lang/parse-okay-regression-20041027.exp b/tests/functional/lang/parse-okay-regression-20041027.exp similarity index 100% rename from tests/lang/parse-okay-regression-20041027.exp rename to tests/functional/lang/parse-okay-regression-20041027.exp diff --git a/tests/lang/parse-okay-regression-20041027.nix b/tests/functional/lang/parse-okay-regression-20041027.nix similarity index 100% rename from tests/lang/parse-okay-regression-20041027.nix rename to tests/functional/lang/parse-okay-regression-20041027.nix diff --git a/tests/lang/parse-okay-regression-751.exp b/tests/functional/lang/parse-okay-regression-751.exp similarity index 100% rename from tests/lang/parse-okay-regression-751.exp rename to tests/functional/lang/parse-okay-regression-751.exp diff --git a/tests/lang/parse-okay-regression-751.nix b/tests/functional/lang/parse-okay-regression-751.nix similarity index 100% rename from tests/lang/parse-okay-regression-751.nix rename to tests/functional/lang/parse-okay-regression-751.nix diff --git a/tests/lang/parse-okay-subversion.exp b/tests/functional/lang/parse-okay-subversion.exp similarity index 100% rename from tests/lang/parse-okay-subversion.exp rename to tests/functional/lang/parse-okay-subversion.exp diff --git a/tests/lang/parse-okay-subversion.nix b/tests/functional/lang/parse-okay-subversion.nix similarity index 100% rename from tests/lang/parse-okay-subversion.nix rename to tests/functional/lang/parse-okay-subversion.nix diff --git a/tests/lang/parse-okay-url.exp b/tests/functional/lang/parse-okay-url.exp similarity index 100% rename from tests/lang/parse-okay-url.exp rename to tests/functional/lang/parse-okay-url.exp diff --git a/tests/lang/parse-okay-url.nix b/tests/functional/lang/parse-okay-url.nix similarity index 100% rename from tests/lang/parse-okay-url.nix rename to tests/functional/lang/parse-okay-url.nix diff --git a/tests/lang/readDir/bar b/tests/functional/lang/readDir/bar similarity index 100% rename from tests/lang/readDir/bar rename to tests/functional/lang/readDir/bar diff --git a/tests/lang/readDir/foo/git-hates-directories b/tests/functional/lang/readDir/foo/git-hates-directories similarity index 100% rename from tests/lang/readDir/foo/git-hates-directories rename to tests/functional/lang/readDir/foo/git-hates-directories diff --git a/tests/lang/readDir/ldir b/tests/functional/lang/readDir/ldir similarity index 100% rename from tests/lang/readDir/ldir rename to tests/functional/lang/readDir/ldir diff --git a/tests/lang/readDir/linked b/tests/functional/lang/readDir/linked similarity index 100% rename from tests/lang/readDir/linked rename to tests/functional/lang/readDir/linked diff --git a/tests/legacy-ssh-store.sh b/tests/functional/legacy-ssh-store.sh similarity index 100% rename from tests/legacy-ssh-store.sh rename to tests/functional/legacy-ssh-store.sh diff --git a/tests/linux-sandbox-cert-test.nix b/tests/functional/linux-sandbox-cert-test.nix similarity index 100% rename from tests/linux-sandbox-cert-test.nix rename to tests/functional/linux-sandbox-cert-test.nix diff --git a/tests/linux-sandbox.sh b/tests/functional/linux-sandbox.sh similarity index 100% rename from tests/linux-sandbox.sh rename to tests/functional/linux-sandbox.sh diff --git a/tests/local-store.sh b/tests/functional/local-store.sh similarity index 100% rename from tests/local-store.sh rename to tests/functional/local-store.sh diff --git a/tests/local.mk b/tests/functional/local.mk similarity index 90% rename from tests/local.mk rename to tests/functional/local.mk index ac418dc0d..4d29f60a1 100644 --- a/tests/local.mk +++ b/tests/functional/local.mk @@ -139,15 +139,16 @@ ifeq ($(HAVE_LOCAL_NIX_BUILD), 1) endif endif -tests/test-libstoreconsumer.sh.test: tests/test-libstoreconsumer/test-libstoreconsumer -tests/plugins.sh: tests/plugins/libplugintest.$(SO_EXT) +$(d)/test-libstoreconsumer.sh.test $(d)/test-libstoreconsumer.sh.test-debug: \ + $(d)/test-libstoreconsumer/test-libstoreconsumer +$(d)/plugins.sh.test $(d)/plugins.sh.test-debug: \ + $(d)/plugins/libplugintest.$(SO_EXT) install-tests += $(foreach x, $(nix_tests), $(d)/$(x)) -clean-files += \ +test-clean-files := \ $(d)/common/vars-and-functions.sh \ $(d)/config.nix -test-deps += \ - tests/common/vars-and-functions.sh \ - tests/config.nix +clean-files += $(test-clean-files) +test-deps += $(test-clean-files) diff --git a/tests/logging.sh b/tests/functional/logging.sh similarity index 100% rename from tests/logging.sh rename to tests/functional/logging.sh diff --git a/tests/misc.sh b/tests/functional/misc.sh similarity index 100% rename from tests/misc.sh rename to tests/functional/misc.sh diff --git a/tests/multiple-outputs.nix b/tests/functional/multiple-outputs.nix similarity index 100% rename from tests/multiple-outputs.nix rename to tests/functional/multiple-outputs.nix diff --git a/tests/multiple-outputs.sh b/tests/functional/multiple-outputs.sh similarity index 100% rename from tests/multiple-outputs.sh rename to tests/functional/multiple-outputs.sh diff --git a/tests/nar-access.nix b/tests/functional/nar-access.nix similarity index 100% rename from tests/nar-access.nix rename to tests/functional/nar-access.nix diff --git a/tests/nar-access.sh b/tests/functional/nar-access.sh similarity index 100% rename from tests/nar-access.sh rename to tests/functional/nar-access.sh diff --git a/tests/nested-sandboxing.sh b/tests/functional/nested-sandboxing.sh similarity index 75% rename from tests/nested-sandboxing.sh rename to tests/functional/nested-sandboxing.sh index d9fa788aa..61fe043c6 100644 --- a/tests/nested-sandboxing.sh +++ b/tests/functional/nested-sandboxing.sh @@ -1,5 +1,5 @@ source common.sh -# This test is run by `tests/nested-sandboxing/runner.nix` in an extra layer of sandboxing. +# This test is run by `tests/functional/nested-sandboxing/runner.nix` in an extra layer of sandboxing. [[ -d /nix/store ]] || skipTest "running this test without Nix's deps being drawn from /nix/store is not yet supported" requireSandboxSupport diff --git a/tests/nested-sandboxing/command.sh b/tests/functional/nested-sandboxing/command.sh similarity index 100% rename from tests/nested-sandboxing/command.sh rename to tests/functional/nested-sandboxing/command.sh diff --git a/tests/nested-sandboxing/runner.nix b/tests/functional/nested-sandboxing/runner.nix similarity index 100% rename from tests/nested-sandboxing/runner.nix rename to tests/functional/nested-sandboxing/runner.nix diff --git a/tests/nix-build-examples.nix b/tests/functional/nix-build-examples.nix similarity index 100% rename from tests/nix-build-examples.nix rename to tests/functional/nix-build-examples.nix diff --git a/tests/nix-build.sh b/tests/functional/nix-build.sh similarity index 100% rename from tests/nix-build.sh rename to tests/functional/nix-build.sh diff --git a/tests/nix-channel.sh b/tests/functional/nix-channel.sh similarity index 100% rename from tests/nix-channel.sh rename to tests/functional/nix-channel.sh diff --git a/tests/nix-collect-garbage-d.sh b/tests/functional/nix-collect-garbage-d.sh similarity index 100% rename from tests/nix-collect-garbage-d.sh rename to tests/functional/nix-collect-garbage-d.sh diff --git a/tests/nix-copy-ssh-ng.sh b/tests/functional/nix-copy-ssh-ng.sh similarity index 100% rename from tests/nix-copy-ssh-ng.sh rename to tests/functional/nix-copy-ssh-ng.sh diff --git a/tests/nix-copy-ssh.sh b/tests/functional/nix-copy-ssh.sh similarity index 100% rename from tests/nix-copy-ssh.sh rename to tests/functional/nix-copy-ssh.sh diff --git a/tests/nix-daemon-untrusting.sh b/tests/functional/nix-daemon-untrusting.sh similarity index 100% rename from tests/nix-daemon-untrusting.sh rename to tests/functional/nix-daemon-untrusting.sh diff --git a/tests/nix-profile.sh b/tests/functional/nix-profile.sh similarity index 100% rename from tests/nix-profile.sh rename to tests/functional/nix-profile.sh diff --git a/tests/nix-shell.sh b/tests/functional/nix-shell.sh similarity index 100% rename from tests/nix-shell.sh rename to tests/functional/nix-shell.sh diff --git a/tests/nix_path.sh b/tests/functional/nix_path.sh similarity index 100% rename from tests/nix_path.sh rename to tests/functional/nix_path.sh diff --git a/tests/optimise-store.sh b/tests/functional/optimise-store.sh similarity index 100% rename from tests/optimise-store.sh rename to tests/functional/optimise-store.sh diff --git a/tests/output-normalization.sh b/tests/functional/output-normalization.sh similarity index 100% rename from tests/output-normalization.sh rename to tests/functional/output-normalization.sh diff --git a/tests/parallel.builder.sh b/tests/functional/parallel.builder.sh similarity index 100% rename from tests/parallel.builder.sh rename to tests/functional/parallel.builder.sh diff --git a/tests/parallel.nix b/tests/functional/parallel.nix similarity index 100% rename from tests/parallel.nix rename to tests/functional/parallel.nix diff --git a/tests/parallel.sh b/tests/functional/parallel.sh similarity index 100% rename from tests/parallel.sh rename to tests/functional/parallel.sh diff --git a/tests/pass-as-file.sh b/tests/functional/pass-as-file.sh similarity index 100% rename from tests/pass-as-file.sh rename to tests/functional/pass-as-file.sh diff --git a/tests/path-from-hash-part.sh b/tests/functional/path-from-hash-part.sh similarity index 100% rename from tests/path-from-hash-part.sh rename to tests/functional/path-from-hash-part.sh diff --git a/tests/path.nix b/tests/functional/path.nix similarity index 100% rename from tests/path.nix rename to tests/functional/path.nix diff --git a/tests/placeholders.sh b/tests/functional/placeholders.sh similarity index 100% rename from tests/placeholders.sh rename to tests/functional/placeholders.sh diff --git a/tests/plugins.sh b/tests/functional/plugins.sh similarity index 100% rename from tests/plugins.sh rename to tests/functional/plugins.sh diff --git a/tests/plugins/local.mk b/tests/functional/plugins/local.mk similarity index 100% rename from tests/plugins/local.mk rename to tests/functional/plugins/local.mk diff --git a/tests/plugins/plugintest.cc b/tests/functional/plugins/plugintest.cc similarity index 100% rename from tests/plugins/plugintest.cc rename to tests/functional/plugins/plugintest.cc diff --git a/tests/post-hook.sh b/tests/functional/post-hook.sh similarity index 100% rename from tests/post-hook.sh rename to tests/functional/post-hook.sh diff --git a/tests/pure-eval.nix b/tests/functional/pure-eval.nix similarity index 100% rename from tests/pure-eval.nix rename to tests/functional/pure-eval.nix diff --git a/tests/pure-eval.sh b/tests/functional/pure-eval.sh similarity index 100% rename from tests/pure-eval.sh rename to tests/functional/pure-eval.sh diff --git a/tests/push-to-store-old.sh b/tests/functional/push-to-store-old.sh similarity index 100% rename from tests/push-to-store-old.sh rename to tests/functional/push-to-store-old.sh diff --git a/tests/push-to-store.sh b/tests/functional/push-to-store.sh similarity index 100% rename from tests/push-to-store.sh rename to tests/functional/push-to-store.sh diff --git a/tests/read-only-store.sh b/tests/functional/read-only-store.sh similarity index 100% rename from tests/read-only-store.sh rename to tests/functional/read-only-store.sh diff --git a/tests/readfile-context.nix b/tests/functional/readfile-context.nix similarity index 100% rename from tests/readfile-context.nix rename to tests/functional/readfile-context.nix diff --git a/tests/readfile-context.sh b/tests/functional/readfile-context.sh similarity index 100% rename from tests/readfile-context.sh rename to tests/functional/readfile-context.sh diff --git a/tests/recursive.nix b/tests/functional/recursive.nix similarity index 100% rename from tests/recursive.nix rename to tests/functional/recursive.nix diff --git a/tests/recursive.sh b/tests/functional/recursive.sh similarity index 100% rename from tests/recursive.sh rename to tests/functional/recursive.sh diff --git a/tests/referrers.sh b/tests/functional/referrers.sh similarity index 100% rename from tests/referrers.sh rename to tests/functional/referrers.sh diff --git a/tests/remote-store.sh b/tests/functional/remote-store.sh similarity index 100% rename from tests/remote-store.sh rename to tests/functional/remote-store.sh diff --git a/tests/repair.sh b/tests/functional/repair.sh similarity index 100% rename from tests/repair.sh rename to tests/functional/repair.sh diff --git a/tests/repl.sh b/tests/functional/repl.sh similarity index 100% rename from tests/repl.sh rename to tests/functional/repl.sh diff --git a/tests/restricted.nix b/tests/functional/restricted.nix similarity index 100% rename from tests/restricted.nix rename to tests/functional/restricted.nix diff --git a/tests/restricted.sh b/tests/functional/restricted.sh similarity index 95% rename from tests/restricted.sh rename to tests/functional/restricted.sh index 17f310a4b..197ae7a10 100644 --- a/tests/restricted.sh +++ b/tests/functional/restricted.sh @@ -9,10 +9,10 @@ nix-instantiate --restrict-eval ./simple.nix -I src=. nix-instantiate --restrict-eval ./simple.nix -I src1=simple.nix -I src2=config.nix -I src3=./simple.builder.sh (! nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix') -nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I src=.. +nix-instantiate --restrict-eval --eval -E 'builtins.readFile ./simple.nix' -I src=../.. -(! nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../src/nix-channel') -nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../src/nix-channel' -I src=../src +(! nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../../src/nix-channel') +nix-instantiate --restrict-eval --eval -E 'builtins.readDir ../../src/nix-channel' -I src=../../src (! nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in ') nix-instantiate --restrict-eval --eval -E 'let __nixPath = [ { prefix = "foo"; path = ./.; } ]; in ' -I src=. diff --git a/tests/search.nix b/tests/functional/search.nix similarity index 100% rename from tests/search.nix rename to tests/functional/search.nix diff --git a/tests/search.sh b/tests/functional/search.sh similarity index 100% rename from tests/search.sh rename to tests/functional/search.sh diff --git a/tests/secure-drv-outputs.nix b/tests/functional/secure-drv-outputs.nix similarity index 100% rename from tests/secure-drv-outputs.nix rename to tests/functional/secure-drv-outputs.nix diff --git a/tests/secure-drv-outputs.sh b/tests/functional/secure-drv-outputs.sh similarity index 100% rename from tests/secure-drv-outputs.sh rename to tests/functional/secure-drv-outputs.sh diff --git a/tests/selfref-gc.sh b/tests/functional/selfref-gc.sh similarity index 100% rename from tests/selfref-gc.sh rename to tests/functional/selfref-gc.sh diff --git a/tests/shell-hello.nix b/tests/functional/shell-hello.nix similarity index 100% rename from tests/shell-hello.nix rename to tests/functional/shell-hello.nix diff --git a/tests/shell.nix b/tests/functional/shell.nix similarity index 100% rename from tests/shell.nix rename to tests/functional/shell.nix diff --git a/tests/shell.sh b/tests/functional/shell.sh similarity index 100% rename from tests/shell.sh rename to tests/functional/shell.sh diff --git a/tests/shell.shebang.rb b/tests/functional/shell.shebang.rb similarity index 100% rename from tests/shell.shebang.rb rename to tests/functional/shell.shebang.rb diff --git a/tests/shell.shebang.sh b/tests/functional/shell.shebang.sh similarity index 100% rename from tests/shell.shebang.sh rename to tests/functional/shell.shebang.sh diff --git a/tests/signing.sh b/tests/functional/signing.sh similarity index 100% rename from tests/signing.sh rename to tests/functional/signing.sh diff --git a/tests/simple-failing.nix b/tests/functional/simple-failing.nix similarity index 100% rename from tests/simple-failing.nix rename to tests/functional/simple-failing.nix diff --git a/tests/simple.builder.sh b/tests/functional/simple.builder.sh similarity index 100% rename from tests/simple.builder.sh rename to tests/functional/simple.builder.sh diff --git a/tests/simple.nix b/tests/functional/simple.nix similarity index 100% rename from tests/simple.nix rename to tests/functional/simple.nix diff --git a/tests/simple.sh b/tests/functional/simple.sh similarity index 100% rename from tests/simple.sh rename to tests/functional/simple.sh diff --git a/tests/ssh-relay.sh b/tests/functional/ssh-relay.sh similarity index 100% rename from tests/ssh-relay.sh rename to tests/functional/ssh-relay.sh diff --git a/tests/store-ping.sh b/tests/functional/store-ping.sh similarity index 100% rename from tests/store-ping.sh rename to tests/functional/store-ping.sh diff --git a/tests/structured-attrs-shell.nix b/tests/functional/structured-attrs-shell.nix similarity index 100% rename from tests/structured-attrs-shell.nix rename to tests/functional/structured-attrs-shell.nix diff --git a/tests/structured-attrs.nix b/tests/functional/structured-attrs.nix similarity index 100% rename from tests/structured-attrs.nix rename to tests/functional/structured-attrs.nix diff --git a/tests/structured-attrs.sh b/tests/functional/structured-attrs.sh similarity index 100% rename from tests/structured-attrs.sh rename to tests/functional/structured-attrs.sh diff --git a/tests/substitute-with-invalid-ca.sh b/tests/functional/substitute-with-invalid-ca.sh similarity index 100% rename from tests/substitute-with-invalid-ca.sh rename to tests/functional/substitute-with-invalid-ca.sh diff --git a/tests/suggestions.sh b/tests/functional/suggestions.sh similarity index 100% rename from tests/suggestions.sh rename to tests/functional/suggestions.sh diff --git a/tests/supplementary-groups.sh b/tests/functional/supplementary-groups.sh similarity index 100% rename from tests/supplementary-groups.sh rename to tests/functional/supplementary-groups.sh diff --git a/tests/tarball.sh b/tests/functional/tarball.sh similarity index 100% rename from tests/tarball.sh rename to tests/functional/tarball.sh diff --git a/tests/test-infra.sh b/tests/functional/test-infra.sh similarity index 100% rename from tests/test-infra.sh rename to tests/functional/test-infra.sh diff --git a/tests/test-libstoreconsumer.sh b/tests/functional/test-libstoreconsumer.sh similarity index 100% rename from tests/test-libstoreconsumer.sh rename to tests/functional/test-libstoreconsumer.sh diff --git a/tests/test-libstoreconsumer/README.md b/tests/functional/test-libstoreconsumer/README.md similarity index 100% rename from tests/test-libstoreconsumer/README.md rename to tests/functional/test-libstoreconsumer/README.md diff --git a/tests/test-libstoreconsumer/local.mk b/tests/functional/test-libstoreconsumer/local.mk similarity index 100% rename from tests/test-libstoreconsumer/local.mk rename to tests/functional/test-libstoreconsumer/local.mk diff --git a/tests/test-libstoreconsumer/main.cc b/tests/functional/test-libstoreconsumer/main.cc similarity index 100% rename from tests/test-libstoreconsumer/main.cc rename to tests/functional/test-libstoreconsumer/main.cc diff --git a/tests/timeout.nix b/tests/functional/timeout.nix similarity index 100% rename from tests/timeout.nix rename to tests/functional/timeout.nix diff --git a/tests/timeout.sh b/tests/functional/timeout.sh similarity index 100% rename from tests/timeout.sh rename to tests/functional/timeout.sh diff --git a/tests/toString-path.sh b/tests/functional/toString-path.sh similarity index 100% rename from tests/toString-path.sh rename to tests/functional/toString-path.sh diff --git a/tests/undefined-variable.nix b/tests/functional/undefined-variable.nix similarity index 100% rename from tests/undefined-variable.nix rename to tests/functional/undefined-variable.nix diff --git a/tests/user-envs-migration.sh b/tests/functional/user-envs-migration.sh similarity index 100% rename from tests/user-envs-migration.sh rename to tests/functional/user-envs-migration.sh diff --git a/tests/user-envs.builder.sh b/tests/functional/user-envs.builder.sh similarity index 100% rename from tests/user-envs.builder.sh rename to tests/functional/user-envs.builder.sh diff --git a/tests/user-envs.nix b/tests/functional/user-envs.nix similarity index 100% rename from tests/user-envs.nix rename to tests/functional/user-envs.nix diff --git a/tests/user-envs.sh b/tests/functional/user-envs.sh similarity index 100% rename from tests/user-envs.sh rename to tests/functional/user-envs.sh diff --git a/tests/why-depends.sh b/tests/functional/why-depends.sh similarity index 100% rename from tests/why-depends.sh rename to tests/functional/why-depends.sh diff --git a/tests/zstd.sh b/tests/functional/zstd.sh similarity index 100% rename from tests/zstd.sh rename to tests/functional/zstd.sh From 644ebaab5f96089bc3a8fd3873e8e7f2131a9074 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 6 Oct 2023 10:53:01 -0400 Subject: [PATCH 201/299] Define NixOS tests in `tests/nixos/default.nix` rather than `flake.nix` I think the our `flake.nix` is currently too large and too scary looking. I think this matters --- if Nix cannot dog-food itself in a way that is elegant, why should other people have confidence that their own code can be elegant and easy to maintain? We could do this at many points in time, but I think around now, when we are thinking about stabilizing parts of Flakes, is an especially good time. This is a first step to make the `flake.nix` smaller, and make individual components responsible for their own packaging. I hope we can do this many more follow-ups like it, until the top-level `flake.nix` is very small and just coordinates between other things. --- flake.nix | 74 ++++++++++++----------------------------- tests/nixos/default.nix | 41 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 53 deletions(-) create mode 100644 tests/nixos/default.nix diff --git a/flake.nix b/flake.nix index c331b2651..bec62fb95 100644 --- a/flake.nix +++ b/flake.nix @@ -509,18 +509,6 @@ }; }; - nixos-lib = import (nixpkgs + "/nixos/lib") { }; - - # https://nixos.org/manual/nixos/unstable/index.html#sec-calling-nixos-tests - runNixOSTestFor = system: test: nixos-lib.runTest { - imports = [ test ]; - hostPkgs = nixpkgsFor.${system}.native; - defaults = { - nixpkgs.pkgs = nixpkgsFor.${system}.native; - }; - _module.args.nixpkgs = nixpkgs; - }; - in { # A Nixpkgs overlay that overrides the 'nix' and # 'nix.perl-bindings' packages. @@ -627,49 +615,29 @@ }; # System tests. - tests.authorization = runNixOSTestFor "x86_64-linux" ./tests/nixos/authorization.nix; + tests = import ./tests/nixos { inherit lib nixpkgs nixpkgsFor; } // { - tests.remoteBuilds = runNixOSTestFor "x86_64-linux" ./tests/nixos/remote-builds.nix; + # Make sure that nix-env still produces the exact same result + # on a particular version of Nixpkgs. + evalNixpkgs = + with nixpkgsFor.x86_64-linux.native; + runCommand "eval-nixos" { buildInputs = [ nix ]; } + '' + type -p nix-env + # Note: we're filtering out nixos-install-tools because https://github.com/NixOS/nixpkgs/pull/153594#issuecomment-1020530593. + time nix-env --store dummy:// -f ${nixpkgs-regression} -qaP --drv-path | sort | grep -v nixos-install-tools > packages + [[ $(sha1sum < packages | cut -c1-40) = ff451c521e61e4fe72bdbe2d0ca5d1809affa733 ]] + mkdir $out + ''; - tests.nix-copy-closure = runNixOSTestFor "x86_64-linux" ./tests/nixos/nix-copy-closure.nix; - - tests.nix-copy = runNixOSTestFor "x86_64-linux" ./tests/nixos/nix-copy.nix; - - tests.nssPreload = runNixOSTestFor "x86_64-linux" ./tests/nixos/nss-preload.nix; - - tests.githubFlakes = runNixOSTestFor "x86_64-linux" ./tests/nixos/github-flakes.nix; - - tests.sourcehutFlakes = runNixOSTestFor "x86_64-linux" ./tests/nixos/sourcehut-flakes.nix; - - tests.tarballFlakes = runNixOSTestFor "x86_64-linux" ./tests/nixos/tarball-flakes.nix; - - tests.containers = runNixOSTestFor "x86_64-linux" ./tests/nixos/containers/containers.nix; - - tests.setuid = lib.genAttrs - ["i686-linux" "x86_64-linux"] - (system: runNixOSTestFor system ./tests/nixos/setuid.nix); - - - # Make sure that nix-env still produces the exact same result - # on a particular version of Nixpkgs. - tests.evalNixpkgs = - with nixpkgsFor.x86_64-linux.native; - runCommand "eval-nixos" { buildInputs = [ nix ]; } - '' - type -p nix-env - # Note: we're filtering out nixos-install-tools because https://github.com/NixOS/nixpkgs/pull/153594#issuecomment-1020530593. - time nix-env --store dummy:// -f ${nixpkgs-regression} -qaP --drv-path | sort | grep -v nixos-install-tools > packages - [[ $(sha1sum < packages | cut -c1-40) = ff451c521e61e4fe72bdbe2d0ca5d1809affa733 ]] - mkdir $out - ''; - - tests.nixpkgsLibTests = - forAllSystems (system: - import (nixpkgs + "/lib/tests/release.nix") - { pkgs = nixpkgsFor.${system}.native; - nixVersions = [ self.packages.${system}.nix ]; - } - ); + nixpkgsLibTests = + forAllSystems (system: + import (nixpkgs + "/lib/tests/release.nix") + { pkgs = nixpkgsFor.${system}.native; + nixVersions = [ self.packages.${system}.nix ]; + } + ); + }; metrics.nixpkgs = import "${nixpkgs-regression}/pkgs/top-level/metrics.nix" { pkgs = nixpkgsFor.x86_64-linux.native; diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix new file mode 100644 index 000000000..b391d7ef2 --- /dev/null +++ b/tests/nixos/default.nix @@ -0,0 +1,41 @@ +{ lib, nixpkgs, nixpkgsFor }: + +let + + nixos-lib = import (nixpkgs + "/nixos/lib") { }; + + # https://nixos.org/manual/nixos/unstable/index.html#sec-calling-nixos-tests + runNixOSTestFor = system: test: nixos-lib.runTest { + imports = [ test ]; + hostPkgs = nixpkgsFor.${system}.native; + defaults = { + nixpkgs.pkgs = nixpkgsFor.${system}.native; + }; + _module.args.nixpkgs = nixpkgs; + }; + +in + +{ + authorization = runNixOSTestFor "x86_64-linux" ./authorization.nix; + + remoteBuilds = runNixOSTestFor "x86_64-linux" ./remote-builds.nix; + + nix-copy-closure = runNixOSTestFor "x86_64-linux" ./nix-copy-closure.nix; + + nix-copy = runNixOSTestFor "x86_64-linux" ./nix-copy.nix; + + nssPreload = runNixOSTestFor "x86_64-linux" ./nss-preload.nix; + + githubFlakes = runNixOSTestFor "x86_64-linux" ./github-flakes.nix; + + sourcehutFlakes = runNixOSTestFor "x86_64-linux" ./sourcehut-flakes.nix; + + tarballFlakes = runNixOSTestFor "x86_64-linux" ./tarball-flakes.nix; + + containers = runNixOSTestFor "x86_64-linux" ./containers/containers.nix; + + setuid = lib.genAttrs + ["i686-linux" "x86_64-linux"] + (system: runNixOSTestFor system ./setuid.nix); +} From 517c547dec086f6332292332571d05dd1cbb6f4b Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 6 Oct 2023 23:34:08 +0200 Subject: [PATCH 202/299] remove duplicate redirects entry --- doc/manual/redirects.js | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/manual/redirects.js b/doc/manual/redirects.js index b2fad19bb..9d083a43d 100644 --- a/doc/manual/redirects.js +++ b/doc/manual/redirects.js @@ -336,7 +336,6 @@ const redirects = { "simple-values": "#primitives", "lists": "#list", "strings": "#string", - "lists": "#list", "attribute-sets": "#attribute-set", }, "installation/installing-binary.html": { From a67cee965a72f60da73713f47ee665a9ffe1873d Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 02:27:06 +0200 Subject: [PATCH 203/299] expand on interpolated expressions --- .../src/language/string-interpolation.md | 120 ++++++++++++++++-- doc/manual/src/language/values.md | 12 +- src/libexpr/primops.cc | 4 +- 3 files changed, 116 insertions(+), 20 deletions(-) diff --git a/doc/manual/src/language/string-interpolation.md b/doc/manual/src/language/string-interpolation.md index ddc6b8230..85e02df37 100644 --- a/doc/manual/src/language/string-interpolation.md +++ b/doc/manual/src/language/string-interpolation.md @@ -1,19 +1,12 @@ # String interpolation -String interpolation is a language feature where a [string], [path], or [attribute name] can contain expressions enclosed in `${ }` (dollar-sign with curly brackets). +String interpolation is a language feature where a [string], [path], or [attribute name][attribute set] can contain expressions enclosed in `${ }` (dollar-sign with curly brackets). -Such a string is an *interpolated string*, and an expression inside is an *interpolated expression*. - -Interpolated expressions must evaluate to one of the following: - -- a [string] -- a [path] -- a [derivation] +Such a construct is called *interpolated string*, and the expression inside is an [interpolated expression](#interpolated-expression). [string]: ./values.md#type-string [path]: ./values.md#type-path -[attribute name]: ./values.md#attribute-set -[derivation]: ../glossary.md#gloss-derivation +[attribute set]: ./values.md#attribute-set ## Examples @@ -80,3 +73,110 @@ let name = "foo"; in ``` { foo = "bar"; } + +# Interpolated expression + +An interpolated expression must evaluate to one of the following: + +- a [string] +- a [path] +- an [attribute set] that has a `__toString` attribute or an `outPath` attribute + + - `__toString` must be a function that takes the attribute set itself and returns a string + - `outPath` must be a string + + This includes [derivations](./derivations.md) or [flake inputs](@docroot@/command-ref/new-cli/nix3-flake.md#flake-inputs) (experimental). + +A string interpolates to itself. + +A path in an interpolated expression is first copied into the Nix store, and the resulting string is the [store path] of the newly created [store object](../glossary.md#gloss-store-object). + +[store path]: ../glossary.md#gloss-store-path + +> **Example** +> +> ```console +> $ mkdir foo +> ``` +> +> Reference the empty directory in an interpolated expression: +> +> ```nix +> "${./foo}" +> ``` +> +> "/nix/store/2hhl2nz5v0khbn06ys82nrk99aa1xxdw-foo" + +A derivation interpolates to the [store path] of its first [output](./derivations.md#attr-outputs). + +> **Example** +> +> ```nix +> let +> pkgs = import {}; +> in +> "${pkgs.hello}" +> ``` +> +> "/nix/store/4xpfqf29z4m8vbhrqcz064wfmb46w5r7-hello-2.12.1" + +An attribute set interpolates to the return value of the function in the `__toString` applied to the attribute set itself. + +> **Example** +> +> ```nix +> let +> a = { +> value = 1; +> __toString = self: toString (self.value + 1); +> }; +> in +> "${a}" +> ``` +> +> "2" + +An attribute set also interpolates to the value of its `outPath` attribute. + +> **Example** +> +> ```nix +> let +> a = { outPath = "foo"; }; +> in +> "${a}" +> ``` +> +> "foo" + +If both `__toString` and `outPath` are present in an attribute set, `__toString` takes precedence. + +> **Example** +> +> ```nix +> let +> a = { __toString = _: "yes"; outPath = throw "no"; }; +> in +> "${a}" +> ``` +> +> "yes" + +If neither is present, an error is thrown. + +> **Example** +> +> ```nix +> let +> a = {}; +> in +> "${a}" +> ``` +> +> error: cannot coerce a set to a string +> +> at «string»:4:2: +> +> 3| in +> 4| "${a}" +> | ^ diff --git a/doc/manual/src/language/values.md b/doc/manual/src/language/values.md index 2ae3e143a..fe0e2e4d5 100644 --- a/doc/manual/src/language/values.md +++ b/doc/manual/src/language/values.md @@ -112,18 +112,16 @@ environment variable `NIX_PATH` will be searched for the given file or directory name. - When an [interpolated string][string interpolation] evaluates to a path, the path is first copied into the Nix store and the resulting string is the [store path] of the newly created [store object]. - - [store path]: ../glossary.md#gloss-store-path - [store object]: ../glossary.md#gloss-store-object - For instance, evaluating `"${./foo.txt}"` will cause `foo.txt` in the current directory to be copied into the Nix store and result in the string `"/nix/store/-foo.txt"`. Note that the Nix language assumes that all input files will remain _unchanged_ while evaluating a Nix expression. For example, assume you used a file path in an interpolated string during a `nix repl` session. - Later in the same session, after having changed the file contents, evaluating the interpolated string with the file path again might not return a new store path, since Nix might not re-read the file contents. + Later in the same session, after having changed the file contents, evaluating the interpolated string with the file path again might not return a new [store path], since Nix might not re-read the file contents. - Paths themselves, except those in angle brackets (`< >`), support [string interpolation]. + [store path]: ../glossary.md#gloss-store-path + + Paths, except those in angle brackets (`< >`), support [string interpolation] and can be used in [interpolated expressions]. + [interpolated expressions]: ./string-interpolation.md#interpolated-expressions At least one slash (`/`) must appear *before* any interpolated expression for the result to be recognized as a path. diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 5de1b2828..1d04aeb2e 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -260,9 +260,7 @@ static RegisterPrimOp primop_import({ .doc = R"( Load, parse and return the Nix expression in the file *path*. - The value *path* can be a path, a string, or an attribute set with an - `__toString` attribute or a `outPath` attribute (as derivations or flake - inputs typically have). + The *path* argument must meet the same criteria as an [interpolated expression](@docroot@/language/string-interpolation.md#interpolated-expression). If *path* is a directory, the file `default.nix` in that directory is loaded. From a7ba8c3f4a69118c4c936fd8fd1415c0446803ca Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 02:46:59 +0200 Subject: [PATCH 204/299] complete example on attribute name interpolation --- .../src/language/string-interpolation.md | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/doc/manual/src/language/string-interpolation.md b/doc/manual/src/language/string-interpolation.md index 85e02df37..2e650e348 100644 --- a/doc/manual/src/language/string-interpolation.md +++ b/doc/manual/src/language/string-interpolation.md @@ -63,16 +63,32 @@ you can instead write ### Attribute name -Attribute names can be created dynamically with string interpolation: + -```nix -let name = "foo"; in -{ - ${name} = "bar"; -} -``` +Attribute names can be interpolated strings. - { foo = "bar"; } +> **Example** +> +> ```nix +> let name = "foo"; in +> { ${name} = 123; } +> ``` +> +> { foo = 123; } + +Attributes can be selected with interpolated strings. + +> **Example** +> +> ```nix +> let name = "foo"; in +> { foo = 123; }.${name} +> ``` +> +> 123 # Interpolated expression From a86a3e5e59dd932d80431e9bbbb9ec3f883b2899 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 03:12:21 +0200 Subject: [PATCH 205/299] document that pure-eval also disables `builtins.nixPath` --- src/libexpr/eval-settings.hh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libexpr/eval-settings.hh b/src/libexpr/eval-settings.hh index 19ba679be..6a5c448b4 100644 --- a/src/libexpr/eval-settings.hh +++ b/src/libexpr/eval-settings.hh @@ -40,7 +40,10 @@ struct EvalSettings : Config Pure evaluation mode ensures that the result of Nix expressions is fully determined by explicitly declared inputs, and not influenced by external state: - Restrict file system and network access to files specified by cryptographic hash - - Disable [`bultins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) and [`builtins.currentTime`](@docroot@/language/builtin-constants.md#builtins-currentTime) + - Disable impure constants: + - [`bultins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) + - [`builtins.currentTime`](@docroot@/language/builtin-constants.md#builtins-currentTime) + - [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath) )" }; From 2fe1ccf7973891cfbd921279a5f94879b2c7dce3 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 03:20:20 +0200 Subject: [PATCH 206/299] describe the effect of `restrict-eval` in a more focused manner --- src/libexpr/eval-settings.hh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libexpr/eval-settings.hh b/src/libexpr/eval-settings.hh index 19ba679be..2f534f496 100644 --- a/src/libexpr/eval-settings.hh +++ b/src/libexpr/eval-settings.hh @@ -29,10 +29,12 @@ struct EvalSettings : Config this, false, "restrict-eval", R"( If set to `true`, the Nix evaluator will not allow access to any - files outside of the Nix search path (as set via the `NIX_PATH` - environment variable or the `-I` option), or to URIs outside of - [`allowed-uris`](../command-ref/conf-file.md#conf-allowed-uris). - The default is `false`. + files outside of + [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath), + or to URIs outside of + [`allowed-uris`](@docroot@/command-ref/conf-file.md#conf-allowed-uris). + + Also the default value for [`nix-path`](#conf-nix-path) is ignored, such that only explicitly set search path entries are taken into account. )"}; Setting pureEval{this, false, "pure-eval", From 630580162686dfd9a74d23acdee7b24ebd6b7c4c Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 02:53:41 +0200 Subject: [PATCH 207/299] reword and reformat description of `builtins.import` --- src/libexpr/primops.cc | 101 ++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 1d04aeb2e..1ff2d5f0b 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -258,62 +258,71 @@ static RegisterPrimOp primop_import({ .args = {"path"}, // TODO turn "normal path values" into link below .doc = R"( - Load, parse and return the Nix expression in the file *path*. - - The *path* argument must meet the same criteria as an [interpolated expression](@docroot@/language/string-interpolation.md#interpolated-expression). - - If *path* is a directory, the file `default.nix` in that directory - is loaded. - - Evaluation aborts if the file doesn’t exist or contains - an incorrect Nix expression. `import` implements Nix’s module - system: you can put any Nix expression (such as a set or a - function) in a separate file, and use it from Nix expressions in - other files. + Load, parse, and return the Nix expression in the file *path*. > **Note** > > Unlike some languages, `import` is a regular function in Nix. - > Paths using the angle bracket syntax (e.g., `import` *\*) - > are normal [path values](@docroot@/language/values.md#type-path). - A Nix expression loaded by `import` must not contain any *free - variables* (identifiers that are not defined in the Nix expression - itself and are not built-in). Therefore, it cannot refer to - variables that are in scope at the call site. For instance, if you - have a calling expression + The *path* argument must meet the same criteria as an [interpolated expression](@docroot@/language/string-interpolation.md#interpolated-expression). - ```nix - rec { - x = 123; - y = import ./foo.nix; - } - ``` + If *path* is a directory, the file `default.nix` in that directory is used if it exists. - then the following `foo.nix` will give an error: + > **Example** + > + > ```console + > $ echo 123 > default.nix + > ``` + > + > Import `default.nix` from the current directory. + > + > ```nix + > import ./. + > ``` + > + > 123 - ```nix - x + 456 - ``` + Evaluation aborts if the file doesn’t exist or contains an invalid Nix expression. - since `x` is not in scope in `foo.nix`. If you want `x` to be - available in `foo.nix`, you should pass it as a function argument: + A Nix expression loaded by `import` must not contain any *free variables*, that is, identifiers that are not defined in the Nix expression itself and are not built-in. + Therefore, it cannot refer to variables that are in scope at the call site. - ```nix - rec { - x = 123; - y = import ./foo.nix x; - } - ``` - - and - - ```nix - x: x + 456 - ``` - - (The function argument doesn’t have to be called `x` in `foo.nix`; - any name would work.) + > **Example** + > + > If you have a calling expression + > + > ```nix + > rec { + > x = 123; + > y = import ./foo.nix; + > } + > ``` + > + > then the following `foo.nix` will give an error: + > + > ```nix + > # foo.nix + > x + 456 + > ``` + > + > since `x` is not in scope in `foo.nix`. + > If you want `x` to be available in `foo.nix`, pass it as a function argument: + > + > ```nix + > rec { + > x = 123; + > y = import ./foo.nix x; + > } + > ``` + > + > and + > + > ```nix + > # foo.nix + > x: x + 456 + > ``` + > + > The function argument doesn’t have to be called `x` in `foo.nix`; any name would work. )", .fun = [](EvalState & state, const PosIdx pos, Value * * args, Value & v) { From f00a5eb11b53a800850468f5e9e4658d05b419f9 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 03:10:20 +0200 Subject: [PATCH 208/299] introduce lookup paths as a distinct language construct so far they did not really have a name, and were at best referred to as "angle bracket syntax". --- doc/manual/src/SUMMARY.md.in | 1 + .../src/language/constructs/lookup-path.md | 27 +++++++++++++++++++ doc/manual/src/language/values.md | 9 +++---- src/libexpr/primops.cc | 6 ++--- 4 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 doc/manual/src/language/constructs/lookup-path.md diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index 2e631058f..e49e77cf5 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -30,6 +30,7 @@ - [Data Types](language/values.md) - [Language Constructs](language/constructs.md) - [String interpolation](language/string-interpolation.md) + - [Lookup path](language/constructs/lookup-path.md) - [Operators](language/operators.md) - [Derivations](language/derivations.md) - [Advanced Attributes](language/advanced-attributes.md) diff --git a/doc/manual/src/language/constructs/lookup-path.md b/doc/manual/src/language/constructs/lookup-path.md new file mode 100644 index 000000000..8ec835309 --- /dev/null +++ b/doc/manual/src/language/constructs/lookup-path.md @@ -0,0 +1,27 @@ +# Lookup path + +> **Syntax** +> +> *lookup-path* = `<` *identifier* [ `/` *identifier* `]`... `>` + +A lookup path is an identifier with an optional path suffix that resolves to a [path value](@docroot@/language/values.md#type-path) if the identifier matches a search path entry. + +The value of a lookup path is determined by [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath). + +See [`builtins.findFile`](@docroot@/language/builtins.md#builtins-findFile) for details on lookup path resolution. + +> **Example** +> +> ```nix +> +>``` +> +> /nix/var/nix/profiles/per-user/root/channels/nixpkgs + +> **Example** +> +> ```nix +> +>``` +> +> /nix/var/nix/profiles/per-user/root/channels/nixpkgs/nixos diff --git a/doc/manual/src/language/values.md b/doc/manual/src/language/values.md index 2ae3e143a..4d8950e85 100644 --- a/doc/manual/src/language/values.md +++ b/doc/manual/src/language/values.md @@ -107,11 +107,6 @@ e.g. `~/foo` would be equivalent to `/home/edolstra/foo` for a user whose home directory is `/home/edolstra`. - Paths can also be specified between angle brackets, e.g. - ``. This means that the directories listed in the - environment variable `NIX_PATH` will be searched for the given file - or directory name. - When an [interpolated string][string interpolation] evaluates to a path, the path is first copied into the Nix store and the resulting string is the [store path] of the newly created [store object]. [store path]: ../glossary.md#gloss-store-path @@ -123,13 +118,15 @@ For example, assume you used a file path in an interpolated string during a `nix repl` session. Later in the same session, after having changed the file contents, evaluating the interpolated string with the file path again might not return a new store path, since Nix might not re-read the file contents. - Paths themselves, except those in angle brackets (`< >`), support [string interpolation]. + Paths support [string interpolation]. At least one slash (`/`) must appear *before* any interpolated expression for the result to be recognized as a path. `a.${foo}/b.${bar}` is a syntactically valid division operation. `./a.${foo}/b.${bar}` is a path. + [Lookup paths](./constructs/lookup-path.md) such as `` resolve to path values. + - Boolean *Booleans* with values `true` and `false`. diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 5de1b2828..a8cb4ccfe 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1723,7 +1723,7 @@ static RegisterPrimOp primop_findFile(PrimOp { If the suffix is found inside that directory, then the entry is a match; the combined absolute path of the directory (now downloaded if need be) and the suffix is returned. - The syntax + [Lookup path](@docroot@/language/constructs/lookup-path.md) expressions can be [desugared](https://en.wikipedia.org/wiki/Syntactic_sugar) using this and [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath): ```nix @@ -4389,9 +4389,9 @@ void EvalState::createBaseEnv() addConstant("__nixPath", v, { .type = nList, .doc = R"( - The search path used to resolve angle bracket path lookups. + List of search path entries used to resolve [lookup paths](@docroot@/language/constructs/lookup-path.md). - Angle bracket expressions can be + Lookup path expressions can be [desugared](https://en.wikipedia.org/wiki/Syntactic_sugar) using this and [`builtins.findFile`](./builtins.html#builtins-findFile): From 896a90520280f2645c32b9a0136ab281843942c3 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 9 Oct 2023 10:07:46 +0200 Subject: [PATCH 209/299] AE -> BE; fix redirects --- doc/manual/redirects.js | 1 + doc/manual/src/contributing/testing.md | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/manual/redirects.js b/doc/manual/redirects.js index 9d083a43d..d1b10109d 100644 --- a/doc/manual/redirects.js +++ b/doc/manual/redirects.js @@ -354,6 +354,7 @@ const redirects = { "installer-tests": "testing.html#installer-tests", "one-time-setup": "testing.html#one-time-setup", "using-the-ci-generated-installer-for-manual-testing": "testing.html#using-the-ci-generated-installer-for-manual-testing", + "characterization-testing": "#characterisation-testing-unit", } }; diff --git a/doc/manual/src/contributing/testing.md b/doc/manual/src/contributing/testing.md index 1a6388e40..3d75ebe7b 100644 --- a/doc/manual/src/contributing/testing.md +++ b/doc/manual/src/contributing/testing.md @@ -62,11 +62,11 @@ The path to the `unit-test-data` directory is passed to the unit test executable You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`. Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option, or the `GTEST_FILTER` environment variable. -### Characterization testing +### Characterisation testing { #characaterisation-testing-unit } -See [below](#characterization-testing-1) for a broader discussion of characterization testing. +See [functional characterisation testing](#characterisation-testing-functional) for a broader discussion of characterisation testing. -Like with the functional characterization, `_NIX_TEST_ACCEPT=1` is also used. +Like with the functional characterisation, `_NIX_TEST_ACCEPT=1` is also used. For example: ```shell-session $ _NIX_TEST_ACCEPT=1 make libstore-tests-exe_RUN @@ -77,8 +77,8 @@ $ _NIX_TEST_ACCEPT=1 make libstore-tests-exe_RUN [ SKIPPED ] WorkerProtoTest.storePath_write ... ``` -will regenerate the "golden master" expected result for the `libnixstore` characterization tests. -The characterization tests will mark themselves "skipped" since they regenerated the expected result instead of actually testing anything. +will regenerate the "golden master" expected result for the `libnixstore` characterisation tests. +The characterisation tests will mark themselves "skipped" since they regenerated the expected result instead of actually testing anything. ## Functional tests @@ -195,9 +195,9 @@ To remove any traces of that: git clean -x --force tests ``` -### Characterization testing +### Characterisation testing { #characterisation-testing-functional } -Occasionally, Nix utilizes a technique called [Characterization Testing](https://en.wikipedia.org/wiki/Characterization_test) as part of the functional tests. +Occasionally, Nix utilizes a technique called [Characterisation Testing](https://en.wikipedia.org/wiki/Characterization_test) as part of the functional tests. This technique is to include the exact output/behavior of a former version of Nix in a test in order to check that Nix continues to produce the same behavior going forward. For example, this technique is used for the language tests, to check both the printed final value if evaluation was successful, and any errors and warnings encountered. @@ -208,7 +208,7 @@ For example: ```bash _NIX_TEST_ACCEPT=1 make tests/functional/lang.sh.test ``` -This convention is shared with the [characterization unit tests](#characterization-testing-1) too. +This convention is shared with the [characterisation unit tests](#characterisation-testing-unit) too. An interesting situation to document is the case when these tests are "overfitted". The language tests are, again, an example of this. @@ -221,7 +221,7 @@ Diagnostic outputs are indeed not a stable interface, but they still are importa By recording the expected output, the test suite guards against accidental changes, and ensure the *result* (not just the code that implements it) of the diagnostic code paths are under code review. Regressions are caught, and improvements always show up in code review. -To ensure that characterization testing doesn't make it harder to intentionally change these interfaces, there always must be an easy way to regenerate the expected output, as we do with `_NIX_TEST_ACCEPT=1`. +To ensure that characterisation testing doesn't make it harder to intentionally change these interfaces, there always must be an easy way to regenerate the expected output, as we do with `_NIX_TEST_ACCEPT=1`. ## Integration tests @@ -235,7 +235,7 @@ You can run them manually with `nix build .#hydraJobs.tests.{testName}` or `nix- After a one-time setup, the Nix repository's GitHub Actions continuous integration (CI) workflow can test the installer each time you push to a branch. -Creating a Cachix cache for your installer tests and adding its authorization token to GitHub enables [two installer-specific jobs in the CI workflow](https://github.com/NixOS/nix/blob/88a45d6149c0e304f6eb2efcc2d7a4d0d569f8af/.github/workflows/ci.yml#L50-L91): +Creating a Cachix cache for your installer tests and adding its authorisation token to GitHub enables [two installer-specific jobs in the CI workflow](https://github.com/NixOS/nix/blob/88a45d6149c0e304f6eb2efcc2d7a4d0d569f8af/.github/workflows/ci.yml#L50-L91): - The `installer` job generates installers for the platforms below and uploads them to your Cachix cache: - `x86_64-linux` From 0246de1896df732739b0874c83b7d05becc83cf3 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 9 Oct 2023 10:14:25 +0200 Subject: [PATCH 210/299] remove unnecessary indentation from markdown list this makes it a bit easier to work with, as some tooling doesn't work well with too much indentation. --- doc/manual/src/command-ref/env-common.md | 175 ++++++++++++----------- 1 file changed, 95 insertions(+), 80 deletions(-) diff --git a/doc/manual/src/command-ref/env-common.md b/doc/manual/src/command-ref/env-common.md index b4a9bb2a9..34e0dbfbd 100644 --- a/doc/manual/src/command-ref/env-common.md +++ b/doc/manual/src/command-ref/env-common.md @@ -2,109 +2,124 @@ Most Nix commands interpret the following environment variables: - - [`IN_NIX_SHELL`](#env-IN_NIX_SHELL)\ - Indicator that tells if the current environment was set up by - `nix-shell`. It can have the values `pure` or `impure`. +- [`IN_NIX_SHELL`](#env-IN_NIX_SHELL) - - [`NIX_PATH`](#env-NIX_PATH)\ - A colon-separated list of directories used to look up the location of Nix - expressions using [paths](@docroot@/language/values.md#type-path) - enclosed in angle brackets (i.e., ``), - e.g. `/home/eelco/Dev:/etc/nixos`. It can be extended using the - [`-I` option](@docroot@/command-ref/opt-common.md#opt-I). + Indicator that tells if the current environment was set up by + `nix-shell`. It can have the values `pure` or `impure`. - If `NIX_PATH` is not set at all, Nix will fall back to the following list in [impure](@docroot@/command-ref/conf-file.md#conf-pure-eval) and [unrestricted](@docroot@/command-ref/conf-file.md#conf-restrict-eval) evaluation mode: +- [`NIX_PATH`](#env-NIX_PATH) - 1. `$HOME/.nix-defexpr/channels` - 2. `nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixpkgs` - 3. `/nix/var/nix/profiles/per-user/root/channels` + A colon-separated list of directories used to look up the location of Nix + expressions using [paths](@docroot@/language/values.md#type-path) + enclosed in angle brackets (i.e., ``), + e.g. `/home/eelco/Dev:/etc/nixos`. It can be extended using the + [`-I` option](@docroot@/command-ref/opt-common.md#opt-I). - If `NIX_PATH` is set to an empty string, resolving search paths will always fail. - For example, attempting to use `` will produce: + If `NIX_PATH` is not set at all, Nix will fall back to the following list in [impure](@docroot@/command-ref/conf-file.md#conf-pure-eval) and [unrestricted](@docroot@/command-ref/conf-file.md#conf-restrict-eval) evaluation mode: - error: file 'nixpkgs' was not found in the Nix search path + 1. `$HOME/.nix-defexpr/channels` + 2. `nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixpkgs` + 3. `/nix/var/nix/profiles/per-user/root/channels` - - [`NIX_IGNORE_SYMLINK_STORE`](#env-NIX_IGNORE_SYMLINK_STORE)\ - Normally, the Nix store directory (typically `/nix/store`) is not - allowed to contain any symlink components. This is to prevent - “impure” builds. Builders sometimes “canonicalise” paths by - resolving all symlink components. Thus, builds on different machines - (with `/nix/store` resolving to different locations) could yield - different results. This is generally not a problem, except when - builds are deployed to machines where `/nix/store` resolves - differently. If you are sure that you’re not going to do that, you - can set `NIX_IGNORE_SYMLINK_STORE` to `1`. + If `NIX_PATH` is set to an empty string, resolving search paths will always fail. + For example, attempting to use `` will produce: - Note that if you’re symlinking the Nix store so that you can put it - on another file system than the root file system, on Linux you’re - better off using `bind` mount points, e.g., + error: file 'nixpkgs' was not found in the Nix search path - ```console - $ mkdir /nix - $ mount -o bind /mnt/otherdisk/nix /nix - ``` +- [`NIX_IGNORE_SYMLINK_STORE`](#env-NIX_IGNORE_SYMLINK_STORE) - Consult the mount 8 manual page for details. + Normally, the Nix store directory (typically `/nix/store`) is not + allowed to contain any symlink components. This is to prevent + “impure” builds. Builders sometimes “canonicalise” paths by + resolving all symlink components. Thus, builds on different machines + (with `/nix/store` resolving to different locations) could yield + different results. This is generally not a problem, except when + builds are deployed to machines where `/nix/store` resolves + differently. If you are sure that you’re not going to do that, you + can set `NIX_IGNORE_SYMLINK_STORE` to `1`. - - [`NIX_STORE_DIR`](#env-NIX_STORE_DIR)\ - Overrides the location of the Nix store (default `prefix/store`). + Note that if you’re symlinking the Nix store so that you can put it + on another file system than the root file system, on Linux you’re + better off using `bind` mount points, e.g., - - [`NIX_DATA_DIR`](#env-NIX_DATA_DIR)\ - Overrides the location of the Nix static data directory (default - `prefix/share`). + ```console + $ mkdir /nix + $ mount -o bind /mnt/otherdisk/nix /nix + ``` - - [`NIX_LOG_DIR`](#env-NIX_LOG_DIR)\ - Overrides the location of the Nix log directory (default - `prefix/var/log/nix`). + Consult the mount 8 manual page for details. - - [`NIX_STATE_DIR`](#env-NIX_STATE_DIR)\ - Overrides the location of the Nix state directory (default - `prefix/var/nix`). +- [`NIX_STORE_DIR`](#env-NIX_STORE_DIR) - - [`NIX_CONF_DIR`](#env-NIX_CONF_DIR)\ - Overrides the location of the system Nix configuration directory - (default `prefix/etc/nix`). + Overrides the location of the Nix store (default `prefix/store`). - - [`NIX_CONFIG`](#env-NIX_CONFIG)\ - Applies settings from Nix configuration from the environment. - The content is treated as if it was read from a Nix configuration file. - Settings are separated by the newline character. +- [`NIX_DATA_DIR`](#env-NIX_DATA_DIR) - - [`NIX_USER_CONF_FILES`](#env-NIX_USER_CONF_FILES)\ - Overrides the location of the Nix user configuration files to load from. + Overrides the location of the Nix static data directory (default + `prefix/share`). - The default are the locations according to the [XDG Base Directory Specification]. - See the [XDG Base Directories](#xdg-base-directories) sub-section for details. +- [`NIX_LOG_DIR`](#env-NIX_LOG_DIR) - The variable is treated as a list separated by the `:` token. + Overrides the location of the Nix log directory (default + `prefix/var/log/nix`). - - [`TMPDIR`](#env-TMPDIR)\ - Use the specified directory to store temporary files. In particular, - this includes temporary build directories; these can take up - substantial amounts of disk space. The default is `/tmp`. +- [`NIX_STATE_DIR`](#env-NIX_STATE_DIR) - - [`NIX_REMOTE`](#env-NIX_REMOTE)\ - This variable should be set to `daemon` if you want to use the Nix - daemon to execute Nix operations. This is necessary in [multi-user - Nix installations](@docroot@/installation/multi-user.md). If the Nix - daemon's Unix socket is at some non-standard path, this variable - should be set to `unix://path/to/socket`. Otherwise, it should be - left unset. + Overrides the location of the Nix state directory (default + `prefix/var/nix`). - - [`NIX_SHOW_STATS`](#env-NIX_SHOW_STATS)\ - If set to `1`, Nix will print some evaluation statistics, such as - the number of values allocated. +- [`NIX_CONF_DIR`](#env-NIX_CONF_DIR) - - [`NIX_COUNT_CALLS`](#env-NIX_COUNT_CALLS)\ - If set to `1`, Nix will print how often functions were called during - Nix expression evaluation. This is useful for profiling your Nix - expressions. + Overrides the location of the system Nix configuration directory + (default `prefix/etc/nix`). - - [`GC_INITIAL_HEAP_SIZE`](#env-GC_INITIAL_HEAP_SIZE)\ - If Nix has been configured to use the Boehm garbage collector, this - variable sets the initial size of the heap in bytes. It defaults to - 384 MiB. Setting it to a low value reduces memory consumption, but - will increase runtime due to the overhead of garbage collection. +- [`NIX_CONFIG`](#env-NIX_CONFIG) + + Applies settings from Nix configuration from the environment. + The content is treated as if it was read from a Nix configuration file. + Settings are separated by the newline character. + +- [`NIX_USER_CONF_FILES`](#env-NIX_USER_CONF_FILES) + + Overrides the location of the Nix user configuration files to load from. + + The default are the locations according to the [XDG Base Directory Specification]. + See the [XDG Base Directories](#xdg-base-directories) sub-section for details. + + The variable is treated as a list separated by the `:` token. + +- [`TMPDIR`](#env-TMPDIR) + + Use the specified directory to store temporary files. In particular, + this includes temporary build directories; these can take up + substantial amounts of disk space. The default is `/tmp`. + +- [`NIX_REMOTE`](#env-NIX_REMOTE) + + This variable should be set to `daemon` if you want to use the Nix + daemon to execute Nix operations. This is necessary in [multi-user + Nix installations](@docroot@/installation/multi-user.md). If the Nix + daemon's Unix socket is at some non-standard path, this variable + should be set to `unix://path/to/socket`. Otherwise, it should be + left unset. + +- [`NIX_SHOW_STATS`](#env-NIX_SHOW_STATS) + + If set to `1`, Nix will print some evaluation statistics, such as + the number of values allocated. + +- [`NIX_COUNT_CALLS`](#env-NIX_COUNT_CALLS) + + If set to `1`, Nix will print how often functions were called during + Nix expression evaluation. This is useful for profiling your Nix + expressions. + +- [`GC_INITIAL_HEAP_SIZE`](#env-GC_INITIAL_HEAP_SIZE) + + If Nix has been configured to use the Boehm garbage collector, this + variable sets the initial size of the heap in bytes. It defaults to + 384 MiB. Setting it to a low value reduces memory consumption, but + will increase runtime due to the overhead of garbage collection. ## XDG Base Directories From 47b3508665bb9dc6a8467841ce487c7426080f17 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 6 Oct 2023 11:57:31 -0400 Subject: [PATCH 211/299] Use positive source filtering for the standalone functional tests job Additionally this skipping of the building is reimplemented to be a bit more robust and use the same idioms as the functionality for skipping the tests. In particular, it will now work even if the source files exist, so we can do this during development too. --- Makefile | 15 +++++++--- Makefile.config.in | 3 +- configure.ac | 11 +++++-- flake.nix | 60 +++++++++++++++++++++++++-------------- local.mk | 2 -- tests/functional/local.mk | 6 +--- 6 files changed, 61 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 6658e3490..4f4ac0c6e 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +-include Makefile.config +clean-files += Makefile.config + +ifeq ($(ENABLE_BUILD), yes) makefiles = \ mk/precompiled-headers.mk \ local.mk \ @@ -18,15 +22,18 @@ makefiles = \ misc/upstart/local.mk \ doc/manual/local.mk \ doc/internal-api/local.mk +endif --include Makefile.config - -ifeq ($(tests), yes) +ifeq ($(ENABLE_BUILD)_$(ENABLE_TESTS), yes_yes) UNIT_TEST_ENV = _NIX_TEST_UNIT_DATA=unit-test-data makefiles += \ src/libutil/tests/local.mk \ src/libstore/tests/local.mk \ - src/libexpr/tests/local.mk \ + src/libexpr/tests/local.mk +endif + +ifeq ($(ENABLE_TESTS), yes) +makefiles += \ tests/functional/local.mk \ tests/functional/ca/local.mk \ tests/functional/dyn-drv/local.mk \ diff --git a/Makefile.config.in b/Makefile.config.in index 707cfe0e3..19992fa20 100644 --- a/Makefile.config.in +++ b/Makefile.config.in @@ -46,5 +46,6 @@ sandbox_shell = @sandbox_shell@ storedir = @storedir@ sysconfdir = @sysconfdir@ system = @system@ -tests = @tests@ +ENABLE_BUILD = @ENABLE_BUILD@ +ENABLE_TESTS = @ENABLE_TESTS@ internal_api_docs = @internal_api_docs@ diff --git a/configure.ac b/configure.ac index 6d78237f0..225baf6b5 100644 --- a/configure.ac +++ b/configure.ac @@ -152,12 +152,17 @@ if test "x$GCC_ATOMIC_BUILTINS_NEED_LIBATOMIC" = xyes; then LDFLAGS="-latomic $LDFLAGS" fi +# Running the functional tests without building Nix is useful for testing +# different pre-built versions of Nix against each other. +AC_ARG_ENABLE(build, AS_HELP_STRING([--disable-build],[Do not build nix]), + ENABLE_BUILD=$enableval, ENABLE_BUILD=yes) +AC_SUBST(ENABLE_BUILD) # Building without tests is useful for bootstrapping with a smaller footprint # or running the tests in a separate derivation. Otherwise, we do compile and # run them. AC_ARG_ENABLE(tests, AS_HELP_STRING([--disable-tests],[Do not build the tests]), - tests=$enableval, tests=yes) -AC_SUBST(tests) + ENABLE_TESTS=$enableval, ENABLE_TESTS=yes) +AC_SUBST(ENABLE_TESTS) # Building without API docs is the default as Nix' C++ interfaces are internal and unstable. AC_ARG_ENABLE(internal_api_docs, AS_HELP_STRING([--enable-internal-api-docs],[Build API docs for Nix's internal unstable C++ interfaces]), @@ -289,7 +294,7 @@ if test "$gc" = yes; then fi -if test "$tests" = yes; then +if test "$ENABLE_TESTS" = yes; then # Look for gtest. PKG_CHECK_MODULES([GTEST], [gtest_main]) diff --git a/flake.nix b/flake.nix index bec62fb95..1fe26853f 100644 --- a/flake.nix +++ b/flake.nix @@ -59,29 +59,40 @@ # that would interfere with repo semantics. fileset.fileFilter (f: f.name != ".gitignore") ./.; + configureFiles = fileset.unions [ + ./.version + ./configure.ac + ./m4 + # TODO: do we really need README.md? It doesn't seem used in the build. + ./README.md + ]; + + topLevelBuildFiles = fileset.unions [ + ./local.mk + ./Makefile + ./Makefile.config.in + ./mk + ]; + + functionalTestFiles = fileset.unions [ + ./tests/functional + (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) + ]; + nixSrc = fileset.toSource { root = ./.; fileset = fileset.intersect baseFiles (fileset.unions [ - ./.version + configureFiles + topLevelBuildFiles ./boehmgc-coroutine-sp-fallback.diff - ./bootstrap.sh - ./configure.ac ./doc - ./local.mk - ./m4 - ./Makefile - ./Makefile.config.in ./misc - ./mk ./precompiled-headers.h ./src - ./tests/functional ./unit-test-data ./COPYING ./scripts/local.mk - (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) - # TODO: do we really need README.md? It doesn't seem used in the build. - ./README.md + functionalTestFiles ]); }; @@ -252,7 +263,6 @@ testNixVersions = pkgs: client: daemon: with commonDeps { inherit pkgs; }; with pkgs.lib; pkgs.stdenv.mkDerivation { NIX_DAEMON_PACKAGE = daemon; NIX_CLIENT_PACKAGE = client; - HAVE_LOCAL_NIX_BUILD = false; name = "nix-tests" + optionalString @@ -261,7 +271,14 @@ "-${client.version}-against-${daemon.version}"; inherit version; - src = nixSrc; + src = fileset.toSource { + root = ./.; + fileset = fileset.intersect baseFiles (fileset.unions [ + configureFiles + topLevelBuildFiles + functionalTestFiles + ]); + }; VERSION_SUFFIX = versionSuffix; @@ -271,19 +288,20 @@ enableParallelBuilding = true; - configureFlags = testConfigureFlags; # otherwise configure fails + configureFlags = + testConfigureFlags # otherwise configure fails + ++ [ "--disable-build" ]; + dontBuild = true; doInstallCheck = true; - buildPhase = '' - # Remove the source files to make sure that we're not accidentally rebuilding Nix - rm src/**/*.cc - ''; - installPhase = '' mkdir -p $out ''; - installCheckPhase = "make installcheck -j$NIX_BUILD_CORES -l$NIX_BUILD_CORES"; + installCheckPhase = '' + mkdir -p src/nix-channel + make installcheck -j$NIX_BUILD_CORES -l$NIX_BUILD_CORES + ''; }; binaryTarball = nix: pkgs: diff --git a/local.mk b/local.mk index 6951c179e..3f3abb9f0 100644 --- a/local.mk +++ b/local.mk @@ -1,5 +1,3 @@ -clean-files += Makefile.config - GLOBAL_CXXFLAGS += -Wno-deprecated-declarations -Werror=switch # Allow switch-enum to be overridden for files that do not support it, usually because of dependency headers. ERROR_SWITCH_ENUM = -Werror=switch-enum diff --git a/tests/functional/local.mk b/tests/functional/local.mk index 4d29f60a1..6f6c94fe6 100644 --- a/tests/functional/local.mk +++ b/tests/functional/local.mk @@ -1,7 +1,3 @@ -# whether to run the tests that assume that we have a local build of -# Nix -HAVE_LOCAL_NIX_BUILD ?= 1 - nix_tests = \ test-infra.sh \ init.sh \ @@ -131,7 +127,7 @@ ifeq ($(HAVE_LIBCPUID), 1) nix_tests += compute-levels.sh endif -ifeq ($(HAVE_LOCAL_NIX_BUILD), 1) +ifeq ($(ENABLE_BUILD), yes) nix_tests += test-libstoreconsumer.sh ifeq ($(BUILD_SHARED_LIBS), 1) From 6654b4e3b4cf4b2181a0d14aa792d56eeab6219a Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 9 Oct 2023 07:56:59 -0400 Subject: [PATCH 212/299] Use positive source filtering for the Perl bindings --- flake.nix | 10 +++++++++- perl/Makefile | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 1fe26853f..301e65545 100644 --- a/flake.nix +++ b/flake.nix @@ -477,7 +477,15 @@ passthru.perl-bindings = with final; perl.pkgs.toPerlModule (currentStdenv.mkDerivation { name = "nix-perl-${version}"; - src = self; + src = fileset.toSource { + root = ./.; + fileset = fileset.intersect baseFiles (fileset.unions [ + ./perl + ./.version + ./m4 + ./mk + ]); + }; nativeBuildInputs = [ buildPackages.autoconf-archive diff --git a/perl/Makefile b/perl/Makefile index c2c95f255..832668dd1 100644 --- a/perl/Makefile +++ b/perl/Makefile @@ -1,6 +1,12 @@ makefiles = local.mk -GLOBAL_CXXFLAGS += -g -Wall -std=c++2a -I ../src +GLOBAL_CXXFLAGS += -g -Wall -std=c++2a + +# A convenience for concurrent development of Nix and its Perl bindings. +# Not needed in a standalone build of the Perl bindings. +ifneq ("$(wildcard ../src)", "") + GLOBAL_CXXFLAGS += -I ../src +endif -include Makefile.config From 81d3a8542a6c919bb2302a877fd4443a6c07a2dc Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 9 Oct 2023 10:21:18 -0400 Subject: [PATCH 213/299] doc: Slight reword of "interpolated expression" in paragraph I was sleepy and confused that "interpolated expression" was a new type of thing at first. This nudges the reader to understand that its just a regular expression, and these conditions are imposed by the interpolation operation. --- doc/manual/src/language/string-interpolation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/manual/src/language/string-interpolation.md b/doc/manual/src/language/string-interpolation.md index 2e650e348..e999b287b 100644 --- a/doc/manual/src/language/string-interpolation.md +++ b/doc/manual/src/language/string-interpolation.md @@ -92,7 +92,7 @@ Attributes can be selected with interpolated strings. # Interpolated expression -An interpolated expression must evaluate to one of the following: +An expression that is interpolated must evaluate to one of the following: - a [string] - a [path] From c32084a12cb922f54829b329fa1527f865e5b8ca Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 9 Oct 2023 16:25:53 +0200 Subject: [PATCH 214/299] printStats -> maybePrintStats --- src/libcmd/command.cc | 2 +- src/libexpr/eval.cc | 2 +- src/libexpr/eval.hh | 2 +- src/nix-build/nix-build.cc | 2 +- src/nix-env/nix-env.cc | 2 +- src/nix-instantiate/nix-instantiate.cc | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libcmd/command.cc b/src/libcmd/command.cc index 4fc197956..a88ba8134 100644 --- a/src/libcmd/command.cc +++ b/src/libcmd/command.cc @@ -98,7 +98,7 @@ EvalCommand::EvalCommand() EvalCommand::~EvalCommand() { if (evalState) - evalState->printStats(); + evalState->maybePrintStats(); } ref EvalCommand::getEvalStore() diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 600444955..b18eb5266 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2491,7 +2491,7 @@ bool EvalState::fullGC() { #endif } -void EvalState::printStats() +void EvalState::maybePrintStats() { bool showStats = getEnv("NIX_SHOW_STATS").value_or("0") != "0"; diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 3bfa34ef6..c95558952 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -714,7 +714,7 @@ public: * Performs a full memory GC before printing the statistics, so that the * GC statistics are more accurate. */ - void printStats(); + void maybePrintStats(); /** * Print statistics, unconditionally, cheaply, without performing a GC first. diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index e2189fc66..2895c5e3c 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -344,7 +344,7 @@ static void main_nix_build(int argc, char * * argv) } } - state->printStats(); + state->maybePrintStats(); auto buildPaths = [&](const std::vector & paths) { /* Note: we do this even when !printMissing to efficiently diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index e455a50fd..01742daa8 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -1531,7 +1531,7 @@ static int main_nix_env(int argc, char * * argv) op(globals, std::move(opFlags), std::move(opArgs)); - globals.state->printStats(); + globals.state->maybePrintStats(); return 0; } diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc index 446b27e66..d40196497 100644 --- a/src/nix-instantiate/nix-instantiate.cc +++ b/src/nix-instantiate/nix-instantiate.cc @@ -189,7 +189,7 @@ static int main_nix_instantiate(int argc, char * * argv) evalOnly, outputKind, xmlOutputSourceLocation, e); } - state->printStats(); + state->maybePrintStats(); return 0; } From 2f0b508c29f1c8eedc1f93c1a2dc31e8eb60de9a Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 9 Oct 2023 12:52:58 -0400 Subject: [PATCH 215/299] Get rid of `bootstrap.sh` For people working on Nix with `nix develop`, it's better to just use `autoreconfPhase` and `configurePhase`, which is standard Nixpkgs / nix shell make from Nixpkgs practice --- it is good to emphasize the degree to which Nix is *just* a regular C++ project which can be worked on in the regular way. (For people running `nix-shell`, the story is similar, except `configurePhase` would use non-writable store paths, which matters for hte times we use output paths before `make install`, so I kept the existing `./configure ...` instruction.) For people building Nix without Nix (e.g. packaging it for another distro) they also don't need `bootstrap.sh`, and can just run `autoreconf -vfi` directly. (More likely, they have their own idioms to do this just as we have `autoreconfPhase`.) --- bootstrap.sh | 4 ---- doc/manual/src/contributing/hacking.md | 6 +++--- doc/manual/src/installation/building-source.md | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) delete mode 100755 bootstrap.sh diff --git a/bootstrap.sh b/bootstrap.sh deleted file mode 100755 index e3e259351..000000000 --- a/bootstrap.sh +++ /dev/null @@ -1,4 +0,0 @@ -#! /bin/sh -e -rm -f aclocal.m4 -mkdir -p config -exec autoreconf -vfi diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md index db393942c..38c144fcc 100644 --- a/doc/manual/src/contributing/hacking.md +++ b/doc/manual/src/contributing/hacking.md @@ -42,8 +42,8 @@ $ nix develop .#native-clang11StdenvPackages To build Nix itself in this shell: ```console -[nix-shell]$ ./bootstrap.sh -[nix-shell]$ ./configure $configureFlags --prefix=$(pwd)/outputs/out +[nix-shell]$ autoreconfPhase +[nix-shell]$ configurePhase [nix-shell]$ make -j $NIX_BUILD_CORES ``` @@ -86,7 +86,7 @@ $ nix-shell --attr devShells.x86_64-linux.native-clang11StdenvPackages To build Nix itself in this shell: ```console -[nix-shell]$ ./bootstrap.sh +[nix-shell]$ autoreconfPhase [nix-shell]$ ./configure $configureFlags --prefix=$(pwd)/outputs/out [nix-shell]$ make -j $NIX_BUILD_CORES ``` diff --git a/doc/manual/src/installation/building-source.md b/doc/manual/src/installation/building-source.md index ed1efffd8..7dad9805a 100644 --- a/doc/manual/src/installation/building-source.md +++ b/doc/manual/src/installation/building-source.md @@ -3,7 +3,7 @@ After cloning Nix's Git repository, issue the following commands: ```console -$ ./bootstrap.sh +$ autoreconf -vfi $ ./configure options... $ make $ make install From be81764320fc28131d23b85575076218eb7424c0 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 25 Mar 2022 04:39:57 +0000 Subject: [PATCH 216/299] Factor out bits of the worker protocol to use elsewhere This introduces some shared infrastructure for our notion of protocols. We can then define multiple protocols in terms of that notion. We an also express how particular protocols depend on each other. For example, we can define a common protocol and a worker protocol, where the second depends on the first in terms of the data types it can read and write. The "serve" protocol can just use the common one for now, but will eventually need its own machinary just like the worker protocol for version-aware serialisers --- doc/internal-api/doxygen.cfg.in | 30 ++++ src/libstore/build/derivation-goal.cc | 10 +- src/libstore/common-protocol-impl.hh | 41 +++++ src/libstore/common-protocol.cc | 98 +++++++++++ src/libstore/common-protocol.hh | 106 ++++++++++++ src/libstore/derivations.cc | 12 +- src/libstore/export-import.cc | 12 +- src/libstore/legacy-ssh-store.cc | 44 ++--- .../length-prefixed-protocol-helper.hh | 162 ++++++++++++++++++ src/libstore/worker-protocol-impl.hh | 111 ++++-------- src/libstore/worker-protocol.cc | 81 +-------- src/libstore/worker-protocol.hh | 60 ++----- src/nix-store/nix-store.cc | 26 +-- 13 files changed, 542 insertions(+), 251 deletions(-) create mode 100644 src/libstore/common-protocol-impl.hh create mode 100644 src/libstore/common-protocol.cc create mode 100644 src/libstore/common-protocol.hh create mode 100644 src/libstore/length-prefixed-protocol-helper.hh diff --git a/doc/internal-api/doxygen.cfg.in b/doc/internal-api/doxygen.cfg.in index 8f526536d..599be2470 100644 --- a/doc/internal-api/doxygen.cfg.in +++ b/doc/internal-api/doxygen.cfg.in @@ -54,6 +54,23 @@ INPUT = \ src/nix-env \ src/nix-store +# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names +# in the source code. If set to NO, only conditional compilation will be +# performed. Macro expansion can be done in a controlled way by setting +# EXPAND_ONLY_PREDEF to YES. +# The default value is: NO. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +MACRO_EXPANSION = YES + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then +# the macro expansion is limited to the macros specified with the PREDEFINED and +# EXPAND_AS_DEFINED tags. +# The default value is: NO. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +EXPAND_ONLY_PREDEF = YES + # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by the # preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of @@ -61,3 +78,16 @@ INPUT = \ # This tag requires that the tag SEARCH_INCLUDES is set to YES. INCLUDE_PATH = @RAPIDCHECK_HEADERS@ + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this +# tag can be used to specify a list of macro names that should be expanded. The +# macro definition that is found in the sources will be used. Use the PREDEFINED +# tag if you want to use a different macro definition that overrules the +# definition found in the source code. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +EXPAND_AS_DEFINED = \ + DECLARE_COMMON_SERIALISER \ + DECLARE_WORKER_SERIALISER \ + DECLARE_SERVE_SERIALISER \ + LENGTH_PREFIXED_PROTO_HELPER diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 83c0a3135..dc4d91079 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -8,8 +8,8 @@ #include "util.hh" #include "archive.hh" #include "compression.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" +#include "common-protocol.hh" +#include "common-protocol-impl.hh" #include "topo-sort.hh" #include "callback.hh" #include "local-store.hh" // TODO remove, along with remaining downcasts @@ -1185,11 +1185,11 @@ HookReply DerivationGoal::tryBuildHook() throw; } - WorkerProto::WriteConn conn { hook->sink }; + CommonProto::WriteConn conn { hook->sink }; /* Tell the hook all the inputs that have to be copied to the remote system. */ - WorkerProto::write(worker.store, conn, inputPaths); + CommonProto::write(worker.store, conn, inputPaths); /* Tell the hooks the missing outputs that have to be copied back from the remote system. */ @@ -1200,7 +1200,7 @@ HookReply DerivationGoal::tryBuildHook() if (buildMode != bmCheck && status.known && status.known->isValid()) continue; missingOutputs.insert(outputName); } - WorkerProto::write(worker.store, conn, missingOutputs); + CommonProto::write(worker.store, conn, missingOutputs); } hook->sink = FdSink(); diff --git a/src/libstore/common-protocol-impl.hh b/src/libstore/common-protocol-impl.hh new file mode 100644 index 000000000..079c182b8 --- /dev/null +++ b/src/libstore/common-protocol-impl.hh @@ -0,0 +1,41 @@ +#pragma once +/** + * @file + * + * Template implementations (as opposed to mere declarations). + * + * This file is an exmample of the "impl.hh" pattern. See the + * contributing guide. + */ + +#include "common-protocol.hh" +#include "length-prefixed-protocol-helper.hh" + +namespace nix { + +/* protocol-agnostic templates */ + +#define COMMON_USE_LENGTH_PREFIX_SERIALISER(TEMPLATE, T) \ + TEMPLATE T CommonProto::Serialise< T >::read(const Store & store, CommonProto::ReadConn conn) \ + { \ + return LengthPrefixedProtoHelper::read(store, conn); \ + } \ + TEMPLATE void CommonProto::Serialise< T >::write(const Store & store, CommonProto::WriteConn conn, const T & t) \ + { \ + LengthPrefixedProtoHelper::write(store, conn, t); \ + } + +COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::vector) +COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +COMMON_USE_LENGTH_PREFIX_SERIALISER(template, std::tuple) + +#define COMMA_ , +COMMON_USE_LENGTH_PREFIX_SERIALISER( + template, + std::map) +#undef COMMA_ + + +/* protocol-specific templates */ + +} diff --git a/src/libstore/common-protocol.cc b/src/libstore/common-protocol.cc new file mode 100644 index 000000000..f906814bc --- /dev/null +++ b/src/libstore/common-protocol.cc @@ -0,0 +1,98 @@ +#include "serialise.hh" +#include "util.hh" +#include "path-with-outputs.hh" +#include "store-api.hh" +#include "build-result.hh" +#include "common-protocol.hh" +#include "common-protocol-impl.hh" +#include "archive.hh" +#include "derivations.hh" + +#include + +namespace nix { + +/* protocol-agnostic definitions */ + +std::string CommonProto::Serialise::read(const Store & store, CommonProto::ReadConn conn) +{ + return readString(conn.from); +} + +void CommonProto::Serialise::write(const Store & store, CommonProto::WriteConn conn, const std::string & str) +{ + conn.to << str; +} + + +StorePath CommonProto::Serialise::read(const Store & store, CommonProto::ReadConn conn) +{ + return store.parseStorePath(readString(conn.from)); +} + +void CommonProto::Serialise::write(const Store & store, CommonProto::WriteConn conn, const StorePath & storePath) +{ + conn.to << store.printStorePath(storePath); +} + + +ContentAddress CommonProto::Serialise::read(const Store & store, CommonProto::ReadConn conn) +{ + return ContentAddress::parse(readString(conn.from)); +} + +void CommonProto::Serialise::write(const Store & store, CommonProto::WriteConn conn, const ContentAddress & ca) +{ + conn.to << renderContentAddress(ca); +} + + +Realisation CommonProto::Serialise::read(const Store & store, CommonProto::ReadConn conn) +{ + std::string rawInput = readString(conn.from); + return Realisation::fromJSON( + nlohmann::json::parse(rawInput), + "remote-protocol" + ); +} + +void CommonProto::Serialise::write(const Store & store, CommonProto::WriteConn conn, const Realisation & realisation) +{ + conn.to << realisation.toJSON().dump(); +} + + +DrvOutput CommonProto::Serialise::read(const Store & store, CommonProto::ReadConn conn) +{ + return DrvOutput::parse(readString(conn.from)); +} + +void CommonProto::Serialise::write(const Store & store, CommonProto::WriteConn conn, const DrvOutput & drvOutput) +{ + conn.to << drvOutput.to_string(); +} + + +std::optional CommonProto::Serialise>::read(const Store & store, CommonProto::ReadConn conn) +{ + auto s = readString(conn.from); + return s == "" ? std::optional {} : store.parseStorePath(s); +} + +void CommonProto::Serialise>::write(const Store & store, CommonProto::WriteConn conn, const std::optional & storePathOpt) +{ + conn.to << (storePathOpt ? store.printStorePath(*storePathOpt) : ""); +} + + +std::optional CommonProto::Serialise>::read(const Store & store, CommonProto::ReadConn conn) +{ + return ContentAddress::parseOpt(readString(conn.from)); +} + +void CommonProto::Serialise>::write(const Store & store, CommonProto::WriteConn conn, const std::optional & caOpt) +{ + conn.to << (caOpt ? renderContentAddress(*caOpt) : ""); +} + +} diff --git a/src/libstore/common-protocol.hh b/src/libstore/common-protocol.hh new file mode 100644 index 000000000..f3f28972a --- /dev/null +++ b/src/libstore/common-protocol.hh @@ -0,0 +1,106 @@ +#pragma once +///@file + +#include "serialise.hh" + +namespace nix { + +class Store; +struct Source; + +// items being serialized +class StorePath; +struct ContentAddress; +struct DrvOutput; +struct Realisation; + + +/** + * Shared serializers between the worker protocol, serve protocol, and a + * few others. + * + * This `struct` is basically just a `namespace`; We use a type rather + * than a namespace just so we can use it as a template argument. + */ +struct CommonProto +{ + /** + * A unidirectional read connection, to be used by the read half of the + * canonical serializers below. + */ + struct ReadConn { + Source & from; + }; + + /** + * A unidirectional write connection, to be used by the write half of the + * canonical serializers below. + */ + struct WriteConn { + Sink & to; + }; + + template + struct Serialise; + + /** + * Wrapper function around `CommonProto::Serialise::write` that allows us to + * infer the type instead of having to write it down explicitly. + */ + template + static void write(const Store & store, WriteConn conn, const T & t) + { + CommonProto::Serialise::write(store, conn, t); + } +}; + +#define DECLARE_COMMON_SERIALISER(T) \ + struct CommonProto::Serialise< T > \ + { \ + static T read(const Store & store, CommonProto::ReadConn conn); \ + static void write(const Store & store, CommonProto::WriteConn conn, const T & str); \ + } + +template<> +DECLARE_COMMON_SERIALISER(std::string); +template<> +DECLARE_COMMON_SERIALISER(StorePath); +template<> +DECLARE_COMMON_SERIALISER(ContentAddress); +template<> +DECLARE_COMMON_SERIALISER(DrvOutput); +template<> +DECLARE_COMMON_SERIALISER(Realisation); + +template +DECLARE_COMMON_SERIALISER(std::vector); +template +DECLARE_COMMON_SERIALISER(std::set); +template +DECLARE_COMMON_SERIALISER(std::tuple); + +#define COMMA_ , +template +DECLARE_COMMON_SERIALISER(std::map); +#undef COMMA_ + +/** + * These use the empty string for the null case, relying on the fact + * that the underlying types never serialize to the empty string. + * + * We do this instead of a generic std::optional instance because + * ordinal tags (0 or 1, here) are a bit of a compatability hazard. For + * the same reason, we don't have a std::variant instances (ordinal + * tags 0...n). + * + * We could the generic instances and then these as specializations for + * compatability, but that's proven a bit finnicky, and also makes the + * worker protocol harder to implement in other languages where such + * specializations may not be allowed. + */ +template<> +DECLARE_COMMON_SERIALISER(std::optional); +template<> +DECLARE_COMMON_SERIALISER(std::optional); + +} diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index 67069c3c9..fc17e520c 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -4,8 +4,8 @@ #include "globals.hh" #include "util.hh" #include "split.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" +#include "common-protocol.hh" +#include "common-protocol-impl.hh" #include "fs-accessor.hh" #include #include @@ -895,8 +895,8 @@ Source & readDerivation(Source & in, const Store & store, BasicDerivation & drv, drv.outputs.emplace(std::move(name), std::move(output)); } - drv.inputSrcs = WorkerProto::Serialise::read(store, - WorkerProto::ReadConn { .from = in }); + drv.inputSrcs = CommonProto::Serialise::read(store, + CommonProto::ReadConn { .from = in }); in >> drv.platform >> drv.builder; drv.args = readStrings(in); @@ -944,8 +944,8 @@ void writeDerivation(Sink & out, const Store & store, const BasicDerivation & dr }, }, i.second.raw); } - WorkerProto::write(store, - WorkerProto::WriteConn { .to = out }, + CommonProto::write(store, + CommonProto::WriteConn { .to = out }, drv.inputSrcs); out << drv.platform << drv.builder << drv.args; out << drv.env.size(); diff --git a/src/libstore/export-import.cc b/src/libstore/export-import.cc index e866aeb42..87b2f8741 100644 --- a/src/libstore/export-import.cc +++ b/src/libstore/export-import.cc @@ -1,8 +1,8 @@ #include "serialise.hh" #include "store-api.hh" #include "archive.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" +#include "common-protocol.hh" +#include "common-protocol-impl.hh" #include @@ -46,8 +46,8 @@ void Store::exportPath(const StorePath & path, Sink & sink) teeSink << exportMagic << printStorePath(path); - WorkerProto::write(*this, - WorkerProto::WriteConn { .to = teeSink }, + CommonProto::write(*this, + CommonProto::WriteConn { .to = teeSink }, info->references); teeSink << (info->deriver ? printStorePath(*info->deriver) : "") @@ -76,8 +76,8 @@ StorePaths Store::importPaths(Source & source, CheckSigsFlag checkSigs) //Activity act(*logger, lvlInfo, "importing path '%s'", info.path); - auto references = WorkerProto::Serialise::read(*this, - WorkerProto::ReadConn { .from = source }); + auto references = CommonProto::Serialise::read(*this, + CommonProto::ReadConn { .from = source }); auto deriver = readString(source); auto narHash = hashString(htSHA256, saved.s); diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index 78b05031a..7bf4476d0 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -6,8 +6,8 @@ #include "build-result.hh" #include "store-api.hh" #include "path-with-outputs.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" +#include "common-protocol.hh" +#include "common-protocol-impl.hh" #include "ssh.hh" #include "derivations.hh" #include "callback.hh" @@ -50,37 +50,37 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor bool good = true; /** - * Coercion to `WorkerProto::ReadConn`. This makes it easy to use the - * factored out worker protocol searlizers with a + * Coercion to `CommonProto::ReadConn`. This makes it easy to use the + * factored out common protocol serialisers with a * `LegacySSHStore::Connection`. * - * The worker protocol connection types are unidirectional, unlike + * The common protocol connection types are unidirectional, unlike * this type. * - * @todo Use server protocol serializers, not worker protocol + * @todo Use server protocol serializers, not common protocol * serializers, once we have made that distiction. */ - operator WorkerProto::ReadConn () + operator CommonProto::ReadConn () { - return WorkerProto::ReadConn { + return CommonProto::ReadConn { .from = from, }; } /* - * Coercion to `WorkerProto::WriteConn`. This makes it easy to use the - * factored out worker protocol searlizers with a + * Coercion to `CommonProto::WriteConn`. This makes it easy to use the + * factored out common protocol searlizers with a * `LegacySSHStore::Connection`. * - * The worker protocol connection types are unidirectional, unlike + * The common protocol connection types are unidirectional, unlike * this type. * - * @todo Use server protocol serializers, not worker protocol + * @todo Use server protocol serializers, not common protocol * serializers, once we have made that distiction. */ - operator WorkerProto::WriteConn () + operator CommonProto::WriteConn () { - return WorkerProto::WriteConn { + return CommonProto::WriteConn { .to = to, }; } @@ -183,7 +183,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor auto deriver = readString(conn->from); if (deriver != "") info->deriver = parseStorePath(deriver); - info->references = WorkerProto::Serialise::read(*this, *conn); + info->references = CommonProto::Serialise::read(*this, *conn); readLongLong(conn->from); // download size info->narSize = readLongLong(conn->from); @@ -217,7 +217,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor << printStorePath(info.path) << (info.deriver ? printStorePath(*info.deriver) : "") << info.narHash.to_string(Base16, false); - WorkerProto::write(*this, *conn, info.references); + CommonProto::write(*this, *conn, info.references); conn->to << info.registrationTime << info.narSize @@ -246,7 +246,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor conn->to << exportMagic << printStorePath(info.path); - WorkerProto::write(*this, *conn, info.references); + CommonProto::write(*this, *conn, info.references); conn->to << (info.deriver ? printStorePath(*info.deriver) : "") << 0 @@ -331,7 +331,7 @@ public: if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 3) conn->from >> status.timesBuilt >> status.isNonDeterministic >> status.startTime >> status.stopTime; if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 6) { - auto builtOutputs = WorkerProto::Serialise::read(*this, *conn); + auto builtOutputs = CommonProto::Serialise::read(*this, *conn); for (auto && [output, realisation] : builtOutputs) status.builtOutputs.insert_or_assign( std::move(output.outputName), @@ -409,10 +409,10 @@ public: conn->to << ServeProto::Command::QueryClosure << includeOutputs; - WorkerProto::write(*this, *conn, paths); + CommonProto::write(*this, *conn, paths); conn->to.flush(); - for (auto & i : WorkerProto::Serialise::read(*this, *conn)) + for (auto & i : CommonProto::Serialise::read(*this, *conn)) out.insert(i); } @@ -425,10 +425,10 @@ public: << ServeProto::Command::QueryValidPaths << false // lock << maybeSubstitute; - WorkerProto::write(*this, *conn, paths); + CommonProto::write(*this, *conn, paths); conn->to.flush(); - return WorkerProto::Serialise::read(*this, *conn); + return CommonProto::Serialise::read(*this, *conn); } void connect() override diff --git a/src/libstore/length-prefixed-protocol-helper.hh b/src/libstore/length-prefixed-protocol-helper.hh new file mode 100644 index 000000000..4061b0cd6 --- /dev/null +++ b/src/libstore/length-prefixed-protocol-helper.hh @@ -0,0 +1,162 @@ +#pragma once +/** + * @file Reusable serialisers for serialization container types in a + * length-prefixed manner. + * + * Used by both the Worker and Serve protocols. + */ + +#include "types.hh" + +namespace nix { + +class Store; + +/** + * Reusable serialisers for serialization container types in a + * length-prefixed manner. + * + * @param T The type of the collection being serialised + * + * @param Inner This the most important parameter; this is the "inner" + * protocol. The user of this will substitute `MyProtocol` or similar + * when making a `MyProtocol::Serialiser>`. Note that the + * inside is allowed to call to call `Inner::Serialiser` on different + * types. This is especially important for `std::map` which doesn't have + * a single `T` but one `K` and one `V`. + */ +template +struct LengthPrefixedProtoHelper; + +/*! + * \typedef LengthPrefixedProtoHelper::S + * + * Read this as simply `using S = Inner::Serialise;`. + * + * It would be nice to use that directly, but C++ doesn't seem to allow + * it. The `typename` keyword needed to refer to `Inner` seems to greedy + * (low precedence), and then C++ complains that `Serialise` is not a + * type parameter but a real type. + * + * Making this `S` alias seems to be the only way to avoid these issues. + */ + +#define LENGTH_PREFIXED_PROTO_HELPER(Inner, T) \ + struct LengthPrefixedProtoHelper< Inner, T > \ + { \ + static T read(const Store & store, typename Inner::ReadConn conn); \ + static void write(const Store & store, typename Inner::WriteConn conn, const T & str); \ + private: \ + template using S = typename Inner::template Serialise; \ + } + +template +LENGTH_PREFIXED_PROTO_HELPER(Inner, std::vector); + +template +LENGTH_PREFIXED_PROTO_HELPER(Inner, std::set); + +template +LENGTH_PREFIXED_PROTO_HELPER(Inner, std::tuple); + +template +#define _X std::map +LENGTH_PREFIXED_PROTO_HELPER(Inner, _X); +#undef _X + +template +std::vector +LengthPrefixedProtoHelper>::read( + const Store & store, typename Inner::ReadConn conn) +{ + std::vector resSet; + auto size = readNum(conn.from); + while (size--) { + resSet.push_back(S::read(store, conn)); + } + return resSet; +} + +template +void +LengthPrefixedProtoHelper>::write( + const Store & store, typename Inner::WriteConn conn, const std::vector & resSet) +{ + conn.to << resSet.size(); + for (auto & key : resSet) { + S::write(store, conn, key); + } +} + +template +std::set +LengthPrefixedProtoHelper>::read( + const Store & store, typename Inner::ReadConn conn) +{ + std::set resSet; + auto size = readNum(conn.from); + while (size--) { + resSet.insert(S::read(store, conn)); + } + return resSet; +} + +template +void +LengthPrefixedProtoHelper>::write( + const Store & store, typename Inner::WriteConn conn, const std::set & resSet) +{ + conn.to << resSet.size(); + for (auto & key : resSet) { + S::write(store, conn, key); + } +} + +template +std::map +LengthPrefixedProtoHelper>::read( + const Store & store, typename Inner::ReadConn conn) +{ + std::map resMap; + auto size = readNum(conn.from); + while (size--) { + auto k = S::read(store, conn); + auto v = S::read(store, conn); + resMap.insert_or_assign(std::move(k), std::move(v)); + } + return resMap; +} + +template +void +LengthPrefixedProtoHelper>::write( + const Store & store, typename Inner::WriteConn conn, const std::map & resMap) +{ + conn.to << resMap.size(); + for (auto & i : resMap) { + S::write(store, conn, i.first); + S::write(store, conn, i.second); + } +} + +template +std::tuple +LengthPrefixedProtoHelper>::read( + const Store & store, typename Inner::ReadConn conn) +{ + return std::tuple { + S::read(store, conn)..., + }; +} + +template +void +LengthPrefixedProtoHelper>::write( + const Store & store, typename Inner::WriteConn conn, const std::tuple & res) +{ + std::apply([&](const Us &... args) { + (S::write(store, conn, args), ...); + }, res); +} + +} diff --git a/src/libstore/worker-protocol-impl.hh b/src/libstore/worker-protocol-impl.hh index 4f797f95a..c043588d6 100644 --- a/src/libstore/worker-protocol-impl.hh +++ b/src/libstore/worker-protocol-impl.hh @@ -9,86 +9,51 @@ */ #include "worker-protocol.hh" +#include "length-prefixed-protocol-helper.hh" namespace nix { +/* protocol-agnostic templates */ + +#define WORKER_USE_LENGTH_PREFIX_SERIALISER(TEMPLATE, T) \ + TEMPLATE T WorkerProto::Serialise< T >::read(const Store & store, WorkerProto::ReadConn conn) \ + { \ + return LengthPrefixedProtoHelper::read(store, conn); \ + } \ + TEMPLATE void WorkerProto::Serialise< T >::write(const Store & store, WorkerProto::WriteConn conn, const T & t) \ + { \ + LengthPrefixedProtoHelper::write(store, conn, t); \ + } + +WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::vector) +WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +WORKER_USE_LENGTH_PREFIX_SERIALISER(template, std::tuple) + +#define COMMA_ , +WORKER_USE_LENGTH_PREFIX_SERIALISER( + template, + std::map) +#undef COMMA_ + +/** + * Use `CommonProto` where possible. + */ template -std::vector WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) +struct WorkerProto::Serialise { - std::vector resSet; - auto size = readNum(conn.from); - while (size--) { - resSet.push_back(WorkerProto::Serialise::read(store, conn)); + static T read(const Store & store, WorkerProto::ReadConn conn) + { + return CommonProto::Serialise::read(store, + CommonProto::ReadConn { .from = conn.from }); } - return resSet; -} - -template -void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::vector & resSet) -{ - conn.to << resSet.size(); - for (auto & key : resSet) { - WorkerProto::Serialise::write(store, conn, key); + static void write(const Store & store, WorkerProto::WriteConn conn, const T & t) + { + CommonProto::Serialise::write(store, + CommonProto::WriteConn { .to = conn.to }, + t); } -} +}; -template -std::set WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) -{ - std::set resSet; - auto size = readNum(conn.from); - while (size--) { - resSet.insert(WorkerProto::Serialise::read(store, conn)); - } - return resSet; -} - -template -void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::set & resSet) -{ - conn.to << resSet.size(); - for (auto & key : resSet) { - WorkerProto::Serialise::write(store, conn, key); - } -} - -template -std::map WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) -{ - std::map resMap; - auto size = readNum(conn.from); - while (size--) { - auto k = WorkerProto::Serialise::read(store, conn); - auto v = WorkerProto::Serialise::read(store, conn); - resMap.insert_or_assign(std::move(k), std::move(v)); - } - return resMap; -} - -template -void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::map & resMap) -{ - conn.to << resMap.size(); - for (auto & i : resMap) { - WorkerProto::Serialise::write(store, conn, i.first); - WorkerProto::Serialise::write(store, conn, i.second); - } -} - -template -std::tuple WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) -{ - return std::tuple { - WorkerProto::Serialise::read(store, conn)..., - }; -} - -template -void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::tuple & res) -{ - std::apply([&](const Us &... args) { - (WorkerProto::Serialise::write(store, conn, args), ...); - }, res); -} +/* protocol-specific templates */ } diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index a23130743..415e66f16 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -12,27 +12,7 @@ namespace nix { -std::string WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) -{ - return readString(conn.from); -} - -void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const std::string & str) -{ - conn.to << str; -} - - -StorePath WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) -{ - return store.parseStorePath(readString(conn.from)); -} - -void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const StorePath & storePath) -{ - conn.to << store.printStorePath(storePath); -} - +/* protocol-specific definitions */ std::optional WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) { @@ -68,17 +48,6 @@ void WorkerProto::Serialise>::write(const Store & sto } -ContentAddress WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) -{ - return ContentAddress::parse(readString(conn.from)); -} - -void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const ContentAddress & ca) -{ - conn.to << renderContentAddress(ca); -} - - DerivedPath WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { auto s = readString(conn.from); @@ -91,32 +60,6 @@ void WorkerProto::Serialise::write(const Store & store, WorkerProto } -Realisation WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) -{ - std::string rawInput = readString(conn.from); - return Realisation::fromJSON( - nlohmann::json::parse(rawInput), - "remote-protocol" - ); -} - -void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const Realisation & realisation) -{ - conn.to << realisation.toJSON().dump(); -} - - -DrvOutput WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) -{ - return DrvOutput::parse(readString(conn.from)); -} - -void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const DrvOutput & drvOutput) -{ - conn.to << drvOutput.to_string(); -} - - KeyedBuildResult WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { auto path = WorkerProto::Serialise::read(store, conn); @@ -168,26 +111,4 @@ void WorkerProto::Serialise::write(const Store & store, WorkerProto } -std::optional WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) -{ - auto s = readString(conn.from); - return s == "" ? std::optional {} : store.parseStorePath(s); -} - -void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::optional & storePathOpt) -{ - conn.to << (storePathOpt ? store.printStorePath(*storePathOpt) : ""); -} - - -std::optional WorkerProto::Serialise>::read(const Store & store, WorkerProto::ReadConn conn) -{ - return ContentAddress::parseOpt(readString(conn.from)); -} - -void WorkerProto::Serialise>::write(const Store & store, WorkerProto::WriteConn conn, const std::optional & caOpt) -{ - conn.to << (caOpt ? renderContentAddress(*caOpt) : ""); -} - } diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index b7f42f24d..c84060103 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -1,7 +1,7 @@ #pragma once ///@file -#include "serialise.hh" +#include "common-protocol.hh" namespace nix { @@ -28,11 +28,7 @@ class Store; struct Source; // items being serialised -class StorePath; -struct ContentAddress; struct DerivedPath; -struct DrvOutput; -struct Realisation; struct BuildResult; struct KeyedBuildResult; enum TrustedFlag : bool; @@ -193,60 +189,32 @@ inline std::ostream & operator << (std::ostream & s, WorkerProto::Op op) * be legal specialization syntax. See below for what that looks like in * practice. */ -#define MAKE_WORKER_PROTO(T) \ - struct WorkerProto::Serialise< T > { \ +#define DECLARE_WORKER_SERIALISER(T) \ + struct WorkerProto::Serialise< T > \ + { \ static T read(const Store & store, WorkerProto::ReadConn conn); \ static void write(const Store & store, WorkerProto::WriteConn conn, const T & t); \ }; template<> -MAKE_WORKER_PROTO(std::string); +DECLARE_WORKER_SERIALISER(DerivedPath); template<> -MAKE_WORKER_PROTO(StorePath); +DECLARE_WORKER_SERIALISER(BuildResult); template<> -MAKE_WORKER_PROTO(ContentAddress); +DECLARE_WORKER_SERIALISER(KeyedBuildResult); template<> -MAKE_WORKER_PROTO(DerivedPath); -template<> -MAKE_WORKER_PROTO(DrvOutput); -template<> -MAKE_WORKER_PROTO(Realisation); -template<> -MAKE_WORKER_PROTO(BuildResult); -template<> -MAKE_WORKER_PROTO(KeyedBuildResult); -template<> -MAKE_WORKER_PROTO(std::optional); +DECLARE_WORKER_SERIALISER(std::optional); template -MAKE_WORKER_PROTO(std::vector); +DECLARE_WORKER_SERIALISER(std::vector); template -MAKE_WORKER_PROTO(std::set); +DECLARE_WORKER_SERIALISER(std::set); template -MAKE_WORKER_PROTO(std::tuple); +DECLARE_WORKER_SERIALISER(std::tuple); +#define COMMA_ , template -#define X_ std::map -MAKE_WORKER_PROTO(X_); -#undef X_ - -/** - * These use the empty string for the null case, relying on the fact - * that the underlying types never serialise to the empty string. - * - * We do this instead of a generic std::optional instance because - * ordinal tags (0 or 1, here) are a bit of a compatability hazard. For - * the same reason, we don't have a std::variant instances (ordinal - * tags 0...n). - * - * We could the generic instances and then these as specializations for - * compatability, but that's proven a bit finnicky, and also makes the - * worker protocol harder to implement in other languages where such - * specializations may not be allowed. - */ -template<> -MAKE_WORKER_PROTO(std::optional); -template<> -MAKE_WORKER_PROTO(std::optional); +DECLARE_WORKER_SERIALISER(std::map); +#undef COMMA_ } diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 96c3f7d7e..6fc22214a 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -11,8 +11,8 @@ #include "serve-protocol.hh" #include "shared.hh" #include "util.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" +#include "common-protocol.hh" +#include "common-protocol-impl.hh" #include "graphml.hh" #include "legacy.hh" #include "path-with-outputs.hh" @@ -821,8 +821,8 @@ static void opServe(Strings opFlags, Strings opArgs) out.flush(); unsigned int clientVersion = readInt(in); - WorkerProto::ReadConn rconn { .from = in }; - WorkerProto::WriteConn wconn { .to = out }; + CommonProto::ReadConn rconn { .from = in }; + CommonProto::WriteConn wconn { .to = out }; auto getBuildSettings = [&]() { // FIXME: changing options here doesn't work if we're @@ -867,7 +867,7 @@ static void opServe(Strings opFlags, Strings opArgs) case ServeProto::Command::QueryValidPaths: { bool lock = readInt(in); bool substitute = readInt(in); - auto paths = WorkerProto::Serialise::read(*store, rconn); + auto paths = CommonProto::Serialise::read(*store, rconn); if (lock && writeAllowed) for (auto & path : paths) store->addTempRoot(path); @@ -876,19 +876,19 @@ static void opServe(Strings opFlags, Strings opArgs) store->substitutePaths(paths); } - WorkerProto::write(*store, wconn, store->queryValidPaths(paths)); + CommonProto::write(*store, wconn, store->queryValidPaths(paths)); break; } case ServeProto::Command::QueryPathInfos: { - auto paths = WorkerProto::Serialise::read(*store, rconn); + auto paths = CommonProto::Serialise::read(*store, rconn); // !!! Maybe we want a queryPathInfos? for (auto & i : paths) { try { auto info = store->queryPathInfo(i); out << store->printStorePath(info->path) << (info->deriver ? store->printStorePath(*info->deriver) : ""); - WorkerProto::write(*store, wconn, info->references); + CommonProto::write(*store, wconn, info->references); // !!! Maybe we want compression? out << info->narSize // downloadSize << info->narSize; @@ -916,7 +916,7 @@ static void opServe(Strings opFlags, Strings opArgs) case ServeProto::Command::ExportPaths: { readInt(in); // obsolete - store->exportPaths(WorkerProto::Serialise::read(*store, rconn), out); + store->exportPaths(CommonProto::Serialise::read(*store, rconn), out); break; } @@ -962,7 +962,7 @@ static void opServe(Strings opFlags, Strings opArgs) DrvOutputs builtOutputs; for (auto & [output, realisation] : status.builtOutputs) builtOutputs.insert_or_assign(realisation.id, realisation); - WorkerProto::write(*store, wconn, builtOutputs); + CommonProto::write(*store, wconn, builtOutputs); } break; @@ -971,9 +971,9 @@ static void opServe(Strings opFlags, Strings opArgs) case ServeProto::Command::QueryClosure: { bool includeOutputs = readInt(in); StorePathSet closure; - store->computeFSClosure(WorkerProto::Serialise::read(*store, rconn), + store->computeFSClosure(CommonProto::Serialise::read(*store, rconn), closure, false, includeOutputs); - WorkerProto::write(*store, wconn, closure); + CommonProto::write(*store, wconn, closure); break; } @@ -988,7 +988,7 @@ static void opServe(Strings opFlags, Strings opArgs) }; if (deriver != "") info.deriver = store->parseStorePath(deriver); - info.references = WorkerProto::Serialise::read(*store, rconn); + info.references = CommonProto::Serialise::read(*store, rconn); in >> info.registrationTime >> info.narSize >> info.ultimate; info.sigs = readStrings(in); info.ca = ContentAddress::parseOpt(readString(in)); From 4de54b21907e3fc41b9e6250396eb01d2b10c4db Mon Sep 17 00:00:00 2001 From: John Ericson Date: Wed, 4 Oct 2023 23:27:50 -0400 Subject: [PATCH 217/299] Unit test the "common protocol" too Copy the relevant tests to ensure the new interfaces added in the last commit are tested. Perhaps I should try to deduplicat these tests some more. However its not clear how to do that outside of a big ugly C++ macro. https://github.com/google/googletest/blob/main/docs/advanced.md has some stuff but it is cumbersome and I didn't figure it out yet. This is done in a separate commit in order to be sure that the first commit really didn't change any behavior; if we changed the implementation and the tests at once, it would be harder to tell whether or not some behavioral changes slipped in what is supposed to be a "pure refactor". Co-Authored-By: Valentin Gagarin --- src/libstore/tests/characterization.hh | 23 +++ src/libstore/tests/common-protocol.cc | 152 ++++++++++++++++++ src/libstore/tests/protocol.hh | 88 ++++++++++ src/libstore/tests/worker-protocol.cc | 90 ++--------- .../common-protocol/content-address.bin | Bin 0 -> 208 bytes .../libstore/common-protocol/drv-output.bin | Bin 0 -> 176 bytes .../optional-content-address.bin | Bin 0 -> 64 bytes .../common-protocol/optional-store-path.bin | Bin 0 -> 72 bytes .../libstore/common-protocol/realisation.bin | Bin 0 -> 520 bytes .../libstore/common-protocol/set.bin | Bin 0 -> 152 bytes .../libstore/common-protocol/store-path.bin | Bin 0 -> 120 bytes .../libstore/common-protocol/string.bin | Bin 0 -> 88 bytes .../libstore/common-protocol/vector.bin | Bin 0 -> 152 bytes 13 files changed, 280 insertions(+), 73 deletions(-) create mode 100644 src/libstore/tests/characterization.hh create mode 100644 src/libstore/tests/common-protocol.cc create mode 100644 src/libstore/tests/protocol.hh create mode 100644 unit-test-data/libstore/common-protocol/content-address.bin create mode 100644 unit-test-data/libstore/common-protocol/drv-output.bin create mode 100644 unit-test-data/libstore/common-protocol/optional-content-address.bin create mode 100644 unit-test-data/libstore/common-protocol/optional-store-path.bin create mode 100644 unit-test-data/libstore/common-protocol/realisation.bin create mode 100644 unit-test-data/libstore/common-protocol/set.bin create mode 100644 unit-test-data/libstore/common-protocol/store-path.bin create mode 100644 unit-test-data/libstore/common-protocol/string.bin create mode 100644 unit-test-data/libstore/common-protocol/vector.bin diff --git a/src/libstore/tests/characterization.hh b/src/libstore/tests/characterization.hh new file mode 100644 index 000000000..5f366cb42 --- /dev/null +++ b/src/libstore/tests/characterization.hh @@ -0,0 +1,23 @@ +#pragma once +///@file + +namespace nix { + +/** + * The path to the `unit-test-data` directory. See the contributing + * guide in the manual for further details. + */ +static Path getUnitTestData() { + return getEnv("_NIX_TEST_UNIT_DATA").value(); +} + +/** + * Whether we should update "golden masters" instead of running tests + * against them. See the contributing guide in the manual for further + * details. + */ +static bool testAccept() { + return getEnv("_NIX_TEST_ACCEPT") == "1"; +} + +} diff --git a/src/libstore/tests/common-protocol.cc b/src/libstore/tests/common-protocol.cc new file mode 100644 index 000000000..de57211f0 --- /dev/null +++ b/src/libstore/tests/common-protocol.cc @@ -0,0 +1,152 @@ +#include + +#include +#include + +#include "common-protocol.hh" +#include "common-protocol-impl.hh" +#include "build-result.hh" +#include "tests/protocol.hh" +#include "tests/characterization.hh" + +namespace nix { + +const char commonProtoDir[] = "common-protocol"; + +using CommonProtoTest = ProtoTest; + +CHARACTERIZATION_TEST( + CommonProtoTest, + string, + "string", + (std::tuple { + "", + "hi", + "white rabbit", + "大白兔", + "oh no \0\0\0 what was that!", + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + storePath, + "store-path", + (std::tuple { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + contentAddress, + "content-address", + (std::tuple { + ContentAddress { + .method = TextIngestionMethod {}, + .hash = hashString(HashType::htSHA256, "Derive(...)"), + }, + ContentAddress { + .method = FileIngestionMethod::Flat, + .hash = hashString(HashType::htSHA1, "blob blob..."), + }, + ContentAddress { + .method = FileIngestionMethod::Recursive, + .hash = hashString(HashType::htSHA256, "(...)"), + }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + drvOutput, + "drv-output", + (std::tuple { + { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "quux", + }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + realisation, + "realisation", + (std::tuple { + Realisation { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + .signatures = { "asdf", "qwer" }, + }, + Realisation { + .id = { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + .signatures = { "asdf", "qwer" }, + .dependentRealisations = { + { + DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "quux", + }, + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + }, + }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + vector, + "vector", + (std::tuple, std::vector, std::vector, std::vector>> { + { }, + { "" }, + { "", "foo", "bar" }, + { {}, { "" }, { "", "1", "2" } }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + set, + "set", + (std::tuple, std::set, std::set, std::set>> { + { }, + { "" }, + { "", "foo", "bar" }, + { {}, { "" }, { "", "1", "2" } }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + optionalStorePath, + "optional-store-path", + (std::tuple, std::optional> { + std::nullopt, + std::optional { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, + }, + })) + +CHARACTERIZATION_TEST( + CommonProtoTest, + optionalContentAddress, + "optional-content-address", + (std::tuple, std::optional> { + std::nullopt, + std::optional { + ContentAddress { + .method = FileIngestionMethod::Flat, + .hash = hashString(HashType::htSHA1, "blob blob..."), + }, + }, + })) + +} diff --git a/src/libstore/tests/protocol.hh b/src/libstore/tests/protocol.hh new file mode 100644 index 000000000..86a900757 --- /dev/null +++ b/src/libstore/tests/protocol.hh @@ -0,0 +1,88 @@ +#include +#include + +#include "tests/libstore.hh" +#include "tests/characterization.hh" + +namespace nix { + +template +class ProtoTest : public LibStoreTest +{ + /** + * Read this as simply `using S = Inner::Serialise;`. + * + * See `LengthPrefixedProtoHelper::S` for the same trick, and its + * rationale. + */ + template using S = typename Proto::template Serialise; + +public: + Path unitTestData = getUnitTestData() + "/libstore/" + protocolDir; + + Path goldenMaster(std::string_view testStem) { + return unitTestData + "/" + testStem + ".bin"; + } + + /** + * Golden test for `T` reading + */ + template + void readTest(PathView testStem, T value) + { + if (testAccept()) + { + GTEST_SKIP() << "Cannot read golden master because another test is also updating it"; + } + else + { + auto expected = readFile(goldenMaster(testStem)); + + T got = ({ + StringSource from { expected }; + S::read( + *store, + typename Proto::ReadConn { .from = from }); + }); + + ASSERT_EQ(got, value); + } + } + + /** + * Golden test for `T` write + */ + template + void writeTest(PathView testStem, const T & value) + { + auto file = goldenMaster(testStem); + + StringSink to; + Proto::write( + *store, + typename Proto::WriteConn { .to = to }, + value); + + if (testAccept()) + { + createDirs(dirOf(file)); + writeFile(file, to.s); + GTEST_SKIP() << "Updating golden master"; + } + else + { + auto expected = readFile(file); + ASSERT_EQ(to.s, expected); + } + } +}; + +#define CHARACTERIZATION_TEST(FIXTURE, NAME, STEM, VALUE) \ + TEST_F(FIXTURE, NAME ## _read) { \ + readTest(STEM, VALUE); \ + } \ + TEST_F(FIXTURE, NAME ## _write) { \ + writeTest(STEM, VALUE); \ + } + +} diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index fa7cbe121..59d7ff96d 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -7,85 +7,17 @@ #include "worker-protocol-impl.hh" #include "derived-path.hh" #include "build-result.hh" -#include "tests/libstore.hh" +#include "tests/protocol.hh" +#include "tests/characterization.hh" namespace nix { -class WorkerProtoTest : public LibStoreTest -{ -public: - Path unitTestData = getEnv("_NIX_TEST_UNIT_DATA").value() + "/libstore/worker-protocol"; +const char workerProtoDir[] = "worker-protocol"; - bool testAccept() { - return getEnv("_NIX_TEST_ACCEPT") == "1"; - } - - Path goldenMaster(std::string_view testStem) { - return unitTestData + "/" + testStem + ".bin"; - } - - /** - * Golden test for `T` reading - */ - template - void readTest(PathView testStem, T value) - { - if (testAccept()) - { - GTEST_SKIP() << "Cannot read golden master because another test is also updating it"; - } - else - { - auto expected = readFile(goldenMaster(testStem)); - - T got = ({ - StringSource from { expected }; - WorkerProto::Serialise::read( - *store, - WorkerProto::ReadConn { .from = from }); - }); - - ASSERT_EQ(got, value); - } - } - - /** - * Golden test for `T` write - */ - template - void writeTest(PathView testStem, const T & value) - { - auto file = goldenMaster(testStem); - - StringSink to; - WorkerProto::write( - *store, - WorkerProto::WriteConn { .to = to }, - value); - - if (testAccept()) - { - createDirs(dirOf(file)); - writeFile(file, to.s); - GTEST_SKIP() << "Updating golden master"; - } - else - { - auto expected = readFile(file); - ASSERT_EQ(to.s, expected); - } - } -}; - -#define CHARACTERIZATION_TEST(NAME, STEM, VALUE) \ - TEST_F(WorkerProtoTest, NAME ## _read) { \ - readTest(STEM, VALUE); \ - } \ - TEST_F(WorkerProtoTest, NAME ## _write) { \ - writeTest(STEM, VALUE); \ - } +using WorkerProtoTest = ProtoTest; CHARACTERIZATION_TEST( + WorkerProtoTest, string, "string", (std::tuple { @@ -97,6 +29,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, storePath, "store-path", (std::tuple { @@ -105,6 +38,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, contentAddress, "content-address", (std::tuple { @@ -123,6 +57,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, derivedPath, "derived-path", (std::tuple { @@ -138,6 +73,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, drvOutput, "drv-output", (std::tuple { @@ -152,6 +88,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, realisation, "realisation", (std::tuple { @@ -183,6 +120,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, buildResult, "build-result", ({ @@ -240,6 +178,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, keyedBuildResult, "keyed-build-result", ({ @@ -275,6 +214,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, optionalTrustedFlag, "optional-trusted-flag", (std::tuple, std::optional, std::optional> { @@ -284,6 +224,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, vector, "vector", (std::tuple, std::vector, std::vector, std::vector>> { @@ -294,6 +235,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, set, "set", (std::tuple, std::set, std::set, std::set>> { @@ -304,6 +246,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, optionalStorePath, "optional-store-path", (std::tuple, std::optional> { @@ -314,6 +257,7 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( + WorkerProtoTest, optionalContentAddress, "optional-content-address", (std::tuple, std::optional> { diff --git a/unit-test-data/libstore/common-protocol/content-address.bin b/unit-test-data/libstore/common-protocol/content-address.bin new file mode 100644 index 0000000000000000000000000000000000000000..8f14bcdb3e50cc72d87106ce159a6fd7b9f75713 GIT binary patch literal 208 zcmY+;K@P$o5QSmy;|7WYrYjRqQfMm$+LWPzDW_MfG4bucKks(>Y#V56lkFOiEzi24 zUMdC5VUCa86OJ&gL0BF^B{HBQEY%f|I<6v7#q+l_PBirI5N~-md%37WE|g33QTE2k zc~-WF&R}7Oxc@o)T?ojpD!+*5=xpO;%IoCKSV5FMh={>xePK|8<${>E)*huNHZ(pX literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/common-protocol/drv-output.bin b/unit-test-data/libstore/common-protocol/drv-output.bin new file mode 100644 index 0000000000000000000000000000000000000000..800a45fd8757a15e8810066aed48be56a600b7d3 GIT binary patch literal 176 zcmY++xeWp_5I|8{*$(Wn=b{B@!gGlfp_LHTb8N(qe)KM%U7Sq@}q)U|6h9n2j1!MT?w+C~^gO WO9=?G&ojrYjy`x4ZufnEe#tjMpDPdm literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/common-protocol/optional-content-address.bin b/unit-test-data/libstore/common-protocol/optional-content-address.bin new file mode 100644 index 0000000000000000000000000000000000000000..f8cfe65ba27fd78ec7787eed7ebec8dfdead2fba GIT binary patch literal 64 zcmZQzfB#ECTBU06lLKJOBUy literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/common-protocol/realisation.bin b/unit-test-data/libstore/common-protocol/realisation.bin new file mode 100644 index 0000000000000000000000000000000000000000..2176c6c4afd96b32fe372de6084ac7f4c7a11d49 GIT binary patch literal 520 zcmdUrO-{o=429t+opq7s&z_l_03#3x z?rW}!>Uc!$VBY>Sr2mUC`<2<)qY;)1KN Po44)luetw6d9AunL$;dR literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/common-protocol/set.bin b/unit-test-data/libstore/common-protocol/set.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce11ede7fe7aa061a30b211a18cd7a336f879de8 GIT binary patch literal 152 ucmZQzfB;4)4WpQ03@8obCnXkvMPU52{CpHXOdBEdVDg4g4KThDln(&+h63;a literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/common-protocol/store-path.bin b/unit-test-data/libstore/common-protocol/store-path.bin new file mode 100644 index 0000000000000000000000000000000000000000..3fc05f2981d3398ab30ec34125c903cdcefa8729 GIT binary patch literal 120 tcmdOAfB^lx%nJSDlKi4n{dB`}^NdR4LR_?NT7Eu*F?srQlM;)-IsqP{BM|@q literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/common-protocol/string.bin b/unit-test-data/libstore/common-protocol/string.bin new file mode 100644 index 0000000000000000000000000000000000000000..aa7b5a604745b735473ec34283177e69320776e4 GIT binary patch literal 88 zcmZQzfB+^aoskJ)@Id+H8JQ)i3Pp)YNtq=eAx^0H( Date: Tue, 10 Oct 2023 00:32:36 +0200 Subject: [PATCH 218/299] annotate admonitions showing syntax also fix typos --- .../src/language/constructs/lookup-path.md | 2 +- doc/manual/src/language/operators.md | 16 ++++++++++++++++ doc/manual/src/language/values.md | 6 +++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/language/constructs/lookup-path.md b/doc/manual/src/language/constructs/lookup-path.md index 8ec835309..e87d2922b 100644 --- a/doc/manual/src/language/constructs/lookup-path.md +++ b/doc/manual/src/language/constructs/lookup-path.md @@ -2,7 +2,7 @@ > **Syntax** > -> *lookup-path* = `<` *identifier* [ `/` *identifier* `]`... `>` +> *lookup-path* = `<` *identifier* [ `/` *identifier* ]... `>` A lookup path is an identifier with an optional path suffix that resolves to a [path value](@docroot@/language/values.md#type-path) if the identifier matches a search path entry. diff --git a/doc/manual/src/language/operators.md b/doc/manual/src/language/operators.md index f8382ae19..a22c71109 100644 --- a/doc/manual/src/language/operators.md +++ b/doc/manual/src/language/operators.md @@ -35,6 +35,8 @@ ## Attribute selection +> **Syntax** +> > *attrset* `.` *attrpath* \[ `or` *expr* \] Select the attribute denoted by attribute path *attrpath* from [attribute set] *attrset*. @@ -42,12 +44,16 @@ If the attribute doesn’t exist, return the *expr* after `or` if provided, othe An attribute path is a dot-separated list of [attribute names](./values.md#attribute-set). +> **Syntax** +> > *attrpath* = *name* [ `.` *name* ]... [Attribute selection]: #attribute-selection ## Has attribute +> **Syntax** +> > *attrset* `?` *attrpath* Test whether [attribute set] *attrset* contains the attribute denoted by *attrpath*. @@ -70,6 +76,8 @@ The `+` operator is overloaded to also work on strings and paths. ## String concatenation +> **Syntax** +> > *string* `+` *string* Concatenate two [string]s and merge their string contexts. @@ -78,6 +86,8 @@ Concatenate two [string]s and merge their string contexts. ## Path concatenation +> **Syntax** +> > *path* `+` *path* Concatenate two [path]s. @@ -87,6 +97,8 @@ The result is a path. ## Path and string concatenation +> **Syntax** +> > *path* + *string* Concatenate *[path]* with *[string]*. @@ -100,6 +112,8 @@ The result is a path. ## String and path concatenation +> **Syntax** +> > *string* + *path* Concatenate *[string]* with *[path]*. @@ -117,6 +131,8 @@ The result is a string. ## Update +> **Syntax** +> > *attrset1* // *attrset2* Update [attribute set] *attrset1* with names and values from *attrset2*. diff --git a/doc/manual/src/language/values.md b/doc/manual/src/language/values.md index 8856d33ae..0bb656746 100644 --- a/doc/manual/src/language/values.md +++ b/doc/manual/src/language/values.md @@ -162,13 +162,17 @@ An attribute set is a collection of name-value-pairs (called *attributes*) enclo An attribute name can be an identifier or a [string](#string). An identifier must start with a letter (`a-z`, `A-Z`) or underscore (`_`), and can otherwise contain letters (`a-z`, `A-Z`), numbers (`0-9`), underscores (`_`), apostrophes (`'`), or dashes (`-`). +> **Syntax** +> > *name* = *identifier* | *string* \ > *identifier* ~ `[a-zA-Z_][a-zA-Z0-9_'-]*` Names and values are separated by an equal sign (`=`). Each value is an arbitrary expression terminated by a semicolon (`;`). -> *attrset* = `{` [ *name* `=` *expr* `;` `]`... `}` +> **Syntax** +> +> *attrset* = `{` [ *name* `=` *expr* `;` ]... `}` Attributes can appear in any order. An attribute name may only occur once. From 7642894a4e1c9916f92a234c8cbaef604a7b7d00 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Sat, 7 Oct 2023 03:49:49 +0200 Subject: [PATCH 219/299] reword documentation on lookup path resolution --- src/libexpr/primops.cc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index bfcac7510..3bed7c5aa 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1698,13 +1698,14 @@ static void prim_findFile(EvalState & state, const PosIdx pos, Value * * args, V static RegisterPrimOp primop_findFile(PrimOp { .name = "__findFile", - .args = {"search path", "lookup path"}, + .args = {"search-path", "lookup-path"}, .doc = R"( - Look up the given path with the given search path. + Find *lookup-path* in *search-path*. - A search path is represented list of [attribute sets](./values.md#attribute-set) with two attributes, `prefix`, and `path`. - `prefix` is a relative path. - `path` denotes a file system location; the exact syntax depends on the command line interface. + A search path is represented list of [attribute sets](./values.md#attribute-set) with two attributes: + - `prefix` is a relative path. + - `path` denotes a file system location + The exact syntax depends on the command line interface. Examples of search path attribute sets: @@ -1722,13 +1723,12 @@ static RegisterPrimOp primop_findFile(PrimOp { } ``` - The lookup algorithm checks each entry until a match is found, returning a [path value](@docroot@/language/values.html#type-path) of the match. + The lookup algorithm checks each entry until a match is found, returning a [path value](@docroot@/language/values.html#type-path) of the match: - This is the process for each entry: - If the lookup path matches `prefix`, then the remainder of the lookup path (the "suffix") is searched for within the directory denoted by `patch`. - Note that the `path` may need to be downloaded at this point to look inside. - If the suffix is found inside that directory, then the entry is a match; - the combined absolute path of the directory (now downloaded if need be) and the suffix is returned. + - If *lookup-path* matches `prefix`, then the remainder of *lookup-path* (the "suffix") is searched for within the directory denoted by `path`. + Note that the `path` may need to be downloaded at this point to look inside. + - If the suffix is found inside that directory, then the entry is a match. + The combined absolute path of the directory (now downloaded if need be) and the suffix is returned. [Lookup path](@docroot@/language/constructs/lookup-path.md) expressions can be [desugared](https://en.wikipedia.org/wiki/Syntactic_sugar) using this and [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath): From f7b8f8aff63b1a4c7289c423d7877c95c0c6ae1f Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 26 May 2023 14:11:08 -0400 Subject: [PATCH 220/299] Introduce separate Serve protocol serialisers To start, it is just a clone of the common protocol. But now that we have the separate protocol implementations, we can add versioning information without the versions of one protocol leaking into another. Using the infrastructure from the previous commit, we don't have to duplicate code for shared behavior. Motivation: No more perverse incentives. [0] did some awkward things because the serialisers did not store the version. I don't want anyone making changes to be pushed towards keeping the serialization logic with the core data types just because it's easier or the alternative is tedious. The actual versioning of the Worker and Serve protocol serialisers (Common remains unversioned as the underlying mini-protocols are not versioned) will happen in subsequent commits / PRs. [0]: fe1f34fa60ad79e339c38e58af071a44774663f7 --- src/libstore/legacy-ssh-store.cc | 45 +++--- src/libstore/serve-protocol-impl.hh | 59 +++++++ src/libstore/serve-protocol.cc | 15 ++ src/libstore/serve-protocol.hh | 87 ++++++++++ src/libstore/tests/serve-protocol.cc | 152 ++++++++++++++++++ src/nix-store/nix-store.cc | 25 ++- .../serve-protocol/content-address.bin | Bin 0 -> 208 bytes .../libstore/serve-protocol/drv-output.bin | Bin 0 -> 176 bytes .../optional-content-address.bin | Bin 0 -> 64 bytes .../serve-protocol/optional-store-path.bin | Bin 0 -> 72 bytes .../libstore/serve-protocol/realisation.bin | Bin 0 -> 520 bytes .../libstore/serve-protocol/set.bin | Bin 0 -> 152 bytes .../libstore/serve-protocol/store-path.bin | Bin 0 -> 120 bytes .../libstore/serve-protocol/string.bin | Bin 0 -> 88 bytes .../libstore/serve-protocol/vector.bin | Bin 0 -> 152 bytes 15 files changed, 344 insertions(+), 39 deletions(-) create mode 100644 src/libstore/serve-protocol-impl.hh create mode 100644 src/libstore/serve-protocol.cc create mode 100644 src/libstore/tests/serve-protocol.cc create mode 100644 unit-test-data/libstore/serve-protocol/content-address.bin create mode 100644 unit-test-data/libstore/serve-protocol/drv-output.bin create mode 100644 unit-test-data/libstore/serve-protocol/optional-content-address.bin create mode 100644 unit-test-data/libstore/serve-protocol/optional-store-path.bin create mode 100644 unit-test-data/libstore/serve-protocol/realisation.bin create mode 100644 unit-test-data/libstore/serve-protocol/set.bin create mode 100644 unit-test-data/libstore/serve-protocol/store-path.bin create mode 100644 unit-test-data/libstore/serve-protocol/string.bin create mode 100644 unit-test-data/libstore/serve-protocol/vector.bin diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index 7bf4476d0..703ded0b2 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -3,11 +3,10 @@ #include "pool.hh" #include "remote-store.hh" #include "serve-protocol.hh" +#include "serve-protocol-impl.hh" #include "build-result.hh" #include "store-api.hh" #include "path-with-outputs.hh" -#include "common-protocol.hh" -#include "common-protocol-impl.hh" #include "ssh.hh" #include "derivations.hh" #include "callback.hh" @@ -50,37 +49,31 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor bool good = true; /** - * Coercion to `CommonProto::ReadConn`. This makes it easy to use the - * factored out common protocol serialisers with a + * Coercion to `ServeProto::ReadConn`. This makes it easy to use the + * factored out serve protocol searlizers with a * `LegacySSHStore::Connection`. * - * The common protocol connection types are unidirectional, unlike + * The serve protocol connection types are unidirectional, unlike * this type. - * - * @todo Use server protocol serializers, not common protocol - * serializers, once we have made that distiction. */ - operator CommonProto::ReadConn () + operator ServeProto::ReadConn () { - return CommonProto::ReadConn { + return ServeProto::ReadConn { .from = from, }; } /* - * Coercion to `CommonProto::WriteConn`. This makes it easy to use the - * factored out common protocol searlizers with a + * Coercion to `ServeProto::WriteConn`. This makes it easy to use the + * factored out serve protocol searlizers with a * `LegacySSHStore::Connection`. * - * The common protocol connection types are unidirectional, unlike + * The serve protocol connection types are unidirectional, unlike * this type. - * - * @todo Use server protocol serializers, not common protocol - * serializers, once we have made that distiction. */ - operator CommonProto::WriteConn () + operator ServeProto::WriteConn () { - return CommonProto::WriteConn { + return ServeProto::WriteConn { .to = to, }; } @@ -183,7 +176,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor auto deriver = readString(conn->from); if (deriver != "") info->deriver = parseStorePath(deriver); - info->references = CommonProto::Serialise::read(*this, *conn); + info->references = ServeProto::Serialise::read(*this, *conn); readLongLong(conn->from); // download size info->narSize = readLongLong(conn->from); @@ -217,7 +210,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor << printStorePath(info.path) << (info.deriver ? printStorePath(*info.deriver) : "") << info.narHash.to_string(Base16, false); - CommonProto::write(*this, *conn, info.references); + ServeProto::write(*this, *conn, info.references); conn->to << info.registrationTime << info.narSize @@ -246,7 +239,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor conn->to << exportMagic << printStorePath(info.path); - CommonProto::write(*this, *conn, info.references); + ServeProto::write(*this, *conn, info.references); conn->to << (info.deriver ? printStorePath(*info.deriver) : "") << 0 @@ -331,7 +324,7 @@ public: if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 3) conn->from >> status.timesBuilt >> status.isNonDeterministic >> status.startTime >> status.stopTime; if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 6) { - auto builtOutputs = CommonProto::Serialise::read(*this, *conn); + auto builtOutputs = ServeProto::Serialise::read(*this, *conn); for (auto && [output, realisation] : builtOutputs) status.builtOutputs.insert_or_assign( std::move(output.outputName), @@ -409,10 +402,10 @@ public: conn->to << ServeProto::Command::QueryClosure << includeOutputs; - CommonProto::write(*this, *conn, paths); + ServeProto::write(*this, *conn, paths); conn->to.flush(); - for (auto & i : CommonProto::Serialise::read(*this, *conn)) + for (auto & i : ServeProto::Serialise::read(*this, *conn)) out.insert(i); } @@ -425,10 +418,10 @@ public: << ServeProto::Command::QueryValidPaths << false // lock << maybeSubstitute; - CommonProto::write(*this, *conn, paths); + ServeProto::write(*this, *conn, paths); conn->to.flush(); - return CommonProto::Serialise::read(*this, *conn); + return ServeProto::Serialise::read(*this, *conn); } void connect() override diff --git a/src/libstore/serve-protocol-impl.hh b/src/libstore/serve-protocol-impl.hh new file mode 100644 index 000000000..a3ce81026 --- /dev/null +++ b/src/libstore/serve-protocol-impl.hh @@ -0,0 +1,59 @@ +#pragma once +/** + * @file + * + * Template implementations (as opposed to mere declarations). + * + * This file is an exmample of the "impl.hh" pattern. See the + * contributing guide. + */ + +#include "serve-protocol.hh" +#include "length-prefixed-protocol-helper.hh" + +namespace nix { + +/* protocol-agnostic templates */ + +#define SERVE_USE_LENGTH_PREFIX_SERIALISER(TEMPLATE, T) \ + TEMPLATE T ServeProto::Serialise< T >::read(const Store & store, ServeProto::ReadConn conn) \ + { \ + return LengthPrefixedProtoHelper::read(store, conn); \ + } \ + TEMPLATE void ServeProto::Serialise< T >::write(const Store & store, ServeProto::WriteConn conn, const T & t) \ + { \ + LengthPrefixedProtoHelper::write(store, conn, t); \ + } + +SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::vector) +SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::set) +SERVE_USE_LENGTH_PREFIX_SERIALISER(template, std::tuple) + +#define COMMA_ , +SERVE_USE_LENGTH_PREFIX_SERIALISER( + template, + std::map) +#undef COMMA_ + +/** + * Use `CommonProto` where possible. + */ +template +struct ServeProto::Serialise +{ + static T read(const Store & store, ServeProto::ReadConn conn) + { + return CommonProto::Serialise::read(store, + CommonProto::ReadConn { .from = conn.from }); + } + static void write(const Store & store, ServeProto::WriteConn conn, const T & t) + { + CommonProto::Serialise::write(store, + CommonProto::WriteConn { .to = conn.to }, + t); + } +}; + +/* protocol-specific templates */ + +} diff --git a/src/libstore/serve-protocol.cc b/src/libstore/serve-protocol.cc new file mode 100644 index 000000000..16a62b5bc --- /dev/null +++ b/src/libstore/serve-protocol.cc @@ -0,0 +1,15 @@ +#include "serialise.hh" +#include "util.hh" +#include "path-with-outputs.hh" +#include "store-api.hh" +#include "serve-protocol.hh" +#include "serve-protocol-impl.hh" +#include "archive.hh" + +#include + +namespace nix { + +/* protocol-specific definitions */ + +} diff --git a/src/libstore/serve-protocol.hh b/src/libstore/serve-protocol.hh index 7e43b3969..e2345d450 100644 --- a/src/libstore/serve-protocol.hh +++ b/src/libstore/serve-protocol.hh @@ -1,6 +1,8 @@ #pragma once ///@file +#include "common-protocol.hh" + namespace nix { #define SERVE_MAGIC_1 0x390c9deb @@ -10,6 +12,11 @@ namespace nix { #define GET_PROTOCOL_MAJOR(x) ((x) & 0xff00) #define GET_PROTOCOL_MINOR(x) ((x) & 0x00ff) + +class Store; +struct Source; + + /** * The "serve protocol", used by ssh:// stores. * @@ -22,6 +29,57 @@ struct ServeProto * Enumeration of all the request types for the protocol. */ enum struct Command : uint64_t; + + /** + * A unidirectional read connection, to be used by the read half of the + * canonical serializers below. + * + * This currently is just a `Source &`, but more fields will be added + * later. + */ + struct ReadConn { + Source & from; + }; + + /** + * A unidirectional write connection, to be used by the write half of the + * canonical serializers below. + * + * This currently is just a `Sink &`, but more fields will be added + * later. + */ + struct WriteConn { + Sink & to; + }; + + /** + * Data type for canonical pairs of serialisers for the serve protocol. + * + * See https://en.cppreference.com/w/cpp/language/adl for the broader + * concept of what is going on here. + */ + template + struct Serialise; + // This is the definition of `Serialise` we *want* to put here, but + // do not do so. + // + // See `worker-protocol.hh` for a longer explanation. +#if 0 + { + static T read(const Store & store, ReadConn conn); + static void write(const Store & store, WriteConn conn, const T & t); + }; +#endif + + /** + * Wrapper function around `ServeProto::Serialise::write` that allows us to + * infer the type instead of having to write it down explicitly. + */ + template + static void write(const Store & store, WriteConn conn, const T & t) + { + ServeProto::Serialise::write(store, conn, t); + } }; enum struct ServeProto::Command : uint64_t @@ -58,4 +116,33 @@ inline std::ostream & operator << (std::ostream & s, ServeProto::Command op) return s << (uint64_t) op; } +/** + * Declare a canonical serialiser pair for the worker protocol. + * + * We specialise the struct merely to indicate that we are implementing + * the function for the given type. + * + * Some sort of `template<...>` must be used with the caller for this to + * be legal specialization syntax. See below for what that looks like in + * practice. + */ +#define DECLARE_SERVE_SERIALISER(T) \ + struct ServeProto::Serialise< T > \ + { \ + static T read(const Store & store, ServeProto::ReadConn conn); \ + static void write(const Store & store, ServeProto::WriteConn conn, const T & t); \ + }; + +template +DECLARE_SERVE_SERIALISER(std::vector); +template +DECLARE_SERVE_SERIALISER(std::set); +template +DECLARE_SERVE_SERIALISER(std::tuple); + +#define COMMA_ , +template +DECLARE_SERVE_SERIALISER(std::map); +#undef COMMA_ + } diff --git a/src/libstore/tests/serve-protocol.cc b/src/libstore/tests/serve-protocol.cc new file mode 100644 index 000000000..5d7df56cf --- /dev/null +++ b/src/libstore/tests/serve-protocol.cc @@ -0,0 +1,152 @@ +#include + +#include +#include + +#include "serve-protocol.hh" +#include "serve-protocol-impl.hh" +#include "build-result.hh" +#include "tests/protocol.hh" +#include "tests/characterization.hh" + +namespace nix { + +const char commonProtoDir[] = "serve-protocol"; + +using ServeProtoTest = ProtoTest; + +CHARACTERIZATION_TEST( + ServeProtoTest, + string, + "string", + (std::tuple { + "", + "hi", + "white rabbit", + "大白兔", + "oh no \0\0\0 what was that!", + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + storePath, + "store-path", + (std::tuple { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + contentAddress, + "content-address", + (std::tuple { + ContentAddress { + .method = TextIngestionMethod {}, + .hash = hashString(HashType::htSHA256, "Derive(...)"), + }, + ContentAddress { + .method = FileIngestionMethod::Flat, + .hash = hashString(HashType::htSHA1, "blob blob..."), + }, + ContentAddress { + .method = FileIngestionMethod::Recursive, + .hash = hashString(HashType::htSHA256, "(...)"), + }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + drvOutput, + "drv-output", + (std::tuple { + { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "quux", + }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + realisation, + "realisation", + (std::tuple { + Realisation { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + .signatures = { "asdf", "qwer" }, + }, + Realisation { + .id = { + .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + .outputName = "baz", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + .signatures = { "asdf", "qwer" }, + .dependentRealisations = { + { + DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "quux", + }, + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + }, + }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + vector, + "vector", + (std::tuple, std::vector, std::vector, std::vector>> { + { }, + { "" }, + { "", "foo", "bar" }, + { {}, { "" }, { "", "1", "2" } }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + set, + "set", + (std::tuple, std::set, std::set, std::set>> { + { }, + { "" }, + { "", "foo", "bar" }, + { {}, { "" }, { "", "1", "2" } }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + optionalStorePath, + "optional-store-path", + (std::tuple, std::optional> { + std::nullopt, + std::optional { + StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, + }, + })) + +CHARACTERIZATION_TEST( + ServeProtoTest, + optionalContentAddress, + "optional-content-address", + (std::tuple, std::optional> { + std::nullopt, + std::optional { + ContentAddress { + .method = FileIngestionMethod::Flat, + .hash = hashString(HashType::htSHA1, "blob blob..."), + }, + }, + })) + +} diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 6fc22214a..9b6c80a75 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -9,10 +9,9 @@ #include "local-store.hh" #include "monitor-fd.hh" #include "serve-protocol.hh" +#include "serve-protocol-impl.hh" #include "shared.hh" #include "util.hh" -#include "common-protocol.hh" -#include "common-protocol-impl.hh" #include "graphml.hh" #include "legacy.hh" #include "path-with-outputs.hh" @@ -821,8 +820,8 @@ static void opServe(Strings opFlags, Strings opArgs) out.flush(); unsigned int clientVersion = readInt(in); - CommonProto::ReadConn rconn { .from = in }; - CommonProto::WriteConn wconn { .to = out }; + ServeProto::ReadConn rconn { .from = in }; + ServeProto::WriteConn wconn { .to = out }; auto getBuildSettings = [&]() { // FIXME: changing options here doesn't work if we're @@ -867,7 +866,7 @@ static void opServe(Strings opFlags, Strings opArgs) case ServeProto::Command::QueryValidPaths: { bool lock = readInt(in); bool substitute = readInt(in); - auto paths = CommonProto::Serialise::read(*store, rconn); + auto paths = ServeProto::Serialise::read(*store, rconn); if (lock && writeAllowed) for (auto & path : paths) store->addTempRoot(path); @@ -876,19 +875,19 @@ static void opServe(Strings opFlags, Strings opArgs) store->substitutePaths(paths); } - CommonProto::write(*store, wconn, store->queryValidPaths(paths)); + ServeProto::write(*store, wconn, store->queryValidPaths(paths)); break; } case ServeProto::Command::QueryPathInfos: { - auto paths = CommonProto::Serialise::read(*store, rconn); + auto paths = ServeProto::Serialise::read(*store, rconn); // !!! Maybe we want a queryPathInfos? for (auto & i : paths) { try { auto info = store->queryPathInfo(i); out << store->printStorePath(info->path) << (info->deriver ? store->printStorePath(*info->deriver) : ""); - CommonProto::write(*store, wconn, info->references); + ServeProto::write(*store, wconn, info->references); // !!! Maybe we want compression? out << info->narSize // downloadSize << info->narSize; @@ -916,7 +915,7 @@ static void opServe(Strings opFlags, Strings opArgs) case ServeProto::Command::ExportPaths: { readInt(in); // obsolete - store->exportPaths(CommonProto::Serialise::read(*store, rconn), out); + store->exportPaths(ServeProto::Serialise::read(*store, rconn), out); break; } @@ -962,7 +961,7 @@ static void opServe(Strings opFlags, Strings opArgs) DrvOutputs builtOutputs; for (auto & [output, realisation] : status.builtOutputs) builtOutputs.insert_or_assign(realisation.id, realisation); - CommonProto::write(*store, wconn, builtOutputs); + ServeProto::write(*store, wconn, builtOutputs); } break; @@ -971,9 +970,9 @@ static void opServe(Strings opFlags, Strings opArgs) case ServeProto::Command::QueryClosure: { bool includeOutputs = readInt(in); StorePathSet closure; - store->computeFSClosure(CommonProto::Serialise::read(*store, rconn), + store->computeFSClosure(ServeProto::Serialise::read(*store, rconn), closure, false, includeOutputs); - CommonProto::write(*store, wconn, closure); + ServeProto::write(*store, wconn, closure); break; } @@ -988,7 +987,7 @@ static void opServe(Strings opFlags, Strings opArgs) }; if (deriver != "") info.deriver = store->parseStorePath(deriver); - info.references = CommonProto::Serialise::read(*store, rconn); + info.references = ServeProto::Serialise::read(*store, rconn); in >> info.registrationTime >> info.narSize >> info.ultimate; info.sigs = readStrings(in); info.ca = ContentAddress::parseOpt(readString(in)); diff --git a/unit-test-data/libstore/serve-protocol/content-address.bin b/unit-test-data/libstore/serve-protocol/content-address.bin new file mode 100644 index 0000000000000000000000000000000000000000..8f14bcdb3e50cc72d87106ce159a6fd7b9f75713 GIT binary patch literal 208 zcmY+;K@P$o5QSmy;|7WYrYjRqQfMm$+LWPzDW_MfG4bucKks(>Y#V56lkFOiEzi24 zUMdC5VUCa86OJ&gL0BF^B{HBQEY%f|I<6v7#q+l_PBirI5N~-md%37WE|g33QTE2k zc~-WF&R}7Oxc@o)T?ojpD!+*5=xpO;%IoCKSV5FMh={>xePK|8<${>E)*huNHZ(pX literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/drv-output.bin b/unit-test-data/libstore/serve-protocol/drv-output.bin new file mode 100644 index 0000000000000000000000000000000000000000..800a45fd8757a15e8810066aed48be56a600b7d3 GIT binary patch literal 176 zcmY++xeWp_5I|8{*$(Wn=b{B@!gGlfp_LHTb8N(qe)KM%U7Sq@}q)U|6h9n2j1!MT?w+C~^gO WO9=?G&ojrYjy`x4ZufnEe#tjMpDPdm literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/optional-content-address.bin b/unit-test-data/libstore/serve-protocol/optional-content-address.bin new file mode 100644 index 0000000000000000000000000000000000000000..f8cfe65ba27fd78ec7787eed7ebec8dfdead2fba GIT binary patch literal 64 zcmZQzfB#ECTBU06lLKJOBUy literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/realisation.bin b/unit-test-data/libstore/serve-protocol/realisation.bin new file mode 100644 index 0000000000000000000000000000000000000000..2176c6c4afd96b32fe372de6084ac7f4c7a11d49 GIT binary patch literal 520 zcmdUrO-{o=429t+opq7s&z_l_03#3x z?rW}!>Uc!$VBY>Sr2mUC`<2<)qY;)1KN Po44)luetw6d9AunL$;dR literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/set.bin b/unit-test-data/libstore/serve-protocol/set.bin new file mode 100644 index 0000000000000000000000000000000000000000..ce11ede7fe7aa061a30b211a18cd7a336f879de8 GIT binary patch literal 152 ucmZQzfB;4)4WpQ03@8obCnXkvMPU52{CpHXOdBEdVDg4g4KThDln(&+h63;a literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/store-path.bin b/unit-test-data/libstore/serve-protocol/store-path.bin new file mode 100644 index 0000000000000000000000000000000000000000..3fc05f2981d3398ab30ec34125c903cdcefa8729 GIT binary patch literal 120 tcmdOAfB^lx%nJSDlKi4n{dB`}^NdR4LR_?NT7Eu*F?srQlM;)-IsqP{BM|@q literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/string.bin b/unit-test-data/libstore/serve-protocol/string.bin new file mode 100644 index 0000000000000000000000000000000000000000..aa7b5a604745b735473ec34283177e69320776e4 GIT binary patch literal 88 zcmZQzfB+^aoskJ)@Id+H8JQ)i3Pp)YNtq=eAx^0H( Date: Wed, 11 Oct 2023 19:58:42 +0800 Subject: [PATCH 221/299] Allow CLI to pass environment variables to FOD builder (#8830) Add a new experimental `impure-env` setting that is a key-value list of environment variables to inject into FOD derivations that specify the corresponding `impureEnvVars`. This allows clients to make use of this feature (without having to change the environment of the daemon itself) and might eventually deprecate the current behaviour (pick whatever is in the environment of the daemon) as it's more principled and might prevent information leakage. --- .../src/language/advanced-attributes.md | 7 ++++ src/libstore/build/local-derivation-goal.cc | 14 ++++++-- src/libstore/globals.hh | 20 +++++++++++ src/libutil/experimental-features.cc | 9 ++++- src/libutil/experimental-features.hh | 1 + tests/functional/impure-env.nix | 16 +++++++++ tests/functional/impure-env.sh | 33 +++++++++++++++++++ tests/functional/local.mk | 3 +- 8 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/functional/impure-env.nix create mode 100644 tests/functional/impure-env.sh diff --git a/doc/manual/src/language/advanced-attributes.md b/doc/manual/src/language/advanced-attributes.md index 7d97b24b6..4e4372331 100644 --- a/doc/manual/src/language/advanced-attributes.md +++ b/doc/manual/src/language/advanced-attributes.md @@ -112,6 +112,13 @@ Derivations can declare some infrequently used optional attributes. > environmental variables come from the environment of the > `nix-build`. + If the [`configurable-impure-env` experimental + feature](@docroot@/contributing/experimental-features.md#xp-feature-configurable-impure-env) + is enabled, these environment variables can also be controlled + through the + [`impure-env`](@docroot@/command-ref/conf-file.md#conf-impure-env) + configuration setting. + - [`outputHash`]{#adv-attr-outputHash}; [`outputHashAlgo`]{#adv-attr-outputHashAlgo}; [`outputHashMode`]{#adv-attr-outputHashMode}\ These attributes declare that the derivation is a so-called *fixed-output derivation*, which means that a cryptographic hash of diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 64b55ca6a..817b20b2f 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1135,8 +1135,18 @@ void LocalDerivationGoal::initEnv() fixed-output derivations is by definition pure (since we already know the cryptographic hash of the output). */ if (!derivationType->isSandboxed()) { - for (auto & i : parsedDrv->getStringsAttr("impureEnvVars").value_or(Strings())) - env[i] = getEnv(i).value_or(""); + auto & impureEnv = settings.impureEnv.get(); + if (!impureEnv.empty()) + experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); + + for (auto & i : parsedDrv->getStringsAttr("impureEnvVars").value_or(Strings())) { + auto envVar = impureEnv.find(i); + if (envVar != impureEnv.end()) { + env[i] = envVar->second; + } else { + env[i] = getEnv(i).value_or(""); + } + } } /* Currently structured log messages piggyback on stderr, but we diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 0569f43b9..bd793c3d6 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -4,6 +4,7 @@ #include "types.hh" #include "config.hh" #include "util.hh" +#include "experimental-features.hh" #include #include @@ -1052,6 +1053,25 @@ public: ``` )" }; + + Setting impureEnv {this, {}, "impure-env", + R"( + A list of items, each in the format of: + + - `name=value`: Set environment variable `name` to `value`. + + If the user is trusted (see `trusted-users` option), when building + a fixed-output derivation, environment variables set in this option + will be passed to the builder if they are listed in [`impureEnvVars`](@docroot@/language/advanced-attributes.md##adv-attr-impureEnvVars). + + This option is useful for, e.g., setting `https_proxy` for + fixed-output derivations and in a multi-user Nix installation, or + setting private access tokens when fetching a private repository. + )", + {}, // aliases + true, // document default + Xp::ConfigurableImpureEnv + }; }; diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 203455b63..74af9aae0 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -12,7 +12,7 @@ struct ExperimentalFeatureDetails std::string_view description; }; -constexpr std::array xpFeatureDetails = {{ +constexpr std::array xpFeatureDetails = {{ { .tag = Xp::CaDerivations, .name = "ca-derivations", @@ -221,6 +221,13 @@ constexpr std::array xpFeatureDetails = {{ Allow the use of the `read-only` parameter in [local store](@docroot@/command-ref/new-cli/nix3-help-stores.md#local-store) URIs. )", }, + { + .tag = Xp::ConfigurableImpureEnv, + .name = "configurable-impure-env", + .description = R"( + Allow the use of the [impure-env](@docroot@/command-ref/conf-file.md#conf-impure-env) setting. + )", + } }}; static_assert( diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh index add592ae6..e02f8353e 100644 --- a/src/libutil/experimental-features.hh +++ b/src/libutil/experimental-features.hh @@ -31,6 +31,7 @@ enum struct ExperimentalFeature DynamicDerivations, ParseTomlTimestamps, ReadOnlyLocalStore, + ConfigurableImpureEnv, }; /** diff --git a/tests/functional/impure-env.nix b/tests/functional/impure-env.nix new file mode 100644 index 000000000..2b0380ed7 --- /dev/null +++ b/tests/functional/impure-env.nix @@ -0,0 +1,16 @@ +{ var, value }: + +with import ./config.nix; + +mkDerivation { + name = "test"; + buildCommand = '' + echo ${var} = "''$${var}" + echo -n "''$${var}" > "$out" + ''; + + impureEnvVars = [ var ]; + + outputHashAlgo = "sha256"; + outputHash = builtins.hashString "sha256" value; +} diff --git a/tests/functional/impure-env.sh b/tests/functional/impure-env.sh new file mode 100644 index 000000000..d9e4a34a2 --- /dev/null +++ b/tests/functional/impure-env.sh @@ -0,0 +1,33 @@ +source common.sh + +# Needs the config option 'impure-env' to work +requireDaemonNewerThan "2.18.0pre20230816" + +enableFeatures "configurable-impure-env" +restartDaemon + +varTest() { + local var="$1"; shift + local value="$1"; shift + nix build --no-link -vL --argstr var "$var" --argstr value "$value" --impure "$@" --file impure-env.nix + clearStore +} + +clearStore +startDaemon + +varTest env_name value --impure-env env_name=value + +echo 'impure-env = set_in_config=config_value' >> "$NIX_CONF_DIR/nix.conf" +set_in_config=daemon_value restartDaemon + +varTest set_in_config config_value +varTest set_in_config client_value --impure-env set_in_config=client_value + +sed -i -e '/^trusted-users =/d' "$NIX_CONF_DIR/nix.conf" + +env_name=daemon_value restartDaemon + +varTest env_name daemon_value --impure-env env_name=client_value + +killDaemon diff --git a/tests/functional/local.mk b/tests/functional/local.mk index 6f6c94fe6..643351163 100644 --- a/tests/functional/local.mk +++ b/tests/functional/local.mk @@ -121,7 +121,8 @@ nix_tests = \ path-from-hash-part.sh \ toString-path.sh \ read-only-store.sh \ - nested-sandboxing.sh + nested-sandboxing.sh \ + impure-env.sh ifeq ($(HAVE_LIBCPUID), 1) nix_tests += compute-levels.sh From b4b1a07f9771d4fd106323672d833a1cb17c7364 Mon Sep 17 00:00:00 2001 From: vicky1999 Date: Fri, 13 Oct 2023 06:48:35 +0530 Subject: [PATCH 222/299] store info alias created --- src/nix/ping-store.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nix/ping-store.cc b/src/nix/ping-store.cc index ec450e8e0..5f8a10e22 100644 --- a/src/nix/ping-store.cc +++ b/src/nix/ping-store.cc @@ -47,3 +47,4 @@ struct CmdPingStore : StoreCommand, MixJSON }; static auto rCmdPingStore = registerCommand2({"store", "ping"}); +static auto rCmdInfoStore = registerCommand2({"store", "info"}); From 5c65379b228cee2c37408e1810c6654da5c91b90 Mon Sep 17 00:00:00 2001 From: vicky1999 Date: Fri, 13 Oct 2023 07:16:05 +0530 Subject: [PATCH 223/299] info store alias added to store-ping --- src/nix/ping-store.cc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/nix/ping-store.cc b/src/nix/ping-store.cc index 5f8a10e22..247d9c6bb 100644 --- a/src/nix/ping-store.cc +++ b/src/nix/ping-store.cc @@ -46,5 +46,15 @@ struct CmdPingStore : StoreCommand, MixJSON } }; +struct CmdInfoStore : CmdPingStore +{ + void run(nix::ref store) override + { + warn("'nix store info' is a deprecated alias for 'nix store ping'"); + CmdPingStore::run(store); + } +}; + + static auto rCmdPingStore = registerCommand2({"store", "ping"}); -static auto rCmdInfoStore = registerCommand2({"store", "info"}); +static auto rCmdInfoStore = registerCommand2({"store", "info"}); From db0d94560b2c72b695b420031ccdb8d1cbfb9f2b Mon Sep 17 00:00:00 2001 From: Michal Sojka Date: Sun, 11 Jun 2023 21:36:56 +0200 Subject: [PATCH 224/299] Document builtins.fetchTree Co-authored-by: Valentin Gagarin Supersedes #6740 --- src/libexpr/primops/fetchTree.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 124165896..af4036460 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -236,6 +236,10 @@ static RegisterPrimOp primop_fetchTree({ ```nix builtins.fetchTree "https://example.com/" ``` + + > **Note** + > + > This function requires the [`flakes` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-flakes) to be enabled. )", .fun = prim_fetchTree, .experimentalFeature = Xp::Flakes, From 856fe1353367836d7d9282ae52e91a47e6bd4683 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 28 Sep 2023 16:52:28 +0200 Subject: [PATCH 225/299] fetchTree cleanup Two changes: * The (probably unintentional) hack to handle paths as tarballs has been removed. This is almost certainly not what users expect and is inconsistent with flakeref handling everywhere else. * The hack to support scp-style Git URLs has been moved to the Git fetcher, so it's now supported not just by fetchTree but by flake inputs. --- src/libexpr/primops/fetchTree.cc | 55 ++++++++++---------------------- src/libfetchers/git.cc | 4 ++- src/libutil/url.cc | 17 ++++++++++ src/libutil/url.hh | 5 +++ src/nix/flake.md | 6 ++++ tests/functional/fetchGit.sh | 2 ++ tests/nixos/github-flakes.nix | 4 +++ 7 files changed, 54 insertions(+), 39 deletions(-) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index af4036460..920839058 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -25,7 +25,6 @@ void emitTreeAttrs( auto attrs = state.buildBindings(10); - state.mkStorePathString(tree.storePath, attrs.alloc(state.sOutPath)); // FIXME: support arbitrary input attributes. @@ -71,36 +70,10 @@ void emitTreeAttrs( v.mkAttrs(attrs); } -std::string fixURI(std::string uri, EvalState & state, const std::string & defaultScheme = "file") -{ - state.checkURI(uri); - if (uri.find("://") == std::string::npos) { - const auto p = ParsedURL { - .scheme = defaultScheme, - .authority = "", - .path = uri - }; - return p.to_string(); - } else { - return uri; - } -} - -std::string fixURIForGit(std::string uri, EvalState & state) -{ - /* Detects scp-style uris (e.g. git@github.com:NixOS/nix) and fixes - * them by removing the `:` and assuming a scheme of `ssh://` - * */ - static std::regex scp_uri("([^/]*)@(.*):(.*)"); - if (uri[0] != '/' && std::regex_match(uri, scp_uri)) - return fixURI(std::regex_replace(uri, scp_uri, "$1@$2/$3"), state, "ssh"); - else - return fixURI(uri, state); -} - struct FetchTreeParams { bool emptyRevFallback = false; bool allowNameArgument = false; + bool isFetchGit = false; }; static void fetchTree( @@ -108,11 +81,12 @@ static void fetchTree( const PosIdx pos, Value * * args, Value & v, - std::optional type, const FetchTreeParams & params = FetchTreeParams{} ) { fetchers::Input input; NixStringContext context; + std::optional type; + if (params.isFetchGit) type = "git"; state.forceValue(*args[0], pos); @@ -142,10 +116,8 @@ static void fetchTree( if (attr.value->type() == nPath || attr.value->type() == nString) { auto s = state.coerceToString(attr.pos, *attr.value, context, "", false, false).toOwned(); attrs.emplace(state.symbols[attr.name], - state.symbols[attr.name] == "url" - ? type == "git" - ? fixURIForGit(s, state) - : fixURI(s, state) + params.isFetchGit && state.symbols[attr.name] == "url" + ? fixGitURL(s) : s); } else if (attr.value->type() == nBool) @@ -170,13 +142,13 @@ static void fetchTree( "while evaluating the first argument passed to the fetcher", false, false).toOwned(); - if (type == "git") { + if (params.isFetchGit) { fetchers::Attrs attrs; attrs.emplace("type", "git"); - attrs.emplace("url", fixURIForGit(url, state)); + attrs.emplace("url", fixGitURL(url)); input = fetchers::Input::fromAttrs(std::move(attrs)); } else { - input = fetchers::Input::fromURL(fixURI(url, state)); + input = fetchers::Input::fromURL(url); } } @@ -186,6 +158,8 @@ static void fetchTree( if (evalSettings.pureEval && !input.isLocked()) state.debugThrowLastTrace(EvalError("in pure evaluation mode, 'fetchTree' requires a locked input, at %s", state.positions[pos])); + state.checkURI(input.toURLString()); + auto [tree, input2] = input.fetch(state.store); state.allowPath(tree.storePath); @@ -195,7 +169,7 @@ static void fetchTree( static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - fetchTree(state, pos, args, v, std::nullopt, FetchTreeParams { .allowNameArgument = false }); + fetchTree(state, pos, args, v, { }); } static RegisterPrimOp primop_fetchTree({ @@ -392,7 +366,12 @@ static RegisterPrimOp primop_fetchTarball({ static void prim_fetchGit(EvalState & state, const PosIdx pos, Value * * args, Value & v) { - fetchTree(state, pos, args, v, "git", FetchTreeParams { .emptyRevFallback = true, .allowNameArgument = true }); + fetchTree(state, pos, args, v, + FetchTreeParams { + .emptyRevFallback = true, + .allowNameArgument = true, + .isFetchGit = true + }); } static RegisterPrimOp primop_fetchGit({ diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index f8d89ab2f..7e9f34790 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -293,7 +293,6 @@ struct GitInputScheme : InputScheme if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "shallow" && name != "submodules" && name != "lastModified" && name != "revCount" && name != "narHash" && name != "allRefs" && name != "name" && name != "dirtyRev" && name != "dirtyShortRev") throw Error("unsupported Git input attribute '%s'", name); - parseURL(getStrAttr(attrs, "url")); maybeGetBoolAttr(attrs, "shallow"); maybeGetBoolAttr(attrs, "submodules"); maybeGetBoolAttr(attrs, "allRefs"); @@ -305,6 +304,9 @@ struct GitInputScheme : InputScheme Input input; input.attrs = attrs; + auto url = fixGitURL(getStrAttr(attrs, "url")); + parseURL(url); + input.attrs["url"] = url; return input; } diff --git a/src/libutil/url.cc b/src/libutil/url.cc index 2970f8d2d..9b438e6cd 100644 --- a/src/libutil/url.cc +++ b/src/libutil/url.cc @@ -158,4 +158,21 @@ ParsedUrlScheme parseUrlScheme(std::string_view scheme) }; } +std::string fixGitURL(const std::string & url) +{ + std::regex scpRegex("([^/]*)@(.*):(.*)"); + if (!hasPrefix(url, "/") && std::regex_match(url, scpRegex)) + return std::regex_replace(url, scpRegex, "ssh://$1@$2/$3"); + else { + if (url.find("://") == std::string::npos) { + return (ParsedURL { + .scheme = "file", + .authority = "", + .path = url + }).to_string(); + } else + return url; + } +} + } diff --git a/src/libutil/url.hh b/src/libutil/url.hh index d2413ec0e..26c2dcc28 100644 --- a/src/libutil/url.hh +++ b/src/libutil/url.hh @@ -45,4 +45,9 @@ struct ParsedUrlScheme { ParsedUrlScheme parseUrlScheme(std::string_view scheme); +/* Detects scp-style uris (e.g. git@github.com:NixOS/nix) and fixes + them by removing the `:` and assuming a scheme of `ssh://`. Also + changes absolute paths into file:// URLs. */ +std::string fixGitURL(const std::string & url); + } diff --git a/src/nix/flake.md b/src/nix/flake.md index 939269c6a..f08648417 100644 --- a/src/nix/flake.md +++ b/src/nix/flake.md @@ -182,6 +182,12 @@ Currently the `type` attribute can be one of the following: git(+http|+https|+ssh|+git|+file|):(//)?(\?)? ``` + or + + ``` + @: + ``` + The `ref` attribute defaults to resolving the `HEAD` reference. The `rev` attribute must denote a commit that exists in the branch diff --git a/tests/functional/fetchGit.sh b/tests/functional/fetchGit.sh index 418b4f63f..fc89f2040 100644 --- a/tests/functional/fetchGit.sh +++ b/tests/functional/fetchGit.sh @@ -35,6 +35,8 @@ unset _NIX_FORCE_HTTP path0=$(nix eval --impure --raw --expr "(builtins.fetchGit file://$TEST_ROOT/worktree).outPath") path0_=$(nix eval --impure --raw --expr "(builtins.fetchTree { type = \"git\"; url = file://$TEST_ROOT/worktree; }).outPath") [[ $path0 = $path0_ ]] +path0_=$(nix eval --impure --raw --expr "(builtins.fetchTree git+file://$TEST_ROOT/worktree).outPath") +[[ $path0 = $path0_ ]] export _NIX_FORCE_HTTP=1 [[ $(tail -n 1 $path0/hello) = "hello" ]] diff --git a/tests/nixos/github-flakes.nix b/tests/nixos/github-flakes.nix index 6de702d17..62ae8871b 100644 --- a/tests/nixos/github-flakes.nix +++ b/tests/nixos/github-flakes.nix @@ -186,6 +186,10 @@ in client.succeed("nix registry pin nixpkgs") client.succeed("nix flake metadata nixpkgs --tarball-ttl 0 >&2") + # Test fetchTree on a github URL. + hash = client.succeed(f"nix eval --raw --expr '(fetchTree {info['url']}).narHash'") + assert hash == info['locked']['narHash'] + # Shut down the web server. The flake should be cached on the client. github.succeed("systemctl stop httpd.service") From 4ce7a53a9ce4eb223f594fcbf627f968f5df02fd Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 28 Sep 2023 17:08:26 +0200 Subject: [PATCH 226/299] Update fetchTree docs --- src/libexpr/primops/fetchTree.cc | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 920839058..1e2cee77b 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -177,12 +177,12 @@ static RegisterPrimOp primop_fetchTree({ .args = {"input"}, .doc = R"( Fetch a source tree or a plain file using one of the supported backends. - *input* can be an attribute set representation of [flake reference](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references) or a URL. - The input should be "locked", that is, it should contain a commit hash or content hash unless impure evaluation (`--impure`) is allowed. + *input* must be a [flake reference](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references), either in attribute set representation or in the URL-like syntax. + The input should be "locked", that is, it should contain a commit hash or content hash unless impure evaluation (`--impure`) is enabled. Here are some examples of how to use `fetchTree`: - - Fetch a GitHub repository: + - Fetch a GitHub repository using the attribute set representation: ```nix builtins.fetchTree { @@ -193,7 +193,7 @@ static RegisterPrimOp primop_fetchTree({ } ``` - This evaluates to attribute set: + This evaluates to the following attribute set: ``` { @@ -205,15 +205,12 @@ static RegisterPrimOp primop_fetchTree({ shortRev = "ae2e6b3"; } ``` - - Fetch a single file from a URL: - ```nix - builtins.fetchTree "https://example.com/" + - Fetch the same GitHub repository using the URL-like syntax: + + ``` + builtins.fetchTree "github:NixOS/nixpkgs/ae2e6b3958682513d28f7d633734571fb18285dd" ``` - - > **Note** - > - > This function requires the [`flakes` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-flakes) to be enabled. )", .fun = prim_fetchTree, .experimentalFeature = Xp::Flakes, From 8eb4f735dc230fb8135b7e513cfc51eb66d937a9 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 29 Sep 2023 10:48:27 +0200 Subject: [PATCH 227/299] fetchTree: Only use the registry if flakes are enabled --- src/libexpr/primops/fetchTree.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 1e2cee77b..3d7a85047 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -152,7 +152,7 @@ static void fetchTree( } } - if (!evalSettings.pureEval && !input.isDirect()) + if (!evalSettings.pureEval && !input.isDirect() && experimentalFeatureSettings.isEnabled(Xp::Flakes)) input = lookupInRegistries(state.store, input).first; if (evalSettings.pureEval && !input.isLocked()) From 4112dd1fc93c9ff03a5a4e8be773c45ebefbbd1f Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 25 Sep 2023 17:08:04 +0200 Subject: [PATCH 228/299] Mark fetchTree as stable --- doc/manual/src/release-notes/rl-next.md | 4 +++- src/libexpr/primops/fetchTree.cc | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 418c1187c..c905f445f 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -6,4 +6,6 @@ - The experimental feature `repl-flake` is no longer needed, as its functionality is now part of the `flakes` experimental feature. To get the previous behavior, use the `--file/--expr` flags accordingly. -- Introduce new flake installable syntax `flakeref#.attrPath` where the "." prefix denotes no searching of default attribute prefixes like `packages.` or `legacyPackages.`. \ No newline at end of file +- Introduce new flake installable syntax `flakeref#.attrPath` where the "." prefix denotes no searching of default attribute prefixes like `packages.` or `legacyPackages.`. + +- `builtins.fetchTree` is now marked as stable. diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 3d7a85047..0335e5f36 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -213,7 +213,6 @@ static RegisterPrimOp primop_fetchTree({ ``` )", .fun = prim_fetchTree, - .experimentalFeature = Xp::Flakes, }); static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v, From a23cc147cb93fe9138b9a2f8283ad3ebbee43088 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 13 Oct 2023 11:00:55 -0400 Subject: [PATCH 229/299] Factor out Perl bindings Nix package Progress breaking up `flake.nix` by introducing separate `default.nix` files which make sense on their own. (This one is a regular `callPackage`-able package.) --- flake.nix | 43 ++++------------------------------------ perl/default.nix | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 39 deletions(-) create mode 100644 perl/default.nix diff --git a/flake.nix b/flake.nix index 301e65545..2263ea572 100644 --- a/flake.nix +++ b/flake.nix @@ -474,45 +474,10 @@ hardeningDisable = lib.optional stdenv.hostPlatform.isStatic "pie"; - passthru.perl-bindings = with final; perl.pkgs.toPerlModule (currentStdenv.mkDerivation { - name = "nix-perl-${version}"; - - src = fileset.toSource { - root = ./.; - fileset = fileset.intersect baseFiles (fileset.unions [ - ./perl - ./.version - ./m4 - ./mk - ]); - }; - - nativeBuildInputs = - [ buildPackages.autoconf-archive - buildPackages.autoreconfHook - buildPackages.pkg-config - ]; - - buildInputs = - [ nix - curl - bzip2 - xz - pkgs.perl - boost - ] - ++ lib.optional (currentStdenv.isLinux || currentStdenv.isDarwin) libsodium - ++ lib.optional currentStdenv.isDarwin darwin.apple_sdk.frameworks.Security; - - configureFlags = [ - "--with-dbi=${perlPackages.DBI}/${pkgs.perl.libPrefix}" - "--with-dbd-sqlite=${perlPackages.DBDSQLite}/${pkgs.perl.libPrefix}" - ]; - - enableParallelBuilding = true; - - postUnpack = "sourceRoot=$sourceRoot/perl"; - }); + passthru.perl-bindings = final.callPackage ./perl { + inherit fileset; + stdenv = currentStdenv; + }; meta.platforms = lib.platforms.unix; }); diff --git a/perl/default.nix b/perl/default.nix new file mode 100644 index 000000000..4687976a1 --- /dev/null +++ b/perl/default.nix @@ -0,0 +1,51 @@ +{ lib, fileset +, stdenv +, perl, perlPackages +, autoconf-archive, autoreconfHook, pkg-config +, nix, curl, bzip2, xz, boost, libsodium, darwin +}: + +perl.pkgs.toPerlModule (stdenv.mkDerivation { + name = "nix-perl-${nix.version}"; + + src = fileset.toSource { + root = ../.; + fileset = fileset.unions [ + ../.version + ../m4 + ../mk + ./MANIFEST + ./Makefile + ./Makefile.config.in + ./configure.ac + ./lib + ./local.mk + ]; + }; + + nativeBuildInputs = + [ autoconf-archive + autoreconfHook + pkg-config + ]; + + buildInputs = + [ nix + curl + bzip2 + xz + perl + boost + ] + ++ lib.optional (stdenv.isLinux || stdenv.isDarwin) libsodium + ++ lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.Security; + + configureFlags = [ + "--with-dbi=${perlPackages.DBI}/${perl.libPrefix}" + "--with-dbd-sqlite=${perlPackages.DBDSQLite}/${perl.libPrefix}" + ]; + + enableParallelBuilding = true; + + postUnpack = "sourceRoot=$sourceRoot/perl"; +}) From e5ce53f3db93ddbc9cca6d51d5fc0e2fd7335517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=A9clairevoyant?= <848000+eclairevoyant@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:08:07 -0400 Subject: [PATCH 230/299] explicitly set meta.mainProgram --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 301e65545..05bb8fad4 100644 --- a/flake.nix +++ b/flake.nix @@ -515,6 +515,7 @@ }); meta.platforms = lib.platforms.unix; + meta.mainProgram = "nix"; }); lowdown-nix = with final; currentStdenv.mkDerivation rec { From c27d2f8da9643a0a87e36ae992b29ea00a50e0d9 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 2 Apr 2023 23:37:55 -0400 Subject: [PATCH 231/299] Add two more completions tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks @ncfavier for catching these regressions in my PR. Co-Authored-By: Naïm Favier --- tests/functional/completions.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/completions.sh b/tests/functional/completions.sh index 19dc61098..7c1e4b287 100644 --- a/tests/functional/completions.sh +++ b/tests/functional/completions.sh @@ -48,6 +48,8 @@ EOF [[ "$(NIX_GET_COMPLETIONS=5 nix build ./foo ./bar --override-input '')" == $'normal\na\t\nb\t' ]] ## With tilde expansion [[ "$(HOME=$PWD NIX_GET_COMPLETIONS=4 nix build '~/foo' --override-input '')" == $'normal\na\t' ]] +[[ "$(HOME=$PWD NIX_GET_COMPLETIONS=5 nix flake show '~/foo' --update-input '')" == $'normal\na\t' ]] +[[ "$(HOME=$PWD NIX_GET_COMPLETIONS=4 nix run '~/foo' --update-input '')" == $'normal\na\t' ]] ## Out of order [[ "$(NIX_GET_COMPLETIONS=3 nix build --update-input '' ./foo)" == $'normal\na\t' ]] [[ "$(NIX_GET_COMPLETIONS=4 nix build ./foo --update-input '' ./bar)" == $'normal\na\t\nb\t' ]] From 483d99c622414504bb1b6a565f30ca6f21284315 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 16 Oct 2023 10:42:15 -0400 Subject: [PATCH 232/299] Add API docs to some args-related functionality --- src/libcmd/command.hh | 78 ++++++++++++++++++++++++++++++++++++------- src/libutil/args.hh | 59 ++++++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 14 deletions(-) diff --git a/src/libcmd/command.hh b/src/libcmd/command.hh index 96236b987..5c4569001 100644 --- a/src/libcmd/command.hh +++ b/src/libcmd/command.hh @@ -34,21 +34,28 @@ struct NixMultiCommand : virtual MultiCommand, virtual Command // For the overloaded run methods #pragma GCC diagnostic ignored "-Woverloaded-virtual" -/* A command that requires a Nix store. */ +/** + * A command that requires a \ref Store "Nix store". + */ struct StoreCommand : virtual Command { StoreCommand(); void run() override; ref getStore(); virtual ref createStore(); + /** + * Main entry point, with a `Store` provided + */ virtual void run(ref) = 0; private: std::shared_ptr _store; }; -/* A command that copies something between `--from` and `--to` - stores. */ +/** + * A command that copies something between `--from` and `--to` \ref + * Store stores. + */ struct CopyCommand : virtual StoreCommand { std::string srcUri, dstUri; @@ -60,6 +67,9 @@ struct CopyCommand : virtual StoreCommand ref getDstStore(); }; +/** + * A command that needs to evaluate Nix language expressions. + */ struct EvalCommand : virtual StoreCommand, MixEvalArgs { bool startReplOnEvalErrors = false; @@ -79,6 +89,10 @@ private: std::shared_ptr evalState; }; +/** + * A mixin class for commands that process flakes, adding a few standard + * flake-related options/flags. + */ struct MixFlakeOptions : virtual Args, EvalCommand { flake::LockFlags lockFlags; @@ -87,6 +101,14 @@ struct MixFlakeOptions : virtual Args, EvalCommand MixFlakeOptions(); + /** + * The completion for some of these flags depends on the flake(s) in + * question. + * + * This method should be implemented to gather all flakerefs the + * command is operating with (presumably specified via some other + * arguments) so that the completions for these flags can use them. + */ virtual std::vector getFlakesForCompletion() { return {}; } @@ -112,15 +134,29 @@ struct SourceExprCommand : virtual Args, MixFlakeOptions virtual Strings getDefaultFlakeAttrPathPrefixes(); + /** + * Complete an installable from the given prefix. + */ void completeInstallable(std::string_view prefix); }; +/** + * A mixin class for commands that need a read-only flag. + * + * What exactly is "read-only" is unspecified, but it will usually be + * the \ref Store "Nix store". + */ struct MixReadOnlyOption : virtual Args { MixReadOnlyOption(); }; -/* Like InstallablesCommand but the installables are not loaded */ +/** + * Like InstallablesCommand but the installables are not loaded. + * + * This is needed by `CmdRepl` which wants to load (and reload) the + * installables itself. + */ struct RawInstallablesCommand : virtual Args, SourceExprCommand { RawInstallablesCommand(); @@ -129,7 +165,7 @@ struct RawInstallablesCommand : virtual Args, SourceExprCommand void run(ref store) override; - // FIXME make const after CmdRepl's override is fixed up + // FIXME make const after `CmdRepl`'s override is fixed up virtual void applyDefaultInstallables(std::vector & rawInstallables); bool readFromStdIn = false; @@ -140,8 +176,11 @@ private: std::vector rawInstallables; }; -/* A command that operates on a list of "installables", which can be - store paths, attribute paths, Nix expressions, etc. */ + +/** + * A command that operates on a list of "installables", which can be + * store paths, attribute paths, Nix expressions, etc. + */ struct InstallablesCommand : RawInstallablesCommand { virtual void run(ref store, Installables && installables) = 0; @@ -149,7 +188,9 @@ struct InstallablesCommand : RawInstallablesCommand void run(ref store, std::vector && rawInstallables) override; }; -/* A command that operates on exactly one "installable" */ +/** + * A command that operates on exactly one "installable". + */ struct InstallableCommand : virtual Args, SourceExprCommand { InstallableCommand(); @@ -175,7 +216,12 @@ struct MixOperateOnOptions : virtual Args MixOperateOnOptions(); }; -/* A command that operates on zero or more store paths. */ +/** + * A command that operates on zero or more extant store paths. + * + * If the argument the user passes is a some sort of recipe for a path + * not yet built, it must be built first. + */ struct BuiltPathsCommand : InstallablesCommand, virtual MixOperateOnOptions { private: @@ -207,7 +253,9 @@ struct StorePathsCommand : public BuiltPathsCommand void run(ref store, BuiltPaths && paths) override; }; -/* A command that operates on exactly one store path. */ +/** + * A command that operates on exactly one store path. + */ struct StorePathCommand : public StorePathsCommand { virtual void run(ref store, const StorePath & storePath) = 0; @@ -215,7 +263,9 @@ struct StorePathCommand : public StorePathsCommand void run(ref store, StorePaths && storePaths) override; }; -/* A helper class for registering commands globally. */ +/** + * A helper class for registering \ref Command commands globally. + */ struct RegisterCommand { typedef std::map, std::function()>> Commands; @@ -271,7 +321,11 @@ struct MixEnvironment : virtual Args { MixEnvironment(); - /* Modify global environ based on ignoreEnvironment, keep, and unset. It's expected that exec will be called before this class goes out of scope, otherwise environ will become invalid. */ + /*** + * Modify global environ based on `ignoreEnvironment`, `keep`, and + * `unset`. It's expected that exec will be called before this class + * goes out of scope, otherwise `environ` will become invalid. + */ void setEnviron(); }; diff --git a/src/libutil/args.hh b/src/libutil/args.hh index 1e5bac650..6457cceed 100644 --- a/src/libutil/args.hh +++ b/src/libutil/args.hh @@ -39,8 +39,21 @@ public: protected: + /** + * The largest `size_t` is used to indicate the "any" arity, for + * handlers/flags/arguments that accept an arbitrary number of + * arguments. + */ static const size_t ArityAny = std::numeric_limits::max(); + /** + * Arguments (flags/options and positional) have a "handler" which is + * caused when the argument is parsed. The handler has an arbitrary side + * effect, including possible affect further command-line parsing. + * + * There are many constructors in order to support many shorthand + * initializations, and this is used a lot. + */ struct Handler { std::function)> fun; @@ -110,7 +123,12 @@ protected: { } }; - /* Options. */ + /** + * Description of flags / options + * + * These are arguments like `-s` or `--long` that can (mostly) + * appear in any order. + */ struct Flag { typedef std::shared_ptr ptr; @@ -130,12 +148,30 @@ protected: static Flag mkHashTypeOptFlag(std::string && longName, std::optional * oht); }; + /** + * Index of all registered "long" flag descriptions (flags like + * `--long`). + */ std::map longFlags; + + /** + * Index of all registered "short" flag descriptions (flags like + * `-s`). + */ std::map shortFlags; + /** + * Process a single flag and its arguments, pulling from an iterator + * of raw CLI args as needed. + */ virtual bool processFlag(Strings::iterator & pos, Strings::iterator end); - /* Positional arguments. */ + /** + * Description of positional arguments + * + * These are arguments that do not start with a `-`, and for which + * the order does matter. + */ struct ExpectedArg { std::string label; @@ -144,8 +180,24 @@ protected: std::function completer; }; + /** + * Queue of expected positional argument forms. + * + * Positional arugment descriptions are inserted on the back. + * + * As positional arguments are passed, these are popped from the + * front, until there are hopefully none left as all args that were + * expected in fact were passed. + */ std::list expectedArgs; + /** + * Process some positional arugments + * + * @param finish: We have parsed everything else, and these are the only + * arguments left. Used because we accumulate some "pending args" we might + * have left over. + */ virtual bool processArgs(const Strings & args, bool finish); virtual Strings::iterator rewriteArgs(Strings & args, Strings::iterator pos) @@ -204,6 +256,9 @@ public: friend class MultiCommand; + /** + * The parent command, used if this is a subcommand. + */ MultiCommand * parent = nullptr; private: From f7a36f981276a535962c1f5b0d02b00b9cd05298 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 16 Oct 2023 12:52:59 -0400 Subject: [PATCH 233/299] Fix language tests a bit - Remove some stray saved error messages that didn't correspond to any test, because they were renamed in d11faa01b5b16773ba97bcb852b4123992829afc. - Need `--eval` in test failure test in order to get in "read-only" mode where we don't try to write to the store. (The other tests already do this.) - Need `--strict` so top-level attribute sets are still forced, like they are without `--eval`. --- tests/functional/lang.sh | 2 +- .../lang/eval-fail-antiquoted-path.err.exp | 1 - .../lang/eval-fail-bad-antiquote-1.err.exp | 10 ---------- .../lang/eval-fail-bad-antiquote-2.err.exp | 1 - .../lang/eval-fail-bad-antiquote-3.err.exp | 10 ---------- .../eval-fail-bad-string-interpolation-2.err.exp | 2 +- .../lang/eval-fail-dup-dynamic-attrs.err.exp | 12 +++++++++++- .../functional/lang/eval-fail-nonexist-path.err.exp | 2 +- tests/functional/lang/parse-fail-dup-attrs-6.err.exp | 1 - 9 files changed, 14 insertions(+), 27 deletions(-) delete mode 100644 tests/functional/lang/eval-fail-antiquoted-path.err.exp delete mode 100644 tests/functional/lang/eval-fail-bad-antiquote-1.err.exp delete mode 100644 tests/functional/lang/eval-fail-bad-antiquote-2.err.exp delete mode 100644 tests/functional/lang/eval-fail-bad-antiquote-3.err.exp delete mode 100644 tests/functional/lang/parse-fail-dup-attrs-6.err.exp diff --git a/tests/functional/lang.sh b/tests/functional/lang.sh index f4760eced..c3acef5ee 100755 --- a/tests/functional/lang.sh +++ b/tests/functional/lang.sh @@ -68,7 +68,7 @@ for i in lang/eval-fail-*.nix; do echo "evaluating $i (should fail)"; i=$(basename "$i" .nix) if - expectStderr 1 nix-instantiate --show-trace "lang/$i.nix" \ + expectStderr 1 nix-instantiate --eval --strict --show-trace "lang/$i.nix" \ | sed "s!$(pwd)!/pwd!g" > "lang/$i.err" then diffAndAccept "$i" err err.exp diff --git a/tests/functional/lang/eval-fail-antiquoted-path.err.exp b/tests/functional/lang/eval-fail-antiquoted-path.err.exp deleted file mode 100644 index 425deba42..000000000 --- a/tests/functional/lang/eval-fail-antiquoted-path.err.exp +++ /dev/null @@ -1 +0,0 @@ -error: getting attributes of path ‘PWD/lang/fnord’: No such file or directory diff --git a/tests/functional/lang/eval-fail-bad-antiquote-1.err.exp b/tests/functional/lang/eval-fail-bad-antiquote-1.err.exp deleted file mode 100644 index cf94f53bc..000000000 --- a/tests/functional/lang/eval-fail-bad-antiquote-1.err.exp +++ /dev/null @@ -1,10 +0,0 @@ -error: - … while evaluating a path segment - - at /pwd/lang/eval-fail-bad-antiquote-1.nix:1:2: - - 1| "${x: x}" - | ^ - 2| - - error: cannot coerce a function to a string diff --git a/tests/functional/lang/eval-fail-bad-antiquote-2.err.exp b/tests/functional/lang/eval-fail-bad-antiquote-2.err.exp deleted file mode 100644 index c8fe39d12..000000000 --- a/tests/functional/lang/eval-fail-bad-antiquote-2.err.exp +++ /dev/null @@ -1 +0,0 @@ -error: operation 'addToStoreFromDump' is not supported by store 'dummy' diff --git a/tests/functional/lang/eval-fail-bad-antiquote-3.err.exp b/tests/functional/lang/eval-fail-bad-antiquote-3.err.exp deleted file mode 100644 index fbefbc826..000000000 --- a/tests/functional/lang/eval-fail-bad-antiquote-3.err.exp +++ /dev/null @@ -1,10 +0,0 @@ -error: - … while evaluating a path segment - - at /pwd/lang/eval-fail-bad-antiquote-3.nix:1:3: - - 1| ''${x: x}'' - | ^ - 2| - - error: cannot coerce a function to a string diff --git a/tests/functional/lang/eval-fail-bad-string-interpolation-2.err.exp b/tests/functional/lang/eval-fail-bad-string-interpolation-2.err.exp index c8fe39d12..dea119ae8 100644 --- a/tests/functional/lang/eval-fail-bad-string-interpolation-2.err.exp +++ b/tests/functional/lang/eval-fail-bad-string-interpolation-2.err.exp @@ -1 +1 @@ -error: operation 'addToStoreFromDump' is not supported by store 'dummy' +error: getting status of '/pwd/lang/fnord': No such file or directory diff --git a/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp b/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp index e01f8e6d0..c5fa67523 100644 --- a/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp +++ b/tests/functional/lang/eval-fail-dup-dynamic-attrs.err.exp @@ -1,4 +1,14 @@ -error: dynamic attribute 'b' already defined at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:11 +error: + … while evaluating the attribute 'set' + + at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:3: + + 1| { + 2| set = { "${"" + "b"}" = 1; }; + | ^ + 3| set = { "${"b" + ""}" = 2; }; + + error: dynamic attribute 'b' already defined at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:11 at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:3:11: diff --git a/tests/functional/lang/eval-fail-nonexist-path.err.exp b/tests/functional/lang/eval-fail-nonexist-path.err.exp index c8fe39d12..dea119ae8 100644 --- a/tests/functional/lang/eval-fail-nonexist-path.err.exp +++ b/tests/functional/lang/eval-fail-nonexist-path.err.exp @@ -1 +1 @@ -error: operation 'addToStoreFromDump' is not supported by store 'dummy' +error: getting status of '/pwd/lang/fnord': No such file or directory diff --git a/tests/functional/lang/parse-fail-dup-attrs-6.err.exp b/tests/functional/lang/parse-fail-dup-attrs-6.err.exp deleted file mode 100644 index 74823fc25..000000000 --- a/tests/functional/lang/parse-fail-dup-attrs-6.err.exp +++ /dev/null @@ -1 +0,0 @@ -error: attribute ‘services.ssh’ at (string):3:3 already defined at (string):2:3 From b3fd7db63f78ec736238016745338277703ad1d5 Mon Sep 17 00:00:00 2001 From: Vladimir Kryachko Date: Mon, 16 Oct 2023 13:00:49 -0400 Subject: [PATCH 234/299] Detect cycles in flake follows. This change results in an error thrown as opposed to segfaulting due to stack overflow. Fixes #9144 --- src/libexpr/flake/lockfile.cc | 22 ++++++++++++++--- tests/functional/flakes/follow-paths.sh | 32 ++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/libexpr/flake/lockfile.cc b/src/libexpr/flake/lockfile.cc index 3c202967a..68443211b 100644 --- a/src/libexpr/flake/lockfile.cc +++ b/src/libexpr/flake/lockfile.cc @@ -45,16 +45,26 @@ StorePath LockedNode::computeStorePath(Store & store) const return lockedRef.input.computeStorePath(store); } -std::shared_ptr LockFile::findInput(const InputPath & path) -{ + +static std::shared_ptr doFind(const ref& root, const InputPath & path, std::vector& visited) { auto pos = root; + auto pathS = printInputPath(path); + auto found = std::find(visited.cbegin(), visited.cend(), pathS); + + if(found != visited.end()) { + std::vector cycle(found, visited.cend()); + cycle.push_back(pathS); + throw Error("follow cycle detected: [%s]", concatStringsSep(" -> ", cycle)); + } + visited.push_back(pathS); + for (auto & elem : path) { if (auto i = get(pos->inputs, elem)) { if (auto node = std::get_if<0>(&*i)) pos = *node; else if (auto follows = std::get_if<1>(&*i)) { - if (auto p = findInput(*follows)) + if (auto p = doFind(root, *follows, visited)) pos = ref(p); else return {}; @@ -66,6 +76,12 @@ std::shared_ptr LockFile::findInput(const InputPath & path) return pos; } +std::shared_ptr LockFile::findInput(const InputPath & path) +{ + std::vector visited; + return doFind(root, path, visited); +} + LockFile::LockFile(const nlohmann::json & json, const Path & path) { auto version = json.value("version", 0); diff --git a/tests/functional/flakes/follow-paths.sh b/tests/functional/flakes/follow-paths.sh index dc97027ac..8573b5511 100644 --- a/tests/functional/flakes/follow-paths.sh +++ b/tests/functional/flakes/follow-paths.sh @@ -167,7 +167,7 @@ nix flake lock "$flakeFollowsA" 2>&1 | grep "warning: input 'B' has an override # # The message was # error: input 'B/D' follows a non-existent input 'B/C/D' -# +# # Note that for `B` to resolve its follow for `D`, it needs `C/D`, for which it needs to resolve the follow on `C` first. flakeFollowsOverloadA="$TEST_ROOT/follows/overload/flakeA" flakeFollowsOverloadB="$TEST_ROOT/follows/overload/flakeA/flakeB" @@ -230,3 +230,33 @@ git -C "$flakeFollowsOverloadA" add flake.nix flakeB/flake.nix \ nix flake metadata "$flakeFollowsOverloadA" nix flake update "$flakeFollowsOverloadA" nix flake lock "$flakeFollowsOverloadA" + +# Now test follow cycle detection +# We construct the following follows graph: +# +# foo +# / ^ +# / \ +# v \ +# bar -> baz +# The message was +# error: follow cycle detected: [baz -> foo -> bar -> baz] +flakeFollowCycle="$TEST_ROOT/follows/followCycle" + +# Test following path flakerefs. +mkdir -p "$flakeFollowCycle" + +cat > $flakeFollowCycle/flake.nix <&1 && fail "nix flake lock should have failed." || true) +echo $checkRes | grep -F "error: follow cycle detected: [baz -> foo -> bar -> baz]" From add066cc7bdd1357ccf471b6ab6e68c470e6f604 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Mon, 16 Oct 2023 19:32:47 +0100 Subject: [PATCH 235/299] Fix broken move --- src/libutil/config.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 38d406e8a..8672edaa8 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -68,6 +68,7 @@ void AbstractConfig::warnUnknownSettings() void AbstractConfig::reapplyUnknownSettings() { auto unknownSettings2 = std::move(unknownSettings); + unknownSettings = {}; for (auto & s : unknownSettings2) set(s.first, s.second); } From d6066c90f87d9ba8ed1ddd8e2a3bd199f8498d6f Mon Sep 17 00:00:00 2001 From: Vladimir Kryachko Date: Mon, 16 Oct 2023 15:47:28 -0400 Subject: [PATCH 236/299] Don't convert InputPaths to strings prematurely. --- src/libexpr/flake/lockfile.cc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libexpr/flake/lockfile.cc b/src/libexpr/flake/lockfile.cc index 68443211b..f3ea9063f 100644 --- a/src/libexpr/flake/lockfile.cc +++ b/src/libexpr/flake/lockfile.cc @@ -2,8 +2,10 @@ #include "store-api.hh" #include "url-parts.hh" +#include #include +#include #include namespace nix::flake { @@ -46,18 +48,18 @@ StorePath LockedNode::computeStorePath(Store & store) const } -static std::shared_ptr doFind(const ref& root, const InputPath & path, std::vector& visited) { +static std::shared_ptr doFind(const ref& root, const InputPath & path, std::vector& visited) { auto pos = root; - auto pathS = printInputPath(path); - auto found = std::find(visited.cbegin(), visited.cend(), pathS); + auto found = std::find(visited.cbegin(), visited.cend(), path); if(found != visited.end()) { - std::vector cycle(found, visited.cend()); - cycle.push_back(pathS); + std::vector cycle; + std::transform(found, visited.cend(), std::back_inserter(cycle), printInputPath); + cycle.push_back(printInputPath(path)); throw Error("follow cycle detected: [%s]", concatStringsSep(" -> ", cycle)); } - visited.push_back(pathS); + visited.push_back(path); for (auto & elem : path) { if (auto i = get(pos->inputs, elem)) { @@ -78,7 +80,7 @@ static std::shared_ptr doFind(const ref& root, const InputPath & pat std::shared_ptr LockFile::findInput(const InputPath & path) { - std::vector visited; + std::vector visited; return doFind(root, path, visited); } From abf7df2b376a1cdbc00527bc662344fd9232ce05 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Mon, 16 Oct 2023 21:47:33 +0100 Subject: [PATCH 237/299] Fix moves that accidentally copy anyway --- src/libexpr/eval.cc | 4 ++-- src/libstore/build/derivation-goal.cc | 2 +- src/libstore/build/local-derivation-goal.cc | 2 +- src/libstore/outputs-spec.cc | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index b18eb5266..5280614a1 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -539,7 +539,7 @@ EvalState::EvalState( auto r = resolveSearchPathPath(i.path); if (!r) continue; - auto path = *std::move(r); + auto path = std::move(*r); if (store->isInStore(path)) { try { @@ -1035,7 +1035,7 @@ std::string EvalState::mkOutputStringRaw( /* In practice, this is testing for the case of CA derivations, or dynamic derivations. */ return optStaticOutputPath - ? store->printStorePath(*std::move(optStaticOutputPath)) + ? store->printStorePath(std::move(*optStaticOutputPath)) /* Downstream we would substitute this for an actual path once we build the floating CA derivation */ : DownstreamPlaceholder::fromSingleDerivedPathBuilt(b, xpSettings).render(); diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index dc4d91079..360c6b70b 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -561,7 +561,7 @@ void DerivationGoal::inputsRealised() attempt = fullDrv.tryResolve(worker.store); } assert(attempt); - Derivation drvResolved { *std::move(attempt) }; + Derivation drvResolved { std::move(*attempt) }; auto pathResolved = writeDerivation(worker.store, drvResolved); diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 817b20b2f..6a02f5ad4 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -2521,7 +2521,7 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() ValidPathInfo newInfo0 { worker.store, outputPathName(drv->name, outputName), - *std::move(optCA), + std::move(*optCA), Hash::dummy, }; if (*scratchPath != newInfo0.path) { diff --git a/src/libstore/outputs-spec.cc b/src/libstore/outputs-spec.cc index d943bc111..21c069223 100644 --- a/src/libstore/outputs-spec.cc +++ b/src/libstore/outputs-spec.cc @@ -63,7 +63,7 @@ std::optional> ExtendedOutputsS auto specOpt = OutputsSpec::parseOpt(s.substr(found + 1)); if (!specOpt) return std::nullopt; - return std::pair { s.substr(0, found), ExtendedOutputsSpec::Explicit { *std::move(specOpt) } }; + return std::pair { s.substr(0, found), ExtendedOutputsSpec::Explicit { std::move(*specOpt) } }; } From 54b350d517932ef11de4b3d638d67ffa5cec1634 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Mon, 16 Oct 2023 21:48:02 +0100 Subject: [PATCH 238/299] Drop some moves that would happen anyway but forbid NRVO where appicable --- src/libexpr/eval.cc | 2 +- src/libexpr/paths.cc | 2 +- src/libstore/content-address.cc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 5280614a1..511a5f434 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2290,7 +2290,7 @@ BackedStringView EvalState::coerceToString( && (!v2->isList() || v2->listSize() != 0)) result += " "; } - return std::move(result); + return result; } } diff --git a/src/libexpr/paths.cc b/src/libexpr/paths.cc index 1d690b722..b6a696f47 100644 --- a/src/libexpr/paths.cc +++ b/src/libexpr/paths.cc @@ -4,7 +4,7 @@ namespace nix { SourcePath EvalState::rootPath(CanonPath path) { - return std::move(path); + return path; } } diff --git a/src/libstore/content-address.cc b/src/libstore/content-address.cc index e290a8d38..ae91b859b 100644 --- a/src/libstore/content-address.cc +++ b/src/libstore/content-address.cc @@ -83,7 +83,7 @@ static std::pair parseContentAddressMethodPrefix if (!hashTypeRaw) throw UsageError("content address hash must be in form ':', but found: %s", wholeInput); HashType hashType = parseHashType(*hashTypeRaw); - return std::move(hashType); + return hashType; }; // Switch on prefix From dcc5f801f4fd88e08182b13304cdfa1b3aed8ece Mon Sep 17 00:00:00 2001 From: vicky1999 Date: Tue, 17 Oct 2023 09:39:59 +0530 Subject: [PATCH 239/299] Store info command help updates --- src/nix/{ping-store.cc => store-info.cc} | 8 ++++---- src/nix/{ping-store.md => store-info.md} | 6 +++--- tests/functional/{store-ping.sh => store-info.sh} | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/nix/{ping-store.cc => store-info.cc} (91%) rename src/nix/{ping-store.md => store-info.md} (82%) rename tests/functional/{store-ping.sh => store-info.sh} (69%) diff --git a/src/nix/ping-store.cc b/src/nix/store-info.cc similarity index 91% rename from src/nix/ping-store.cc rename to src/nix/store-info.cc index 247d9c6bb..a7c595761 100644 --- a/src/nix/ping-store.cc +++ b/src/nix/store-info.cc @@ -17,7 +17,7 @@ struct CmdPingStore : StoreCommand, MixJSON std::string doc() override { return - #include "ping-store.md" + #include "store-info.md" ; } @@ -50,11 +50,11 @@ struct CmdInfoStore : CmdPingStore { void run(nix::ref store) override { - warn("'nix store info' is a deprecated alias for 'nix store ping'"); + warn("'nix store ping' is a deprecated alias for 'nix store info'"); CmdPingStore::run(store); } }; -static auto rCmdPingStore = registerCommand2({"store", "ping"}); -static auto rCmdInfoStore = registerCommand2({"store", "info"}); +static auto rCmdPingStore = registerCommand2({"store", "info"}); +static auto rCmdInfoStore = registerCommand2({"store", "ping"}); diff --git a/src/nix/ping-store.md b/src/nix/store-info.md similarity index 82% rename from src/nix/ping-store.md rename to src/nix/store-info.md index 8c846791b..f86efd722 100644 --- a/src/nix/ping-store.md +++ b/src/nix/store-info.md @@ -5,19 +5,19 @@ R""( * Test whether connecting to a remote Nix store via SSH works: ```console - # nix store ping --store ssh://mac1 + # nix store info --store ssh://mac1 ``` * Test whether a URL is a valid binary cache: ```console - # nix store ping --store https://cache.nixos.org + # nix store info --store https://cache.nixos.org ``` * Test whether the Nix daemon is up and running: ```console - # nix store ping --store daemon + # nix store info --store daemon ``` # Description diff --git a/tests/functional/store-ping.sh b/tests/functional/store-info.sh similarity index 69% rename from tests/functional/store-ping.sh rename to tests/functional/store-info.sh index 9846c7d3d..c002e50be 100644 --- a/tests/functional/store-ping.sh +++ b/tests/functional/store-info.sh @@ -1,7 +1,7 @@ source common.sh -STORE_INFO=$(nix store ping 2>&1) -STORE_INFO_JSON=$(nix store ping --json) +STORE_INFO=$(nix store info 2>&1) +STORE_INFO_JSON=$(nix store info --json) echo "$STORE_INFO" | grep "Store URL: ${NIX_REMOTE}" @@ -11,7 +11,7 @@ if [[ -v NIX_DAEMON_PACKAGE ]] && isDaemonNewer "2.7.0pre20220126"; then [[ "$(echo "$STORE_INFO_JSON" | jq -r ".version")" == "$DAEMON_VERSION" ]] fi -expect 127 NIX_REMOTE=unix:$PWD/store nix store ping || \ - fail "nix store ping on a non-existent store should fail" +expect 127 NIX_REMOTE=unix:$PWD/store nix store info || \ + fail "nix store info on a non-existent store should fail" [[ "$(echo "$STORE_INFO_JSON" | jq -r ".url")" == "${NIX_REMOTE:-local}" ]] From f62b5500ff96cc46e610da13dcf72c134935642d Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 17 Oct 2023 14:52:34 +0200 Subject: [PATCH 240/299] fetchTree: Require the flakes experimental feature for the URL syntax --- src/libexpr/primops/fetchTree.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 0335e5f36..3431ff013 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -148,6 +148,11 @@ static void fetchTree( attrs.emplace("url", fixGitURL(url)); input = fetchers::Input::fromAttrs(std::move(attrs)); } else { + if (!experimentalFeatureSettings.isEnabled(Xp::Flakes)) + state.debugThrowLastTrace(EvalError({ + .msg = hintfmt("passing a string argument to 'fetchTree' requires the 'flakes' experimental feature"), + .errPos = state.positions[pos] + })); input = fetchers::Input::fromURL(url); } } @@ -180,6 +185,10 @@ static RegisterPrimOp primop_fetchTree({ *input* must be a [flake reference](@docroot@/command-ref/new-cli/nix3-flake.md#flake-references), either in attribute set representation or in the URL-like syntax. The input should be "locked", that is, it should contain a commit hash or content hash unless impure evaluation (`--impure`) is enabled. + > **Note** + > + > The URL-like syntax requires the [`flakes` experimental feature](@docroot@/contributing/experimental-features.md#xp-feature-flakes) to be enabled. + Here are some examples of how to use `fetchTree`: - Fetch a GitHub repository using the attribute set representation: From 3470cd68c46334d9d2c55e890f7fe50fa9ebe35c Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Tue, 17 Oct 2023 14:57:29 +0200 Subject: [PATCH 241/299] Mark some fetchers as experimental --- src/libfetchers/github.cc | 5 +++++ src/libfetchers/path.cc | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 291f457f0..6c5c792ec 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -223,6 +223,11 @@ struct GitArchiveInputScheme : InputScheme return {result.tree.storePath, input}; } + + std::optional experimentalFeature() override + { + return Xp::Flakes; + } }; struct GitHubInputScheme : GitArchiveInputScheme diff --git a/src/libfetchers/path.cc b/src/libfetchers/path.cc index 01f1be978..092975f5d 100644 --- a/src/libfetchers/path.cc +++ b/src/libfetchers/path.cc @@ -125,6 +125,11 @@ struct PathInputScheme : InputScheme return {std::move(*storePath), input}; } + + std::optional experimentalFeature() override + { + return Xp::Flakes; + } }; static auto rPathInputScheme = OnStartup([] { registerInputScheme(std::make_unique()); }); From ff684260950cd31efffbe9804494d42bc226df59 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Tue, 17 Oct 2023 11:15:36 -0400 Subject: [PATCH 242/299] Name the protocol version types This makes the code clearer, and will help us replace them with proper structs and get rid of the macros later. --- src/libstore/daemon.cc | 10 +++++----- src/libstore/legacy-ssh-store.cc | 2 +- src/libstore/remote-store-connection.hh | 2 +- src/libstore/serve-protocol.hh | 7 +++++++ src/libstore/worker-protocol.hh | 7 +++++++ src/nix-store/nix-store.cc | 2 +- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 8cbf6f044..fbff64e89 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -45,9 +45,9 @@ struct TunnelLogger : public Logger Sync state_; - unsigned int clientVersion; + WorkerProto::Version clientVersion; - TunnelLogger(FdSink & to, unsigned int clientVersion) + TunnelLogger(FdSink & to, WorkerProto::Version clientVersion) : to(to), clientVersion(clientVersion) { } void enqueueMsg(const std::string & s) @@ -261,7 +261,7 @@ struct ClientSettings } }; -static std::vector readDerivedPaths(Store & store, unsigned int clientVersion, WorkerProto::ReadConn conn) +static std::vector readDerivedPaths(Store & store, WorkerProto::Version clientVersion, WorkerProto::ReadConn conn) { std::vector reqs; if (GET_PROTOCOL_MINOR(clientVersion) >= 30) { @@ -274,7 +274,7 @@ static std::vector readDerivedPaths(Store & store, unsigned int cli } static void performOp(TunnelLogger * logger, ref store, - TrustedFlag trusted, RecursiveFlag recursive, unsigned int clientVersion, + TrustedFlag trusted, RecursiveFlag recursive, WorkerProto::Version clientVersion, Source & from, BufferedSink & to, WorkerProto::Op op) { WorkerProto::ReadConn rconn { .from = from }; @@ -1017,7 +1017,7 @@ void processConnection( if (magic != WORKER_MAGIC_1) throw Error("protocol mismatch"); to << WORKER_MAGIC_2 << PROTOCOL_VERSION; to.flush(); - unsigned int clientVersion = readInt(from); + WorkerProto::Version clientVersion = readInt(from); if (clientVersion < 0x10a) throw Error("the Nix client version is too old"); diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index 703ded0b2..efced20d0 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -45,7 +45,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor std::unique_ptr sshConn; FdSink to; FdSource from; - int remoteVersion; + ServeProto::Version remoteVersion; bool good = true; /** diff --git a/src/libstore/remote-store-connection.hh b/src/libstore/remote-store-connection.hh index ce4740a9c..8738e4d95 100644 --- a/src/libstore/remote-store-connection.hh +++ b/src/libstore/remote-store-connection.hh @@ -30,7 +30,7 @@ struct RemoteStore::Connection * sides support. (If the maximum doesn't exist, we would fail to * establish a connection and produce a value of this type.) */ - unsigned int daemonVersion; + WorkerProto::Version daemonVersion; /** * Whether the remote side trusts us or not. diff --git a/src/libstore/serve-protocol.hh b/src/libstore/serve-protocol.hh index e2345d450..dddb7c54d 100644 --- a/src/libstore/serve-protocol.hh +++ b/src/libstore/serve-protocol.hh @@ -30,6 +30,13 @@ struct ServeProto */ enum struct Command : uint64_t; + /** + * Version type for the protocol. + * + * @todo Convert to struct with separate major vs minor fields. + */ + using Version = unsigned int; + /** * A unidirectional read connection, to be used by the read half of the * canonical serializers below. diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index c84060103..e9fc8d957 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -47,6 +47,13 @@ struct WorkerProto */ enum struct Op : uint64_t; + /** + * Version type for the protocol. + * + * @todo Convert to struct with separate major vs minor fields. + */ + using Version = unsigned int; + /** * A unidirectional read connection, to be used by the read half of the * canonical serializers below. diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 9b6c80a75..ea53e49b0 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -818,7 +818,7 @@ static void opServe(Strings opFlags, Strings opArgs) if (magic != SERVE_MAGIC_1) throw Error("protocol mismatch"); out << SERVE_MAGIC_2 << SERVE_PROTOCOL_VERSION; out.flush(); - unsigned int clientVersion = readInt(in); + ServeProto::Version clientVersion = readInt(in); ServeProto::ReadConn rconn { .from = in }; ServeProto::WriteConn wconn { .to = out }; From e36c9175f49c0fac4f0511d4cae5ab991fd9cb7d Mon Sep 17 00:00:00 2001 From: John Ericson Date: Fri, 25 Mar 2022 04:40:49 +0000 Subject: [PATCH 243/299] Add protocol versions to `{Worker,Serve}Proto::*Conn` This will allow us to factor out logic, which is currently scattered inline, into several reusable instances The tests are also updated to support versioning. Currently all Worker and Serve protocol tests are using the minimum version, since no version-specific serialisers have been created yet. But in subsequent commits when that changes, we will test individual versions to ensure complete coverage. --- src/libstore/daemon.cc | 15 ++++- src/libstore/legacy-ssh-store.cc | 2 + src/libstore/path-info.cc | 10 +++- src/libstore/remote-store-connection.hh | 2 + src/libstore/serve-protocol.hh | 8 +-- src/libstore/tests/common-protocol.cc | 73 +++++++++++++++++++++---- src/libstore/tests/protocol.hh | 45 ++++++++------- src/libstore/tests/serve-protocol.cc | 38 +++++++++---- src/libstore/tests/worker-protocol.cc | 49 ++++++++++++----- src/libstore/worker-protocol.hh | 8 +-- src/nix-store/nix-store.cc | 10 +++- 11 files changed, 185 insertions(+), 75 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index fbff64e89..d61e97a64 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -277,8 +277,14 @@ static void performOp(TunnelLogger * logger, ref store, TrustedFlag trusted, RecursiveFlag recursive, WorkerProto::Version clientVersion, Source & from, BufferedSink & to, WorkerProto::Op op) { - WorkerProto::ReadConn rconn { .from = from }; - WorkerProto::WriteConn wconn { .to = to }; + WorkerProto::ReadConn rconn { + .from = from, + .version = clientVersion, + }; + WorkerProto::WriteConn wconn { + .to = to, + .version = clientVersion, + }; switch (op) { @@ -1052,7 +1058,10 @@ void processConnection( auto temp = trusted ? store->isTrustedClient() : std::optional { NotTrusted }; - WorkerProto::WriteConn wconn { .to = to }; + WorkerProto::WriteConn wconn { + .to = to, + .version = clientVersion, + }; WorkerProto::write(*store, wconn, temp); } diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index efced20d0..9143be72e 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -60,6 +60,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor { return ServeProto::ReadConn { .from = from, + .version = remoteVersion, }; } @@ -75,6 +76,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor { return ServeProto::WriteConn { .to = to, + .version = remoteVersion, }; } }; diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index ccb57104f..a9231ce8d 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -141,7 +141,10 @@ ValidPathInfo ValidPathInfo::read(Source & source, const Store & store, unsigned ValidPathInfo info(path, narHash); if (deriver != "") info.deriver = store.parseStorePath(deriver); info.references = WorkerProto::Serialise::read(store, - WorkerProto::ReadConn { .from = source }); + WorkerProto::ReadConn { + .from = source, + .version = format, + }); source >> info.registrationTime >> info.narSize; if (format >= 16) { source >> info.ultimate; @@ -163,7 +166,10 @@ void ValidPathInfo::write( sink << (deriver ? store.printStorePath(*deriver) : "") << narHash.to_string(Base16, false); WorkerProto::write(store, - WorkerProto::WriteConn { .to = sink }, + WorkerProto::WriteConn { + .to = sink, + .version = format, + }, references); sink << registrationTime << narSize; if (format >= 16) { diff --git a/src/libstore/remote-store-connection.hh b/src/libstore/remote-store-connection.hh index 8738e4d95..e4a9cacb9 100644 --- a/src/libstore/remote-store-connection.hh +++ b/src/libstore/remote-store-connection.hh @@ -70,6 +70,7 @@ struct RemoteStore::Connection { return WorkerProto::ReadConn { .from = from, + .version = daemonVersion, }; } @@ -85,6 +86,7 @@ struct RemoteStore::Connection { return WorkerProto::WriteConn { .to = to, + .version = daemonVersion, }; } diff --git a/src/libstore/serve-protocol.hh b/src/libstore/serve-protocol.hh index dddb7c54d..a627c6ad6 100644 --- a/src/libstore/serve-protocol.hh +++ b/src/libstore/serve-protocol.hh @@ -40,23 +40,19 @@ struct ServeProto /** * A unidirectional read connection, to be used by the read half of the * canonical serializers below. - * - * This currently is just a `Source &`, but more fields will be added - * later. */ struct ReadConn { Source & from; + Version version; }; /** * A unidirectional write connection, to be used by the write half of the * canonical serializers below. - * - * This currently is just a `Sink &`, but more fields will be added - * later. */ struct WriteConn { Sink & to; + Version version; }; /** diff --git a/src/libstore/tests/common-protocol.cc b/src/libstore/tests/common-protocol.cc index de57211f0..61c2cb70c 100644 --- a/src/libstore/tests/common-protocol.cc +++ b/src/libstore/tests/common-protocol.cc @@ -13,10 +13,71 @@ namespace nix { const char commonProtoDir[] = "common-protocol"; -using CommonProtoTest = ProtoTest; +class CommonProtoTest : public ProtoTest +{ +public: + /** + * Golden test for `T` reading + */ + template + void readTest(PathView testStem, T value) + { + if (testAccept()) + { + GTEST_SKIP() << "Cannot read golden master because another test is also updating it"; + } + else + { + auto expected = readFile(goldenMaster(testStem)); + + T got = ({ + StringSource from { expected }; + CommonProto::Serialise::read( + *store, + CommonProto::ReadConn { .from = from }); + }); + + ASSERT_EQ(got, value); + } + } + + /** + * Golden test for `T` write + */ + template + void writeTest(PathView testStem, const T & value) + { + auto file = goldenMaster(testStem); + + StringSink to; + CommonProto::write( + *store, + CommonProto::WriteConn { .to = to }, + value); + + if (testAccept()) + { + createDirs(dirOf(file)); + writeFile(file, to.s); + GTEST_SKIP() << "Updating golden master"; + } + else + { + auto expected = readFile(file); + ASSERT_EQ(to.s, expected); + } + } +}; + +#define CHARACTERIZATION_TEST(NAME, STEM, VALUE) \ + TEST_F(CommonProtoTest, NAME ## _read) { \ + readTest(STEM, VALUE); \ + } \ + TEST_F(CommonProtoTest, NAME ## _write) { \ + writeTest(STEM, VALUE); \ + } CHARACTERIZATION_TEST( - CommonProtoTest, string, "string", (std::tuple { @@ -28,7 +89,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, storePath, "store-path", (std::tuple { @@ -37,7 +97,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, contentAddress, "content-address", (std::tuple { @@ -56,7 +115,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, drvOutput, "drv-output", (std::tuple { @@ -71,7 +129,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, realisation, "realisation", (std::tuple { @@ -103,7 +160,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, vector, "vector", (std::tuple, std::vector, std::vector, std::vector>> { @@ -114,7 +170,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, set, "set", (std::tuple, std::set, std::set, std::set>> { @@ -125,7 +180,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, optionalStorePath, "optional-store-path", (std::tuple, std::optional> { @@ -136,7 +190,6 @@ CHARACTERIZATION_TEST( })) CHARACTERIZATION_TEST( - CommonProtoTest, optionalContentAddress, "optional-content-address", (std::tuple, std::optional> { diff --git a/src/libstore/tests/protocol.hh b/src/libstore/tests/protocol.hh index 86a900757..496915745 100644 --- a/src/libstore/tests/protocol.hh +++ b/src/libstore/tests/protocol.hh @@ -9,26 +9,23 @@ namespace nix { template class ProtoTest : public LibStoreTest { - /** - * Read this as simply `using S = Inner::Serialise;`. - * - * See `LengthPrefixedProtoHelper::S` for the same trick, and its - * rationale. - */ - template using S = typename Proto::template Serialise; - -public: +protected: Path unitTestData = getUnitTestData() + "/libstore/" + protocolDir; Path goldenMaster(std::string_view testStem) { return unitTestData + "/" + testStem + ".bin"; } +}; +template +class VersionedProtoTest : public ProtoTest +{ +public: /** * Golden test for `T` reading */ template - void readTest(PathView testStem, T value) + void readTest(PathView testStem, typename Proto::Version version, T value) { if (testAccept()) { @@ -36,13 +33,16 @@ public: } else { - auto expected = readFile(goldenMaster(testStem)); + auto expected = readFile(ProtoTest::goldenMaster(testStem)); T got = ({ StringSource from { expected }; - S::read( - *store, - typename Proto::ReadConn { .from = from }); + Proto::template Serialise::read( + *LibStoreTest::store, + typename Proto::ReadConn { + .from = from, + .version = version, + }); }); ASSERT_EQ(got, value); @@ -53,14 +53,17 @@ public: * Golden test for `T` write */ template - void writeTest(PathView testStem, const T & value) + void writeTest(PathView testStem, typename Proto::Version version, const T & value) { - auto file = goldenMaster(testStem); + auto file = ProtoTest::goldenMaster(testStem); StringSink to; Proto::write( - *store, - typename Proto::WriteConn { .to = to }, + *LibStoreTest::store, + typename Proto::WriteConn { + .to = to, + .version = version, + }, value); if (testAccept()) @@ -77,12 +80,12 @@ public: } }; -#define CHARACTERIZATION_TEST(FIXTURE, NAME, STEM, VALUE) \ +#define VERSIONED_CHARACTERIZATION_TEST(FIXTURE, NAME, STEM, VERSION, VALUE) \ TEST_F(FIXTURE, NAME ## _read) { \ - readTest(STEM, VALUE); \ + readTest(STEM, VERSION, VALUE); \ } \ TEST_F(FIXTURE, NAME ## _write) { \ - writeTest(STEM, VALUE); \ + writeTest(STEM, VERSION, VALUE); \ } } diff --git a/src/libstore/tests/serve-protocol.cc b/src/libstore/tests/serve-protocol.cc index 5d7df56cf..ba798ab1c 100644 --- a/src/libstore/tests/serve-protocol.cc +++ b/src/libstore/tests/serve-protocol.cc @@ -11,14 +11,22 @@ namespace nix { -const char commonProtoDir[] = "serve-protocol"; +const char serveProtoDir[] = "serve-protocol"; -using ServeProtoTest = ProtoTest; +struct ServeProtoTest : VersionedProtoTest +{ + /** + * For serializers that don't care about the minimum version, we + * used the oldest one: 1.0. + */ + ServeProto::Version defaultVersion = 1 << 8 | 0; +}; -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, string, "string", + defaultVersion, (std::tuple { "", "hi", @@ -27,19 +35,21 @@ CHARACTERIZATION_TEST( "oh no \0\0\0 what was that!", })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, storePath, "store-path", + defaultVersion, (std::tuple { StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, contentAddress, "content-address", + defaultVersion, (std::tuple { ContentAddress { .method = TextIngestionMethod {}, @@ -55,10 +65,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, drvOutput, "drv-output", + defaultVersion, (std::tuple { { .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), @@ -70,10 +81,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, realisation, "realisation", + defaultVersion, (std::tuple { Realisation { .id = DrvOutput { @@ -102,10 +114,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, vector, "vector", + defaultVersion, (std::tuple, std::vector, std::vector, std::vector>> { { }, { "" }, @@ -113,10 +126,11 @@ CHARACTERIZATION_TEST( { {}, { "" }, { "", "1", "2" } }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, set, "set", + defaultVersion, (std::tuple, std::set, std::set, std::set>> { { }, { "" }, @@ -124,10 +138,11 @@ CHARACTERIZATION_TEST( { {}, { "" }, { "", "1", "2" } }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, optionalStorePath, "optional-store-path", + defaultVersion, (std::tuple, std::optional> { std::nullopt, std::optional { @@ -135,10 +150,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, optionalContentAddress, "optional-content-address", + defaultVersion, (std::tuple, std::optional> { std::nullopt, std::optional { diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index 59d7ff96d..63fecce96 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -14,12 +14,21 @@ namespace nix { const char workerProtoDir[] = "worker-protocol"; -using WorkerProtoTest = ProtoTest; +struct WorkerProtoTest : VersionedProtoTest +{ + /** + * For serializers that don't care about the minimum version, we + * used the oldest one: 1.0. + */ + WorkerProto::Version defaultVersion = 1 << 8 | 0; +}; -CHARACTERIZATION_TEST( + +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, string, "string", + defaultVersion, (std::tuple { "", "hi", @@ -28,19 +37,21 @@ CHARACTERIZATION_TEST( "oh no \0\0\0 what was that!", })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, storePath, "store-path", + defaultVersion, (std::tuple { StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, contentAddress, "content-address", + defaultVersion, (std::tuple { ContentAddress { .method = TextIngestionMethod {}, @@ -56,10 +67,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, derivedPath, "derived-path", + defaultVersion, (std::tuple { DerivedPath::Opaque { .path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, @@ -72,10 +84,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, drvOutput, "drv-output", + defaultVersion, (std::tuple { { .drvHash = Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), @@ -87,10 +100,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, realisation, "realisation", + defaultVersion, (std::tuple { Realisation { .id = DrvOutput { @@ -119,10 +133,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, buildResult, "build-result", + defaultVersion, ({ using namespace std::literals::chrono_literals; std::tuple t { @@ -177,10 +192,11 @@ CHARACTERIZATION_TEST( t; })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, keyedBuildResult, "keyed-build-result", + defaultVersion, ({ using namespace std::literals::chrono_literals; std::tuple t { @@ -213,20 +229,22 @@ CHARACTERIZATION_TEST( t; })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, optionalTrustedFlag, "optional-trusted-flag", + defaultVersion, (std::tuple, std::optional, std::optional> { std::nullopt, std::optional { Trusted }, std::optional { NotTrusted }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, vector, "vector", + defaultVersion, (std::tuple, std::vector, std::vector, std::vector>> { { }, { "" }, @@ -234,10 +252,11 @@ CHARACTERIZATION_TEST( { {}, { "" }, { "", "1", "2" } }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, set, "set", + defaultVersion, (std::tuple, std::set, std::set, std::set>> { { }, { "" }, @@ -245,10 +264,11 @@ CHARACTERIZATION_TEST( { {}, { "" }, { "", "1", "2" } }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, optionalStorePath, "optional-store-path", + defaultVersion, (std::tuple, std::optional> { std::nullopt, std::optional { @@ -256,10 +276,11 @@ CHARACTERIZATION_TEST( }, })) -CHARACTERIZATION_TEST( +VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, optionalContentAddress, "optional-content-address", + defaultVersion, (std::tuple, std::optional> { std::nullopt, std::optional { diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index e9fc8d957..2d55d926a 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -57,23 +57,19 @@ struct WorkerProto /** * A unidirectional read connection, to be used by the read half of the * canonical serializers below. - * - * This currently is just a `Source &`, but more fields will be added - * later. */ struct ReadConn { Source & from; + Version version; }; /** * A unidirectional write connection, to be used by the write half of the * canonical serializers below. - * - * This currently is just a `Sink &`, but more fields will be added - * later. */ struct WriteConn { Sink & to; + Version version; }; /** diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index ea53e49b0..11d2e86e3 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -820,8 +820,14 @@ static void opServe(Strings opFlags, Strings opArgs) out.flush(); ServeProto::Version clientVersion = readInt(in); - ServeProto::ReadConn rconn { .from = in }; - ServeProto::WriteConn wconn { .to = out }; + ServeProto::ReadConn rconn { + .from = in, + .version = clientVersion, + }; + ServeProto::WriteConn wconn { + .to = out, + .version = clientVersion, + }; auto getBuildSettings = [&]() { // FIXME: changing options here doesn't work if we're From a0f071f1d3b05140fa1c97de5a7612d1b938ffbf Mon Sep 17 00:00:00 2001 From: vicky1999 Date: Wed, 18 Oct 2023 00:12:10 +0530 Subject: [PATCH 244/299] store info sh renamed --- tests/functional/local.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/local.mk b/tests/functional/local.mk index 643351163..3679349f8 100644 --- a/tests/functional/local.mk +++ b/tests/functional/local.mk @@ -113,7 +113,7 @@ nix_tests = \ pass-as-file.sh \ nix-profile.sh \ suggestions.sh \ - store-ping.sh \ + store-info.sh \ fetchClosure.sh \ completions.sh \ flakes/show.sh \ From 891dfb435943f68f165a0c5921f0af8e9e7362e8 Mon Sep 17 00:00:00 2001 From: vicky1999 Date: Wed, 18 Oct 2023 00:14:11 +0530 Subject: [PATCH 245/299] updated store ping to store info in files --- doc/manual/src/advanced-topics/distributed-builds.md | 4 ++-- doc/manual/src/release-notes/rl-2.15.md | 2 +- doc/manual/src/release-notes/rl-2.7.md | 2 +- tests/functional/legacy-ssh-store.sh | 4 ++-- tests/functional/local-store.sh | 4 ++-- tests/functional/nix-copy-ssh-ng.sh | 2 +- tests/functional/remote-store.sh | 8 ++++---- tests/installer/default.nix | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/manual/src/advanced-topics/distributed-builds.md b/doc/manual/src/advanced-topics/distributed-builds.md index 73a113d35..507c5ecb7 100644 --- a/doc/manual/src/advanced-topics/distributed-builds.md +++ b/doc/manual/src/advanced-topics/distributed-builds.md @@ -12,14 +12,14 @@ machine is accessible via SSH and that it has Nix installed. You can test whether connecting to the remote Nix instance works, e.g. ```console -$ nix store ping --store ssh://mac +$ nix store info --store ssh://mac ``` will try to connect to the machine named `mac`. It is possible to specify an SSH identity file as part of the remote store URI, e.g. ```console -$ nix store ping --store ssh://mac?ssh-key=/home/alice/my-key +$ nix store info --store ssh://mac?ssh-key=/home/alice/my-key ``` Since builds should be non-interactive, the key should not have a diff --git a/doc/manual/src/release-notes/rl-2.15.md b/doc/manual/src/release-notes/rl-2.15.md index 133121999..4faf0b143 100644 --- a/doc/manual/src/release-notes/rl-2.15.md +++ b/doc/manual/src/release-notes/rl-2.15.md @@ -44,7 +44,7 @@ (The store always had to check whether it trusts the client, but now the client is informed of the store's decision.) This is useful for scripting interactions with (non-legacy-ssh) remote Nix stores. - `nix store ping` and `nix doctor` now display this information. + `nix store info` and `nix doctor` now display this information. * The new command `nix derivation add` allows adding derivations to the store without involving the Nix language. It exists to round out our collection of basic utility/plumbing commands, and allow for a low barrier-to-entry way of experimenting with alternative front-ends to the Nix Store. diff --git a/doc/manual/src/release-notes/rl-2.7.md b/doc/manual/src/release-notes/rl-2.7.md index 2f3879422..dd649e166 100644 --- a/doc/manual/src/release-notes/rl-2.7.md +++ b/doc/manual/src/release-notes/rl-2.7.md @@ -24,7 +24,7 @@ [repository](https://github.com/NixOS/bundlers) has various bundlers implemented. -* `nix store ping` now reports the version of the remote Nix daemon. +* `nix store info` now reports the version of the remote Nix daemon. * `nix flake {init,new}` now display information about which files have been created. diff --git a/tests/functional/legacy-ssh-store.sh b/tests/functional/legacy-ssh-store.sh index 71b716b84..894efccd4 100644 --- a/tests/functional/legacy-ssh-store.sh +++ b/tests/functional/legacy-ssh-store.sh @@ -1,4 +1,4 @@ source common.sh -# Check that store ping trusted doesn't yet work with ssh:// -nix --store ssh://localhost?remote-store=$TEST_ROOT/other-store store ping --json | jq -e 'has("trusted") | not' +# Check that store info trusted doesn't yet work with ssh:// +nix --store ssh://localhost?remote-store=$TEST_ROOT/other-store store info --json | jq -e 'has("trusted") | not' diff --git a/tests/functional/local-store.sh b/tests/functional/local-store.sh index 89502f864..f7c8eb3f1 100644 --- a/tests/functional/local-store.sh +++ b/tests/functional/local-store.sh @@ -18,5 +18,5 @@ PATH2=$(nix path-info --store "$PWD/x" $CORRECT_PATH) PATH3=$(nix path-info --store "local?root=$PWD/x" $CORRECT_PATH) [ $CORRECT_PATH == $PATH3 ] -# Ensure store ping trusted works with local store -nix --store ./x store ping --json | jq -e '.trusted' +# Ensure store info trusted works with local store +nix --store ./x store info --json | jq -e '.trusted' diff --git a/tests/functional/nix-copy-ssh-ng.sh b/tests/functional/nix-copy-ssh-ng.sh index 45e53c9c0..463b5e0c4 100644 --- a/tests/functional/nix-copy-ssh-ng.sh +++ b/tests/functional/nix-copy-ssh-ng.sh @@ -9,7 +9,7 @@ rm -rf "$remoteRoot" outPath=$(nix-build --no-out-link dependencies.nix) -nix store ping --store "ssh-ng://localhost?store=$NIX_STORE_DIR&remote-store=$remoteRoot%3fstore=$NIX_STORE_DIR%26real=$remoteRoot$NIX_STORE_DIR" +nix store info --store "ssh-ng://localhost?store=$NIX_STORE_DIR&remote-store=$remoteRoot%3fstore=$NIX_STORE_DIR%26real=$remoteRoot$NIX_STORE_DIR" # Regression test for https://github.com/NixOS/nix/issues/6253 nix copy --to "ssh-ng://localhost?store=$NIX_STORE_DIR&remote-store=$remoteRoot%3fstore=$NIX_STORE_DIR%26real=$remoteRoot$NIX_STORE_DIR" $outPath --no-check-sigs & diff --git a/tests/functional/remote-store.sh b/tests/functional/remote-store.sh index 50e6f24b9..5c7bfde46 100644 --- a/tests/functional/remote-store.sh +++ b/tests/functional/remote-store.sh @@ -5,17 +5,17 @@ clearStore # Ensure "fake ssh" remote store works just as legacy fake ssh would. nix --store ssh-ng://localhost?remote-store=$TEST_ROOT/other-store doctor -# Ensure that store ping trusted works with ssh-ng:// -nix --store ssh-ng://localhost?remote-store=$TEST_ROOT/other-store store ping --json | jq -e '.trusted' +# Ensure that store info trusted works with ssh-ng:// +nix --store ssh-ng://localhost?remote-store=$TEST_ROOT/other-store store info --json | jq -e '.trusted' startDaemon if isDaemonNewer "2.15pre0"; then # Ensure that ping works trusted with new daemon - nix store ping --json | jq -e '.trusted' + nix store info --json | jq -e '.trusted' else # And the the field is absent with the old daemon - nix store ping --json | jq -e 'has("trusted") | not' + nix store info --json | jq -e 'has("trusted") | not' fi # Test import-from-derivation through the daemon. diff --git a/tests/installer/default.nix b/tests/installer/default.nix index 49cfd2bcc..238c6ac8e 100644 --- a/tests/installer/default.nix +++ b/tests/installer/default.nix @@ -213,7 +213,7 @@ let source /etc/bashrc || true nix-env --version - nix --extra-experimental-features nix-command store ping + nix --extra-experimental-features nix-command store info out=\$(nix-build --no-substitute -E 'derivation { name = "foo"; system = "x86_64-linux"; builder = "/bin/sh"; args = ["-c" "echo foobar > \$out"]; }') [[ \$(cat \$out) = foobar ]] From ea38605d116e1c8db55a8a8cb7400724b931458d Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 18 Oct 2023 15:32:31 +0200 Subject: [PATCH 246/299] Introduce FSInputAccessor and use it Backported from the lazy-trees branch. Note that this doesn't yet use the access control features of FSInputAccessor. --- src/libexpr/attr-path.cc | 2 +- src/libexpr/eval.cc | 35 +++-- src/libexpr/eval.hh | 3 + src/libexpr/flake/flake.cc | 6 +- src/libexpr/nixexpr.hh | 7 +- src/libexpr/parser.y | 9 +- src/libexpr/paths.cc | 3 +- src/libexpr/primops.cc | 18 +-- src/libexpr/tests/json.cc | 2 +- src/libexpr/tests/libexpr.hh | 15 ++- src/libexpr/value.hh | 17 ++- src/libfetchers/fs-input-accessor.cc | 141 ++++++++++++++++++++ src/libfetchers/fs-input-accessor.hh | 33 +++++ src/libfetchers/input-accessor.cc | 189 ++++++++++++++++++++------- src/libfetchers/input-accessor.hh | 105 ++++++++++++--- src/libstore/store-api.cc | 14 +- src/libstore/store-api.hh | 15 ++- src/nix/main.cc | 14 +- 18 files changed, 502 insertions(+), 126 deletions(-) create mode 100644 src/libfetchers/fs-input-accessor.cc create mode 100644 src/libfetchers/fs-input-accessor.hh diff --git a/src/libexpr/attr-path.cc b/src/libexpr/attr-path.cc index ab654c1b0..d12345710 100644 --- a/src/libexpr/attr-path.cc +++ b/src/libexpr/attr-path.cc @@ -132,7 +132,7 @@ std::pair findPackageFilename(EvalState & state, Value & v if (colon == std::string::npos) fail(); std::string filename(fn, 0, colon); auto lineno = std::stoi(std::string(fn, colon + 1, std::string::npos)); - return {CanonPath(fn.substr(0, colon)), lineno}; + return {SourcePath{path.accessor, CanonPath(fn.substr(0, colon))}, lineno}; } catch (std::invalid_argument & e) { fail(); abort(); diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 511a5f434..87791fa52 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -12,6 +12,7 @@ #include "function-trace.hh" #include "profiles.hh" #include "print.hh" +#include "fs-input-accessor.hh" #include #include @@ -503,6 +504,18 @@ EvalState::EvalState( , sOutputSpecified(symbols.create("outputSpecified")) , repair(NoRepair) , emptyBindings(0) + , rootFS( + makeFSInputAccessor( + CanonPath::root, + evalSettings.restrictEval || evalSettings.pureEval + ? std::optional>(std::set()) + : std::nullopt, + [](const CanonPath & path) -> RestrictedPathError { + auto modeInformation = evalSettings.pureEval + ? "in pure evaluation mode (use '--impure' to override)" + : "in restricted mode"; + throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation); + })) , derivationInternal(rootPath(CanonPath("/builtin/derivation.nix"))) , store(store) , buildStore(buildStore ? buildStore : store) @@ -518,6 +531,8 @@ EvalState::EvalState( , baseEnv(allocEnv(128)) , staticBaseEnv{std::make_shared(false, nullptr)} { + rootFS->allowPath(CanonPath::root); // FIXME + countCalls = getEnv("NIX_COUNT_CALLS").value_or("0") != "0"; assert(gcInitialised); @@ -599,7 +614,7 @@ SourcePath EvalState::checkSourcePath(const SourcePath & path_) */ Path abspath = canonPath(path_.path.abs()); - if (hasPrefix(abspath, corepkgsPrefix)) return CanonPath(abspath); + if (hasPrefix(abspath, corepkgsPrefix)) return rootPath(CanonPath(abspath)); for (auto & i : *allowedPaths) { if (isDirOrInDir(abspath, i)) { @@ -617,7 +632,7 @@ SourcePath EvalState::checkSourcePath(const SourcePath & path_) /* Resolve symlinks. */ debug("checking access to '%s'", abspath); - SourcePath path = CanonPath(canonPath(abspath, true)); + SourcePath path = rootPath(CanonPath(canonPath(abspath, true))); for (auto & i : *allowedPaths) { if (isDirOrInDir(path.path.abs(), i)) { @@ -649,12 +664,12 @@ void EvalState::checkURI(const std::string & uri) /* If the URI is a path, then check it against allowedPaths as well. */ if (hasPrefix(uri, "/")) { - checkSourcePath(CanonPath(uri)); + checkSourcePath(rootPath(CanonPath(uri))); return; } if (hasPrefix(uri, "file://")) { - checkSourcePath(CanonPath(std::string(uri, 7))); + checkSourcePath(rootPath(CanonPath(std::string(uri, 7)))); return; } @@ -950,7 +965,7 @@ void Value::mkStringMove(const char * s, const NixStringContext & context) void Value::mkPath(const SourcePath & path) { - mkPath(makeImmutableString(path.path.abs())); + mkPath(&*path.accessor, makeImmutableString(path.path.abs())); } @@ -2037,7 +2052,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) else if (firstType == nPath) { if (!context.empty()) state.error("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow(); - v.mkPath(CanonPath(canonPath(str()))); + v.mkPath(state.rootPath(CanonPath(canonPath(str())))); } else v.mkStringMove(c_str(), context); } @@ -2236,7 +2251,7 @@ BackedStringView EvalState::coerceToString( !canonicalizePath && !copyToStore ? // FIXME: hack to preserve path literals that end in a // slash, as in /foo/${x}. - v._path + v._path.path : copyToStore ? store->printStorePath(copyPathToStore(context, v.path())) : std::string(v.path().path.abs()); @@ -2329,7 +2344,7 @@ SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned(); if (path == "" || path[0] != '/') error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow(); - return CanonPath(path); + return rootPath(CanonPath(path)); } @@ -2429,7 +2444,9 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v return v1.string_view().compare(v2.string_view()) == 0; case nPath: - return strcmp(v1._path, v2._path) == 0; + return + v1._path.accessor == v2._path.accessor + && strcmp(v1._path.path, v2._path.path) == 0; case nNull: return true; diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index c95558952..4bb0a6a0b 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -24,6 +24,7 @@ class EvalState; class StorePath; struct SingleDerivedPath; enum RepairFlag : bool; +struct FSInputAccessor; /** @@ -211,6 +212,8 @@ public: Bindings emptyBindings; + const ref rootFS; + const SourcePath derivationInternal; /** diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index a6212c12f..2f91ad433 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -223,9 +223,9 @@ static Flake getFlake( throw Error("source tree referenced by '%s' does not contain a '%s/flake.nix' file", lockedRef, lockedRef.subdir); Value vInfo; - state.evalFile(CanonPath(flakeFile), vInfo, true); // FIXME: symlink attack + state.evalFile(state.rootPath(CanonPath(flakeFile)), vInfo, true); // FIXME: symlink attack - expectType(state, nAttrs, vInfo, state.positions.add({CanonPath(flakeFile)}, 1, 1)); + expectType(state, nAttrs, vInfo, state.positions.add({state.rootPath(CanonPath(flakeFile))}, 1, 1)); if (auto description = vInfo.attrs->get(state.sDescription)) { expectType(state, nString, *description->value, description->pos); @@ -741,7 +741,7 @@ void callFlake(EvalState & state, state.vCallFlake = allocRootValue(state.allocValue()); state.eval(state.parseExprFromString( #include "call-flake.nix.gen.hh" - , CanonPath::root), **state.vCallFlake); + , state.rootPath(CanonPath::root)), **state.vCallFlake); } state.callFunction(**state.vCallFlake, *vLocks, *vTmp1, noPos); diff --git a/src/libexpr/nixexpr.hh b/src/libexpr/nixexpr.hh index e766996c8..10099d49e 100644 --- a/src/libexpr/nixexpr.hh +++ b/src/libexpr/nixexpr.hh @@ -20,7 +20,6 @@ MakeError(Abort, EvalError); MakeError(TypeError, EvalError); MakeError(UndefinedVarError, Error); MakeError(MissingArgumentError, EvalError); -MakeError(RestrictedPathError, Error); /** * Position objects. @@ -200,9 +199,13 @@ struct ExprString : Expr struct ExprPath : Expr { + ref accessor; std::string s; Value v; - ExprPath(std::string s) : s(std::move(s)) { v.mkPath(this->s.c_str()); }; + ExprPath(ref accessor, std::string s) : accessor(accessor), s(std::move(s)) + { + v.mkPath(&*accessor, this->s.c_str()); + } Value * maybeThunk(EvalState & state, Env & env) override; COMMON_METHODS }; diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 70228e1e2..78436c6a2 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -64,6 +64,7 @@ struct StringToken { #include "parser-tab.hh" #include "lexer-tab.hh" +#include "fs-input-accessor.hh" YY_DECL; @@ -520,7 +521,7 @@ path_start /* add back in the trailing '/' to the first segment */ if ($1.p[$1.l-1] == '/' && $1.l > 1) path += "/"; - $$ = new ExprPath(std::move(path)); + $$ = new ExprPath(ref(data->state.rootFS), std::move(path)); } | HPATH { if (evalSettings.pureEval) { @@ -530,7 +531,7 @@ path_start ); } Path path(getHome() + std::string($1.p + 1, $1.l - 1)); - $$ = new ExprPath(std::move(path)); + $$ = new ExprPath(ref(data->state.rootFS), std::move(path)); } ; @@ -756,11 +757,11 @@ SourcePath EvalState::findFile(const SearchPath & searchPath, const std::string_ auto r = *rOpt; Path res = suffix == "" ? r : concatStrings(r, "/", suffix); - if (pathExists(res)) return CanonPath(canonPath(res)); + if (pathExists(res)) return rootPath(CanonPath(canonPath(res))); } if (hasPrefix(path, "nix/")) - return CanonPath(concatStrings(corepkgsPrefix, path.substr(4))); + return rootPath(CanonPath(concatStrings(corepkgsPrefix, path.substr(4)))); debugThrow(ThrownError({ .msg = hintfmt(evalSettings.pureEval diff --git a/src/libexpr/paths.cc b/src/libexpr/paths.cc index b6a696f47..099607638 100644 --- a/src/libexpr/paths.cc +++ b/src/libexpr/paths.cc @@ -1,10 +1,11 @@ #include "eval.hh" +#include "fs-input-accessor.hh" namespace nix { SourcePath EvalState::rootPath(CanonPath path) { - return path; + return {rootFS, std::move(path)}; } } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 3bed7c5aa..dd3fa12ee 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -202,7 +202,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v state.vImportedDrvToDerivation = allocRootValue(state.allocValue()); state.eval(state.parseExprFromString( #include "imported-drv-to-derivation.nix.gen.hh" - , CanonPath::root), **state.vImportedDrvToDerivation); + , state.rootPath(CanonPath::root)), **state.vImportedDrvToDerivation); } state.forceFunction(**state.vImportedDrvToDerivation, pos, "while evaluating imported-drv-to-derivation.nix.gen.hh"); @@ -213,7 +213,7 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v else if (path2 == corepkgsPrefix + "fetchurl.nix") { state.eval(state.parseExprFromString( #include "fetchurl.nix.gen.hh" - , CanonPath::root), v); + , state.rootPath(CanonPath::root)), v); } else { @@ -599,7 +599,8 @@ struct CompareValues case nString: return v1->string_view().compare(v2->string_view()) < 0; case nPath: - return strcmp(v1->_path, v2->_path) < 0; + // FIXME: handle accessor? + return strcmp(v1->_path.path, v2->_path.path) < 0; case nList: // Lexicographic comparison for (size_t i = 0;; i++) { @@ -2203,7 +2204,7 @@ static void addPath( path = evalSettings.pureEval && expectedHash ? path - : state.checkSourcePath(CanonPath(path)).path.abs(); + : state.checkSourcePath(state.rootPath(CanonPath(path))).path.abs(); PathFilter filter = filterFun ? ([&](const Path & path) { auto st = lstat(path); @@ -2236,9 +2237,10 @@ static void addPath( }); if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { - StorePath dstPath = settings.readOnlyMode - ? state.store->computeStorePathForPath(name, path, method, htSHA256, filter).first - : state.store->addToStore(name, path, method, htSHA256, filter, state.repair, refs); + // FIXME + if (method != FileIngestionMethod::Recursive) + throw Error("'recursive = false' is not implemented"); + auto dstPath = state.rootPath(CanonPath(path)).fetchToStore(state.store, name, &filter, state.repair); if (expectedHash && expectedStorePath != dstPath) state.debugThrowLastTrace(Error("store path mismatch in (possibly filtered) path added from '%s'", path)); state.allowAndSetStorePathString(dstPath, v); @@ -4447,7 +4449,7 @@ void EvalState::createBaseEnv() // the parser needs two NUL bytes as terminators; one of them // is implied by being a C string. "\0"; - eval(parse(code, sizeof(code), derivationInternal, {CanonPath::root}, staticBaseEnv), *vDerivation); + eval(parse(code, sizeof(code), derivationInternal, rootPath(CanonPath::root), staticBaseEnv), *vDerivation); } diff --git a/src/libexpr/tests/json.cc b/src/libexpr/tests/json.cc index 7586bdd9b..f4cc118d6 100644 --- a/src/libexpr/tests/json.cc +++ b/src/libexpr/tests/json.cc @@ -62,7 +62,7 @@ namespace nix { // not supported by store 'dummy'" thrown in the test body. TEST_F(JSONValueTest, DISABLED_Path) { Value v; - v.mkPath("test"); + v.mkPath(state.rootPath(CanonPath("/test"))); ASSERT_EQ(getJSONValue(v), "\"/nix/store/g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-x\""); } } /* namespace nix */ diff --git a/src/libexpr/tests/libexpr.hh b/src/libexpr/tests/libexpr.hh index 97b05ac46..968431446 100644 --- a/src/libexpr/tests/libexpr.hh +++ b/src/libexpr/tests/libexpr.hh @@ -103,14 +103,17 @@ namespace nix { } MATCHER_P(IsPathEq, p, fmt("Is a path equal to \"%1%\"", p)) { - if (arg.type() != nPath) { - *result_listener << "Expected a path got " << arg.type(); - return false; - } else if (std::string_view(arg._path) != p) { - *result_listener << "Expected a path that equals \"" << p << "\" but got: " << arg.c_str(); + if (arg.type() != nPath) { + *result_listener << "Expected a path got " << arg.type(); + return false; + } else { + auto path = arg.path(); + if (path.path != CanonPath(p)) { + *result_listener << "Expected a path that equals \"" << p << "\" but got: " << path.path; return false; } - return true; + } + return true; } diff --git a/src/libexpr/value.hh b/src/libexpr/value.hh index 7a1165cac..622e613ea 100644 --- a/src/libexpr/value.hh +++ b/src/libexpr/value.hh @@ -189,7 +189,12 @@ public: const char * c_str; const char * * context; // must be in sorted order } string; - const char * _path; + + struct { + InputAccessor * accessor; + const char * path; + } _path; + Bindings * attrs; struct { size_t size; @@ -286,11 +291,12 @@ public: void mkPath(const SourcePath & path); - inline void mkPath(const char * path) + inline void mkPath(InputAccessor * accessor, const char * path) { clearValue(); internalType = tPath; - _path = path; + _path.accessor = accessor; + _path.path = path; } inline void mkNull() @@ -437,7 +443,10 @@ public: SourcePath path() const { assert(internalType == tPath); - return SourcePath{CanonPath(_path)}; + return SourcePath { + .accessor = ref(_path.accessor->shared_from_this()), + .path = CanonPath(CanonPath::unchecked_t(), _path.path) + }; } std::string_view string_view() const diff --git a/src/libfetchers/fs-input-accessor.cc b/src/libfetchers/fs-input-accessor.cc new file mode 100644 index 000000000..a35955465 --- /dev/null +++ b/src/libfetchers/fs-input-accessor.cc @@ -0,0 +1,141 @@ +#include "fs-input-accessor.hh" +#include "store-api.hh" + +namespace nix { + +struct FSInputAccessorImpl : FSInputAccessor +{ + CanonPath root; + std::optional> allowedPaths; + MakeNotAllowedError makeNotAllowedError; + + FSInputAccessorImpl( + const CanonPath & root, + std::optional> && allowedPaths, + MakeNotAllowedError && makeNotAllowedError) + : root(root) + , allowedPaths(std::move(allowedPaths)) + , makeNotAllowedError(std::move(makeNotAllowedError)) + { + } + + std::string readFile(const CanonPath & path) override + { + auto absPath = makeAbsPath(path); + checkAllowed(absPath); + return nix::readFile(absPath.abs()); + } + + bool pathExists(const CanonPath & path) override + { + auto absPath = makeAbsPath(path); + return isAllowed(absPath) && nix::pathExists(absPath.abs()); + } + + Stat lstat(const CanonPath & path) override + { + auto absPath = makeAbsPath(path); + checkAllowed(absPath); + auto st = nix::lstat(absPath.abs()); + return Stat { + .type = + S_ISREG(st.st_mode) ? tRegular : + S_ISDIR(st.st_mode) ? tDirectory : + S_ISLNK(st.st_mode) ? tSymlink : + tMisc, + .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR + }; + } + + DirEntries readDirectory(const CanonPath & path) override + { + auto absPath = makeAbsPath(path); + checkAllowed(absPath); + DirEntries res; + for (auto & entry : nix::readDirectory(absPath.abs())) { + std::optional type; + switch (entry.type) { + case DT_REG: type = Type::tRegular; break; + case DT_LNK: type = Type::tSymlink; break; + case DT_DIR: type = Type::tDirectory; break; + } + if (isAllowed(absPath + entry.name)) + res.emplace(entry.name, type); + } + return res; + } + + std::string readLink(const CanonPath & path) override + { + auto absPath = makeAbsPath(path); + checkAllowed(absPath); + return nix::readLink(absPath.abs()); + } + + CanonPath makeAbsPath(const CanonPath & path) + { + return root + path; + } + + void checkAllowed(const CanonPath & absPath) override + { + if (!isAllowed(absPath)) + throw makeNotAllowedError + ? makeNotAllowedError(absPath) + : RestrictedPathError("access to path '%s' is forbidden", absPath); + } + + bool isAllowed(const CanonPath & absPath) + { + if (!absPath.isWithin(root)) + return false; + + if (allowedPaths) { + auto p = absPath.removePrefix(root); + if (!p.isAllowed(*allowedPaths)) + return false; + } + + return true; + } + + void allowPath(CanonPath path) override + { + if (allowedPaths) + allowedPaths->insert(std::move(path)); + } + + bool hasAccessControl() override + { + return (bool) allowedPaths; + } + + std::optional getPhysicalPath(const CanonPath & path) override + { + return makeAbsPath(path); + } +}; + +ref makeFSInputAccessor( + const CanonPath & root, + std::optional> && allowedPaths, + MakeNotAllowedError && makeNotAllowedError) +{ + return make_ref(root, std::move(allowedPaths), std::move(makeNotAllowedError)); +} + +ref makeStorePathAccessor( + ref store, + const StorePath & storePath, + MakeNotAllowedError && makeNotAllowedError) +{ + return makeFSInputAccessor(CanonPath(store->toRealPath(storePath)), {}, std::move(makeNotAllowedError)); +} + +SourcePath getUnfilteredRootPath(CanonPath path) +{ + static auto rootFS = makeFSInputAccessor(CanonPath::root); + return {rootFS, path}; +} + +} diff --git a/src/libfetchers/fs-input-accessor.hh b/src/libfetchers/fs-input-accessor.hh new file mode 100644 index 000000000..19a5211c8 --- /dev/null +++ b/src/libfetchers/fs-input-accessor.hh @@ -0,0 +1,33 @@ +#pragma once + +#include "input-accessor.hh" + +namespace nix { + +class StorePath; +class Store; + +struct FSInputAccessor : InputAccessor +{ + virtual void checkAllowed(const CanonPath & absPath) = 0; + + virtual void allowPath(CanonPath path) = 0; + + virtual bool hasAccessControl() = 0; +}; + +typedef std::function MakeNotAllowedError; + +ref makeFSInputAccessor( + const CanonPath & root, + std::optional> && allowedPaths = {}, + MakeNotAllowedError && makeNotAllowedError = {}); + +ref makeStorePathAccessor( + ref store, + const StorePath & storePath, + MakeNotAllowedError && makeNotAllowedError = {}); + +SourcePath getUnfilteredRootPath(CanonPath path); + +} diff --git a/src/libfetchers/input-accessor.cc b/src/libfetchers/input-accessor.cc index f37a8058b..fec16e99f 100644 --- a/src/libfetchers/input-accessor.cc +++ b/src/libfetchers/input-accessor.cc @@ -3,12 +3,149 @@ namespace nix { +static std::atomic nextNumber{0}; + +InputAccessor::InputAccessor() + : number(++nextNumber) +{ +} + +// FIXME: merge with archive.cc. +void InputAccessor::dumpPath( + const CanonPath & path, + Sink & sink, + PathFilter & filter) +{ + auto dumpContents = [&](const CanonPath & path) + { + // FIXME: pipe + auto s = readFile(path); + sink << "contents" << s.size(); + sink(s); + writePadding(s.size(), sink); + }; + + std::function dump; + + dump = [&](const CanonPath & path) { + checkInterrupt(); + + auto st = lstat(path); + + sink << "("; + + if (st.type == tRegular) { + sink << "type" << "regular"; + if (st.isExecutable) + sink << "executable" << ""; + dumpContents(path); + } + + else if (st.type == tDirectory) { + sink << "type" << "directory"; + + /* If we're on a case-insensitive system like macOS, undo + the case hack applied by restorePath(). */ + std::map unhacked; + for (auto & i : readDirectory(path)) + if (/* archiveSettings.useCaseHack */ false) { // FIXME + std::string name(i.first); + size_t pos = i.first.find(caseHackSuffix); + if (pos != std::string::npos) { + debug("removing case hack suffix from '%s'", path + i.first); + name.erase(pos); + } + if (!unhacked.emplace(name, i.first).second) + throw Error("file name collision in between '%s' and '%s'", + (path + unhacked[name]), + (path + i.first)); + } else + unhacked.emplace(i.first, i.first); + + for (auto & i : unhacked) + if (filter((path + i.first).abs())) { + sink << "entry" << "(" << "name" << i.first << "node"; + dump(path + i.second); + sink << ")"; + } + } + + else if (st.type == tSymlink) + sink << "type" << "symlink" << "target" << readLink(path); + + else throw Error("file '%s' has an unsupported type", path); + + sink << ")"; + }; + + sink << narVersionMagic1; + dump(path); +} + +Hash InputAccessor::hashPath( + const CanonPath & path, + PathFilter & filter, + HashType ht) +{ + HashSink sink(ht); + dumpPath(path, sink, filter); + return sink.finish().first; +} + +StorePath InputAccessor::fetchToStore( + ref store, + const CanonPath & path, + std::string_view name, + PathFilter * filter, + RepairFlag repair) +{ + Activity act(*logger, lvlChatty, actUnknown, fmt("copying '%s' to the store", showPath(path))); + + auto source = sinkToSource([&](Sink & sink) { + dumpPath(path, sink, filter ? *filter : defaultPathFilter); + }); + + auto storePath = + settings.readOnlyMode + ? store->computeStorePathFromDump(*source, name).first + : store->addToStoreFromDump(*source, name, FileIngestionMethod::Recursive, htSHA256, repair); + + return storePath; +} + +std::optional InputAccessor::maybeLstat(const CanonPath & path) +{ + // FIXME: merge these into one operation. + if (!pathExists(path)) + return {}; + return lstat(path); +} + +std::string InputAccessor::showPath(const CanonPath & path) +{ + return path.abs(); +} + +SourcePath InputAccessor::root() +{ + return {ref(shared_from_this()), CanonPath::root}; +} + std::ostream & operator << (std::ostream & str, const SourcePath & path) { str << path.to_string(); return str; } +StorePath SourcePath::fetchToStore( + ref store, + std::string_view name, + PathFilter * filter, + RepairFlag repair) const +{ + return accessor->fetchToStore(store, path, name, filter, repair); +} + std::string_view SourcePath::baseName() const { return path.baseName().value_or("source"); @@ -18,60 +155,12 @@ SourcePath SourcePath::parent() const { auto p = path.parent(); assert(p); - return std::move(*p); -} - -InputAccessor::Stat SourcePath::lstat() const -{ - auto st = nix::lstat(path.abs()); - return InputAccessor::Stat { - .type = - S_ISREG(st.st_mode) ? InputAccessor::tRegular : - S_ISDIR(st.st_mode) ? InputAccessor::tDirectory : - S_ISLNK(st.st_mode) ? InputAccessor::tSymlink : - InputAccessor::tMisc, - .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR - }; -} - -std::optional SourcePath::maybeLstat() const -{ - // FIXME: merge these into one operation. - if (!pathExists()) - return {}; - return lstat(); -} - -InputAccessor::DirEntries SourcePath::readDirectory() const -{ - InputAccessor::DirEntries res; - for (auto & entry : nix::readDirectory(path.abs())) { - std::optional type; - switch (entry.type) { - case DT_REG: type = InputAccessor::Type::tRegular; break; - case DT_LNK: type = InputAccessor::Type::tSymlink; break; - case DT_DIR: type = InputAccessor::Type::tDirectory; break; - } - res.emplace(entry.name, type); - } - return res; -} - -StorePath SourcePath::fetchToStore( - ref store, - std::string_view name, - PathFilter * filter, - RepairFlag repair) const -{ - return - settings.readOnlyMode - ? store->computeStorePathForPath(name, path.abs(), FileIngestionMethod::Recursive, htSHA256, filter ? *filter : defaultPathFilter).first - : store->addToStore(name, path.abs(), FileIngestionMethod::Recursive, htSHA256, filter ? *filter : defaultPathFilter, repair); + return {accessor, std::move(*p)}; } SourcePath SourcePath::resolveSymlinks() const { - SourcePath res(CanonPath::root); + auto res = accessor->root(); int linksAllowed = 1024; diff --git a/src/libfetchers/input-accessor.hh b/src/libfetchers/input-accessor.hh index 5a2f17f62..a0f08e295 100644 --- a/src/libfetchers/input-accessor.hh +++ b/src/libfetchers/input-accessor.hh @@ -5,14 +5,29 @@ #include "archive.hh" #include "canon-path.hh" #include "repair-flag.hh" +#include "hash.hh" namespace nix { +MakeError(RestrictedPathError, Error); + +struct SourcePath; class StorePath; class Store; -struct InputAccessor +struct InputAccessor : public std::enable_shared_from_this { + const size_t number; + + InputAccessor(); + + virtual ~InputAccessor() + { } + + virtual std::string readFile(const CanonPath & path) = 0; + + virtual bool pathExists(const CanonPath & path) = 0; + enum Type { tRegular, tSymlink, tDirectory, /** @@ -32,9 +47,63 @@ struct InputAccessor bool isExecutable = false; // regular files only }; + virtual Stat lstat(const CanonPath & path) = 0; + + std::optional maybeLstat(const CanonPath & path); + typedef std::optional DirEntry; typedef std::map DirEntries; + + virtual DirEntries readDirectory(const CanonPath & path) = 0; + + virtual std::string readLink(const CanonPath & path) = 0; + + virtual void dumpPath( + const CanonPath & path, + Sink & sink, + PathFilter & filter = defaultPathFilter); + + Hash hashPath( + const CanonPath & path, + PathFilter & filter = defaultPathFilter, + HashType ht = htSHA256); + + StorePath fetchToStore( + ref store, + const CanonPath & path, + std::string_view name = "source", + PathFilter * filter = nullptr, + RepairFlag repair = NoRepair); + + /* Return a corresponding path in the root filesystem, if + possible. This is only possible for inputs that are + materialized in the root filesystem. */ + virtual std::optional getPhysicalPath(const CanonPath & path) + { return std::nullopt; } + + bool operator == (const InputAccessor & x) const + { + return number == x.number; + } + + bool operator < (const InputAccessor & x) const + { + return number < x.number; + } + + void setPathDisplay(std::string displayPrefix, std::string displaySuffix = ""); + + virtual std::string showPath(const CanonPath & path); + + SourcePath root(); + + /* Return the maximum last-modified time of the files in this + tree, if available. */ + virtual std::optional getLastModified() + { + return std::nullopt; + } }; /** @@ -45,12 +114,9 @@ struct InputAccessor */ struct SourcePath { + ref accessor; CanonPath path; - SourcePath(CanonPath path) - : path(std::move(path)) - { } - std::string_view baseName() const; /** @@ -64,39 +130,42 @@ struct SourcePath * return its contents; otherwise throw an error. */ std::string readFile() const - { return nix::readFile(path.abs()); } + { return accessor->readFile(path); } /** * Return whether this `SourcePath` denotes a file (of any type) * that exists */ bool pathExists() const - { return nix::pathExists(path.abs()); } + { return accessor->pathExists(path); } /** * Return stats about this `SourcePath`, or throw an exception if * it doesn't exist. */ - InputAccessor::Stat lstat() const; + InputAccessor::Stat lstat() const + { return accessor->lstat(path); } /** * Return stats about this `SourcePath`, or std::nullopt if it * doesn't exist. */ - std::optional maybeLstat() const; + std::optional maybeLstat() const + { return accessor->maybeLstat(path); } /** * If this `SourcePath` denotes a directory (not a symlink), * return its directory entries; otherwise throw an error. */ - InputAccessor::DirEntries readDirectory() const; + InputAccessor::DirEntries readDirectory() const + { return accessor->readDirectory(path); } /** * If this `SourcePath` denotes a symlink, return its target; * otherwise throw an error. */ std::string readLink() const - { return nix::readLink(path.abs()); } + { return accessor->readLink(path); } /** * Dump this `SourcePath` to `sink` as a NAR archive. @@ -104,7 +173,7 @@ struct SourcePath void dumpPath( Sink & sink, PathFilter & filter = defaultPathFilter) const - { return nix::dumpPath(path.abs(), sink, filter); } + { return accessor->dumpPath(path, sink, filter); } /** * Copy this `SourcePath` to the Nix store. @@ -120,7 +189,7 @@ struct SourcePath * it has a physical location. */ std::optional getPhysicalPath() const - { return path; } + { return accessor->getPhysicalPath(path); } std::string to_string() const { return path.abs(); } @@ -129,7 +198,7 @@ struct SourcePath * Append a `CanonPath` to this path. */ SourcePath operator + (const CanonPath & x) const - { return {path + x}; } + { return {accessor, path + x}; } /** * Append a single component `c` to this path. `c` must not @@ -137,21 +206,21 @@ struct SourcePath * and `c`. */ SourcePath operator + (std::string_view c) const - { return {path + c}; } + { return {accessor, path + c}; } bool operator == (const SourcePath & x) const { - return path == x.path; + return std::tie(accessor, path) == std::tie(x.accessor, x.path); } bool operator != (const SourcePath & x) const { - return path != x.path; + return std::tie(accessor, path) != std::tie(x.accessor, x.path); } bool operator < (const SourcePath & x) const { - return path < x.path; + return std::tie(accessor, path) < std::tie(x.accessor, x.path); } /** diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 28689e100..e761c0e96 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -225,12 +225,16 @@ StorePath Store::makeFixedOutputPathFromCA(std::string_view name, const ContentA } -std::pair Store::computeStorePathForPath(std::string_view name, - const Path & srcPath, FileIngestionMethod method, HashType hashAlgo, PathFilter & filter) const +std::pair Store::computeStorePathFromDump( + Source & dump, + std::string_view name, + FileIngestionMethod method, + HashType hashAlgo, + const StorePathSet & references) const { - Hash h = method == FileIngestionMethod::Recursive - ? hashPath(hashAlgo, srcPath, filter).first - : hashFile(hashAlgo, srcPath); + HashSink sink(hashAlgo); + dump.drainInto(sink); + auto h = sink.finish().first; FixedOutputInfo caInfo { .method = method, .hash = h, diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 4e233bac3..bd69999f8 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -292,14 +292,15 @@ public: StorePath makeFixedOutputPathFromCA(std::string_view name, const ContentAddressWithReferences & ca) const; /** - * Preparatory part of addToStore(). - * - * @return the store path to which srcPath is to be copied - * and the cryptographic hash of the contents of srcPath. + * Read-only variant of addToStoreFromDump(). It returns the store + * path to which a NAR or flat file would be written. */ - std::pair computeStorePathForPath(std::string_view name, - const Path & srcPath, FileIngestionMethod method = FileIngestionMethod::Recursive, - HashType hashAlgo = htSHA256, PathFilter & filter = defaultPathFilter) const; + std::pair computeStorePathFromDump( + Source & dump, + std::string_view name, + FileIngestionMethod method = FileIngestionMethod::Recursive, + HashType hashAlgo = htSHA256, + const StorePathSet & references = {}) const; /** * Preparatory part of addTextToStore(). diff --git a/src/nix/main.cc b/src/nix/main.cc index 879baa5f5..6cd7ace51 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -204,30 +204,30 @@ static void showHelp(std::vector subcommand, NixArgs & toplevel) auto vGenerateManpage = state.allocValue(); state.eval(state.parseExprFromString( #include "generate-manpage.nix.gen.hh" - , CanonPath::root), *vGenerateManpage); + , state.rootPath(CanonPath::root)), *vGenerateManpage); auto vUtils = state.allocValue(); state.cacheFile( - CanonPath("/utils.nix"), CanonPath("/utils.nix"), + state.rootPath(CanonPath("/utils.nix")), state.rootPath(CanonPath("/utils.nix")), state.parseExprFromString( #include "utils.nix.gen.hh" - , CanonPath::root), + , state.rootPath(CanonPath::root)), *vUtils); auto vSettingsInfo = state.allocValue(); state.cacheFile( - CanonPath("/generate-settings.nix"), CanonPath("/generate-settings.nix"), + state.rootPath(CanonPath("/generate-settings.nix")), state.rootPath(CanonPath("/generate-settings.nix")), state.parseExprFromString( #include "generate-settings.nix.gen.hh" - , CanonPath::root), + , state.rootPath(CanonPath::root)), *vSettingsInfo); auto vStoreInfo = state.allocValue(); state.cacheFile( - CanonPath("/generate-store-info.nix"), CanonPath("/generate-store-info.nix"), + state.rootPath(CanonPath("/generate-store-info.nix")), state.rootPath(CanonPath("/generate-store-info.nix")), state.parseExprFromString( #include "generate-store-info.nix.gen.hh" - , CanonPath::root), + , state.rootPath(CanonPath::root)), *vStoreInfo); auto vDump = state.allocValue(); From df73c6eb8cb14fe11f2d5b27e2e28135c7c5dbaf Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 18 Oct 2023 17:34:58 +0200 Subject: [PATCH 247/299] Introduce MemoryInputAccessor and use it for corepkgs MemoryInputAccessor is an in-memory virtual filesystem that returns files like . This removes the need for special hacks to handle those files. --- doc/manual/generate-manpage.nix | 2 +- doc/manual/local.mk | 2 +- src/libexpr/eval.cc | 77 ++++++++++++++++-------- src/libexpr/eval.hh | 32 +++++----- src/libexpr/flake/flake.cc | 10 +-- src/libexpr/parser.y | 5 +- src/libexpr/primops.cc | 29 +++------ src/libexpr/tests/error_traces.cc | 14 ++--- src/libfetchers/memory-input-accessor.cc | 54 +++++++++++++++++ src/libfetchers/memory-input-accessor.hh | 15 +++++ src/nix/main.cc | 34 ++++------- 11 files changed, 176 insertions(+), 98 deletions(-) create mode 100644 src/libfetchers/memory-input-accessor.cc create mode 100644 src/libfetchers/memory-input-accessor.hh diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index d81eac182..14136016d 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -2,7 +2,7 @@ let inherit (builtins) attrNames attrValues fromJSON listToAttrs mapAttrs groupBy concatStringsSep concatMap length lessThan replaceStrings sort; - inherit (import ./utils.nix) attrsToList concatStrings optionalString filterAttrs trim squash unique; + inherit (import ) attrsToList concatStrings optionalString filterAttrs trim squash unique; showStoreDocs = import ./generate-store-info.nix; in diff --git a/doc/manual/local.mk b/doc/manual/local.mk index 702fb1ff8..2e9f08678 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -32,7 +32,7 @@ dummy-env = env -i \ NIX_STATE_DIR=/dummy \ NIX_CONFIG='cores = 0' -nix-eval = $(dummy-env) $(bindir)/nix eval --experimental-features nix-command -I nix/corepkgs=corepkgs --store dummy:// --impure --raw +nix-eval = $(dummy-env) $(bindir)/nix eval --experimental-features nix-command -I nix=doc/manual --store dummy:// --impure --raw # re-implement mdBook's include directive to make it usable for terminal output and for proper @docroot@ substitution define process-includes diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 87791fa52..a28bb2101 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -13,6 +13,7 @@ #include "profiles.hh" #include "print.hh" #include "fs-input-accessor.hh" +#include "memory-input-accessor.hh" #include #include @@ -516,7 +517,16 @@ EvalState::EvalState( : "in restricted mode"; throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation); })) - , derivationInternal(rootPath(CanonPath("/builtin/derivation.nix"))) + , corepkgsFS(makeMemoryInputAccessor()) + , internalFS(makeMemoryInputAccessor()) + , derivationInternal{corepkgsFS->addFile( + CanonPath("derivation-internal.nix"), + #include "primops/derivation.nix.gen.hh" + )} + , callFlakeInternal{internalFS->addFile( + CanonPath("call-flake.nix"), + #include "flake/call-flake.nix.gen.hh" + )} , store(store) , buildStore(buildStore ? buildStore : store) , debugRepl(nullptr) @@ -531,7 +541,8 @@ EvalState::EvalState( , baseEnv(allocEnv(128)) , staticBaseEnv{std::make_shared(false, nullptr)} { - rootFS->allowPath(CanonPath::root); // FIXME + // For now, don't rely on FSInputAccessor for access control. + rootFS->allowPath(CanonPath::root); countCalls = getEnv("NIX_COUNT_CALLS").value_or("0") != "0"; @@ -570,6 +581,11 @@ EvalState::EvalState( } } + corepkgsFS->addFile( + CanonPath("fetchurl.nix"), + #include "fetchurl.nix.gen.hh" + ); + createBaseEnv(); } @@ -600,6 +616,7 @@ void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & SourcePath EvalState::checkSourcePath(const SourcePath & path_) { + if (path_.accessor != rootFS) return path_; if (!allowedPaths) return path_; auto i = resolvedPaths.find(path_.path.abs()); @@ -614,8 +631,6 @@ SourcePath EvalState::checkSourcePath(const SourcePath & path_) */ Path abspath = canonPath(path_.path.abs()); - if (hasPrefix(abspath, corepkgsPrefix)) return rootPath(CanonPath(abspath)); - for (auto & i : *allowedPaths) { if (isDirOrInDir(abspath, i)) { found = true; @@ -1180,24 +1195,6 @@ void EvalState::evalFile(const SourcePath & path_, Value & v, bool mustBeTrivial if (!e) e = parseExprFromFile(checkSourcePath(resolvedPath)); - cacheFile(path, resolvedPath, e, v, mustBeTrivial); -} - - -void EvalState::resetFileCache() -{ - fileEvalCache.clear(); - fileParseCache.clear(); -} - - -void EvalState::cacheFile( - const SourcePath & path, - const SourcePath & resolvedPath, - Expr * e, - Value & v, - bool mustBeTrivial) -{ fileParseCache[resolvedPath] = e; try { @@ -1226,6 +1223,13 @@ void EvalState::cacheFile( } +void EvalState::resetFileCache() +{ + fileEvalCache.clear(); + fileParseCache.clear(); +} + + void EvalState::eval(Expr * e, Value & v) { e->eval(*this, baseEnv, v); @@ -2341,10 +2345,31 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx) { - auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned(); - if (path == "" || path[0] != '/') - error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow(); - return rootPath(CanonPath(path)); + try { + forceValue(v, pos); + + if (v.type() == nString) { + copyContext(v, context); + auto s = v.string_view(); + if (!hasPrefix(s, "/")) + error("string '%s' doesn't represent an absolute path", s).atPos(pos).debugThrow(); + return rootPath(CanonPath(s)); + } + } catch (Error & e) { + e.addTrace(positions[pos], errorCtx); + throw; + } + + if (v.type() == nPath) + return v.path(); + + if (v.type() == nAttrs) { + auto i = v.attrs->find(sOutPath); + if (i != v.attrs->end()) + return coerceToPath(pos, *i->value, context, errorCtx); + } + + error("cannot coerce %1% to a path", showType(v)).withTrace(pos, errorCtx).debugThrow(); } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 4bb0a6a0b..048dff42b 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -25,6 +25,7 @@ class StorePath; struct SingleDerivedPath; enum RepairFlag : bool; struct FSInputAccessor; +struct MemoryInputAccessor; /** @@ -212,10 +213,26 @@ public: Bindings emptyBindings; + /** + * The accessor for the root filesystem. + */ const ref rootFS; + /** + * The in-memory filesystem for paths. + */ + const ref corepkgsFS; + + /** + * In-memory filesystem for internal, non-user-callable Nix + * expressions like call-flake.nix. + */ + const ref internalFS; + const SourcePath derivationInternal; + const SourcePath callFlakeInternal; + /** * Store used to materialise .drv files. */ @@ -226,7 +243,6 @@ public: */ const ref buildStore; - RootValue vCallFlake = nullptr; RootValue vImportedDrvToDerivation = nullptr; /** @@ -408,16 +424,6 @@ public: */ void evalFile(const SourcePath & path, Value & v, bool mustBeTrivial = false); - /** - * Like `evalFile`, but with an already parsed expression. - */ - void cacheFile( - const SourcePath & path, - const SourcePath & resolvedPath, - Expr * e, - Value & v, - bool mustBeTrivial = false); - void resetFileCache(); /** @@ -427,7 +433,7 @@ public: SourcePath findFile(const SearchPath & searchPath, const std::string_view path, const PosIdx pos = noPos); /** - * Try to resolve a search path value (not the optinal key part) + * Try to resolve a search path value (not the optional key part) * * If the specified search path element is a URI, download it. * @@ -832,8 +838,6 @@ struct InvalidPathError : EvalError #endif }; -static const std::string corepkgsPrefix{"/__corepkgs__/"}; - template void ErrorBuilder::debugThrow() { diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index 2f91ad433..acc84ea4f 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -737,14 +737,10 @@ void callFlake(EvalState & state, vRootSubdir->mkString(lockedFlake.flake.lockedRef.subdir); - if (!state.vCallFlake) { - state.vCallFlake = allocRootValue(state.allocValue()); - state.eval(state.parseExprFromString( - #include "call-flake.nix.gen.hh" - , state.rootPath(CanonPath::root)), **state.vCallFlake); - } + auto vCallFlake = state.allocValue(); + state.evalFile(state.callFlakeInternal, *vCallFlake); - state.callFunction(**state.vCallFlake, *vLocks, *vTmp1, noPos); + state.callFunction(*vCallFlake, *vLocks, *vTmp1, noPos); state.callFunction(*vTmp1, *vRootSrc, *vTmp2, noPos); state.callFunction(*vTmp2, *vRootSubdir, vRes, noPos); } diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 78436c6a2..19812cc49 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -64,7 +64,6 @@ struct StringToken { #include "parser-tab.hh" #include "lexer-tab.hh" -#include "fs-input-accessor.hh" YY_DECL; @@ -650,6 +649,8 @@ formal #include "fetchers.hh" #include "store-api.hh" #include "flake/flake.hh" +#include "fs-input-accessor.hh" +#include "memory-input-accessor.hh" namespace nix { @@ -761,7 +762,7 @@ SourcePath EvalState::findFile(const SearchPath & searchPath, const std::string_ } if (hasPrefix(path, "nix/")) - return rootPath(CanonPath(concatStrings(corepkgsPrefix, path.substr(4)))); + return {corepkgsFS, CanonPath(path.substr(3))}; debugThrow(ThrownError({ .msg = hintfmt(evalSettings.pureEval diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index dd3fa12ee..f1c24a75c 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -121,13 +121,15 @@ static SourcePath realisePath(EvalState & state, const PosIdx pos, Value & v, co auto path = state.coerceToPath(noPos, v, context, "while realising the context of a path"); try { - StringMap rewrites = state.realiseContext(context); - - auto realPath = state.rootPath(CanonPath(state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context))); + if (!context.empty()) { + auto rewrites = state.realiseContext(context); + auto realPath = state.toRealPath(rewriteStrings(path.path.abs(), rewrites), context); + return {path.accessor, CanonPath(realPath)}; + } return flags.checkForPureEval - ? state.checkSourcePath(realPath) - : realPath; + ? state.checkSourcePath(path) + : path; } catch (Error & e) { e.addTrace(state.positions[pos], "while realising the context of path '%s'", path); throw; @@ -210,12 +212,6 @@ static void import(EvalState & state, const PosIdx pos, Value & vPath, Value * v state.forceAttrs(v, pos, "while calling imported-drv-to-derivation.nix.gen.hh"); } - else if (path2 == corepkgsPrefix + "fetchurl.nix") { - state.eval(state.parseExprFromString( - #include "fetchurl.nix.gen.hh" - , state.rootPath(CanonPath::root)), v); - } - else { if (!vScope) state.evalFile(path, v); @@ -1486,7 +1482,7 @@ static void prim_storePath(EvalState & state, const PosIdx pos, Value * * args, })); NixStringContext context; - auto path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).path; + auto path = state.checkSourcePath(state.coerceToPath(pos, *args[0], context, "while evaluating the first argument passed to 'builtins.storePath'")).path; /* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink directly in the store. The latter condition is necessary so e.g. nix-push does the right thing. */ @@ -2257,7 +2253,7 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg { NixStringContext context; auto path = state.coerceToPath(pos, *args[1], context, - "while evaluating the second argument (the path to filter) passed to builtins.filterSource"); + "while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'"); state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource"); addPath(state, pos, path.baseName(), path.path.abs(), args[0], FileIngestionMethod::Recursive, std::nullopt, v, context); } @@ -4444,12 +4440,7 @@ void EvalState::createBaseEnv() /* Note: we have to initialize the 'derivation' constant *after* building baseEnv/staticBaseEnv because it uses 'builtins'. */ - char code[] = - #include "primops/derivation.nix.gen.hh" - // the parser needs two NUL bytes as terminators; one of them - // is implied by being a C string. - "\0"; - eval(parse(code, sizeof(code), derivationInternal, rootPath(CanonPath::root), staticBaseEnv), *vDerivation); + evalFile(derivationInternal, *vDerivation); } diff --git a/src/libexpr/tests/error_traces.cc b/src/libexpr/tests/error_traces.cc index 285651256..6da8a5954 100644 --- a/src/libexpr/tests/error_traces.cc +++ b/src/libexpr/tests/error_traces.cc @@ -295,7 +295,7 @@ namespace nix { TEST_F(ErrorTraceTest, toPath) { ASSERT_TRACE2("toPath []", TypeError, - hintfmt("cannot coerce %s to a string", "a list"), + hintfmt("cannot coerce %s to a path", "a list"), hintfmt("while evaluating the first argument passed to builtins.toPath")); ASSERT_TRACE2("toPath \"foo\"", @@ -309,8 +309,8 @@ namespace nix { TEST_F(ErrorTraceTest, storePath) { ASSERT_TRACE2("storePath true", TypeError, - hintfmt("cannot coerce %s to a string", "a Boolean"), - hintfmt("while evaluating the first argument passed to builtins.storePath")); + hintfmt("cannot coerce %s to a path", "a Boolean"), + hintfmt("while evaluating the first argument passed to 'builtins.storePath'")); } @@ -318,7 +318,7 @@ namespace nix { TEST_F(ErrorTraceTest, pathExists) { ASSERT_TRACE2("pathExists []", TypeError, - hintfmt("cannot coerce %s to a string", "a list"), + hintfmt("cannot coerce %s to a path", "a list"), hintfmt("while realising the context of a path")); ASSERT_TRACE2("pathExists \"zorglub\"", @@ -377,13 +377,13 @@ namespace nix { TEST_F(ErrorTraceTest, filterSource) { ASSERT_TRACE2("filterSource [] []", TypeError, - hintfmt("cannot coerce %s to a string", "a list"), - hintfmt("while evaluating the second argument (the path to filter) passed to builtins.filterSource")); + hintfmt("cannot coerce %s to a path", "a list"), + hintfmt("while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'")); ASSERT_TRACE2("filterSource [] \"foo\"", EvalError, hintfmt("string '%s' doesn't represent an absolute path", "foo"), - hintfmt("while evaluating the second argument (the path to filter) passed to builtins.filterSource")); + hintfmt("while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'")); ASSERT_TRACE2("filterSource [] ./.", TypeError, diff --git a/src/libfetchers/memory-input-accessor.cc b/src/libfetchers/memory-input-accessor.cc new file mode 100644 index 000000000..817d063ba --- /dev/null +++ b/src/libfetchers/memory-input-accessor.cc @@ -0,0 +1,54 @@ +#include "memory-input-accessor.hh" + +namespace nix { + +struct MemoryInputAccessorImpl : MemoryInputAccessor +{ + std::map files; + + std::string readFile(const CanonPath & path) override + { + auto i = files.find(path); + if (i == files.end()) + throw Error("file '%s' does not exist", path); + return i->second; + } + + bool pathExists(const CanonPath & path) override + { + auto i = files.find(path); + return i != files.end(); + } + + Stat lstat(const CanonPath & path) override + { + auto i = files.find(path); + if (i != files.end()) + return Stat { .type = tRegular, .isExecutable = false }; + throw Error("file '%s' does not exist", path); + } + + DirEntries readDirectory(const CanonPath & path) override + { + return {}; + } + + std::string readLink(const CanonPath & path) override + { + throw UnimplementedError("MemoryInputAccessor::readLink"); + } + + SourcePath addFile(CanonPath path, std::string && contents) override + { + files.emplace(path, std::move(contents)); + + return {ref(shared_from_this()), std::move(path)}; + } +}; + +ref makeMemoryInputAccessor() +{ + return make_ref(); +} + +} diff --git a/src/libfetchers/memory-input-accessor.hh b/src/libfetchers/memory-input-accessor.hh new file mode 100644 index 000000000..b75b02bfd --- /dev/null +++ b/src/libfetchers/memory-input-accessor.hh @@ -0,0 +1,15 @@ +#include "input-accessor.hh" + +namespace nix { + +/** + * An input accessor for an in-memory file system. + */ +struct MemoryInputAccessor : InputAccessor +{ + virtual SourcePath addFile(CanonPath path, std::string && contents) = 0; +}; + +ref makeMemoryInputAccessor(); + +} diff --git a/src/nix/main.cc b/src/nix/main.cc index 6cd7ace51..ee3878cc7 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -12,6 +12,7 @@ #include "finally.hh" #include "loggers.hh" #include "markdown.hh" +#include "memory-input-accessor.hh" #include #include @@ -206,29 +207,20 @@ static void showHelp(std::vector subcommand, NixArgs & toplevel) #include "generate-manpage.nix.gen.hh" , state.rootPath(CanonPath::root)), *vGenerateManpage); - auto vUtils = state.allocValue(); - state.cacheFile( - state.rootPath(CanonPath("/utils.nix")), state.rootPath(CanonPath("/utils.nix")), - state.parseExprFromString( - #include "utils.nix.gen.hh" - , state.rootPath(CanonPath::root)), - *vUtils); + state.corepkgsFS->addFile( + CanonPath("utils.nix"), + #include "utils.nix.gen.hh" + ); - auto vSettingsInfo = state.allocValue(); - state.cacheFile( - state.rootPath(CanonPath("/generate-settings.nix")), state.rootPath(CanonPath("/generate-settings.nix")), - state.parseExprFromString( - #include "generate-settings.nix.gen.hh" - , state.rootPath(CanonPath::root)), - *vSettingsInfo); + state.corepkgsFS->addFile( + CanonPath("/generate-settings.nix"), + #include "generate-settings.nix.gen.hh" + ); - auto vStoreInfo = state.allocValue(); - state.cacheFile( - state.rootPath(CanonPath("/generate-store-info.nix")), state.rootPath(CanonPath("/generate-store-info.nix")), - state.parseExprFromString( - #include "generate-store-info.nix.gen.hh" - , state.rootPath(CanonPath::root)), - *vStoreInfo); + state.corepkgsFS->addFile( + CanonPath("/generate-store-info.nix"), + #include "generate-store-info.nix.gen.hh" + ); auto vDump = state.allocValue(); vDump->mkString(toplevel.dumpCli()); From e9ddf0b4008262542b422c76f60c3fb80982cd74 Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Mon, 9 Oct 2023 10:53:47 +0800 Subject: [PATCH 248/299] Simplify parseHashTypeOpt Remove redundant "else" after "return". Use std::nullopt to increase readability. --- src/libutil/hash.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 2c36d9d94..6d56c1989 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -389,10 +389,10 @@ Hash compressHash(const Hash & hash, unsigned int newSize) std::optional parseHashTypeOpt(std::string_view s) { if (s == "md5") return htMD5; - else if (s == "sha1") return htSHA1; - else if (s == "sha256") return htSHA256; - else if (s == "sha512") return htSHA512; - else return std::optional {}; + if (s == "sha1") return htSHA1; + if (s == "sha256") return htSHA256; + if (s == "sha512") return htSHA512; + return std::nullopt; } HashType parseHashType(std::string_view s) From aff177d8609c392b4edf59819a086440cca101a7 Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Mon, 9 Oct 2023 11:00:14 +0800 Subject: [PATCH 249/299] Elaborate the "unknown hash algorithm" error List the allowed hash formats --- src/libexpr/tests/error_traces.cc | 2 +- src/libutil/hash.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libexpr/tests/error_traces.cc b/src/libexpr/tests/error_traces.cc index 285651256..1af601ec8 100644 --- a/src/libexpr/tests/error_traces.cc +++ b/src/libexpr/tests/error_traces.cc @@ -1084,7 +1084,7 @@ namespace nix { ASSERT_TRACE1("hashString \"foo\" \"content\"", UsageError, - hintfmt("unknown hash algorithm '%s'", "foo")); + hintfmt("unknown hash algorithm '%s', expect 'md5', 'sha1', 'sha256', or 'sha512'", "foo")); ASSERT_TRACE2("hashString \"sha256\" {}", TypeError, diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 6d56c1989..251440363 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -401,7 +401,7 @@ HashType parseHashType(std::string_view s) if (opt_h) return *opt_h; else - throw UsageError("unknown hash algorithm '%1%'", s); + throw UsageError("unknown hash algorithm '%1%', expect 'md5', 'sha1', 'sha256', or 'sha512'", s); } std::string_view printHashType(HashType ht) From 838c70f62116328ce01cb41a01886e4f1b9a727f Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Tue, 10 Oct 2023 18:53:01 +0800 Subject: [PATCH 250/299] treewide: Rename hashBase to hashFormat hashBase is ambiguous, since it's not about the digital bases, but about the format of hashes. Base16, Base32 and Base64 are all character maps for binary encoding. Rename the enum Base to HashFormat. Rename variables of type HashFormat from [hash]Base to hashFormat, including CmdHashBase::hashFormat and CmdToBase::hashFormat. --- src/libstore/store-api.cc | 6 ++--- src/libstore/store-api.hh | 2 +- src/libutil/hash.cc | 8 +++---- src/libutil/hash.hh | 4 ++-- src/libutil/tests/hash.cc | 16 +++++++------- src/nix/hash.cc | 46 +++++++++++++++++++-------------------- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 28689e100..fa23f976e 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -938,7 +938,7 @@ StorePathSet Store::exportReferences(const StorePathSet & storePaths, const Stor json Store::pathInfoToJSON(const StorePathSet & storePaths, bool includeImpureInfo, bool showClosureSize, - Base hashBase, + HashFormat hashFormat, AllowInvalidFlag allowInvalid) { json::array_t jsonList = json::array(); @@ -951,7 +951,7 @@ json Store::pathInfoToJSON(const StorePathSet & storePaths, jsonPath["path"] = printStorePath(info->path); jsonPath["valid"] = true; - jsonPath["narHash"] = info->narHash.to_string(hashBase, true); + jsonPath["narHash"] = info->narHash.to_string(hashFormat, true); jsonPath["narSize"] = info->narSize; { @@ -993,7 +993,7 @@ json Store::pathInfoToJSON(const StorePathSet & storePaths, if (!narInfo->url.empty()) jsonPath["url"] = narInfo->url; if (narInfo->fileHash) - jsonPath["downloadHash"] = narInfo->fileHash->to_string(hashBase, true); + jsonPath["downloadHash"] = narInfo->fileHash->to_string(hashFormat, true); if (narInfo->fileSize) jsonPath["downloadSize"] = narInfo->fileSize; if (showClosureSize) diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index 4e233bac3..f30ccc950 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -676,7 +676,7 @@ public: */ nlohmann::json pathInfoToJSON(const StorePathSet & storePaths, bool includeImpureInfo, bool showClosureSize, - Base hashBase = Base32, + HashFormat hashFormat = Base32, AllowInvalidFlag allowInvalid = DisallowInvalid); /** diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 251440363..401a65ec3 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -115,14 +115,14 @@ std::string printHash16or32(const Hash & hash) } -std::string Hash::to_string(Base base, bool includeType) const +std::string Hash::to_string(HashFormat hashFormat, bool includeType) const { std::string s; - if (base == SRI || includeType) { + if (hashFormat == SRI || includeType) { s += printHashType(type); - s += base == SRI ? '-' : ':'; + s += hashFormat == SRI ? '-' : ':'; } - switch (base) { + switch (hashFormat) { case Base16: s += printHash16(*this); break; diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index c3aa5cd81..73c2183ef 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -23,7 +23,7 @@ extern std::set hashTypes; extern const std::string base32Chars; -enum Base : int { Base64, Base32, Base16, SRI }; +enum HashFormat : int { Base64, Base32, Base16, SRI }; struct Hash @@ -114,7 +114,7 @@ public: * or base-64. By default, this is prefixed by the hash type * (e.g. "sha256:"). */ - std::string to_string(Base base, bool includeType) const; + std::string to_string(HashFormat hashFormat, bool includeType) const; std::string gitRev() const { diff --git a/src/libutil/tests/hash.cc b/src/libutil/tests/hash.cc index e4e928b3b..d14e5b26d 100644 --- a/src/libutil/tests/hash.cc +++ b/src/libutil/tests/hash.cc @@ -18,28 +18,28 @@ namespace nix { // values taken from: https://tools.ietf.org/html/rfc1321 auto s1 = ""; auto hash = hashString(HashType::htMD5, s1); - ASSERT_EQ(hash.to_string(Base::Base16, true), "md5:d41d8cd98f00b204e9800998ecf8427e"); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), "md5:d41d8cd98f00b204e9800998ecf8427e"); } TEST(hashString, testKnownMD5Hashes2) { // values taken from: https://tools.ietf.org/html/rfc1321 auto s2 = "abc"; auto hash = hashString(HashType::htMD5, s2); - ASSERT_EQ(hash.to_string(Base::Base16, true), "md5:900150983cd24fb0d6963f7d28e17f72"); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), "md5:900150983cd24fb0d6963f7d28e17f72"); } TEST(hashString, testKnownSHA1Hashes1) { // values taken from: https://tools.ietf.org/html/rfc3174 auto s = "abc"; auto hash = hashString(HashType::htSHA1, s); - ASSERT_EQ(hash.to_string(Base::Base16, true),"sha1:a9993e364706816aba3e25717850c26c9cd0d89d"); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true),"sha1:a9993e364706816aba3e25717850c26c9cd0d89d"); } TEST(hashString, testKnownSHA1Hashes2) { // values taken from: https://tools.ietf.org/html/rfc3174 auto s = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; auto hash = hashString(HashType::htSHA1, s); - ASSERT_EQ(hash.to_string(Base::Base16, true),"sha1:84983e441c3bd26ebaae4aa1f95129e5e54670f1"); + ASSERT_EQ(hash.to_string(HashFormat::Base16, true),"sha1:84983e441c3bd26ebaae4aa1f95129e5e54670f1"); } TEST(hashString, testKnownSHA256Hashes1) { @@ -47,7 +47,7 @@ namespace nix { auto s = "abc"; auto hash = hashString(HashType::htSHA256, s); - ASSERT_EQ(hash.to_string(Base::Base16, true), + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); } @@ -55,7 +55,7 @@ namespace nix { // values taken from: https://tools.ietf.org/html/rfc4634 auto s = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; auto hash = hashString(HashType::htSHA256, s); - ASSERT_EQ(hash.to_string(Base::Base16, true), + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), "sha256:248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"); } @@ -63,7 +63,7 @@ namespace nix { // values taken from: https://tools.ietf.org/html/rfc4634 auto s = "abc"; auto hash = hashString(HashType::htSHA512, s); - ASSERT_EQ(hash.to_string(Base::Base16, true), + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), "sha512:ddaf35a193617abacc417349ae20413112e6fa4e89a9" "7ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd" "454d4423643ce80e2a9ac94fa54ca49f"); @@ -74,7 +74,7 @@ namespace nix { auto s = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu"; auto hash = hashString(HashType::htSHA512, s); - ASSERT_EQ(hash.to_string(Base::Base16, true), + ASSERT_EQ(hash.to_string(HashFormat::Base16, true), "sha512:8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa1" "7299aeadb6889018501d289e4900f7e4331b99dec4b5433a" "c7d329eeb6dd26545e96e55b874be909"); diff --git a/src/nix/hash.cc b/src/nix/hash.cc index 9feca9345..6af6bd19f 100644 --- a/src/nix/hash.cc +++ b/src/nix/hash.cc @@ -11,7 +11,7 @@ using namespace nix; struct CmdHashBase : Command { FileIngestionMethod mode; - Base base = SRI; + HashFormat hashFormat = SRI; bool truncate = false; HashType ht = htSHA256; std::vector paths; @@ -22,25 +22,25 @@ struct CmdHashBase : Command addFlag({ .longName = "sri", .description = "Print the hash in SRI format.", - .handler = {&base, SRI}, + .handler = {&hashFormat, SRI}, }); addFlag({ .longName = "base64", .description = "Print the hash in base-64 format.", - .handler = {&base, Base64}, + .handler = {&hashFormat, Base64}, }); addFlag({ .longName = "base32", .description = "Print the hash in base-32 (Nix-specific) format.", - .handler = {&base, Base32}, + .handler = {&hashFormat, Base32}, }); addFlag({ .longName = "base16", .description = "Print the hash in base-16 format.", - .handler = {&base, Base16}, + .handler = {&hashFormat, Base16}, }); addFlag(Flag::mkHashTypeFlag("type", &ht)); @@ -94,18 +94,18 @@ struct CmdHashBase : Command Hash h = hashSink->finish().first; if (truncate && h.hashSize > 20) h = compressHash(h, 20); - logger->cout(h.to_string(base, base == SRI)); + logger->cout(h.to_string(hashFormat, hashFormat == SRI)); } } }; struct CmdToBase : Command { - Base base; + HashFormat hashFormat; std::optional ht; std::vector args; - CmdToBase(Base base) : base(base) + CmdToBase(HashFormat hashFormat) : hashFormat(hashFormat) { addFlag(Flag::mkHashTypeOptFlag("type", &ht)); expectArgs("strings", &args); @@ -114,16 +114,16 @@ struct CmdToBase : Command std::string description() override { return fmt("convert a hash to %s representation", - base == Base16 ? "base-16" : - base == Base32 ? "base-32" : - base == Base64 ? "base-64" : + hashFormat == Base16 ? "base-16" : + hashFormat == Base32 ? "base-32" : + hashFormat == Base64 ? "base-64" : "SRI"); } void run() override { for (auto s : args) - logger->cout(Hash::parseAny(s, ht).to_string(base, base == SRI)); + logger->cout(Hash::parseAny(s, ht).to_string(hashFormat, hashFormat == SRI)); } }; @@ -162,7 +162,7 @@ static int compatNixHash(int argc, char * * argv) { std::optional ht; bool flat = false; - Base base = Base16; + HashFormat hashFormat = Base16; bool truncate = false; enum { opHash, opTo } op = opHash; std::vector ss; @@ -173,10 +173,10 @@ static int compatNixHash(int argc, char * * argv) else if (*arg == "--version") printVersion("nix-hash"); else if (*arg == "--flat") flat = true; - else if (*arg == "--base16") base = Base16; - else if (*arg == "--base32") base = Base32; - else if (*arg == "--base64") base = Base64; - else if (*arg == "--sri") base = SRI; + else if (*arg == "--base16") hashFormat = Base16; + else if (*arg == "--base32") hashFormat = Base32; + else if (*arg == "--base64") hashFormat = Base64; + else if (*arg == "--sri") hashFormat = SRI; else if (*arg == "--truncate") truncate = true; else if (*arg == "--type") { std::string s = getArg(*arg, arg, end); @@ -184,19 +184,19 @@ static int compatNixHash(int argc, char * * argv) } else if (*arg == "--to-base16") { op = opTo; - base = Base16; + hashFormat = Base16; } else if (*arg == "--to-base32") { op = opTo; - base = Base32; + hashFormat = Base32; } else if (*arg == "--to-base64") { op = opTo; - base = Base64; + hashFormat = Base64; } else if (*arg == "--to-sri") { op = opTo; - base = SRI; + hashFormat = SRI; } else if (*arg != "" && arg->at(0) == '-') return false; @@ -209,14 +209,14 @@ static int compatNixHash(int argc, char * * argv) CmdHashBase cmd(flat ? FileIngestionMethod::Flat : FileIngestionMethod::Recursive); if (!ht.has_value()) ht = htMD5; cmd.ht = ht.value(); - cmd.base = base; + cmd.hashFormat = hashFormat; cmd.truncate = truncate; cmd.paths = ss; cmd.run(); } else { - CmdToBase cmd(base); + CmdToBase cmd(hashFormat); cmd.args = ss; if (ht.has_value()) cmd.ht = ht; cmd.run(); From 5043e6cf4ea537dfe599470797c5b310ab0e94b9 Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Wed, 18 Oct 2023 17:08:06 +0800 Subject: [PATCH 251/299] Document HashFormat --- src/libutil/hash.hh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index 73c2183ef..0db9bcd6d 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -23,7 +23,21 @@ extern std::set hashTypes; extern const std::string base32Chars; -enum HashFormat : int { Base64, Base32, Base16, SRI }; +/** + * @brief Enumeration representing the hash formats. + */ +enum HashFormat : int { + /// @brief Base 64 encoding. + /// @see [IETF RFC 4648, section 4](https://datatracker.ietf.org/doc/html/rfc4648#section-4). + Base64, + /// @brief Nix-specific base-32 encoding. @see base32Chars + Base32, + /// @brief Lowercase hexadecimal encoding. @see base16Chars + Base16, + /// @brief ":", format of the SRI integrity attribute. + /// @see W3C recommendation [Subresource Intergrity](https://www.w3.org/TR/SRI/). + SRI +}; struct Hash From e026f3e1ae533ab476e58258c7d84814189df9ff Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Fri, 13 Oct 2023 09:48:15 +0800 Subject: [PATCH 252/299] treewide: Reference HashFormat members with scope Base* -> HashFormat::Base* --- perl/lib/Nix/Store.xs | 12 +++--- src/libexpr/eval-cache.cc | 2 +- src/libexpr/primops.cc | 4 +- src/libexpr/primops/fetchTree.cc | 4 +- src/libfetchers/fetchers.cc | 4 +- src/libfetchers/git.cc | 4 +- src/libfetchers/github.cc | 8 ++-- src/libfetchers/mercurial.cc | 4 +- src/libfetchers/tarball.cc | 2 +- src/libstore/binary-cache-store.cc | 2 +- src/libstore/build/local-derivation-goal.cc | 6 +-- src/libstore/builtins/fetchurl.cc | 2 +- src/libstore/content-address.cc | 2 +- src/libstore/daemon.cc | 2 +- src/libstore/derivations.cc | 12 +++--- src/libstore/downstream-placeholder.cc | 4 +- src/libstore/export-import.cc | 2 +- src/libstore/gc.cc | 2 +- src/libstore/legacy-ssh-store.cc | 2 +- src/libstore/local-store.cc | 14 +++---- src/libstore/nar-info-disk-cache.cc | 4 +- src/libstore/nar-info.cc | 4 +- src/libstore/optimise-store.cc | 4 +- src/libstore/path-info.cc | 4 +- src/libstore/path.cc | 2 +- src/libstore/realisation.hh | 2 +- src/libstore/remote-store.cc | 2 +- src/libstore/store-api.cc | 6 +-- src/libstore/store-api.hh | 2 +- src/libutil/hash.cc | 16 +++---- src/libutil/hash.hh | 4 +- src/nix-store/nix-store.cc | 8 ++-- src/nix/flake.cc | 8 ++-- src/nix/hash.cc | 46 ++++++++++----------- src/nix/path-info.cc | 2 +- src/nix/prefetch.cc | 4 +- src/nix/verify.cc | 4 +- 37 files changed, 108 insertions(+), 108 deletions(-) diff --git a/perl/lib/Nix/Store.xs b/perl/lib/Nix/Store.xs index e96885e4c..08f812b31 100644 --- a/perl/lib/Nix/Store.xs +++ b/perl/lib/Nix/Store.xs @@ -78,7 +78,7 @@ SV * queryReferences(char * path) SV * queryPathHash(char * path) PPCODE: try { - auto s = store()->queryPathInfo(store()->parseStorePath(path))->narHash.to_string(Base32, true); + auto s = store()->queryPathInfo(store()->parseStorePath(path))->narHash.to_string(HashFormat::Base32, true); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); } catch (Error & e) { croak("%s", e.what()); @@ -104,7 +104,7 @@ SV * queryPathInfo(char * path, int base32) XPUSHs(&PL_sv_undef); else XPUSHs(sv_2mortal(newSVpv(store()->printStorePath(*info->deriver).c_str(), 0))); - auto s = info->narHash.to_string(base32 ? Base32 : Base16, true); + auto s = info->narHash.to_string(base32 ? HashFormat::Base32 : HashFormat::Base16, true); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); mXPUSHi(info->registrationTime); mXPUSHi(info->narSize); @@ -206,7 +206,7 @@ SV * hashPath(char * algo, int base32, char * path) PPCODE: try { Hash h = hashPath(parseHashType(algo), path).first; - auto s = h.to_string(base32 ? Base32 : Base16, false); + auto s = h.to_string(base32 ? HashFormat::Base32 : HashFormat::Base16, false); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); } catch (Error & e) { croak("%s", e.what()); @@ -217,7 +217,7 @@ SV * hashFile(char * algo, int base32, char * path) PPCODE: try { Hash h = hashFile(parseHashType(algo), path); - auto s = h.to_string(base32 ? Base32 : Base16, false); + auto s = h.to_string(base32 ? HashFormat::Base32 : HashFormat::Base16, false); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); } catch (Error & e) { croak("%s", e.what()); @@ -228,7 +228,7 @@ SV * hashString(char * algo, int base32, char * s) PPCODE: try { Hash h = hashString(parseHashType(algo), s); - auto s = h.to_string(base32 ? Base32 : Base16, false); + auto s = h.to_string(base32 ? HashFormat::Base32 : HashFormat::Base16, false); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); } catch (Error & e) { croak("%s", e.what()); @@ -239,7 +239,7 @@ SV * convertHash(char * algo, char * s, int toBase32) PPCODE: try { auto h = Hash::parseAny(s, parseHashType(algo)); - auto s = h.to_string(toBase32 ? Base32 : Base16, false); + auto s = h.to_string(toBase32 ? HashFormat::Base32 : HashFormat::Base16, false); XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0))); } catch (Error & e) { croak("%s", e.what()); diff --git a/src/libexpr/eval-cache.cc b/src/libexpr/eval-cache.cc index 391b32a77..10fc799a9 100644 --- a/src/libexpr/eval-cache.cc +++ b/src/libexpr/eval-cache.cc @@ -50,7 +50,7 @@ struct AttrDb Path cacheDir = getCacheDir() + "/nix/eval-cache-v5"; createDirs(cacheDir); - Path dbPath = cacheDir + "/" + fingerprint.to_string(Base16, false) + ".sqlite"; + Path dbPath = cacheDir + "/" + fingerprint.to_string(HashFormat::Base16, false) + ".sqlite"; state->db = SQLite(dbPath); state->db.isCache(); diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 3bed7c5aa..830579dbf 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1758,7 +1758,7 @@ static void prim_hashFile(EvalState & state, const PosIdx pos, Value * * args, V auto path = realisePath(state, pos, *args[1]); - v.mkString(hashString(*ht, path.readFile()).to_string(Base16, false)); + v.mkString(hashString(*ht, path.readFile()).to_string(HashFormat::Base16, false)); } static RegisterPrimOp primop_hashFile({ @@ -3760,7 +3760,7 @@ static void prim_hashString(EvalState & state, const PosIdx pos, Value * * args, NixStringContext context; // discarded auto s = state.forceString(*args[1], context, pos, "while evaluating the second argument passed to builtins.hashString"); - v.mkString(hashString(*ht, s).to_string(Base16, false)); + v.mkString(hashString(*ht, s).to_string(HashFormat::Base16, false)); } static RegisterPrimOp primop_hashString({ diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 3431ff013..976037ff9 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -31,7 +31,7 @@ void emitTreeAttrs( auto narHash = input.getNarHash(); assert(narHash); - attrs.alloc("narHash").mkString(narHash->to_string(SRI, true)); + attrs.alloc("narHash").mkString(narHash->to_string(HashFormat::SRI, true)); if (input.getType() == "git") attrs.alloc("submodules").mkBool( @@ -297,7 +297,7 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v : hashFile(htSHA256, state.store->toRealPath(storePath)); if (hash != *expectedHash) state.debugThrowLastTrace(EvalError((unsigned int) 102, "hash mismatch in file downloaded from '%s':\n specified: %s\n got: %s", - *url, expectedHash->to_string(Base32, true), hash.to_string(Base32, true))); + *url, expectedHash->to_string(HashFormat::Base32, true), hash.to_string(HashFormat::Base32, true))); } state.allowAndSetStorePathString(storePath, v); diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index e3d3ac562..c54c39cf0 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -147,12 +147,12 @@ std::pair Input::fetch(ref store) const }; auto narHash = store->queryPathInfo(tree.storePath)->narHash; - input.attrs.insert_or_assign("narHash", narHash.to_string(SRI, true)); + input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); if (auto prevNarHash = getNarHash()) { if (narHash != *prevNarHash) throw Error((unsigned int) 102, "NAR hash mismatch in input '%s' (%s), expected '%s', got '%s'", - to_string(), tree.actualPath, prevNarHash->to_string(SRI, true), narHash.to_string(SRI, true)); + to_string(), tree.actualPath, prevNarHash->to_string(HashFormat::SRI, true), narHash.to_string(HashFormat::SRI, true)); } if (auto prevLastModified = getLastModified()) { diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 7e9f34790..c1a6dce43 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -46,7 +46,7 @@ bool touchCacheFile(const Path & path, time_t touch_time) Path getCachePath(std::string_view key) { return getCacheDir() + "/nix/gitv3/" + - hashString(htSHA256, key).to_string(Base32, false); + hashString(htSHA256, key).to_string(HashFormat::Base32, false); } // Returns the name of the HEAD branch. @@ -417,7 +417,7 @@ struct GitInputScheme : InputScheme auto checkHashType = [&](const std::optional & hash) { if (hash.has_value() && !(hash->type == htSHA1 || hash->type == htSHA256)) - throw Error("Hash '%s' is not supported by Git. Supported types are sha1 and sha256.", hash->to_string(Base16, true)); + throw Error("Hash '%s' is not supported by Git. Supported types are sha1 and sha256.", hash->to_string(HashFormat::Base16, true)); }; auto getLockedAttrs = [&]() diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 6c5c792ec..d7450defe 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -125,7 +125,7 @@ struct GitArchiveInputScheme : InputScheme auto path = owner + "/" + repo; assert(!(ref && rev)); if (ref) path += "/" + *ref; - if (rev) path += "/" + rev->to_string(Base16, false); + if (rev) path += "/" + rev->to_string(HashFormat::Base16, false); return ParsedURL { .scheme = type(), .path = path, @@ -296,7 +296,7 @@ struct GitHubInputScheme : GitArchiveInputScheme : "https://api.%s/repos/%s/%s/tarball/%s"; const auto url = fmt(urlFmt, host, getOwner(input), getRepo(input), - input.getRev()->to_string(Base16, false)); + input.getRev()->to_string(HashFormat::Base16, false)); return DownloadUrl { url, headers }; } @@ -362,7 +362,7 @@ struct GitLabInputScheme : GitArchiveInputScheme auto host = maybeGetStrAttr(input.attrs, "host").value_or("gitlab.com"); auto url = fmt("https://%s/api/v4/projects/%s%%2F%s/repository/archive.tar.gz?sha=%s", host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), - input.getRev()->to_string(Base16, false)); + input.getRev()->to_string(HashFormat::Base16, false)); Headers headers = makeHeadersWithAuthTokens(host); return DownloadUrl { url, headers }; @@ -449,7 +449,7 @@ struct SourceHutInputScheme : GitArchiveInputScheme auto host = maybeGetStrAttr(input.attrs, "host").value_or("git.sr.ht"); auto url = fmt("https://%s/%s/%s/archive/%s.tar.gz", host, getStrAttr(input.attrs, "owner"), getStrAttr(input.attrs, "repo"), - input.getRev()->to_string(Base16, false)); + input.getRev()->to_string(HashFormat::Base16, false)); Headers headers = makeHeadersWithAuthTokens(host); return DownloadUrl { url, headers }; diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/mercurial.cc index 51fd1ed42..b0d2d1909 100644 --- a/src/libfetchers/mercurial.cc +++ b/src/libfetchers/mercurial.cc @@ -206,7 +206,7 @@ struct MercurialInputScheme : InputScheme auto checkHashType = [&](const std::optional & hash) { if (hash.has_value() && hash->type != htSHA1) - throw Error("Hash '%s' is not supported by Mercurial. Only sha1 is supported.", hash->to_string(Base16, true)); + throw Error("Hash '%s' is not supported by Mercurial. Only sha1 is supported.", hash->to_string(HashFormat::Base16, true)); }; @@ -252,7 +252,7 @@ struct MercurialInputScheme : InputScheme } } - Path cacheDir = fmt("%s/nix/hg/%s", getCacheDir(), hashString(htSHA256, actualUrl).to_string(Base32, false)); + Path cacheDir = fmt("%s/nix/hg/%s", getCacheDir(), hashString(htSHA256, actualUrl).to_string(HashFormat::Base32, false)); /* If this is a commit hash that we already have, we don't have to pull again. */ diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index e3a41e75e..269a56526 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -250,7 +250,7 @@ struct CurlInputScheme : InputScheme // NAR hashes are preferred over file hashes since tar/zip // files don't have a canonical representation. if (auto narHash = input.getNarHash()) - url.query.insert_or_assign("narHash", narHash->to_string(SRI, true)); + url.query.insert_or_assign("narHash", narHash->to_string(HashFormat::SRI, true)); return url; } diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index b4fea693f..2a91233ec 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -164,7 +164,7 @@ ref BinaryCacheStore::addToStoreCommon( auto [fileHash, fileSize] = fileHashSink.finish(); narInfo->fileHash = fileHash; narInfo->fileSize = fileSize; - narInfo->url = "nar/" + narInfo->fileHash->to_string(Base32, false) + ".nar" + narInfo->url = "nar/" + narInfo->fileHash->to_string(HashFormat::Base32, false) + ".nar" + (compression == "xz" ? ".xz" : compression == "bzip2" ? ".bz2" : compression == "zstd" ? ".zst" : diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 6a02f5ad4..40a0385af 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1066,7 +1066,7 @@ void LocalDerivationGoal::initTmpDir() { env[i.first] = i.second; } else { auto hash = hashString(htSHA256, i.first); - std::string fn = ".attr-" + hash.to_string(Base32, false); + std::string fn = ".attr-" + hash.to_string(HashFormat::Base32, false); Path p = tmpDir + "/" + fn; writeFile(p, rewriteStrings(i.second, inputRewrites)); chownToBuilder(p); @@ -2583,8 +2583,8 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() delayedException = std::make_exception_ptr( BuildError("hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s", worker.store.printStorePath(drvPath), - wanted.to_string(SRI, true), - got.to_string(SRI, true))); + wanted.to_string(HashFormat::SRI, true), + got.to_string(HashFormat::SRI, true))); } if (!newInfo0.references.empty()) delayedException = std::make_exception_ptr( diff --git a/src/libstore/builtins/fetchurl.cc b/src/libstore/builtins/fetchurl.cc index 7d7924d77..357800333 100644 --- a/src/libstore/builtins/fetchurl.cc +++ b/src/libstore/builtins/fetchurl.cc @@ -65,7 +65,7 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData) if (!hasSuffix(hashedMirror, "/")) hashedMirror += '/'; std::optional ht = parseHashTypeOpt(getAttr("outputHashAlgo")); Hash h = newHashAllowEmpty(getAttr("outputHash"), ht); - fetch(hashedMirror + printHashType(h.type) + "/" + h.to_string(Base16, false)); + fetch(hashedMirror + printHashType(h.type) + "/" + h.to_string(HashFormat::Base16, false)); return; } catch (Error & e) { debug(e.what()); diff --git a/src/libstore/content-address.cc b/src/libstore/content-address.cc index ae91b859b..52c60154c 100644 --- a/src/libstore/content-address.cc +++ b/src/libstore/content-address.cc @@ -60,7 +60,7 @@ std::string ContentAddress::render() const + makeFileIngestionPrefix(method); }, }, method.raw) - + this->hash.to_string(Base32, true); + + this->hash.to_string(HashFormat::Base32, true); } /** diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 8cbf6f044..5860dd548 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -334,7 +334,7 @@ static void performOp(TunnelLogger * logger, ref store, logger->startWork(); auto hash = store->queryPathInfo(path)->narHash; logger->stopWork(); - to << hash.to_string(Base16, false); + to << hash.to_string(HashFormat::Base16, false); break; } diff --git a/src/libstore/derivations.cc b/src/libstore/derivations.cc index fc17e520c..a5ceb29dc 100644 --- a/src/libstore/derivations.cc +++ b/src/libstore/derivations.cc @@ -542,7 +542,7 @@ std::string Derivation::unparse(const Store & store, bool maskOutputs, [&](const DerivationOutput::CAFixed & dof) { s += ','; printUnquotedString(s, maskOutputs ? "" : store.printStorePath(dof.path(store, name, i.first))); s += ','; printUnquotedString(s, dof.ca.printMethodAlgo()); - s += ','; printUnquotedString(s, dof.ca.hash.to_string(Base16, false)); + s += ','; printUnquotedString(s, dof.ca.hash.to_string(HashFormat::Base16, false)); }, [&](const DerivationOutput::CAFloating & dof) { s += ','; printUnquotedString(s, ""); @@ -775,7 +775,7 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut auto & dof = std::get(i.second.raw); auto hash = hashString(htSHA256, "fixed:out:" + dof.ca.printMethodAlgo() + ":" - + dof.ca.hash.to_string(Base16, false) + ":" + + dof.ca.hash.to_string(HashFormat::Base16, false) + ":" + store.printStorePath(dof.path(store, drv.name, i.first))); outputHashes.insert_or_assign(i.first, std::move(hash)); } @@ -820,7 +820,7 @@ DrvHash hashDerivationModulo(Store & store, const Derivation & drv, bool maskOut const auto h = get(res.hashes, outputName); if (!h) throw Error("no hash for output '%s' of derivation '%s'", outputName, drv.name); - inputs2[h->to_string(Base16, false)].value.insert(outputName); + inputs2[h->to_string(HashFormat::Base16, false)].value.insert(outputName); } } @@ -925,7 +925,7 @@ void writeDerivation(Sink & out, const Store & store, const BasicDerivation & dr [&](const DerivationOutput::CAFixed & dof) { out << store.printStorePath(dof.path(store, drv.name, i.first)) << dof.ca.printMethodAlgo() - << dof.ca.hash.to_string(Base16, false); + << dof.ca.hash.to_string(HashFormat::Base16, false); }, [&](const DerivationOutput::CAFloating & dof) { out << "" @@ -957,7 +957,7 @@ void writeDerivation(Sink & out, const Store & store, const BasicDerivation & dr std::string hashPlaceholder(const OutputNameView outputName) { // FIXME: memoize? - return "/" + hashString(htSHA256, concatStrings("nix-output:", outputName)).to_string(Base32, false); + return "/" + hashString(htSHA256, concatStrings("nix-output:", outputName)).to_string(HashFormat::Base32, false); } @@ -1162,7 +1162,7 @@ nlohmann::json DerivationOutput::toJSON( [&](const DerivationOutput::CAFixed & dof) { res["path"] = store.printStorePath(dof.path(store, drvName, outputName)); res["hashAlgo"] = dof.ca.printMethodAlgo(); - res["hash"] = dof.ca.hash.to_string(Base16, false); + res["hash"] = dof.ca.hash.to_string(HashFormat::Base16, false); // FIXME print refs? }, [&](const DerivationOutput::CAFloating & dof) { diff --git a/src/libstore/downstream-placeholder.cc b/src/libstore/downstream-placeholder.cc index 7e3f7548d..ca9f7476e 100644 --- a/src/libstore/downstream-placeholder.cc +++ b/src/libstore/downstream-placeholder.cc @@ -5,7 +5,7 @@ namespace nix { std::string DownstreamPlaceholder::render() const { - return "/" + hash.to_string(Base32, false); + return "/" + hash.to_string(HashFormat::Base32, false); } @@ -31,7 +31,7 @@ DownstreamPlaceholder DownstreamPlaceholder::unknownDerivation( xpSettings.require(Xp::DynamicDerivations); auto compressed = compressHash(placeholder.hash, 20); auto clearText = "nix-computed-output:" - + compressed.to_string(Base32, false) + + compressed.to_string(HashFormat::Base32, false) + ":" + std::string { outputName }; return DownstreamPlaceholder { hashString(htSHA256, clearText) diff --git a/src/libstore/export-import.cc b/src/libstore/export-import.cc index 87b2f8741..91b7e30db 100644 --- a/src/libstore/export-import.cc +++ b/src/libstore/export-import.cc @@ -41,7 +41,7 @@ void Store::exportPath(const StorePath & path, Sink & sink) Hash hash = hashSink.currentHash().first; if (hash != info->narHash && info->narHash != Hash(info->narHash.type)) throw Error("hash of path '%s' has changed from '%s' to '%s'!", - printStorePath(path), info->narHash.to_string(Base32, true), hash.to_string(Base32, true)); + printStorePath(path), info->narHash.to_string(HashFormat::Base32, true), hash.to_string(HashFormat::Base32, true)); teeSink << exportMagic diff --git a/src/libstore/gc.cc b/src/libstore/gc.cc index 516cbef83..fb7895817 100644 --- a/src/libstore/gc.cc +++ b/src/libstore/gc.cc @@ -43,7 +43,7 @@ static void makeSymlink(const Path & link, const Path & target) void LocalStore::addIndirectRoot(const Path & path) { - std::string hash = hashString(htSHA1, path).to_string(Base32, false); + std::string hash = hashString(htSHA1, path).to_string(HashFormat::Base32, false); Path realRoot = canonPath(fmt("%1%/%2%/auto/%3%", stateDir, gcRootsDir, hash)); makeSymlink(realRoot, path); } diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index 703ded0b2..46c90ee31 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -209,7 +209,7 @@ struct LegacySSHStore : public virtual LegacySSHStoreConfig, public virtual Stor << ServeProto::Command::AddToStoreNar << printStorePath(info.path) << (info.deriver ? printStorePath(*info.deriver) : "") - << info.narHash.to_string(Base16, false); + << info.narHash.to_string(HashFormat::Base16, false); ServeProto::write(*this, *conn, info.references); conn->to << info.registrationTime diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 17b4ecc73..1c2f6023a 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -826,7 +826,7 @@ uint64_t LocalStore::addValidPath(State & state, state.stmts->RegisterValidPath.use() (printStorePath(info.path)) - (info.narHash.to_string(Base16, true)) + (info.narHash.to_string(HashFormat::Base16, true)) (info.registrationTime == 0 ? time(0) : info.registrationTime) (info.deriver ? printStorePath(*info.deriver) : "", (bool) info.deriver) (info.narSize, info.narSize != 0) @@ -933,7 +933,7 @@ void LocalStore::updatePathInfo(State & state, const ValidPathInfo & info) { state.stmts->UpdatePathInfo.use() (info.narSize, info.narSize != 0) - (info.narHash.to_string(Base16, true)) + (info.narHash.to_string(HashFormat::Base16, true)) (info.ultimate ? 1 : 0, info.ultimate) (concatStringsSep(" ", info.sigs), !info.sigs.empty()) (renderContentAddress(info.ca), (bool) info.ca) @@ -1236,7 +1236,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, if (hashResult.first != info.narHash) throw Error("hash mismatch importing path '%s';\n specified: %s\n got: %s", - printStorePath(info.path), info.narHash.to_string(Base32, true), hashResult.first.to_string(Base32, true)); + printStorePath(info.path), info.narHash.to_string(HashFormat::Base32, true), hashResult.first.to_string(HashFormat::Base32, true)); if (hashResult.second != info.narSize) throw Error("size mismatch importing path '%s';\n specified: %s\n got: %s", @@ -1252,8 +1252,8 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source, if (specified.hash != actualHash.hash) { throw Error("ca hash mismatch importing path '%s';\n specified: %s\n got: %s", printStorePath(info.path), - specified.hash.to_string(Base32, true), - actualHash.hash.to_string(Base32, true)); + specified.hash.to_string(HashFormat::Base32, true), + actualHash.hash.to_string(HashFormat::Base32, true)); } } @@ -1545,7 +1545,7 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair) for (auto & link : readDirectory(linksDir)) { printMsg(lvlTalkative, "checking contents of '%s'", link.name); Path linkPath = linksDir + "/" + link.name; - std::string hash = hashPath(htSHA256, linkPath).first.to_string(Base32, false); + std::string hash = hashPath(htSHA256, linkPath).first.to_string(HashFormat::Base32, false); if (hash != link.name) { printError("link '%s' was modified! expected hash '%s', got '%s'", linkPath, link.name, hash); @@ -1578,7 +1578,7 @@ bool LocalStore::verifyStore(bool checkContents, RepairFlag repair) if (info->narHash != nullHash && info->narHash != current.first) { printError("path '%s' was modified! expected hash '%s', got '%s'", - printStorePath(i), info->narHash.to_string(Base32, true), current.first.to_string(Base32, true)); + printStorePath(i), info->narHash.to_string(HashFormat::Base32, true), current.first.to_string(HashFormat::Base32, true)); if (repair) repairPath(i); else errors = true; } else { diff --git a/src/libstore/nar-info-disk-cache.cc b/src/libstore/nar-info-disk-cache.cc index c7176d30f..cdbcf7e74 100644 --- a/src/libstore/nar-info-disk-cache.cc +++ b/src/libstore/nar-info-disk-cache.cc @@ -332,9 +332,9 @@ public: (std::string(info->path.name())) (narInfo ? narInfo->url : "", narInfo != 0) (narInfo ? narInfo->compression : "", narInfo != 0) - (narInfo && narInfo->fileHash ? narInfo->fileHash->to_string(Base32, true) : "", narInfo && narInfo->fileHash) + (narInfo && narInfo->fileHash ? narInfo->fileHash->to_string(HashFormat::Base32, true) : "", narInfo && narInfo->fileHash) (narInfo ? narInfo->fileSize : 0, narInfo != 0 && narInfo->fileSize) - (info->narHash.to_string(Base32, true)) + (info->narHash.to_string(HashFormat::Base32, true)) (info->narSize) (concatStringsSep(" ", info->shortRefs())) (info->deriver ? std::string(info->deriver->to_string()) : "", (bool) info->deriver) diff --git a/src/libstore/nar-info.cc b/src/libstore/nar-info.cc index d17253741..ee2ddfd81 100644 --- a/src/libstore/nar-info.cc +++ b/src/libstore/nar-info.cc @@ -105,10 +105,10 @@ std::string NarInfo::to_string(const Store & store) const assert(compression != ""); res += "Compression: " + compression + "\n"; assert(fileHash && fileHash->type == htSHA256); - res += "FileHash: " + fileHash->to_string(Base32, true) + "\n"; + res += "FileHash: " + fileHash->to_string(HashFormat::Base32, true) + "\n"; res += "FileSize: " + std::to_string(fileSize) + "\n"; assert(narHash.type == htSHA256); - res += "NarHash: " + narHash.to_string(Base32, true) + "\n"; + res += "NarHash: " + narHash.to_string(HashFormat::Base32, true) + "\n"; res += "NarSize: " + std::to_string(narSize) + "\n"; res += "References: " + concatStringsSep(" ", shortRefs()) + "\n"; diff --git a/src/libstore/optimise-store.cc b/src/libstore/optimise-store.cc index 4a79cf4a1..23c6a41e4 100644 --- a/src/libstore/optimise-store.cc +++ b/src/libstore/optimise-store.cc @@ -146,10 +146,10 @@ void LocalStore::optimisePath_(Activity * act, OptimiseStats & stats, contents of the symlink (i.e. the result of readlink()), not the contents of the target (which may not even exist). */ Hash hash = hashPath(htSHA256, path).first; - debug("'%1%' has hash '%2%'", path, hash.to_string(Base32, true)); + debug("'%1%' has hash '%2%'", path, hash.to_string(HashFormat::Base32, true)); /* Check if this is a known hash. */ - Path linkPath = linksDir + "/" + hash.to_string(Base32, false); + Path linkPath = linksDir + "/" + hash.to_string(HashFormat::Base32, false); /* Maybe delete the link, if it has been corrupted. */ if (pathExists(linkPath)) { diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index ccb57104f..7ad3247da 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -12,7 +12,7 @@ std::string ValidPathInfo::fingerprint(const Store & store) const store.printStorePath(path)); return "1;" + store.printStorePath(path) + ";" - + narHash.to_string(Base32, true) + ";" + + narHash.to_string(HashFormat::Base32, true) + ";" + std::to_string(narSize) + ";" + concatStringsSep(",", store.printStorePathSet(references)); } @@ -161,7 +161,7 @@ void ValidPathInfo::write( if (includePath) sink << store.printStorePath(path); sink << (deriver ? store.printStorePath(*deriver) : "") - << narHash.to_string(Base16, false); + << narHash.to_string(HashFormat::Base16, false); WorkerProto::write(store, WorkerProto::WriteConn { .to = sink }, references); diff --git a/src/libstore/path.cc b/src/libstore/path.cc index 3c6b9fc10..ec3e53232 100644 --- a/src/libstore/path.cc +++ b/src/libstore/path.cc @@ -35,7 +35,7 @@ StorePath::StorePath(std::string_view _baseName) } StorePath::StorePath(const Hash & hash, std::string_view _name) - : baseName((hash.to_string(Base32, false) + "-").append(std::string(_name))) + : baseName((hash.to_string(HashFormat::Base32, false) + "-").append(std::string(_name))) { checkName(baseName, name()); } diff --git a/src/libstore/realisation.hh b/src/libstore/realisation.hh index 559483ce3..4ba2123d8 100644 --- a/src/libstore/realisation.hh +++ b/src/libstore/realisation.hh @@ -39,7 +39,7 @@ struct DrvOutput { std::string to_string() const; std::string strHash() const - { return drvHash.to_string(Base16, true); } + { return drvHash.to_string(HashFormat::Base16, true); } static DrvOutput parse(const std::string &); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index a639346d1..1704bfbb0 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -541,7 +541,7 @@ void RemoteStore::addToStore(const ValidPathInfo & info, Source & source, conn->to << WorkerProto::Op::AddToStoreNar << printStorePath(info.path) << (info.deriver ? printStorePath(*info.deriver) : "") - << info.narHash.to_string(Base16, false); + << info.narHash.to_string(HashFormat::Base16, false); WorkerProto::write(*this, *conn, info.references); conn->to << info.registrationTime << info.narSize << info.ultimate << info.sigs << renderContentAddress(info.ca) diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index fa23f976e..0c58cca0c 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -154,7 +154,7 @@ StorePath Store::makeStorePath(std::string_view type, StorePath Store::makeStorePath(std::string_view type, const Hash & hash, std::string_view name) const { - return makeStorePath(type, hash.to_string(Base16, true), name); + return makeStorePath(type, hash.to_string(HashFormat::Base16, true), name); } @@ -192,7 +192,7 @@ StorePath Store::makeFixedOutputPath(std::string_view name, const FixedOutputInf hashString(htSHA256, "fixed:out:" + makeFileIngestionPrefix(info.method) - + info.hash.to_string(Base16, true) + ":"), + + info.hash.to_string(HashFormat::Base16, true) + ":"), name); } } @@ -884,7 +884,7 @@ std::string Store::makeValidityRegistration(const StorePathSet & paths, auto info = queryPathInfo(i); if (showHash) { - s += info->narHash.to_string(Base16, false) + "\n"; + s += info->narHash.to_string(HashFormat::Base16, false) + "\n"; s += fmt("%1%\n", info->narSize); } diff --git a/src/libstore/store-api.hh b/src/libstore/store-api.hh index f30ccc950..2d27f7d51 100644 --- a/src/libstore/store-api.hh +++ b/src/libstore/store-api.hh @@ -676,7 +676,7 @@ public: */ nlohmann::json pathInfoToJSON(const StorePathSet & storePaths, bool includeImpureInfo, bool showClosureSize, - HashFormat hashFormat = Base32, + HashFormat hashFormat = HashFormat::Base32, AllowInvalidFlag allowInvalid = DisallowInvalid); /** diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 401a65ec3..b58b5f08d 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -111,26 +111,26 @@ static std::string printHash32(const Hash & hash) std::string printHash16or32(const Hash & hash) { assert(hash.type); - return hash.to_string(hash.type == htMD5 ? Base16 : Base32, false); + return hash.to_string(hash.type == htMD5 ? HashFormat::Base16 : HashFormat::Base32, false); } std::string Hash::to_string(HashFormat hashFormat, bool includeType) const { std::string s; - if (hashFormat == SRI || includeType) { + if (hashFormat == HashFormat::SRI || includeType) { s += printHashType(type); - s += hashFormat == SRI ? '-' : ':'; + s += hashFormat == HashFormat::SRI ? '-' : ':'; } switch (hashFormat) { - case Base16: + case HashFormat::Base16: s += printHash16(*this); break; - case Base32: + case HashFormat::Base32: s += printHash32(*this); break; - case Base64: - case SRI: + case HashFormat::Base64: + case HashFormat::SRI: s += base64Encode(std::string_view((const char *) hash, hashSize)); break; } @@ -267,7 +267,7 @@ Hash newHashAllowEmpty(std::string_view hashStr, std::optional ht) if (!ht) throw BadHash("empty hash requires explicit hash type"); Hash h(*ht); - warn("found empty hash, assuming '%s'", h.to_string(SRI, true)); + warn("found empty hash, assuming '%s'", h.to_string(HashFormat::SRI, true)); return h; } else return Hash::parseAny(hashStr, ht); diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index 0db9bcd6d..783e30496 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -132,12 +132,12 @@ public: std::string gitRev() const { - return to_string(Base16, false); + return to_string(HashFormat::Base16, false); } std::string gitShortRev() const { - return std::string(to_string(Base16, false), 0, 7); + return std::string(to_string(HashFormat::Base16, false), 0, 7); } static Hash dummy; diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 9b6c80a75..5e494bcbf 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -406,7 +406,7 @@ static void opQuery(Strings opFlags, Strings opArgs) auto info = store->queryPathInfo(j); if (query == qHash) { assert(info->narHash.type == htSHA256); - cout << fmt("%s\n", info->narHash.to_string(Base32, true)); + cout << fmt("%s\n", info->narHash.to_string(HashFormat::Base32, true)); } else if (query == qSize) cout << fmt("%d\n", info->narSize); } @@ -769,8 +769,8 @@ static void opVerifyPath(Strings opFlags, Strings opArgs) if (current.first != info->narHash) { printError("path '%s' was modified! expected hash '%s', got '%s'", store->printStorePath(path), - info->narHash.to_string(Base32, true), - current.first.to_string(Base32, true)); + info->narHash.to_string(HashFormat::Base32, true), + current.first.to_string(HashFormat::Base32, true)); status = 1; } } @@ -892,7 +892,7 @@ static void opServe(Strings opFlags, Strings opArgs) out << info->narSize // downloadSize << info->narSize; if (GET_PROTOCOL_MINOR(clientVersion) >= 4) - out << info->narHash.to_string(Base32, true) + out << info->narHash.to_string(HashFormat::Base32, true) << renderContentAddress(info->ca) << info->sigs; } catch (InvalidPath &) { diff --git a/src/nix/flake.cc b/src/nix/flake.cc index a77b5fcb8..ceb112c03 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -179,7 +179,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON j["url"] = flake.lockedRef.to_string(); // FIXME: rename to lockedUrl j["locked"] = fetchers::attrsToJSON(flake.lockedRef.toAttrs()); if (auto rev = flake.lockedRef.input.getRev()) - j["revision"] = rev->to_string(Base16, false); + j["revision"] = rev->to_string(HashFormat::Base16, false); if (auto dirtyRev = fetchers::maybeGetStrAttr(flake.lockedRef.toAttrs(), "dirtyRev")) j["dirtyRevision"] = *dirtyRev; if (auto revCount = flake.lockedRef.input.getRevCount()) @@ -206,7 +206,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON if (auto rev = flake.lockedRef.input.getRev()) logger->cout( ANSI_BOLD "Revision:" ANSI_NORMAL " %s", - rev->to_string(Base16, false)); + rev->to_string(HashFormat::Base16, false)); if (auto dirtyRev = fetchers::maybeGetStrAttr(flake.lockedRef.toAttrs(), "dirtyRev")) logger->cout( ANSI_BOLD "Revision:" ANSI_NORMAL " %s", @@ -1345,7 +1345,7 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON if (json) { auto res = nlohmann::json::object(); res["storePath"] = store->printStorePath(tree.storePath); - res["hash"] = hash.to_string(SRI, true); + res["hash"] = hash.to_string(HashFormat::SRI, true); res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs()); res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs()); logger->cout(res.dump()); @@ -1353,7 +1353,7 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON notice("Downloaded '%s' to '%s' (hash '%s').", lockedRef.to_string(), store->printStorePath(tree.storePath), - hash.to_string(SRI, true)); + hash.to_string(HashFormat::SRI, true)); } } }; diff --git a/src/nix/hash.cc b/src/nix/hash.cc index 6af6bd19f..d6595dcca 100644 --- a/src/nix/hash.cc +++ b/src/nix/hash.cc @@ -11,7 +11,7 @@ using namespace nix; struct CmdHashBase : Command { FileIngestionMethod mode; - HashFormat hashFormat = SRI; + HashFormat hashFormat = HashFormat::SRI; bool truncate = false; HashType ht = htSHA256; std::vector paths; @@ -22,25 +22,25 @@ struct CmdHashBase : Command addFlag({ .longName = "sri", .description = "Print the hash in SRI format.", - .handler = {&hashFormat, SRI}, + .handler = {&hashFormat, HashFormat::SRI}, }); addFlag({ .longName = "base64", .description = "Print the hash in base-64 format.", - .handler = {&hashFormat, Base64}, + .handler = {&hashFormat, HashFormat::Base64}, }); addFlag({ .longName = "base32", .description = "Print the hash in base-32 (Nix-specific) format.", - .handler = {&hashFormat, Base32}, + .handler = {&hashFormat, HashFormat::Base32}, }); addFlag({ .longName = "base16", .description = "Print the hash in base-16 format.", - .handler = {&hashFormat, Base16}, + .handler = {&hashFormat, HashFormat::Base16}, }); addFlag(Flag::mkHashTypeFlag("type", &ht)); @@ -94,7 +94,7 @@ struct CmdHashBase : Command Hash h = hashSink->finish().first; if (truncate && h.hashSize > 20) h = compressHash(h, 20); - logger->cout(h.to_string(hashFormat, hashFormat == SRI)); + logger->cout(h.to_string(hashFormat, hashFormat == HashFormat::SRI)); } } }; @@ -114,16 +114,16 @@ struct CmdToBase : Command std::string description() override { return fmt("convert a hash to %s representation", - hashFormat == Base16 ? "base-16" : - hashFormat == Base32 ? "base-32" : - hashFormat == Base64 ? "base-64" : + hashFormat == HashFormat::Base16 ? "base-16" : + hashFormat == HashFormat::Base32 ? "base-32" : + hashFormat == HashFormat::Base64 ? "base-64" : "SRI"); } void run() override { for (auto s : args) - logger->cout(Hash::parseAny(s, ht).to_string(hashFormat, hashFormat == SRI)); + logger->cout(Hash::parseAny(s, ht).to_string(hashFormat, hashFormat == HashFormat::SRI)); } }; @@ -133,10 +133,10 @@ struct CmdHash : NixMultiCommand : MultiCommand({ {"file", []() { return make_ref(FileIngestionMethod::Flat);; }}, {"path", []() { return make_ref(FileIngestionMethod::Recursive); }}, - {"to-base16", []() { return make_ref(Base16); }}, - {"to-base32", []() { return make_ref(Base32); }}, - {"to-base64", []() { return make_ref(Base64); }}, - {"to-sri", []() { return make_ref(SRI); }}, + {"to-base16", []() { return make_ref(HashFormat::Base16); }}, + {"to-base32", []() { return make_ref(HashFormat::Base32); }}, + {"to-base64", []() { return make_ref(HashFormat::Base64); }}, + {"to-sri", []() { return make_ref(HashFormat::SRI); }}, }) { } @@ -162,7 +162,7 @@ static int compatNixHash(int argc, char * * argv) { std::optional ht; bool flat = false; - HashFormat hashFormat = Base16; + HashFormat hashFormat = HashFormat::Base16; bool truncate = false; enum { opHash, opTo } op = opHash; std::vector ss; @@ -173,10 +173,10 @@ static int compatNixHash(int argc, char * * argv) else if (*arg == "--version") printVersion("nix-hash"); else if (*arg == "--flat") flat = true; - else if (*arg == "--base16") hashFormat = Base16; - else if (*arg == "--base32") hashFormat = Base32; - else if (*arg == "--base64") hashFormat = Base64; - else if (*arg == "--sri") hashFormat = SRI; + else if (*arg == "--base16") hashFormat = HashFormat::Base16; + else if (*arg == "--base32") hashFormat = HashFormat::Base32; + else if (*arg == "--base64") hashFormat = HashFormat::Base64; + else if (*arg == "--sri") hashFormat = HashFormat::SRI; else if (*arg == "--truncate") truncate = true; else if (*arg == "--type") { std::string s = getArg(*arg, arg, end); @@ -184,19 +184,19 @@ static int compatNixHash(int argc, char * * argv) } else if (*arg == "--to-base16") { op = opTo; - hashFormat = Base16; + hashFormat = HashFormat::Base16; } else if (*arg == "--to-base32") { op = opTo; - hashFormat = Base32; + hashFormat = HashFormat::Base32; } else if (*arg == "--to-base64") { op = opTo; - hashFormat = Base64; + hashFormat = HashFormat::Base64; } else if (*arg == "--to-sri") { op = opTo; - hashFormat = SRI; + hashFormat = HashFormat::SRI; } else if (*arg != "" && arg->at(0) == '-') return false; diff --git a/src/nix/path-info.cc b/src/nix/path-info.cc index 613c5b191..c16864d30 100644 --- a/src/nix/path-info.cc +++ b/src/nix/path-info.cc @@ -90,7 +90,7 @@ struct CmdPathInfo : StorePathsCommand, MixJSON std::cout << store->pathInfoToJSON( // FIXME: preserve order? StorePathSet(storePaths.begin(), storePaths.end()), - true, showClosureSize, SRI, AllowInvalid).dump(); + true, showClosureSize, HashFormat::SRI, AllowInvalid).dump(); } else { diff --git a/src/nix/prefetch.cc b/src/nix/prefetch.cc index b67d381ca..3ed7946a8 100644 --- a/src/nix/prefetch.cc +++ b/src/nix/prefetch.cc @@ -310,13 +310,13 @@ struct CmdStorePrefetchFile : StoreCommand, MixJSON if (json) { auto res = nlohmann::json::object(); res["storePath"] = store->printStorePath(storePath); - res["hash"] = hash.to_string(SRI, true); + res["hash"] = hash.to_string(HashFormat::SRI, true); logger->cout(res.dump()); } else { notice("Downloaded '%s' to '%s' (hash '%s').", url, store->printStorePath(storePath), - hash.to_string(SRI, true)); + hash.to_string(HashFormat::SRI, true)); } } }; diff --git a/src/nix/verify.cc b/src/nix/verify.cc index 0b306cc11..adaa33c0c 100644 --- a/src/nix/verify.cc +++ b/src/nix/verify.cc @@ -108,8 +108,8 @@ struct CmdVerify : StorePathsCommand act2.result(resCorruptedPath, store->printStorePath(info->path)); printError("path '%s' was modified! expected hash '%s', got '%s'", store->printStorePath(info->path), - info->narHash.to_string(Base32, true), - hash.first.to_string(Base32, true)); + info->narHash.to_string(HashFormat::Base32, true), + hash.first.to_string(HashFormat::Base32, true)); } } From 231b0fca6df668f904dfb86ef858db556caebdff Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Fri, 13 Oct 2023 18:48:36 +0800 Subject: [PATCH 253/299] Migrate HashFormat to scoped enumeration (enum struct) --- src/libutil/hash.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index 783e30496..d2bf1cb23 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -26,7 +26,7 @@ extern const std::string base32Chars; /** * @brief Enumeration representing the hash formats. */ -enum HashFormat : int { +enum struct HashFormat : int { /// @brief Base 64 encoding. /// @see [IETF RFC 4648, section 4](https://datatracker.ietf.org/doc/html/rfc4648#section-4). Base64, From 6b47635180c740b7478f187b3ae9e35b36deeed7 Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li Date: Mon, 9 Oct 2023 11:03:16 +0800 Subject: [PATCH 254/299] Add helper function parseHashFormat[Opt] printHashFormat Add hash format analogy of parseHashTypeOpt, parseHashType, and printHashType. Co-authored-by: Valentin Gagarin --- src/libutil/hash.cc | 35 +++++++++++++++++++++++++++++++++++ src/libutil/hash.hh | 15 +++++++++++++++ src/libutil/tests/hash.cc | 15 +++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index b58b5f08d..e297c245b 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -386,6 +386,41 @@ Hash compressHash(const Hash & hash, unsigned int newSize) } +std::optional parseHashFormatOpt(std::string_view hashFormatName) +{ + if (hashFormatName == "base16") return HashFormat::Base16; + if (hashFormatName == "base32") return HashFormat::Base32; + if (hashFormatName == "base64") return HashFormat::Base64; + if (hashFormatName == "sri") return HashFormat::SRI; + return std::nullopt; +} + +HashFormat parseHashFormat(std::string_view hashFormatName) +{ + auto opt_f = parseHashFormatOpt(hashFormatName); + if (opt_f) + return *opt_f; + throw UsageError("unknown hash format '%1%', expect 'base16', 'base32', 'base64', or 'sri'", hashFormatName); +} + +std::string_view printHashFormat(HashFormat HashFormat) +{ + switch (HashFormat) { + case HashFormat::Base64: + return "base64"; + case HashFormat::Base32: + return "base32"; + case HashFormat::Base16: + return "base16"; + case HashFormat::SRI: + return "sri"; + default: + // illegal hash base enum value internally, as opposed to external input + // which should be validated with nice error message. + assert(false); + } +} + std::optional parseHashTypeOpt(std::string_view s) { if (s == "md5") return htMD5; diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index d2bf1cb23..cab3e6eca 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -181,6 +181,21 @@ HashResult hashPath(HashType ht, const Path & path, */ Hash compressHash(const Hash & hash, unsigned int newSize); +/** + * Parse a string representing a hash format. + */ +HashFormat parseHashFormat(std::string_view hashFormatName); + +/** + * std::optional version of parseHashFormat that doesn't throw error. + */ +std::optional parseHashFormatOpt(std::string_view hashFormatName); + +/** + * The reverse of parseHashFormat. + */ +std::string_view printHashFormat(HashFormat hashFormat); + /** * Parse a string representing a hash type. */ diff --git a/src/libutil/tests/hash.cc b/src/libutil/tests/hash.cc index d14e5b26d..9a5ebbb30 100644 --- a/src/libutil/tests/hash.cc +++ b/src/libutil/tests/hash.cc @@ -79,6 +79,21 @@ namespace nix { "7299aeadb6889018501d289e4900f7e4331b99dec4b5433a" "c7d329eeb6dd26545e96e55b874be909"); } + + /* ---------------------------------------------------------------------------- + * parseHashFormat, parseHashFormatOpt, printHashFormat + * --------------------------------------------------------------------------*/ + + TEST(hashFormat, testRoundTripPrintParse) { + for (const HashFormat hashFormat: { HashFormat::Base64, HashFormat::Base32, HashFormat::Base16, HashFormat::SRI}) { + ASSERT_EQ(parseHashFormat(printHashFormat(hashFormat)), hashFormat); + ASSERT_EQ(*parseHashFormatOpt(printHashFormat(hashFormat)), hashFormat); + } + } + + TEST(hashFormat, testParseHashFormatOptException) { + ASSERT_EQ(parseHashFormatOpt("sha0042"), std::nullopt); + } } namespace rc { From 5088e6563a4e88b7e6615623d3ed655fdebd3345 Mon Sep 17 00:00:00 2001 From: Yueh-Shun Li <44064051+ShamrockLee@users.noreply.github.com> Date: Sun, 29 Jan 2023 14:03:06 +0800 Subject: [PATCH 255/299] primops: add builtins.convertHash Co-authored-by: Valentin Gagarin --- doc/manual/src/release-notes/rl-next.md | 2 + src/libexpr/primops.cc | 95 +++++++++++++++++++ .../functional/lang/eval-okay-convertHash.exp | 1 + .../functional/lang/eval-okay-convertHash.nix | 31 ++++++ 4 files changed, 129 insertions(+) create mode 100644 tests/functional/lang/eval-okay-convertHash.exp create mode 100644 tests/functional/lang/eval-okay-convertHash.nix diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index c905f445f..46ce2abac 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -8,4 +8,6 @@ - Introduce new flake installable syntax `flakeref#.attrPath` where the "." prefix denotes no searching of default attribute prefixes like `packages.` or `legacyPackages.`. +- Introduce a new built-in function [`builtins.convertHash`](@docroot@/language/builtins.md#builtins-convertHash). + - `builtins.fetchTree` is now marked as stable. diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 830579dbf..14beb069f 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -3774,6 +3774,101 @@ static RegisterPrimOp primop_hashString({ .fun = prim_hashString, }); +static void prim_convertHash(EvalState & state, const PosIdx pos, Value * * args, Value & v) +{ + state.forceAttrs(*args[0], pos, "while evaluating the first argument passed to builtins.convertHash"); + auto &inputAttrs = args[0]->attrs; + + Bindings::iterator iteratorHash = getAttr(state, state.symbols.create("hash"), inputAttrs, "while locating the attribute 'hash'"); + auto hash = state.forceStringNoCtx(*iteratorHash->value, pos, "while evaluating the attribute 'hash'"); + + Bindings::iterator iteratorHashAlgo = inputAttrs->find(state.symbols.create("hashAlgo")); + std::optional ht = std::nullopt; + if (iteratorHashAlgo != inputAttrs->end()) { + ht = parseHashType(state.forceStringNoCtx(*iteratorHashAlgo->value, pos, "while evaluating the attribute 'hashAlgo'")); + } + + Bindings::iterator iteratorToHashFormat = getAttr(state, state.symbols.create("toHashFormat"), args[0]->attrs, "while locating the attribute 'toHashFormat'"); + HashFormat hf = parseHashFormat(state.forceStringNoCtx(*iteratorToHashFormat->value, pos, "while evaluating the attribute 'toHashFormat'")); + + v.mkString(Hash::parseAny(hash, ht).to_string(hf, hf == HashFormat::SRI)); +} + +static RegisterPrimOp primop_convertHash({ + .name = "__convertHash", + .args = {"args"}, + .doc = R"( + Return the specified representation of a hash string, based on the attributes presented in *args*: + + - `hash` + + The hash to be converted. + The hash format is detected automatically. + + - `hashAlgo` + + The algorithm used to create the hash. Must be one of + - `"md5"` + - `"sha1"` + - `"sha256"` + - `"sha512"` + + The attribute may be omitted when `hash` is an [SRI hash](https://www.w3.org/TR/SRI/#the-integrity-attribute) or when the hash is prefixed with the hash algorithm name followed by a colon. + That `:` syntax is supported for backwards compatibility with existing tooling. + + - `toHashFormat` + + The format of the resulting hash. Must be one of + - `"base16"` + - `"base32"` + - `"base64"` + - `"sri"` + + The result hash is the *toHashFormat* representation of the hash *hash*. + + > **Example** + > + > Convert a SHA256 hash in Base16 to SRI: + > + > ```nix + > builtins.convertHash { + > hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + > toHashFormat = "sri"; + > hashAlgo = "sha256"; + > } + > ``` + > + > "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" + + > **Example** + > + > Convert a SHA256 hash in SRI to Base16: + > + > ```nix + > builtins.convertHash { + > hash = "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="; + > toHashFormat = "base16"; + > } + > ``` + > + > "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + > **Example** + > + > Convert a hash in the form `:` in Base16 to SRI: + > + > ```nix + > builtins.convertHash { + > hash = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + > toHashFormat = "sri"; + > } + > ``` + > + > "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" + )", + .fun = prim_convertHash, +}); + struct RegexCache { // TODO use C++20 transparent comparison when available diff --git a/tests/functional/lang/eval-okay-convertHash.exp b/tests/functional/lang/eval-okay-convertHash.exp new file mode 100644 index 000000000..60e0a3c49 --- /dev/null +++ b/tests/functional/lang/eval-okay-convertHash.exp @@ -0,0 +1 @@ +{ hashesBase16 = [ "d41d8cd98f00b204e9800998ecf8427e" "6c69ee7f211c640419d5366cc076ae46" "bb3438fbabd460ea6dbd27d153e2233b" "da39a3ee5e6b4b0d3255bfef95601890afd80709" "cd54e8568c1b37cf1e5badb0779bcbf382212189" "6d12e10b1d331dad210e47fd25d4f260802b7e77" "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" "900a4469df00ccbfd0c145c6d1e4b7953dd0afafadd7534e3a4019e8d38fc663" "ad0387b3bd8652f730ca46d25f9c170af0fd589f42e7f23f5a9e6412d97d7e56" "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" "9d0886f8c6b389398a16257bc79780fab9831c7fc11c8ab07fa732cb7b348feade382f92617c9c5305fefba0af02ab5fd39a587d330997ff5bd0db19f7666653" "21644b72aa259e5a588cd3afbafb1d4310f4889680f6c83b9d531596a5a284f34dbebff409d23bcc86aee6bad10c891606f075c6f4755cb536da27db5693f3a7" ]; hashesBase32 = [ "3y8bwfr609h3lh9ch0izcqq7fl" "26mrvc0v1nslch8r0w45zywsbc" "1v4gi57l97pmnylq6lmgxkhd5v" "143xibwh31h9bvxzalr0sjvbbvpa6ffs" "i4hj30pkrfdpgc5dbcgcydqviibfhm6d" "fxz2p030yba2bza71qhss79k3l5y24kd" "0mdqa9w1p6cmli6976v4wi0sw9r4p5prkj7lzfd1877wk11c9c73" "0qy6iz9yh6a079757mxdmypx0gcmnzjd3ij5q78bzk00vxll82lh" "0mkygpci4r4yb8zz5rs2kxcgvw0a2yf5zlj6r8qgfll6pnrqf0xd" "0zdl9zrg8r3i9c1g90lgg9ip5ijzv3yhz91i0zzn3r8ap9ws784gkp9dk9j3aglhgf1amqb0pj21mh7h1nxcl18akqvvf7ggqsy30yg" "19ncrpp37dx0nzzjw4k6zaqkb9mzaq2myhgpzh5aff7qqcj5wwdxslg6ixwncm7gyq8l761gwf87fgsh2bwfyr52s53k2dkqvw8c24x" "2kz74snvckxldmmbisz9ikmy031d28cs6xfdbl6rhxx42glpyz4vww4lajrc5akklxwixl0js4g84233pxvmbykiic5m7i5m9r4nr11" ]; hashesBase64 = [ "1B2M2Y8AsgTpgAmY7PhCfg==" "bGnufyEcZAQZ1TZswHauRg==" "uzQ4+6vUYOptvSfRU+IjOw==" "2jmj7l5rSw0yVb/vlWAYkK/YBwk=" "zVToVowbN88eW62wd5vL84IhIYk=" "bRLhCx0zHa0hDkf9JdTyYIArfnc=" "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" "kApEad8AzL/QwUXG0eS3lT3Qr6+t11NOOkAZ6NOPxmM=" "rQOHs72GUvcwykbSX5wXCvD9WJ9C5/I/Wp5kEtl9flY=" "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" "nQiG+MaziTmKFiV7x5eA+rmDHH/BHIqwf6cyy3s0j+reOC+SYXycUwX++6CvAqtf05pYfTMJl/9b0NsZ92ZmUw==" "IWRLcqolnlpYjNOvuvsdQxD0iJaA9sg7nVMVlqWihPNNvr/0CdI7zIau5rrRDIkWBvB1xvR1XLU22ifbVpPzpw==" ]; hashesSRI = [ "md5-1B2M2Y8AsgTpgAmY7PhCfg==" "md5-bGnufyEcZAQZ1TZswHauRg==" "md5-uzQ4+6vUYOptvSfRU+IjOw==" "sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk=" "sha1-zVToVowbN88eW62wd5vL84IhIYk=" "sha1-bRLhCx0zHa0hDkf9JdTyYIArfnc=" "sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" "sha256-kApEad8AzL/QwUXG0eS3lT3Qr6+t11NOOkAZ6NOPxmM=" "sha256-rQOHs72GUvcwykbSX5wXCvD9WJ9C5/I/Wp5kEtl9flY=" "sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" "sha512-nQiG+MaziTmKFiV7x5eA+rmDHH/BHIqwf6cyy3s0j+reOC+SYXycUwX++6CvAqtf05pYfTMJl/9b0NsZ92ZmUw==" "sha512-IWRLcqolnlpYjNOvuvsdQxD0iJaA9sg7nVMVlqWihPNNvr/0CdI7zIau5rrRDIkWBvB1xvR1XLU22ifbVpPzpw==" ]; } diff --git a/tests/functional/lang/eval-okay-convertHash.nix b/tests/functional/lang/eval-okay-convertHash.nix new file mode 100644 index 000000000..cf4909aaf --- /dev/null +++ b/tests/functional/lang/eval-okay-convertHash.nix @@ -0,0 +1,31 @@ +let + hashAlgos = [ "md5" "md5" "md5" "sha1" "sha1" "sha1" "sha256" "sha256" "sha256" "sha512" "sha512" "sha512" ]; + hashesBase16 = import ./eval-okay-hashstring.exp; + map2 = f: { fsts, snds }: if fsts == [ ] then [ ] else [ (f (builtins.head fsts) (builtins.head snds)) ] ++ map2 f { fsts = builtins.tail fsts; snds = builtins.tail snds; }; + map2' = f: fsts: snds: map2 f { inherit fsts snds; }; + getOutputHashes = hashes: { + hashesBase16 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "base16";}) hashAlgos hashes; + hashesBase32 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "base32";}) hashAlgos hashes; + hashesBase64 = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "base64";}) hashAlgos hashes; + hashesSRI = map2' (hashAlgo: hash: builtins.convertHash { inherit hash hashAlgo; toHashFormat = "sri" ;}) hashAlgos hashes; + }; + getOutputHashesColon = hashes: { + hashesBase16 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "base16";}) hashAlgos hashes; + hashesBase32 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "base32";}) hashAlgos hashes; + hashesBase64 = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "base64";}) hashAlgos hashes; + hashesSRI = map2' (hashAlgo: hashBody: builtins.convertHash { hash = hashAlgo + ":" + hashBody; toHashFormat = "sri" ;}) hashAlgos hashes; + }; + outputHashes = getOutputHashes hashesBase16; +in +# map2'` +assert map2' (s1: s2: s1 + s2) [ "a" "b" ] [ "c" "d" ] == [ "ac" "bd" ]; +# hashesBase16 +assert outputHashes.hashesBase16 == hashesBase16; +# standard SRI hashes +assert outputHashes.hashesSRI == (map2' (hashAlgo: hashBody: hashAlgo + "-" + hashBody) hashAlgos outputHashes.hashesBase64); +# without prefix +assert builtins.all (x: getOutputHashes x == outputHashes) (builtins.attrValues outputHashes); +# colon-separated. +# Note that colon prefix must not be applied to the standard SRI. e.g. "sha256:sha256-..." is illegal. +assert builtins.all (x: getOutputHashesColon x == outputHashes) (with outputHashes; [ hashesBase16 hashesBase32 hashesBase64 ]); +outputHashes From d2c0051784e4a338254445f8f680abf1e37e9024 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 18 Oct 2023 23:35:07 +0200 Subject: [PATCH 256/299] Remove obsolete corepkgs references --- doc/manual/local.mk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/manual/local.mk b/doc/manual/local.mk index 2e9f08678..8bf16e9dd 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -125,7 +125,7 @@ $(d)/src/command-ref/experimental-features-shortlist.md: $(d)/xp-features.json $ @mv $@.tmp $@ $(d)/xp-features.json: $(bindir)/nix - $(trace-gen) $(dummy-env) NIX_PATH=nix/corepkgs=corepkgs $(bindir)/nix __dump-xp-features > $@.tmp + $(trace-gen) $(dummy-env) $(bindir)/nix __dump-xp-features > $@.tmp @mv $@.tmp $@ $(d)/src/language/builtins.md: $(d)/language.json $(d)/generate-builtins.nix $(d)/src/language/builtins-prefix.md $(bindir)/nix @@ -141,7 +141,7 @@ $(d)/src/language/builtin-constants.md: $(d)/language.json $(d)/generate-builtin @mv $@.tmp $@ $(d)/language.json: $(bindir)/nix - $(trace-gen) $(dummy-env) NIX_PATH=nix/corepkgs=corepkgs $(bindir)/nix __dump-language > $@.tmp + $(trace-gen) $(dummy-env) $(bindir)/nix __dump-language > $@.tmp @mv $@.tmp $@ # Generate the HTML manual. From 34c559352579b9e1a501ec2fc435f3e21cf6e78f Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 19 Oct 2023 10:57:45 +0200 Subject: [PATCH 257/299] add a link to all maintainer meeting notes linking to the discourse category will by default show a view sorted by most recent post, which makes it hard to find particular meeting notes. this also adds a procedural detail about the notes, to make that more explicit and less dependent on being present in the meetings. --- maintainers/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/maintainers/README.md b/maintainers/README.md index 8eeb47e8b..5be4f9d04 100644 --- a/maintainers/README.md +++ b/maintainers/README.md @@ -50,7 +50,9 @@ The team meets twice a week: 1. Code review on pull requests from [In review](#in-review). 2. Other chores and tasks. -Meeting notes are collected on a [collaborative scratchpad](https://pad.lassul.us/Cv7FpYx-Ri-4VjUykQOLAw), and published on Discourse under the [Nix category](https://discourse.nixos.org/c/dev/nix/50). +Meeting notes are collected on a [collaborative scratchpad](https://pad.lassul.us/Cv7FpYx-Ri-4VjUykQOLAw). +Notes on issues and pull requests are posted as comments and linked from the meeting notes, so they are easy to find from both places. +[All meeting notes](https://discourse.nixos.org/search?expanded=true&q=Nix%20team%20meeting%20minutes%20%23%20%23dev%3Anix%20in%3Atitle%20order%3Alatest_topic) are published on Discourse under the [Nix category](https://discourse.nixos.org/c/dev/nix/50). ## Project board protocol From 9adac237e70003625b626cef04979ce746e9dd7e Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 19 Oct 2023 11:40:27 +0200 Subject: [PATCH 258/299] update link to label GitHub now displays a banner and has a dedicated page[1] for good first issues, but that uses a different label name as we had in place. I renamed the label on GitHub, this is updating the link. [1]: https://github.com/NixOS/nix/contribute --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73210b303..556365375 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). 1. Search for related issues that cover what you're going to work on. It could help to mention there that you will work on the issue. - Issues labeled [good first issue](https://github.com/NixOS/nix/labels/good-first-issue) should be relatively easy to fix and are likely to get merged quickly. + Issues labeled [good first issue](https://github.com/NixOS/nix/labels/good%20first%20issue) should be relatively easy to fix and are likely to get merged quickly. Pull requests addressing issues labeled [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) or [RFC](https://github.com/NixOS/nix/labels/RFC) are especially welcomed by maintainers and will receive prioritised review. If there is no relevant issue yet and you're not sure whether your change is likely to be accepted, [open an issue](https://github.com/NixOS/nix/issues/new/choose) yourself. From 36b15d905e8ca6f5eb4f73fb842600b6e091708e Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Thu, 19 Oct 2023 11:47:10 +0200 Subject: [PATCH 259/299] link to popular issues from the contributing guide this also adds a hint to contributors about making far-reaching changes, complementing the recent update to the maintainers' handbook on how to deal with those. --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73210b303..71970f307 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,9 @@ Check out the [security policy](https://github.com/NixOS/nix/security/policy). Issues labeled [good first issue](https://github.com/NixOS/nix/labels/good-first-issue) should be relatively easy to fix and are likely to get merged quickly. Pull requests addressing issues labeled [idea approved](https://github.com/NixOS/nix/labels/idea%20approved) or [RFC](https://github.com/NixOS/nix/labels/RFC) are especially welcomed by maintainers and will receive prioritised review. + If you are proficient with C++, addressing one of the [popular issues](https://github.com/NixOS/nix/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) will be highly appreciated by maintainers and Nix users all over the world. + For far-reaching changes, please investigate possible blockers and design implications, and coordinate with maintainers before investing too much time in writing code that may not end up getting merged. + If there is no relevant issue yet and you're not sure whether your change is likely to be accepted, [open an issue](https://github.com/NixOS/nix/issues/new/choose) yourself. 2. Check for [pull requests](https://github.com/NixOS/nix/pulls) that might already cover the contribution you are about to make. From 12214fef09151e26abfcaea0095ab157388822e3 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 14:19:10 +0200 Subject: [PATCH 260/299] InputAccessor::fetchToStore(): Support arbitrary ingestion methods --- src/libexpr/eval.cc | 2 +- src/libexpr/primops.cc | 5 +---- src/libfetchers/input-accessor.cc | 8 +++++--- src/libfetchers/input-accessor.hh | 3 +++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index a28bb2101..3b0fe93e1 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2329,7 +2329,7 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat auto dstPath = i != srcToStore.end() ? i->second : [&]() { - auto dstPath = path.fetchToStore(store, path.baseName(), nullptr, repair); + auto dstPath = path.fetchToStore(store, path.baseName(), FileIngestionMethod::Recursive, nullptr, repair); allowPath(dstPath); srcToStore.insert_or_assign(path, dstPath); printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath)); diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index f1c24a75c..f416aa639 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -2233,10 +2233,7 @@ static void addPath( }); if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) { - // FIXME - if (method != FileIngestionMethod::Recursive) - throw Error("'recursive = false' is not implemented"); - auto dstPath = state.rootPath(CanonPath(path)).fetchToStore(state.store, name, &filter, state.repair); + auto dstPath = state.rootPath(CanonPath(path)).fetchToStore(state.store, name, method, &filter, state.repair); if (expectedHash && expectedStorePath != dstPath) state.debugThrowLastTrace(Error("store path mismatch in (possibly filtered) path added from '%s'", path)); state.allowAndSetStorePathString(dstPath, v); diff --git a/src/libfetchers/input-accessor.cc b/src/libfetchers/input-accessor.cc index fec16e99f..63f1f4ad2 100644 --- a/src/libfetchers/input-accessor.cc +++ b/src/libfetchers/input-accessor.cc @@ -96,6 +96,7 @@ StorePath InputAccessor::fetchToStore( ref store, const CanonPath & path, std::string_view name, + FileIngestionMethod method, PathFilter * filter, RepairFlag repair) { @@ -107,8 +108,8 @@ StorePath InputAccessor::fetchToStore( auto storePath = settings.readOnlyMode - ? store->computeStorePathFromDump(*source, name).first - : store->addToStoreFromDump(*source, name, FileIngestionMethod::Recursive, htSHA256, repair); + ? store->computeStorePathFromDump(*source, name, method, htSHA256).first + : store->addToStoreFromDump(*source, name, method, htSHA256, repair); return storePath; } @@ -140,10 +141,11 @@ std::ostream & operator << (std::ostream & str, const SourcePath & path) StorePath SourcePath::fetchToStore( ref store, std::string_view name, + FileIngestionMethod method, PathFilter * filter, RepairFlag repair) const { - return accessor->fetchToStore(store, path, name, filter, repair); + return accessor->fetchToStore(store, path, name, method, filter, repair); } std::string_view SourcePath::baseName() const diff --git a/src/libfetchers/input-accessor.hh b/src/libfetchers/input-accessor.hh index a0f08e295..003c7226c 100644 --- a/src/libfetchers/input-accessor.hh +++ b/src/libfetchers/input-accessor.hh @@ -6,6 +6,7 @@ #include "canon-path.hh" #include "repair-flag.hh" #include "hash.hh" +#include "content-address.hh" namespace nix { @@ -73,6 +74,7 @@ struct InputAccessor : public std::enable_shared_from_this ref store, const CanonPath & path, std::string_view name = "source", + FileIngestionMethod method = FileIngestionMethod::Recursive, PathFilter * filter = nullptr, RepairFlag repair = NoRepair); @@ -181,6 +183,7 @@ struct SourcePath StorePath fetchToStore( ref store, std::string_view name = "source", + FileIngestionMethod method = FileIngestionMethod::Recursive, PathFilter * filter = nullptr, RepairFlag repair = NoRepair) const; From f16af08e8393420f6234ab59bde716dd145703d7 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 14:20:50 +0200 Subject: [PATCH 261/299] Fix macOS compilation --- src/libexpr/eval.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 3b0fe93e1..7180b9fbe 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -616,7 +616,9 @@ void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & SourcePath EvalState::checkSourcePath(const SourcePath & path_) { - if (path_.accessor != rootFS) return path_; + // Don't check non-rootFS accessors, they're in a different namespace. + if (path_.accessor != ref(rootFS)) return path_; + if (!allowedPaths) return path_; auto i = resolvedPaths.find(path_.path.abs()); From 06c57899e306bd6a19fce5d83b2ffce487067cdb Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 14:22:05 +0200 Subject: [PATCH 262/299] Remove FIXME --- src/libexpr/primops.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index f416aa639..b971fd052 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -595,7 +595,9 @@ struct CompareValues case nString: return v1->string_view().compare(v2->string_view()) < 0; case nPath: - // FIXME: handle accessor? + // Note: we don't take the accessor into account + // since it's not obvious how to compare them in a + // reproducible way. return strcmp(v1->_path.path, v2->_path.path) < 0; case nList: // Lexicographic comparison From 9bc7b4f463b6a06746bd1658d0170865e3fc8337 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 19 Oct 2023 14:39:41 +0200 Subject: [PATCH 263/299] doc: generic closure supported key types (#9183) * doc: generic closure supported key types Co-authored-by: Valentin Gagarin --- src/libexpr/primops.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 14beb069f..e5c5eb5b1 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -734,6 +734,14 @@ static RegisterPrimOp primop_genericClosure(PrimOp { ``` [ { key = 5; } { key = 16; } { key = 8; } { key = 4; } { key = 2; } { key = 1; } ] ``` + + `key` can be one of the following types: + - [Number](@docroot@/language/values.md#type-number) + - [Boolean](@docroot@/language/values.md#type-boolean) + - [String](@docroot@/language/values.md#type-string) + - [Path](@docroot@/language/values.md#type-path) + - [List](@docroot@/language/values.md#list) + )", .fun = prim_genericClosure, }); From fb6a3910c4b4ebec2ab2e422a7454b8f3ba71f85 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 14:45:26 +0200 Subject: [PATCH 264/299] Move most of InputAccessor into libutil --- src/libfetchers/input-accessor.cc | 102 ---------------------------- src/libfetchers/input-accessor.hh | 85 +---------------------- src/libutil/source-accessor.cc | 108 ++++++++++++++++++++++++++++++ src/libutil/source-accessor.hh | 96 ++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 185 deletions(-) create mode 100644 src/libutil/source-accessor.cc create mode 100644 src/libutil/source-accessor.hh diff --git a/src/libfetchers/input-accessor.cc b/src/libfetchers/input-accessor.cc index 63f1f4ad2..9bddf7c2e 100644 --- a/src/libfetchers/input-accessor.cc +++ b/src/libfetchers/input-accessor.cc @@ -3,95 +3,6 @@ namespace nix { -static std::atomic nextNumber{0}; - -InputAccessor::InputAccessor() - : number(++nextNumber) -{ -} - -// FIXME: merge with archive.cc. -void InputAccessor::dumpPath( - const CanonPath & path, - Sink & sink, - PathFilter & filter) -{ - auto dumpContents = [&](const CanonPath & path) - { - // FIXME: pipe - auto s = readFile(path); - sink << "contents" << s.size(); - sink(s); - writePadding(s.size(), sink); - }; - - std::function dump; - - dump = [&](const CanonPath & path) { - checkInterrupt(); - - auto st = lstat(path); - - sink << "("; - - if (st.type == tRegular) { - sink << "type" << "regular"; - if (st.isExecutable) - sink << "executable" << ""; - dumpContents(path); - } - - else if (st.type == tDirectory) { - sink << "type" << "directory"; - - /* If we're on a case-insensitive system like macOS, undo - the case hack applied by restorePath(). */ - std::map unhacked; - for (auto & i : readDirectory(path)) - if (/* archiveSettings.useCaseHack */ false) { // FIXME - std::string name(i.first); - size_t pos = i.first.find(caseHackSuffix); - if (pos != std::string::npos) { - debug("removing case hack suffix from '%s'", path + i.first); - name.erase(pos); - } - if (!unhacked.emplace(name, i.first).second) - throw Error("file name collision in between '%s' and '%s'", - (path + unhacked[name]), - (path + i.first)); - } else - unhacked.emplace(i.first, i.first); - - for (auto & i : unhacked) - if (filter((path + i.first).abs())) { - sink << "entry" << "(" << "name" << i.first << "node"; - dump(path + i.second); - sink << ")"; - } - } - - else if (st.type == tSymlink) - sink << "type" << "symlink" << "target" << readLink(path); - - else throw Error("file '%s' has an unsupported type", path); - - sink << ")"; - }; - - sink << narVersionMagic1; - dump(path); -} - -Hash InputAccessor::hashPath( - const CanonPath & path, - PathFilter & filter, - HashType ht) -{ - HashSink sink(ht); - dumpPath(path, sink, filter); - return sink.finish().first; -} - StorePath InputAccessor::fetchToStore( ref store, const CanonPath & path, @@ -114,19 +25,6 @@ StorePath InputAccessor::fetchToStore( return storePath; } -std::optional InputAccessor::maybeLstat(const CanonPath & path) -{ - // FIXME: merge these into one operation. - if (!pathExists(path)) - return {}; - return lstat(path); -} - -std::string InputAccessor::showPath(const CanonPath & path) -{ - return path.abs(); -} - SourcePath InputAccessor::root() { return {ref(shared_from_this()), CanonPath::root}; diff --git a/src/libfetchers/input-accessor.hh b/src/libfetchers/input-accessor.hh index 003c7226c..524a94cd1 100644 --- a/src/libfetchers/input-accessor.hh +++ b/src/libfetchers/input-accessor.hh @@ -1,11 +1,9 @@ #pragma once +#include "source-accessor.hh" #include "ref.hh" #include "types.hh" -#include "archive.hh" -#include "canon-path.hh" #include "repair-flag.hh" -#include "hash.hh" #include "content-address.hh" namespace nix { @@ -16,60 +14,8 @@ struct SourcePath; class StorePath; class Store; -struct InputAccessor : public std::enable_shared_from_this +struct InputAccessor : SourceAccessor, std::enable_shared_from_this { - const size_t number; - - InputAccessor(); - - virtual ~InputAccessor() - { } - - virtual std::string readFile(const CanonPath & path) = 0; - - virtual bool pathExists(const CanonPath & path) = 0; - - enum Type { - tRegular, tSymlink, tDirectory, - /** - Any other node types that may be encountered on the file system, such as device nodes, sockets, named pipe, and possibly even more exotic things. - - Responsible for `"unknown"` from `builtins.readFileType "/dev/null"`. - - Unlike `DT_UNKNOWN`, this must not be used for deferring the lookup of types. - */ - tMisc - }; - - struct Stat - { - Type type = tMisc; - //uint64_t fileSize = 0; // regular files only - bool isExecutable = false; // regular files only - }; - - virtual Stat lstat(const CanonPath & path) = 0; - - std::optional maybeLstat(const CanonPath & path); - - typedef std::optional DirEntry; - - typedef std::map DirEntries; - - virtual DirEntries readDirectory(const CanonPath & path) = 0; - - virtual std::string readLink(const CanonPath & path) = 0; - - virtual void dumpPath( - const CanonPath & path, - Sink & sink, - PathFilter & filter = defaultPathFilter); - - Hash hashPath( - const CanonPath & path, - PathFilter & filter = defaultPathFilter, - HashType ht = htSHA256); - StorePath fetchToStore( ref store, const CanonPath & path, @@ -78,34 +24,7 @@ struct InputAccessor : public std::enable_shared_from_this PathFilter * filter = nullptr, RepairFlag repair = NoRepair); - /* Return a corresponding path in the root filesystem, if - possible. This is only possible for inputs that are - materialized in the root filesystem. */ - virtual std::optional getPhysicalPath(const CanonPath & path) - { return std::nullopt; } - - bool operator == (const InputAccessor & x) const - { - return number == x.number; - } - - bool operator < (const InputAccessor & x) const - { - return number < x.number; - } - - void setPathDisplay(std::string displayPrefix, std::string displaySuffix = ""); - - virtual std::string showPath(const CanonPath & path); - SourcePath root(); - - /* Return the maximum last-modified time of the files in this - tree, if available. */ - virtual std::optional getLastModified() - { - return std::nullopt; - } }; /** diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc new file mode 100644 index 000000000..23ec55c89 --- /dev/null +++ b/src/libutil/source-accessor.cc @@ -0,0 +1,108 @@ +#include "source-accessor.hh" +#include "archive.hh" + +namespace nix { + +static std::atomic nextNumber{0}; + +SourceAccessor::SourceAccessor() + : number(++nextNumber) +{ +} + +// FIXME: merge with archive.cc. +void SourceAccessor::dumpPath( + const CanonPath & path, + Sink & sink, + PathFilter & filter) +{ + auto dumpContents = [&](const CanonPath & path) + { + // FIXME: pipe + auto s = readFile(path); + sink << "contents" << s.size(); + sink(s); + writePadding(s.size(), sink); + }; + + std::function dump; + + dump = [&](const CanonPath & path) { + checkInterrupt(); + + auto st = lstat(path); + + sink << "("; + + if (st.type == tRegular) { + sink << "type" << "regular"; + if (st.isExecutable) + sink << "executable" << ""; + dumpContents(path); + } + + else if (st.type == tDirectory) { + sink << "type" << "directory"; + + /* If we're on a case-insensitive system like macOS, undo + the case hack applied by restorePath(). */ + std::map unhacked; + for (auto & i : readDirectory(path)) + if (/* archiveSettings.useCaseHack */ false) { // FIXME + std::string name(i.first); + size_t pos = i.first.find(caseHackSuffix); + if (pos != std::string::npos) { + debug("removing case hack suffix from '%s'", path + i.first); + name.erase(pos); + } + if (!unhacked.emplace(name, i.first).second) + throw Error("file name collision in between '%s' and '%s'", + (path + unhacked[name]), + (path + i.first)); + } else + unhacked.emplace(i.first, i.first); + + for (auto & i : unhacked) + if (filter((path + i.first).abs())) { + sink << "entry" << "(" << "name" << i.first << "node"; + dump(path + i.second); + sink << ")"; + } + } + + else if (st.type == tSymlink) + sink << "type" << "symlink" << "target" << readLink(path); + + else throw Error("file '%s' has an unsupported type", path); + + sink << ")"; + }; + + sink << narVersionMagic1; + dump(path); +} + +Hash SourceAccessor::hashPath( + const CanonPath & path, + PathFilter & filter, + HashType ht) +{ + HashSink sink(ht); + dumpPath(path, sink, filter); + return sink.finish().first; +} + +std::optional SourceAccessor::maybeLstat(const CanonPath & path) +{ + // FIXME: merge these into one operation. + if (!pathExists(path)) + return {}; + return lstat(path); +} + +std::string SourceAccessor::showPath(const CanonPath & path) +{ + return path.abs(); +} + +} diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh new file mode 100644 index 000000000..c2d35d6b5 --- /dev/null +++ b/src/libutil/source-accessor.hh @@ -0,0 +1,96 @@ +#pragma once + +#include "canon-path.hh" +#include "hash.hh" + +namespace nix { + +/** + * A read-only filesystem abstraction. This is used by the Nix + * evaluator and elsewhere for accessing sources in various + * filesystem-like entities (such as the real filesystem, tarballs or + * Git repositories). + */ +struct SourceAccessor +{ + const size_t number; + + SourceAccessor(); + + virtual ~SourceAccessor() + { } + + virtual std::string readFile(const CanonPath & path) = 0; + + virtual bool pathExists(const CanonPath & path) = 0; + + enum Type { + tRegular, tSymlink, tDirectory, + /** + Any other node types that may be encountered on the file system, such as device nodes, sockets, named pipe, and possibly even more exotic things. + + Responsible for `"unknown"` from `builtins.readFileType "/dev/null"`. + + Unlike `DT_UNKNOWN`, this must not be used for deferring the lookup of types. + */ + tMisc + }; + + struct Stat + { + Type type = tMisc; + //uint64_t fileSize = 0; // regular files only + bool isExecutable = false; // regular files only + }; + + virtual Stat lstat(const CanonPath & path) = 0; + + std::optional maybeLstat(const CanonPath & path); + + typedef std::optional DirEntry; + + typedef std::map DirEntries; + + virtual DirEntries readDirectory(const CanonPath & path) = 0; + + virtual std::string readLink(const CanonPath & path) = 0; + + virtual void dumpPath( + const CanonPath & path, + Sink & sink, + PathFilter & filter = defaultPathFilter); + + Hash hashPath( + const CanonPath & path, + PathFilter & filter = defaultPathFilter, + HashType ht = htSHA256); + + /* Return a corresponding path in the root filesystem, if + possible. This is only possible for filesystems that are + materialized in the root filesystem. */ + virtual std::optional getPhysicalPath(const CanonPath & path) + { return std::nullopt; } + + bool operator == (const SourceAccessor & x) const + { + return number == x.number; + } + + bool operator < (const SourceAccessor & x) const + { + return number < x.number; + } + + void setPathDisplay(std::string displayPrefix, std::string displaySuffix = ""); + + virtual std::string showPath(const CanonPath & path); + + /* Return the maximum last-modified time of the files in this + tree, if available. */ + virtual std::optional getLastModified() + { + return std::nullopt; + } +}; + +} From 9f572eb0e35d3017a160cef89b7417cf6b4a53e2 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 15:07:56 +0200 Subject: [PATCH 265/299] Unify the two implementations of dumpPath() --- src/libutil/archive.cc | 183 +++++++++++++++++++-------------- src/libutil/source-accessor.cc | 72 ------------- 2 files changed, 108 insertions(+), 147 deletions(-) diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 0f0cae7c0..e48fc7522 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -14,6 +14,7 @@ #include "archive.hh" #include "util.hh" #include "config.hh" +#include "source-accessor.hh" namespace nix { @@ -36,91 +37,134 @@ static GlobalConfig::Register rArchiveSettings(&archiveSettings); PathFilter defaultPathFilter = [](const Path &) { return true; }; -static void dumpContents(const Path & path, off_t size, - Sink & sink) +void SourceAccessor::dumpPath( + const CanonPath & path, + Sink & sink, + PathFilter & filter) { - sink << "contents" << size; + auto dumpContents = [&](const CanonPath & path) + { + /* It would be nice if this was streaming, but we need the + size before the contents. */ + auto s = readFile(path); + sink << "contents" << s.size(); + sink(s); + writePadding(s.size(), sink); + }; - AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); - if (!fd) throw SysError("opening file '%1%'", path); + std::function dump; - std::vector buf(65536); - size_t left = size; + dump = [&](const CanonPath & path) { + checkInterrupt(); - while (left > 0) { - auto n = std::min(left, buf.size()); - readFull(fd.get(), buf.data(), n); - left -= n; - sink({buf.data(), n}); - } + auto st = lstat(path); - writePadding(size, sink); + sink << "("; + + if (st.type == tRegular) { + sink << "type" << "regular"; + if (st.isExecutable) + sink << "executable" << ""; + dumpContents(path); + } + + else if (st.type == tDirectory) { + sink << "type" << "directory"; + + /* If we're on a case-insensitive system like macOS, undo + the case hack applied by restorePath(). */ + std::map unhacked; + for (auto & i : readDirectory(path)) + if (archiveSettings.useCaseHack) { + std::string name(i.first); + size_t pos = i.first.find(caseHackSuffix); + if (pos != std::string::npos) { + debug("removing case hack suffix from '%s'", path + i.first); + name.erase(pos); + } + if (!unhacked.emplace(name, i.first).second) + throw Error("file name collision in between '%s' and '%s'", + (path + unhacked[name]), + (path + i.first)); + } else + unhacked.emplace(i.first, i.first); + + for (auto & i : unhacked) + if (filter((path + i.first).abs())) { + sink << "entry" << "(" << "name" << i.first << "node"; + dump(path + i.second); + sink << ")"; + } + } + + else if (st.type == tSymlink) + sink << "type" << "symlink" << "target" << readLink(path); + + else throw Error("file '%s' has an unsupported type", path); + + sink << ")"; + }; + + sink << narVersionMagic1; + dump(path); } -static time_t dump(const Path & path, Sink & sink, PathFilter & filter) +struct FSSourceAccessor : SourceAccessor { - checkInterrupt(); + time_t mtime = 0; // most recent mtime seen - auto st = lstat(path); - time_t result = st.st_mtime; - - sink << "("; - - if (S_ISREG(st.st_mode)) { - sink << "type" << "regular"; - if (st.st_mode & S_IXUSR) - sink << "executable" << ""; - dumpContents(path, st.st_size, sink); + std::string readFile(const CanonPath & path) override + { + return nix::readFile(path.abs()); } - else if (S_ISDIR(st.st_mode)) { - sink << "type" << "directory"; + bool pathExists(const CanonPath & path) override + { + return nix::pathExists(path.abs()); + } - /* If we're on a case-insensitive system like macOS, undo - the case hack applied by restorePath(). */ - std::map unhacked; - for (auto & i : readDirectory(path)) - if (archiveSettings.useCaseHack) { - std::string name(i.name); - size_t pos = i.name.find(caseHackSuffix); - if (pos != std::string::npos) { - debug("removing case hack suffix from '%1%'", path + "/" + i.name); - name.erase(pos); - } - if (!unhacked.emplace(name, i.name).second) - throw Error("file name collision in between '%1%' and '%2%'", - (path + "/" + unhacked[name]), - (path + "/" + i.name)); - } else - unhacked.emplace(i.name, i.name); + Stat lstat(const CanonPath & path) override + { + auto st = nix::lstat(path.abs()); + mtime = std::max(mtime, st.st_mtime); + return Stat { + .type = + S_ISREG(st.st_mode) ? tRegular : + S_ISDIR(st.st_mode) ? tDirectory : + S_ISLNK(st.st_mode) ? tSymlink : + tMisc, + .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR + }; + } - for (auto & i : unhacked) - if (filter(path + "/" + i.first)) { - sink << "entry" << "(" << "name" << i.first << "node"; - auto tmp_mtime = dump(path + "/" + i.second, sink, filter); - if (tmp_mtime > result) { - result = tmp_mtime; - } - sink << ")"; + DirEntries readDirectory(const CanonPath & path) override + { + DirEntries res; + for (auto & entry : nix::readDirectory(path.abs())) { + std::optional type; + switch (entry.type) { + case DT_REG: type = Type::tRegular; break; + case DT_LNK: type = Type::tSymlink; break; + case DT_DIR: type = Type::tDirectory; break; } + res.emplace(entry.name, type); + } + return res; } - else if (S_ISLNK(st.st_mode)) - sink << "type" << "symlink" << "target" << readLink(path); - - else throw Error("file '%1%' has an unsupported type", path); - - sink << ")"; - - return result; -} + std::string readLink(const CanonPath & path) override + { + return nix::readLink(path.abs()); + } +}; time_t dumpPathAndGetMtime(const Path & path, Sink & sink, PathFilter & filter) { - sink << narVersionMagic1; - return dump(path, sink, filter); + FSSourceAccessor accessor; + accessor.dumpPath(CanonPath::fromCwd(path), sink, filter); + return accessor.mtime; } void dumpPath(const Path & path, Sink & sink, PathFilter & filter) @@ -141,17 +185,6 @@ static SerialisationError badArchive(const std::string & s) } -#if 0 -static void skipGeneric(Source & source) -{ - if (readString(source) == "(") { - while (readString(source) != ")") - skipGeneric(source); - } -} -#endif - - static void parseContents(ParseSink & sink, Source & source, const Path & path) { uint64_t size = readLongLong(source); diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc index 23ec55c89..e3adee5f1 100644 --- a/src/libutil/source-accessor.cc +++ b/src/libutil/source-accessor.cc @@ -10,78 +10,6 @@ SourceAccessor::SourceAccessor() { } -// FIXME: merge with archive.cc. -void SourceAccessor::dumpPath( - const CanonPath & path, - Sink & sink, - PathFilter & filter) -{ - auto dumpContents = [&](const CanonPath & path) - { - // FIXME: pipe - auto s = readFile(path); - sink << "contents" << s.size(); - sink(s); - writePadding(s.size(), sink); - }; - - std::function dump; - - dump = [&](const CanonPath & path) { - checkInterrupt(); - - auto st = lstat(path); - - sink << "("; - - if (st.type == tRegular) { - sink << "type" << "regular"; - if (st.isExecutable) - sink << "executable" << ""; - dumpContents(path); - } - - else if (st.type == tDirectory) { - sink << "type" << "directory"; - - /* If we're on a case-insensitive system like macOS, undo - the case hack applied by restorePath(). */ - std::map unhacked; - for (auto & i : readDirectory(path)) - if (/* archiveSettings.useCaseHack */ false) { // FIXME - std::string name(i.first); - size_t pos = i.first.find(caseHackSuffix); - if (pos != std::string::npos) { - debug("removing case hack suffix from '%s'", path + i.first); - name.erase(pos); - } - if (!unhacked.emplace(name, i.first).second) - throw Error("file name collision in between '%s' and '%s'", - (path + unhacked[name]), - (path + i.first)); - } else - unhacked.emplace(i.first, i.first); - - for (auto & i : unhacked) - if (filter((path + i.first).abs())) { - sink << "entry" << "(" << "name" << i.first << "node"; - dump(path + i.second); - sink << ")"; - } - } - - else if (st.type == tSymlink) - sink << "type" << "symlink" << "target" << readLink(path); - - else throw Error("file '%s' has an unsupported type", path); - - sink << ")"; - }; - - sink << narVersionMagic1; - dump(path); -} - Hash SourceAccessor::hashPath( const CanonPath & path, PathFilter & filter, From 50156302c033323895ababb6c190086cbc5cec46 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 15:20:10 +0200 Subject: [PATCH 266/299] Deduplicate FSSourceAccessor and FSInputAccessor --- src/libfetchers/fs-input-accessor.cc | 31 +++++------------ src/libutil/archive.cc | 52 +--------------------------- src/libutil/source-accessor.cc | 49 ++++++++++++++++++++++++++ src/libutil/source-accessor.hh | 24 +++++++++++++ 4 files changed, 82 insertions(+), 74 deletions(-) diff --git a/src/libfetchers/fs-input-accessor.cc b/src/libfetchers/fs-input-accessor.cc index a35955465..e40faf03f 100644 --- a/src/libfetchers/fs-input-accessor.cc +++ b/src/libfetchers/fs-input-accessor.cc @@ -3,7 +3,7 @@ namespace nix { -struct FSInputAccessorImpl : FSInputAccessor +struct FSInputAccessorImpl : FSInputAccessor, PosixSourceAccessor { CanonPath root; std::optional> allowedPaths; @@ -23,28 +23,20 @@ struct FSInputAccessorImpl : FSInputAccessor { auto absPath = makeAbsPath(path); checkAllowed(absPath); - return nix::readFile(absPath.abs()); + return PosixSourceAccessor::readFile(absPath); } bool pathExists(const CanonPath & path) override { auto absPath = makeAbsPath(path); - return isAllowed(absPath) && nix::pathExists(absPath.abs()); + return isAllowed(absPath) && PosixSourceAccessor::pathExists(absPath); } Stat lstat(const CanonPath & path) override { auto absPath = makeAbsPath(path); checkAllowed(absPath); - auto st = nix::lstat(absPath.abs()); - return Stat { - .type = - S_ISREG(st.st_mode) ? tRegular : - S_ISDIR(st.st_mode) ? tDirectory : - S_ISLNK(st.st_mode) ? tSymlink : - tMisc, - .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR - }; + return PosixSourceAccessor::lstat(absPath); } DirEntries readDirectory(const CanonPath & path) override @@ -52,16 +44,9 @@ struct FSInputAccessorImpl : FSInputAccessor auto absPath = makeAbsPath(path); checkAllowed(absPath); DirEntries res; - for (auto & entry : nix::readDirectory(absPath.abs())) { - std::optional type; - switch (entry.type) { - case DT_REG: type = Type::tRegular; break; - case DT_LNK: type = Type::tSymlink; break; - case DT_DIR: type = Type::tDirectory; break; - } - if (isAllowed(absPath + entry.name)) - res.emplace(entry.name, type); - } + for (auto & entry : PosixSourceAccessor::readDirectory(absPath)) + if (isAllowed(absPath + entry.first)) + res.emplace(entry); return res; } @@ -69,7 +54,7 @@ struct FSInputAccessorImpl : FSInputAccessor { auto absPath = makeAbsPath(path); checkAllowed(absPath); - return nix::readLink(absPath.abs()); + return PosixSourceAccessor::readLink(absPath); } CanonPath makeAbsPath(const CanonPath & path) diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index e48fc7522..6508ba807 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -110,59 +110,9 @@ void SourceAccessor::dumpPath( } -struct FSSourceAccessor : SourceAccessor -{ - time_t mtime = 0; // most recent mtime seen - - std::string readFile(const CanonPath & path) override - { - return nix::readFile(path.abs()); - } - - bool pathExists(const CanonPath & path) override - { - return nix::pathExists(path.abs()); - } - - Stat lstat(const CanonPath & path) override - { - auto st = nix::lstat(path.abs()); - mtime = std::max(mtime, st.st_mtime); - return Stat { - .type = - S_ISREG(st.st_mode) ? tRegular : - S_ISDIR(st.st_mode) ? tDirectory : - S_ISLNK(st.st_mode) ? tSymlink : - tMisc, - .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR - }; - } - - DirEntries readDirectory(const CanonPath & path) override - { - DirEntries res; - for (auto & entry : nix::readDirectory(path.abs())) { - std::optional type; - switch (entry.type) { - case DT_REG: type = Type::tRegular; break; - case DT_LNK: type = Type::tSymlink; break; - case DT_DIR: type = Type::tDirectory; break; - } - res.emplace(entry.name, type); - } - return res; - } - - std::string readLink(const CanonPath & path) override - { - return nix::readLink(path.abs()); - } -}; - - time_t dumpPathAndGetMtime(const Path & path, Sink & sink, PathFilter & filter) { - FSSourceAccessor accessor; + PosixSourceAccessor accessor; accessor.dumpPath(CanonPath::fromCwd(path), sink, filter); return accessor.mtime; } diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc index e3adee5f1..d5c8cbcdd 100644 --- a/src/libutil/source-accessor.cc +++ b/src/libutil/source-accessor.cc @@ -33,4 +33,53 @@ std::string SourceAccessor::showPath(const CanonPath & path) return path.abs(); } +std::string PosixSourceAccessor::readFile(const CanonPath & path) +{ + return nix::readFile(path.abs()); +} + +bool PosixSourceAccessor::pathExists(const CanonPath & path) +{ + return nix::pathExists(path.abs()); +} + +SourceAccessor::Stat PosixSourceAccessor::lstat(const CanonPath & path) +{ + auto st = nix::lstat(path.abs()); + mtime = std::max(mtime, st.st_mtime); + return Stat { + .type = + S_ISREG(st.st_mode) ? tRegular : + S_ISDIR(st.st_mode) ? tDirectory : + S_ISLNK(st.st_mode) ? tSymlink : + tMisc, + .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR + }; +} + +SourceAccessor::DirEntries PosixSourceAccessor::readDirectory(const CanonPath & path) +{ + DirEntries res; + for (auto & entry : nix::readDirectory(path.abs())) { + std::optional type; + switch (entry.type) { + case DT_REG: type = Type::tRegular; break; + case DT_LNK: type = Type::tSymlink; break; + case DT_DIR: type = Type::tDirectory; break; + } + res.emplace(entry.name, type); + } + return res; +} + +std::string PosixSourceAccessor::readLink(const CanonPath & path) +{ + return nix::readLink(path.abs()); +} + +std::optional PosixSourceAccessor::getPhysicalPath(const CanonPath & path) +{ + return path; +} + } diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh index c2d35d6b5..a251ae31c 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/source-accessor.hh @@ -93,4 +93,28 @@ struct SourceAccessor } }; +/** + * A source accessor that uses the Unix filesystem. + */ +struct PosixSourceAccessor : SourceAccessor +{ + /** + * The most recent mtime seen by lstat(). This is a hack to + * support dumpPathAndGetMtime(). Should remove this eventually. + */ + time_t mtime = 0; + + std::string readFile(const CanonPath & path) override; + + bool pathExists(const CanonPath & path) override; + + Stat lstat(const CanonPath & path) override; + + DirEntries readDirectory(const CanonPath & path) override; + + std::string readLink(const CanonPath & path) override; + + std::optional getPhysicalPath(const CanonPath & path) override; +}; + } From 5be7705ddf9397846ca9391d464af1cc27110f06 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 19 Oct 2023 19:20:21 +0200 Subject: [PATCH 267/299] Remove stuff we don't need yet --- src/libexpr/eval.cc | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 7180b9fbe..8235b83c5 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -505,18 +505,7 @@ EvalState::EvalState( , sOutputSpecified(symbols.create("outputSpecified")) , repair(NoRepair) , emptyBindings(0) - , rootFS( - makeFSInputAccessor( - CanonPath::root, - evalSettings.restrictEval || evalSettings.pureEval - ? std::optional>(std::set()) - : std::nullopt, - [](const CanonPath & path) -> RestrictedPathError { - auto modeInformation = evalSettings.pureEval - ? "in pure evaluation mode (use '--impure' to override)" - : "in restricted mode"; - throw RestrictedPathError("access to absolute path '%1%' is forbidden %2%", path, modeInformation); - })) + , rootFS(makeFSInputAccessor(CanonPath::root)) , corepkgsFS(makeMemoryInputAccessor()) , internalFS(makeMemoryInputAccessor()) , derivationInternal{corepkgsFS->addFile( @@ -541,9 +530,6 @@ EvalState::EvalState( , baseEnv(allocEnv(128)) , staticBaseEnv{std::make_shared(false, nullptr)} { - // For now, don't rely on FSInputAccessor for access control. - rootFS->allowPath(CanonPath::root); - countCalls = getEnv("NIX_COUNT_CALLS").value_or("0") != "0"; assert(gcInitialised); From 87c4f4a972ce732962cc0881d848d8bf979893c2 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Thu, 19 Oct 2023 18:24:13 +0100 Subject: [PATCH 268/299] libutil: Move some non-template implememntations from config.hh to config.cc --- src/libutil/config.cc | 45 +++++++++++++++++++++++++++++++++++++++++++ src/libutil/config.hh | 31 +++++++---------------------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/libutil/config.cc b/src/libutil/config.cc index 8672edaa8..f5c7fb7d5 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -9,6 +9,10 @@ namespace nix { +Config::Config(StringMap initials) + : AbstractConfig(std::move(initials)) +{ } + bool Config::set(const std::string & name, const std::string & value) { bool append = false; @@ -59,6 +63,10 @@ void Config::addSetting(AbstractSetting * setting) } } +AbstractConfig::AbstractConfig(StringMap initials) + : unknownSettings(std::move(initials)) +{ } + void AbstractConfig::warnUnknownSettings() { for (auto & s : unknownSettings) @@ -199,6 +207,13 @@ AbstractSetting::AbstractSetting( { } +AbstractSetting::~AbstractSetting() +{ + // Check against a gcc miscompilation causing our constructor + // not to run (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80431). + assert(created == 123); +} + nlohmann::json AbstractSetting::toJSON() { return nlohmann::json(toJSONObject()); @@ -220,6 +235,9 @@ void AbstractSetting::convertToArg(Args & args, const std::string & category) { } + +bool AbstractSetting::isOverridden() const { return overridden; } + template<> std::string BaseSetting::parse(const std::string & str) const { return str; @@ -385,11 +403,33 @@ static Path parsePath(const AbstractSetting & s, const std::string & str) return canonPath(str); } +PathSetting::PathSetting(Config * options, + const Path & def, + const std::string & name, + const std::string & description, + const std::set & aliases) + : BaseSetting(def, true, name, description, aliases) +{ + options->addSetting(this); +} + Path PathSetting::parse(const std::string & str) const { return parsePath(*this, str); } + +OptionalPathSetting::OptionalPathSetting(Config * options, + const std::optional & def, + const std::string & name, + const std::string & description, + const std::set & aliases) + : BaseSetting>(def, true, name, description, aliases) +{ + options->addSetting(this); +} + + std::optional OptionalPathSetting::parse(const std::string & str) const { if (str == "") @@ -398,6 +438,11 @@ std::optional OptionalPathSetting::parse(const std::string & str) const return parsePath(*this, str); } +void OptionalPathSetting::operator =(const std::optional & v) +{ + this->assign(v); +} + bool GlobalConfig::set(const std::string & name, const std::string & value) { for (auto & config : *configRegistrations) diff --git a/src/libutil/config.hh b/src/libutil/config.hh index 96c2cd75d..47c7be473 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -52,9 +52,7 @@ class AbstractConfig protected: StringMap unknownSettings; - AbstractConfig(const StringMap & initials = {}) - : unknownSettings(initials) - { } + AbstractConfig(StringMap initials = {}); public: @@ -163,9 +161,7 @@ private: public: - Config(const StringMap & initials = {}) - : AbstractConfig(initials) - { } + Config(StringMap initials = {}); bool set(const std::string & name, const std::string & value) override; @@ -206,12 +202,7 @@ protected: const std::set & aliases, std::optional experimentalFeature = std::nullopt); - virtual ~AbstractSetting() - { - // Check against a gcc miscompilation causing our constructor - // not to run (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80431). - assert(created == 123); - } + virtual ~AbstractSetting(); virtual void set(const std::string & value, bool append = false) = 0; @@ -229,7 +220,7 @@ protected: virtual void convertToArg(Args & args, const std::string & category); - bool isOverridden() const { return overridden; } + bool isOverridden() const; }; /** @@ -365,11 +356,7 @@ public: const Path & def, const std::string & name, const std::string & description, - const std::set & aliases = {}) - : BaseSetting(def, true, name, description, aliases) - { - options->addSetting(this); - } + const std::set & aliases = {}); Path parse(const std::string & str) const override; @@ -391,15 +378,11 @@ public: const std::optional & def, const std::string & name, const std::string & description, - const std::set & aliases = {}) - : BaseSetting>(def, true, name, description, aliases) - { - options->addSetting(this); - } + const std::set & aliases = {}); std::optional parse(const std::string & str) const override; - void operator =(const std::optional & v) { this->assign(v); } + void operator =(const std::optional & v); }; struct GlobalConfig : public AbstractConfig From 55f06b6f30fa212d48cfd6d121a845863fb2980b Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Thu, 19 Oct 2023 18:24:54 +0100 Subject: [PATCH 269/299] libutil: Remove non-needed constructor --- src/libutil/config.cc | 4 ++-- src/libutil/config.hh | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libutil/config.cc b/src/libutil/config.cc index f5c7fb7d5..8e06273ee 100644 --- a/src/libutil/config.cc +++ b/src/libutil/config.cc @@ -33,9 +33,9 @@ bool Config::set(const std::string & name, const std::string & value) void Config::addSetting(AbstractSetting * setting) { - _settings.emplace(setting->name, Config::SettingData(false, setting)); + _settings.emplace(setting->name, Config::SettingData{false, setting}); for (auto & alias : setting->aliases) - _settings.emplace(alias, Config::SettingData(true, setting)); + _settings.emplace(alias, Config::SettingData{true, setting}); bool set = false; diff --git a/src/libutil/config.hh b/src/libutil/config.hh index 47c7be473..f09a9b0b8 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -148,9 +148,6 @@ public: { bool isAlias; AbstractSetting * setting; - SettingData(bool isAlias, AbstractSetting * setting) - : isAlias(isAlias), setting(setting) - { } }; typedef std::map Settings; From b0f4ac29d3c0534b91fff7b37191d4d7d5a96395 Mon Sep 17 00:00:00 2001 From: Jacek Galowicz Date: Thu, 19 Oct 2023 18:25:17 +0100 Subject: [PATCH 270/299] libutil: Use c++ style cast --- src/libutil/config.hh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libutil/config.hh b/src/libutil/config.hh index f09a9b0b8..38c3ce0c4 100644 --- a/src/libutil/config.hh +++ b/src/libutil/config.hh @@ -312,8 +312,7 @@ public: template std::ostream & operator <<(std::ostream & str, const BaseSetting & opt) { - str << (const T &) opt; - return str; + return str << static_cast(opt); } template From 42f26eb42e3b115f39570bb14445d7e5f33220d7 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Oct 2023 02:45:47 +0200 Subject: [PATCH 271/299] doc: complexity for '?' operator (#9184) Co-authored-by: Valentin Gagarin Co-authored-by: Robert Hensing --- doc/manual/src/language/operators.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/manual/src/language/operators.md b/doc/manual/src/language/operators.md index a22c71109..07b43a881 100644 --- a/doc/manual/src/language/operators.md +++ b/doc/manual/src/language/operators.md @@ -63,6 +63,8 @@ The result is a [Boolean] value. [Has attribute]: #has-attribute +After evaluating *attrset* and *attrpath*, the computational complexity is O(log(*n*)) for *n* attributes in the *attrset* + ## Arithmetic Numbers are type-compatible: From e58566a057692bbfbd30a6639248adfcf81df108 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Fri, 20 Oct 2023 05:11:03 +0200 Subject: [PATCH 272/299] doc: add reference to hasAttr in `?` operator (#9185) Co-authored-by: Valentin Gagarin --- doc/manual/src/language/operators.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/manual/src/language/operators.md b/doc/manual/src/language/operators.md index 07b43a881..cc825b4cf 100644 --- a/doc/manual/src/language/operators.md +++ b/doc/manual/src/language/operators.md @@ -59,6 +59,8 @@ An attribute path is a dot-separated list of [attribute names](./values.md#attri Test whether [attribute set] *attrset* contains the attribute denoted by *attrpath*. The result is a [Boolean] value. +See also: [`builtins.hasAttr`](@docroot@/language/builtins.md#builtins-hasAttr) + [Boolean]: ./values.md#type-boolean [Has attribute]: #has-attribute From 9277eb276bf0a942e88fcf499f6a6b9c262be853 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 Oct 2023 16:59:35 +0200 Subject: [PATCH 273/299] libstore: Add apple-virt to system features when available I'm sure that we'll adjust the implementation over time, but this at least discerns between an apple silicon bare metal machine and a tart VM. --- doc/manual/src/release-notes/rl-next.md | 2 ++ src/libstore/globals.cc | 31 +++++++++++++++++++++++++ src/libstore/globals.hh | 4 ++++ 3 files changed, 37 insertions(+) diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index 46ce2abac..d0b516dfb 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -8,6 +8,8 @@ - Introduce new flake installable syntax `flakeref#.attrPath` where the "." prefix denotes no searching of default attribute prefixes like `packages.` or `legacyPackages.`. +- Nix adds `apple-virt` to the default system features on macOS systems that support virtualization. This is similar to what's done for the `kvm` system feature on Linux hosts. + - Introduce a new built-in function [`builtins.convertHash`](@docroot@/language/builtins.md#builtins-convertHash). - `builtins.fetchTree` is now marked as stable. diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index 5a4cb1824..9c25d9868 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -24,6 +24,9 @@ #include "config-impl.hh" +#ifdef __APPLE__ +#include +#endif namespace nix { @@ -154,6 +157,29 @@ unsigned int Settings::getDefaultCores() return concurrency; } +#if __APPLE__ +static bool hasVirt() { + + int hasVMM; + int hvSupport; + size_t size; + + size = sizeof(hasVMM); + if (sysctlbyname("kern.hv_vmm_present", &hasVMM, &size, NULL, 0) == 0) { + if (hasVMM) + return false; + } + + // whether the kernel and hardware supports virt + size = sizeof(hvSupport); + if (sysctlbyname("kern.hv_support", &hvSupport, &size, NULL, 0) == 0) { + return hvSupport == 1; + } else { + return false; + } +} +#endif + StringSet Settings::getDefaultSystemFeatures() { /* For backwards compatibility, accept some "features" that are @@ -170,6 +196,11 @@ StringSet Settings::getDefaultSystemFeatures() features.insert("kvm"); #endif + #if __APPLE__ + if (hasVirt()) + features.insert("apple-virt"); + #endif + return features; } diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 5ec332417..ff7d3df1e 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -714,6 +714,10 @@ public: System features are user-defined, but Nix sets the following defaults: + - `apple-virt` + + Included on darwin if virtualization is available. + - `kvm` Included by default if `/dev/kvm` is accessible. From bb645c5d02025158c8dbb0c2a60e7cba8251f062 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 20 Oct 2023 10:19:07 +0200 Subject: [PATCH 274/299] system-features doc: kvm is Linux-only --- src/libstore/globals.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index ff7d3df1e..e90f70f5f 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -720,7 +720,7 @@ public: - `kvm` - Included by default if `/dev/kvm` is accessible. + Included on Linux if `/dev/kvm` is accessible. - `nixos-test`, `benchmark`, `big-parallel` From df10dc630fd13082e13400ec46acefe4ad85a715 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 12:36:18 +0200 Subject: [PATCH 275/299] Doxygen Co-authored-by: John Ericson --- src/libutil/source-accessor.hh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh index a251ae31c..4074b2e63 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/source-accessor.hh @@ -65,9 +65,11 @@ struct SourceAccessor PathFilter & filter = defaultPathFilter, HashType ht = htSHA256); - /* Return a corresponding path in the root filesystem, if - possible. This is only possible for filesystems that are - materialized in the root filesystem. */ + /** + * Return a corresponding path in the root filesystem, if + * possible. This is only possible for filesystems that are + * materialized in the root filesystem. + */ virtual std::optional getPhysicalPath(const CanonPath & path) { return std::nullopt; } From bacceaea919ddb26d1882fec16462cdcb4e1d740 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 12:40:46 +0200 Subject: [PATCH 276/299] Move getLastModified(), remove setPathDisplay() --- src/libfetchers/input-accessor.hh | 9 +++++++++ src/libutil/source-accessor.hh | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libfetchers/input-accessor.hh b/src/libfetchers/input-accessor.hh index 524a94cd1..5dc05a363 100644 --- a/src/libfetchers/input-accessor.hh +++ b/src/libfetchers/input-accessor.hh @@ -16,6 +16,15 @@ class Store; struct InputAccessor : SourceAccessor, std::enable_shared_from_this { + /** + * Return the maximum last-modified time of the files in this + * tree, if available. + */ + virtual std::optional getLastModified() + { + return std::nullopt; + } + StorePath fetchToStore( ref store, const CanonPath & path, diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh index 4074b2e63..53408eb6c 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/source-accessor.hh @@ -83,16 +83,7 @@ struct SourceAccessor return number < x.number; } - void setPathDisplay(std::string displayPrefix, std::string displaySuffix = ""); - virtual std::string showPath(const CanonPath & path); - - /* Return the maximum last-modified time of the files in this - tree, if available. */ - virtual std::optional getLastModified() - { - return std::nullopt; - } }; /** From 173abec0bce03016b001c415822793a309c40e0b Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 13:04:39 +0200 Subject: [PATCH 277/299] coerceToPath(): Handle __toString, add tests --- src/libexpr/eval.cc | 27 ++++++++++--------- src/libexpr/tests/error_traces.cc | 8 +++--- .../functional/lang/eval-okay-pathexists.nix | 2 ++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 8235b83c5..e3ecb987c 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2335,29 +2335,32 @@ SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext { try { forceValue(v, pos); - - if (v.type() == nString) { - copyContext(v, context); - auto s = v.string_view(); - if (!hasPrefix(s, "/")) - error("string '%s' doesn't represent an absolute path", s).atPos(pos).debugThrow(); - return rootPath(CanonPath(s)); - } } catch (Error & e) { e.addTrace(positions[pos], errorCtx); throw; } + /* Handle path values directly, without coercing to a string. */ if (v.type() == nPath) return v.path(); + /* Similarly, handle __toString where the result may be a path + value. */ if (v.type() == nAttrs) { - auto i = v.attrs->find(sOutPath); - if (i != v.attrs->end()) - return coerceToPath(pos, *i->value, context, errorCtx); + auto i = v.attrs->find(sToString); + if (i != v.attrs->end()) { + Value v1; + callFunction(*i->value, v, v1, pos); + return coerceToPath(pos, v1, context, errorCtx); + } } - error("cannot coerce %1% to a path", showType(v)).withTrace(pos, errorCtx).debugThrow(); + /* Any other value should be coercable to a string, interpreted + relative to the root filesystem. */ + auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned(); + if (path == "" || path[0] != '/') + error("string '%1%' doesn't represent an absolute path", path).withTrace(pos, errorCtx).debugThrow(); + return rootPath(CanonPath(path)); } diff --git a/src/libexpr/tests/error_traces.cc b/src/libexpr/tests/error_traces.cc index 6da8a5954..9cd9f0a60 100644 --- a/src/libexpr/tests/error_traces.cc +++ b/src/libexpr/tests/error_traces.cc @@ -295,7 +295,7 @@ namespace nix { TEST_F(ErrorTraceTest, toPath) { ASSERT_TRACE2("toPath []", TypeError, - hintfmt("cannot coerce %s to a path", "a list"), + hintfmt("cannot coerce %s to a string", "a list"), hintfmt("while evaluating the first argument passed to builtins.toPath")); ASSERT_TRACE2("toPath \"foo\"", @@ -309,7 +309,7 @@ namespace nix { TEST_F(ErrorTraceTest, storePath) { ASSERT_TRACE2("storePath true", TypeError, - hintfmt("cannot coerce %s to a path", "a Boolean"), + hintfmt("cannot coerce %s to a string", "a Boolean"), hintfmt("while evaluating the first argument passed to 'builtins.storePath'")); } @@ -318,7 +318,7 @@ namespace nix { TEST_F(ErrorTraceTest, pathExists) { ASSERT_TRACE2("pathExists []", TypeError, - hintfmt("cannot coerce %s to a path", "a list"), + hintfmt("cannot coerce %s to a string", "a list"), hintfmt("while realising the context of a path")); ASSERT_TRACE2("pathExists \"zorglub\"", @@ -377,7 +377,7 @@ namespace nix { TEST_F(ErrorTraceTest, filterSource) { ASSERT_TRACE2("filterSource [] []", TypeError, - hintfmt("cannot coerce %s to a path", "a list"), + hintfmt("cannot coerce %s to a string", "a list"), hintfmt("while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'")); ASSERT_TRACE2("filterSource [] \"foo\"", diff --git a/tests/functional/lang/eval-okay-pathexists.nix b/tests/functional/lang/eval-okay-pathexists.nix index c5e7a62de..31697f66a 100644 --- a/tests/functional/lang/eval-okay-pathexists.nix +++ b/tests/functional/lang/eval-okay-pathexists.nix @@ -25,5 +25,7 @@ builtins.pathExists (./lib.nix) && builtins.pathExists (builtins.toString ./. + "/../lang/..//") && builtins.pathExists (builtins.toPath (builtins.toString ./lib.nix)) && !builtins.pathExists (builtins.toPath (builtins.toString ./bla.nix)) +&& builtins.pathExists (builtins.toPath { __toString = x: builtins.toString ./lib.nix; }) +&& builtins.pathExists (builtins.toPath { outPath = builtins.toString ./lib.nix; }) && builtins.pathExists ./lib.nix && !builtins.pathExists ./bla.nix From 7a086a32bce3338bdc1ce669a51599a99bc17b9f Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 13:32:15 +0200 Subject: [PATCH 278/299] fetchToStore(): Handle flat ingestion method and add test --- src/libfetchers/input-accessor.cc | 5 ++++- tests/functional/lang/eval-okay-path.exp | 2 +- tests/functional/lang/eval-okay-path.nix | 22 +++++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/libfetchers/input-accessor.cc b/src/libfetchers/input-accessor.cc index 9bddf7c2e..488350849 100644 --- a/src/libfetchers/input-accessor.cc +++ b/src/libfetchers/input-accessor.cc @@ -14,7 +14,10 @@ StorePath InputAccessor::fetchToStore( Activity act(*logger, lvlChatty, actUnknown, fmt("copying '%s' to the store", showPath(path))); auto source = sinkToSource([&](Sink & sink) { - dumpPath(path, sink, filter ? *filter : defaultPathFilter); + if (method == FileIngestionMethod::Recursive) + dumpPath(path, sink, filter ? *filter : defaultPathFilter); + else + sink(readFile(path)); // FIXME: stream }); auto storePath = diff --git a/tests/functional/lang/eval-okay-path.exp b/tests/functional/lang/eval-okay-path.exp index 3ce7f8283..635e2243a 100644 --- a/tests/functional/lang/eval-okay-path.exp +++ b/tests/functional/lang/eval-okay-path.exp @@ -1 +1 @@ -"/nix/store/ya937r4ydw0l6kayq8jkyqaips9c75jm-output" +[ "/nix/store/ya937r4ydw0l6kayq8jkyqaips9c75jm-output" "/nix/store/m7y372g6jb0g4hh1dzmj847rd356fhnz-output" ] diff --git a/tests/functional/lang/eval-okay-path.nix b/tests/functional/lang/eval-okay-path.nix index e67168cf3..599b33541 100644 --- a/tests/functional/lang/eval-okay-path.nix +++ b/tests/functional/lang/eval-okay-path.nix @@ -1,7 +1,15 @@ -builtins.path - { path = ./.; - filter = path: _: baseNameOf path == "data"; - recursive = true; - sha256 = "1yhm3gwvg5a41yylymgblsclk95fs6jy72w0wv925mmidlhcq4sw"; - name = "output"; - } +[ + (builtins.path + { path = ./.; + filter = path: _: baseNameOf path == "data"; + recursive = true; + sha256 = "1yhm3gwvg5a41yylymgblsclk95fs6jy72w0wv925mmidlhcq4sw"; + name = "output"; + }) + (builtins.path + { path = ./data; + recursive = false; + sha256 = "0k4lwj58f2w5yh92ilrwy9917pycipbrdrr13vbb3yd02j09vfxm"; + name = "output"; + }) +] From 57db3be9e448042814d386def2d8af16a2a4c4b9 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 16:36:41 +0200 Subject: [PATCH 279/299] SourceAccessor::readFile(): Support reading into a sink --- src/libfetchers/fs-input-accessor.cc | 7 +++- src/libfetchers/input-accessor.cc | 2 +- src/libutil/archive.cc | 15 ++++--- src/libutil/source-accessor.cc | 58 +++++++++++++++++++++++++++- src/libutil/source-accessor.hh | 25 +++++++++++- 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/libfetchers/fs-input-accessor.cc b/src/libfetchers/fs-input-accessor.cc index e40faf03f..3444c4643 100644 --- a/src/libfetchers/fs-input-accessor.cc +++ b/src/libfetchers/fs-input-accessor.cc @@ -19,11 +19,14 @@ struct FSInputAccessorImpl : FSInputAccessor, PosixSourceAccessor { } - std::string readFile(const CanonPath & path) override + void readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback) override { auto absPath = makeAbsPath(path); checkAllowed(absPath); - return PosixSourceAccessor::readFile(absPath); + PosixSourceAccessor::readFile(absPath, sink, sizeCallback); } bool pathExists(const CanonPath & path) override diff --git a/src/libfetchers/input-accessor.cc b/src/libfetchers/input-accessor.cc index 488350849..d1d450cf7 100644 --- a/src/libfetchers/input-accessor.cc +++ b/src/libfetchers/input-accessor.cc @@ -17,7 +17,7 @@ StorePath InputAccessor::fetchToStore( if (method == FileIngestionMethod::Recursive) dumpPath(path, sink, filter ? *filter : defaultPathFilter); else - sink(readFile(path)); // FIXME: stream + readFile(path, sink); }); auto storePath = diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 6508ba807..0cd54e5db 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -44,12 +44,15 @@ void SourceAccessor::dumpPath( { auto dumpContents = [&](const CanonPath & path) { - /* It would be nice if this was streaming, but we need the - size before the contents. */ - auto s = readFile(path); - sink << "contents" << s.size(); - sink(s); - writePadding(s.size(), sink); + sink << "contents"; + std::optional size; + readFile(path, sink, [&](uint64_t _size) + { + size = _size; + sink << _size; + }); + assert(size); + writePadding(*size, sink); }; std::function dump; diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc index d5c8cbcdd..2d03d3d7a 100644 --- a/src/libutil/source-accessor.cc +++ b/src/libutil/source-accessor.cc @@ -10,6 +10,28 @@ SourceAccessor::SourceAccessor() { } +std::string SourceAccessor::readFile(const CanonPath & path) +{ + StringSink sink; + std::optional size; + readFile(path, sink, [&](uint64_t _size) + { + size = _size; + }); + assert(size && *size == sink.s.size()); + return std::move(sink.s); +} + +void SourceAccessor::readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback) +{ + auto s = readFile(path); + sizeCallback(s.size()); + sink(s); +} + Hash SourceAccessor::hashPath( const CanonPath & path, PathFilter & filter, @@ -33,9 +55,41 @@ std::string SourceAccessor::showPath(const CanonPath & path) return path.abs(); } -std::string PosixSourceAccessor::readFile(const CanonPath & path) +void PosixSourceAccessor::readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback) { - return nix::readFile(path.abs()); + // FIXME: add O_NOFOLLOW since symlinks should be resolved by the + // caller? + AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (!fd) + throw SysError("opening file '%1%'", path); + + struct stat st; + if (fstat(fd.get(), &st) == -1) + throw SysError("statting file"); + + sizeCallback(st.st_size); + + off_t left = st.st_size; + + std::vector buf(64 * 1024); + while (left) { + checkInterrupt(); + ssize_t rd = read(fd.get(), buf.data(), (size_t) std::min(left, (off_t) buf.size())); + if (rd == -1) { + if (errno != EINTR) + throw SysError("reading from file '%s'", showPath(path)); + } + else if (rd == 0) + throw SysError("unexpected end-of-file reading '%s'", showPath(path)); + else { + assert(rd <= left); + sink({(char *) buf.data(), (size_t) rd}); + left -= rd; + } + } } bool PosixSourceAccessor::pathExists(const CanonPath & path) diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh index 53408eb6c..f3504c9bb 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/source-accessor.hh @@ -5,6 +5,8 @@ namespace nix { +struct Sink; + /** * A read-only filesystem abstraction. This is used by the Nix * evaluator and elsewhere for accessing sources in various @@ -20,7 +22,23 @@ struct SourceAccessor virtual ~SourceAccessor() { } - virtual std::string readFile(const CanonPath & path) = 0; + /** + * Return the contents of a file as a string. + */ + virtual std::string readFile(const CanonPath & path); + + /** + * Write the contents of a file as a sink. `sizeCallback` must be + * called with the size of the file before any data is written to + * the sink. + * + * Note: subclasses of `SourceAccessor` need to implement at least + * one of the `readFile()` variants. + */ + virtual void readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback = [](uint64_t size){}); virtual bool pathExists(const CanonPath & path) = 0; @@ -97,7 +115,10 @@ struct PosixSourceAccessor : SourceAccessor */ time_t mtime = 0; - std::string readFile(const CanonPath & path) override; + void readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback) override; bool pathExists(const CanonPath & path) override; From bcf5c31950a01d73c17cee301d3dd363c91cd23d Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 16:58:33 +0200 Subject: [PATCH 280/299] Add future FIXME --- src/libexpr/eval.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index e3ecb987c..d26cde423 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -2461,6 +2461,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v case nPath: return + // FIXME: compare accessors by their fingerprint. v1._path.accessor == v2._path.accessor && strcmp(v1._path.path, v2._path.path) == 0; From af302267e5e02c8b373779fac979f20e094e7cfa Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 17:18:42 +0200 Subject: [PATCH 281/299] Input::hasAllInfo(): Remove --- src/libfetchers/fetchers.cc | 9 +-------- src/libfetchers/fetchers.hh | 11 ----------- src/libfetchers/git.cc | 9 --------- src/libfetchers/github.cc | 5 ----- src/libfetchers/indirect.cc | 5 ----- src/libfetchers/mercurial.cc | 7 ------- src/libfetchers/path.cc | 5 ----- src/libfetchers/tarball.cc | 6 ------ 8 files changed, 1 insertion(+), 56 deletions(-) diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index c54c39cf0..000609f09 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -89,11 +89,6 @@ Attrs Input::toAttrs() const return attrs; } -bool Input::hasAllInfo() const -{ - return getNarHash() && scheme && scheme->hasAllInfo(*this); -} - bool Input::operator ==(const Input & other) const { return attrs == other.attrs; @@ -117,7 +112,7 @@ std::pair Input::fetch(ref store) const /* The tree may already be in the Nix store, or it could be substituted (which is often faster than fetching from the original source). So check that. */ - if (hasAllInfo()) { + if (getNarHash()) { try { auto storePath = computeStorePath(*store); @@ -175,8 +170,6 @@ std::pair Input::fetch(ref store) const input.locked = true; - assert(input.hasAllInfo()); - return {std::move(tree), input}; } diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index b55aabb6f..f52175e41 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -79,15 +79,6 @@ public: */ bool isLocked() const { return locked; } - /** - * Check whether the input carries all necessary info required - * for cache insertion and substitution. - * These fields are used to uniquely identify cached trees - * within the "tarball TTL" window without necessarily - * indicating that the input's origin is unchanged. - */ - bool hasAllInfo() const; - bool operator ==(const Input & other) const; bool contains(const Input & other) const; @@ -144,8 +135,6 @@ struct InputScheme virtual ParsedURL toURL(const Input & input) const; - virtual bool hasAllInfo(const Input & input) const = 0; - virtual Input applyOverrides( const Input & input, std::optional ref, diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index c1a6dce43..26b8987d6 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -321,15 +321,6 @@ struct GitInputScheme : InputScheme return url; } - bool hasAllInfo(const Input & input) const override - { - bool maybeDirty = !input.getRef(); - bool shallow = maybeGetBoolAttr(input.attrs, "shallow").value_or(false); - return - maybeGetIntAttr(input.attrs, "lastModified") - && (shallow || maybeDirty || maybeGetIntAttr(input.attrs, "revCount")); - } - Input applyOverrides( const Input & input, std::optional ref, diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index d7450defe..b824140a6 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -132,11 +132,6 @@ struct GitArchiveInputScheme : InputScheme }; } - bool hasAllInfo(const Input & input) const override - { - return input.getRev() && maybeGetIntAttr(input.attrs, "lastModified"); - } - Input applyOverrides( const Input & _input, std::optional ref, diff --git a/src/libfetchers/indirect.cc b/src/libfetchers/indirect.cc index e09d8bfb8..947849802 100644 --- a/src/libfetchers/indirect.cc +++ b/src/libfetchers/indirect.cc @@ -78,11 +78,6 @@ struct IndirectInputScheme : InputScheme return url; } - bool hasAllInfo(const Input & input) const override - { - return false; - } - Input applyOverrides( const Input & _input, std::optional ref, diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/mercurial.cc index b0d2d1909..f830a3271 100644 --- a/src/libfetchers/mercurial.cc +++ b/src/libfetchers/mercurial.cc @@ -98,13 +98,6 @@ struct MercurialInputScheme : InputScheme return url; } - bool hasAllInfo(const Input & input) const override - { - // FIXME: ugly, need to distinguish between dirty and clean - // default trees. - return input.getRef() == "default" || maybeGetIntAttr(input.attrs, "revCount"); - } - Input applyOverrides( const Input & input, std::optional ref, diff --git a/src/libfetchers/path.cc b/src/libfetchers/path.cc index 092975f5d..d829609b5 100644 --- a/src/libfetchers/path.cc +++ b/src/libfetchers/path.cc @@ -66,11 +66,6 @@ struct PathInputScheme : InputScheme }; } - bool hasAllInfo(const Input & input) const override - { - return true; - } - std::optional getSourcePath(const Input & input) override { return getStrAttr(input.attrs, "path"); diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index 269a56526..ba47d59a7 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -253,12 +253,6 @@ struct CurlInputScheme : InputScheme url.query.insert_or_assign("narHash", narHash->to_string(HashFormat::SRI, true)); return url; } - - bool hasAllInfo(const Input & input) const override - { - return true; - } - }; struct FileInputScheme : CurlInputScheme From 3e6b9f9357e4573740a4d9e477c4f4fa69c74fa5 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 16 Oct 2023 01:17:33 -0400 Subject: [PATCH 282/299] Remove `prevInfos` as its dead code It is unused since 8e0946e8df968391d1430af8377bdb51204e4666 removed support for the repeat and enforce-determinism options. --- src/libstore/build/local-derivation-goal.hh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libstore/build/local-derivation-goal.hh b/src/libstore/build/local-derivation-goal.hh index 0a05081c7..8191af7a6 100644 --- a/src/libstore/build/local-derivation-goal.hh +++ b/src/libstore/build/local-derivation-goal.hh @@ -120,14 +120,6 @@ struct LocalDerivationGoal : public DerivationGoal */ OutputPathMap scratchOutputs; - /** - * Path registration info from the previous round, if we're - * building multiple times. Since this contains the hash, it - * allows us to compare whether two rounds produced the same - * result. - */ - std::map prevInfos; - uid_t sandboxUid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID(); } gid_t sandboxGid() { return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID(); } From 862d16436b7606bba9064e2bf2a4c48bd883bb42 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 16 Oct 2023 01:22:34 -0400 Subject: [PATCH 283/299] Remove the `ValidPathInfo` `==` operator It is dead code. It was added in 8e0946e8df968391d1430af8377bdb51204e4666 as part of the repeated / enforce-determinism feature, but that was removed in 8fdd156a650f9b2ce9ae8cd74edcf16225478292. It is not good because it skips many fields. For testing purposes we will soon want to add a new one that doesn't skip fields, but we want to make sure making == sensitive to those fields won't change how Nix works. Proving in this commit that the old version is dead code achieves that. --- src/libstore/path-info.hh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libstore/path-info.hh b/src/libstore/path-info.hh index 221523622..fea6d0e5f 100644 --- a/src/libstore/path-info.hh +++ b/src/libstore/path-info.hh @@ -72,14 +72,6 @@ struct ValidPathInfo */ std::optional ca; - bool operator == (const ValidPathInfo & i) const - { - return - path == i.path - && narHash == i.narHash - && references == i.references; - } - /** * Return a fingerprint of the store path to be used in binary * cache signatures. It contains the store path, the base-32 From 0f7e9d051340531332b7c6854675006cc8a16f50 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 19:13:35 +0200 Subject: [PATCH 284/299] Input: Remove 'direct' field --- src/libfetchers/fetchers.cc | 5 +++++ src/libfetchers/fetchers.hh | 6 ++++-- src/libfetchers/indirect.cc | 5 +++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index 000609f09..5f6167868 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -84,6 +84,11 @@ std::string Input::to_string() const return toURL().to_string(); } +bool Input::isDirect() const +{ + return !scheme || scheme->isDirect(*this); +} + Attrs Input::toAttrs() const { return attrs; diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index f52175e41..4119dd17b 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -35,7 +35,6 @@ struct Input std::shared_ptr scheme; // note: can be null Attrs attrs; bool locked = false; - bool direct = true; /** * path of the parent of this input, used for relative path resolution @@ -71,7 +70,7 @@ public: * Check whether this is a "direct" input, that is, not * one that goes through a registry. */ - bool isDirect() const { return direct; } + bool isDirect() const; /** * Check whether this is a "locked" input, that is, @@ -152,6 +151,9 @@ struct InputScheme * Is this `InputScheme` part of an experimental feature? */ virtual std::optional experimentalFeature(); + + virtual bool isDirect(const Input & input) const + { return true; } }; void registerInputScheme(std::shared_ptr && fetcher); diff --git a/src/libfetchers/indirect.cc b/src/libfetchers/indirect.cc index 947849802..9a71df3d4 100644 --- a/src/libfetchers/indirect.cc +++ b/src/libfetchers/indirect.cc @@ -41,7 +41,6 @@ struct IndirectInputScheme : InputScheme // FIXME: forbid query params? Input input; - input.direct = false; input.attrs.insert_or_assign("type", "indirect"); input.attrs.insert_or_assign("id", id); if (rev) input.attrs.insert_or_assign("rev", rev->gitRev()); @@ -63,7 +62,6 @@ struct IndirectInputScheme : InputScheme throw BadURL("'%s' is not a valid flake ID", id); Input input; - input.direct = false; input.attrs = attrs; return input; } @@ -98,6 +96,9 @@ struct IndirectInputScheme : InputScheme { return Xp::Flakes; } + + bool isDirect(const Input & input) const override + { return false; } }; static auto rIndirectInputScheme = OnStartup([] { registerInputScheme(std::make_unique()); }); From 85e5ac403fa9adb82105664bb6c4df501d4a0a25 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Fri, 20 Oct 2023 10:28:26 -0700 Subject: [PATCH 285/299] docker: publish images to ghcr.io (#8066) * docker: publish images to ghcr.io docker.com announced their intention to remove the free plan used by OSS. The nixos/nix image is essential to various CI runs to build with nix. To provide a continuity plan, this commit pushes the image to ghcr.io as well. Co-authored-by: Sandro --- .github/workflows/ci.yml | 23 +++++++++++++++++++ .../src/installation/installing-docker.md | 8 +++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c9c24dad..afe4dc2e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,9 @@ jobs: docker_push_image: needs: [check_secrets, tests] + permissions: + contents: read + packages: write if: >- github.event_name == 'push' && github.ref_name == 'master' && @@ -126,6 +129,9 @@ jobs: - run: docker load -i ./result/image.tar.gz - run: docker tag nix:$NIX_VERSION nixos/nix:$NIX_VERSION - run: docker tag nix:$NIX_VERSION nixos/nix:master + # We'll deploy the newly built image to both Docker Hub and Github Container Registry. + # + # Push to Docker Hub first - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -133,3 +139,20 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - run: docker push nixos/nix:$NIX_VERSION - run: docker push nixos/nix:master + # Push to GitHub Container Registry as well + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Push image + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/nix + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + + docker tag nix:$NIX_VERSION $IMAGE_ID:$NIX_VERSION + docker tag nix:$NIX_VERSION $IMAGE_ID:master + docker push $IMAGE_ID:$NIX_VERSION + docker push $IMAGE_ID:master diff --git a/doc/manual/src/installation/installing-docker.md b/doc/manual/src/installation/installing-docker.md index 9d6d8f2d9..6f77d6a57 100644 --- a/doc/manual/src/installation/installing-docker.md +++ b/doc/manual/src/installation/installing-docker.md @@ -3,14 +3,14 @@ To run the latest stable release of Nix with Docker run the following command: ```console -$ docker run -ti nixos/nix -Unable to find image 'nixos/nix:latest' locally -latest: Pulling from nixos/nix +$ docker run -ti ghcr.io/nixos/nix +Unable to find image 'ghcr.io/nixos/nix:latest' locally +latest: Pulling from ghcr.io/nixos/nix 5843afab3874: Pull complete b52bf13f109c: Pull complete 1e2415612aa3: Pull complete Digest: sha256:27f6e7f60227e959ee7ece361f75d4844a40e1cc6878b6868fe30140420031ff -Status: Downloaded newer image for nixos/nix:latest +Status: Downloaded newer image for ghcr.io/nixos/nix:latest 35ca4ada6e96:/# nix --version nix (Nix) 2.3.12 35ca4ada6e96:/# exit From 935c9981de03248fd5687c5d0363f572af723144 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 20 Oct 2023 19:50:21 +0200 Subject: [PATCH 286/299] Remove fetchers::Tree and move tarball-related stuff into its own header --- src/libcmd/common-eval-args.cc | 5 ++-- src/libexpr/flake/flake.cc | 34 ++++++++++----------- src/libexpr/flake/flake.hh | 6 ++-- src/libexpr/flake/flakeref.cc | 6 ++-- src/libexpr/flake/flakeref.hh | 2 +- src/libexpr/parser.y | 6 ++-- src/libexpr/primops/fetchMercurial.cc | 6 ++-- src/libexpr/primops/fetchTree.cc | 13 ++++---- src/libfetchers/fetchers.cc | 18 +++++------ src/libfetchers/fetchers.hh | 41 ++----------------------- src/libfetchers/github.cc | 5 ++-- src/libfetchers/registry.cc | 2 +- src/libfetchers/tarball.cc | 7 +++-- src/libfetchers/tarball.hh | 43 +++++++++++++++++++++++++++ src/nix-channel/nix-channel.cc | 2 +- src/nix/flake.cc | 18 +++++------ 16 files changed, 111 insertions(+), 103 deletions(-) create mode 100644 src/libfetchers/tarball.hh diff --git a/src/libcmd/common-eval-args.cc b/src/libcmd/common-eval-args.cc index e36bda52f..e6df49a7a 100644 --- a/src/libcmd/common-eval-args.cc +++ b/src/libcmd/common-eval-args.cc @@ -9,6 +9,7 @@ #include "flake/flakeref.hh" #include "store-api.hh" #include "command.hh" +#include "tarball.hh" namespace nix { @@ -168,14 +169,14 @@ SourcePath lookupFileArg(EvalState & state, std::string_view s) { if (EvalSettings::isPseudoUrl(s)) { auto storePath = fetchers::downloadTarball( - state.store, EvalSettings::resolvePseudoUrl(s), "source", false).tree.storePath; + state.store, EvalSettings::resolvePseudoUrl(s), "source", false).storePath; return state.rootPath(CanonPath(state.store->toRealPath(storePath))); } else if (hasPrefix(s, "flake:")) { experimentalFeatureSettings.require(Xp::Flakes); auto flakeRef = parseFlakeRef(std::string(s.substr(6)), {}, true, false); - auto storePath = flakeRef.resolve(state.store).fetchTree(state.store).first.storePath; + auto storePath = flakeRef.resolve(state.store).fetchTree(state.store).first; return state.rootPath(CanonPath(state.store->toRealPath(storePath))); } diff --git a/src/libexpr/flake/flake.cc b/src/libexpr/flake/flake.cc index a6212c12f..b32c7c086 100644 --- a/src/libexpr/flake/flake.cc +++ b/src/libexpr/flake/flake.cc @@ -15,7 +15,7 @@ using namespace flake; namespace flake { -typedef std::pair FetchedFlake; +typedef std::pair FetchedFlake; typedef std::vector> FlakeCache; static std::optional lookupInFlakeCache( @@ -34,7 +34,7 @@ static std::optional lookupInFlakeCache( return std::nullopt; } -static std::tuple fetchOrSubstituteTree( +static std::tuple fetchOrSubstituteTree( EvalState & state, const FlakeRef & originalRef, bool allowLookup, @@ -61,16 +61,16 @@ static std::tuple fetchOrSubstituteTree( flakeCache.push_back({originalRef, *fetched}); } - auto [tree, lockedRef] = *fetched; + auto [storePath, lockedRef] = *fetched; debug("got tree '%s' from '%s'", - state.store->printStorePath(tree.storePath), lockedRef); + state.store->printStorePath(storePath), lockedRef); - state.allowPath(tree.storePath); + state.allowPath(storePath); - assert(!originalRef.input.getNarHash() || tree.storePath == originalRef.input.computeStorePath(*state.store)); + assert(!originalRef.input.getNarHash() || storePath == originalRef.input.computeStorePath(*state.store)); - return {std::move(tree), resolvedRef, lockedRef}; + return {std::move(storePath), resolvedRef, lockedRef}; } static void forceTrivialValue(EvalState & state, Value & value, const PosIdx pos) @@ -202,21 +202,21 @@ static Flake getFlake( FlakeCache & flakeCache, InputPath lockRootPath) { - auto [sourceInfo, resolvedRef, lockedRef] = fetchOrSubstituteTree( + auto [storePath, resolvedRef, lockedRef] = fetchOrSubstituteTree( state, originalRef, allowLookup, flakeCache); // Guard against symlink attacks. - auto flakeDir = canonPath(sourceInfo.actualPath + "/" + lockedRef.subdir, true); + auto flakeDir = canonPath(state.store->toRealPath(storePath) + "/" + lockedRef.subdir, true); auto flakeFile = canonPath(flakeDir + "/flake.nix", true); - if (!isInDir(flakeFile, sourceInfo.actualPath)) + if (!isInDir(flakeFile, state.store->toRealPath(storePath))) throw Error("'flake.nix' file of flake '%s' escapes from '%s'", - lockedRef, state.store->printStorePath(sourceInfo.storePath)); + lockedRef, state.store->printStorePath(storePath)); Flake flake { .originalRef = originalRef, .resolvedRef = resolvedRef, .lockedRef = lockedRef, - .sourceInfo = std::make_shared(std::move(sourceInfo)) + .storePath = storePath, }; if (!pathExists(flakeFile)) @@ -346,7 +346,7 @@ LockedFlake lockFlake( // FIXME: symlink attack auto oldLockFile = LockFile::read( lockFlags.referenceLockFilePath.value_or( - flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock")); + state.store->toRealPath(flake.storePath) + "/" + flake.lockedRef.subdir + "/flake.lock")); debug("old lock file: %s", oldLockFile); @@ -574,7 +574,7 @@ LockedFlake lockFlake( oldLock ? std::dynamic_pointer_cast(oldLock) : LockFile::read( - inputFlake.sourceInfo->actualPath + "/" + inputFlake.lockedRef.subdir + "/flake.lock").root.get_ptr(), + state.store->toRealPath(inputFlake.storePath) + "/" + inputFlake.lockedRef.subdir + "/flake.lock").root.get_ptr(), oldLock ? lockRootPath : inputPath, localPath, false); @@ -598,7 +598,7 @@ LockedFlake lockFlake( }; // Bring in the current ref for relative path resolution if we have it - auto parentPath = canonPath(flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir, true); + auto parentPath = canonPath(state.store->toRealPath(flake.storePath) + "/" + flake.lockedRef.subdir, true); computeLocks( flake.inputs, @@ -729,7 +729,7 @@ void callFlake(EvalState & state, emitTreeAttrs( state, - *lockedFlake.flake.sourceInfo, + lockedFlake.flake.storePath, lockedFlake.flake.lockedRef.input, *vRootSrc, false, @@ -893,7 +893,7 @@ Fingerprint LockedFlake::getFingerprint() const // flake.sourceInfo.storePath for the fingerprint. return hashString(htSHA256, fmt("%s;%s;%d;%d;%s", - flake.sourceInfo->storePath.to_string(), + flake.storePath.to_string(), flake.lockedRef.subdir, flake.lockedRef.input.getRevCount().value_or(0), flake.lockedRef.input.getLastModified().value_or(0), diff --git a/src/libexpr/flake/flake.hh b/src/libexpr/flake/flake.hh index c1d1b71e5..d5ad3eade 100644 --- a/src/libexpr/flake/flake.hh +++ b/src/libexpr/flake/flake.hh @@ -10,8 +10,6 @@ namespace nix { class EvalState; -namespace fetchers { struct Tree; } - namespace flake { struct FlakeInput; @@ -84,7 +82,7 @@ struct Flake */ bool forceDirty = false; std::optional description; - std::shared_ptr sourceInfo; + StorePath storePath; FlakeInputs inputs; /** * 'nixConfig' attribute @@ -193,7 +191,7 @@ void callFlake( void emitTreeAttrs( EvalState & state, - const fetchers::Tree & tree, + const StorePath & storePath, const fetchers::Input & input, Value & v, bool emptyRevFallback = false, diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index 93ad33a33..16f45ace7 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -272,10 +272,10 @@ FlakeRef FlakeRef::fromAttrs(const fetchers::Attrs & attrs) fetchers::maybeGetStrAttr(attrs, "dir").value_or("")); } -std::pair FlakeRef::fetchTree(ref store) const +std::pair FlakeRef::fetchTree(ref store) const { - auto [tree, lockedInput] = input.fetch(store); - return {std::move(tree), FlakeRef(std::move(lockedInput), subdir)}; + auto [storePath, lockedInput] = input.fetch(store); + return {std::move(storePath), FlakeRef(std::move(lockedInput), subdir)}; } std::tuple parseFlakeRefWithFragmentAndExtendedOutputsSpec( diff --git a/src/libexpr/flake/flakeref.hh b/src/libexpr/flake/flakeref.hh index ffb2e50de..5d78f49b6 100644 --- a/src/libexpr/flake/flakeref.hh +++ b/src/libexpr/flake/flakeref.hh @@ -63,7 +63,7 @@ struct FlakeRef static FlakeRef fromAttrs(const fetchers::Attrs & attrs); - std::pair fetchTree(ref store) const; + std::pair fetchTree(ref store) const; }; std::ostream & operator << (std::ostream & str, const FlakeRef & flakeRef); diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 70228e1e2..8b73bd056 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -646,7 +646,7 @@ formal #include "eval.hh" #include "filetransfer.hh" -#include "fetchers.hh" +#include "tarball.hh" #include "store-api.hh" #include "flake/flake.hh" @@ -783,7 +783,7 @@ std::optional EvalState::resolveSearchPathPath(const SearchPath::Pa if (EvalSettings::isPseudoUrl(value)) { try { auto storePath = fetchers::downloadTarball( - store, EvalSettings::resolvePseudoUrl(value), "source", false).tree.storePath; + store, EvalSettings::resolvePseudoUrl(value), "source", false).storePath; res = { store->toRealPath(storePath) }; } catch (FileTransferError & e) { logWarning({ @@ -797,7 +797,7 @@ std::optional EvalState::resolveSearchPathPath(const SearchPath::Pa experimentalFeatureSettings.require(Xp::Flakes); auto flakeRef = parseFlakeRef(value.substr(6), {}, true, false); debug("fetching flake search path element '%s''", value); - auto storePath = flakeRef.resolve(store).fetchTree(store).first.storePath; + auto storePath = flakeRef.resolve(store).fetchTree(store).first; res = { store->toRealPath(storePath) }; } diff --git a/src/libexpr/primops/fetchMercurial.cc b/src/libexpr/primops/fetchMercurial.cc index b9ff01c16..e76ce455d 100644 --- a/src/libexpr/primops/fetchMercurial.cc +++ b/src/libexpr/primops/fetchMercurial.cc @@ -71,10 +71,10 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a auto input = fetchers::Input::fromAttrs(std::move(attrs)); // FIXME: use name - auto [tree, input2] = input.fetch(state.store); + auto [storePath, input2] = input.fetch(state.store); auto attrs2 = state.buildBindings(8); - state.mkStorePathString(tree.storePath, attrs2.alloc(state.sOutPath)); + state.mkStorePathString(storePath, attrs2.alloc(state.sOutPath)); if (input2.getRef()) attrs2.alloc("branch").mkString(*input2.getRef()); // Backward compatibility: set 'rev' to @@ -86,7 +86,7 @@ static void prim_fetchMercurial(EvalState & state, const PosIdx pos, Value * * a attrs2.alloc("revCount").mkInt(*revCount); v.mkAttrs(attrs2); - state.allowPath(tree.storePath); + state.allowPath(storePath); } static RegisterPrimOp r_fetchMercurial({ diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 976037ff9..a99b0e500 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -5,6 +5,7 @@ #include "fetchers.hh" #include "filetransfer.hh" #include "registry.hh" +#include "tarball.hh" #include "url.hh" #include @@ -15,7 +16,7 @@ namespace nix { void emitTreeAttrs( EvalState & state, - const fetchers::Tree & tree, + const StorePath & storePath, const fetchers::Input & input, Value & v, bool emptyRevFallback, @@ -25,7 +26,7 @@ void emitTreeAttrs( auto attrs = state.buildBindings(10); - state.mkStorePathString(tree.storePath, attrs.alloc(state.sOutPath)); + state.mkStorePathString(storePath, attrs.alloc(state.sOutPath)); // FIXME: support arbitrary input attributes. @@ -165,11 +166,11 @@ static void fetchTree( state.checkURI(input.toURLString()); - auto [tree, input2] = input.fetch(state.store); + auto [storePath, input2] = input.fetch(state.store); - state.allowPath(tree.storePath); + state.allowPath(storePath); - emitTreeAttrs(state, tree, input2, v, params.emptyRevFallback, false); + emitTreeAttrs(state, storePath, input2, v, params.emptyRevFallback, false); } static void prim_fetchTree(EvalState & state, const PosIdx pos, Value * * args, Value & v) @@ -288,7 +289,7 @@ static void fetch(EvalState & state, const PosIdx pos, Value * * args, Value & v // https://github.com/NixOS/nix/issues/4313 auto storePath = unpack - ? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).tree.storePath + ? fetchers::downloadTarball(state.store, *url, name, (bool) expectedHash).storePath : fetchers::downloadFile(state.store, *url, name, (bool) expectedHash).storePath; if (expectedHash) { diff --git a/src/libfetchers/fetchers.cc b/src/libfetchers/fetchers.cc index 5f6167868..5688c4dc1 100644 --- a/src/libfetchers/fetchers.cc +++ b/src/libfetchers/fetchers.cc @@ -109,7 +109,7 @@ bool Input::contains(const Input & other) const return false; } -std::pair Input::fetch(ref store) const +std::pair Input::fetch(ref store) const { if (!scheme) throw Error("cannot fetch unsupported input '%s'", attrsToJSON(toAttrs())); @@ -126,7 +126,7 @@ std::pair Input::fetch(ref store) const debug("using substituted/cached input '%s' in '%s'", to_string(), store->printStorePath(storePath)); - return {Tree { .actualPath = store->toRealPath(storePath), .storePath = std::move(storePath) }, *this}; + return {std::move(storePath), *this}; } catch (Error & e) { debug("substitution of input '%s' failed: %s", to_string(), e.what()); } @@ -141,18 +141,16 @@ std::pair Input::fetch(ref store) const } }(); - Tree tree { - .actualPath = store->toRealPath(storePath), - .storePath = storePath, - }; - - auto narHash = store->queryPathInfo(tree.storePath)->narHash; + auto narHash = store->queryPathInfo(storePath)->narHash; input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true)); if (auto prevNarHash = getNarHash()) { if (narHash != *prevNarHash) throw Error((unsigned int) 102, "NAR hash mismatch in input '%s' (%s), expected '%s', got '%s'", - to_string(), tree.actualPath, prevNarHash->to_string(HashFormat::SRI, true), narHash.to_string(HashFormat::SRI, true)); + to_string(), + store->printStorePath(storePath), + prevNarHash->to_string(HashFormat::SRI, true), + narHash.to_string(HashFormat::SRI, true)); } if (auto prevLastModified = getLastModified()) { @@ -175,7 +173,7 @@ std::pair Input::fetch(ref store) const input.locked = true; - return {std::move(tree), input}; + return {std::move(storePath), input}; } Input Input::applyOverrides( diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index 4119dd17b..ac605ff8e 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -13,12 +13,6 @@ namespace nix { class Store; } namespace nix::fetchers { -struct Tree -{ - Path actualPath; - StorePath storePath; -}; - struct InputScheme; /** @@ -83,10 +77,10 @@ public: bool contains(const Input & other) const; /** - * Fetch the input into the Nix store, returning the location in - * the Nix store and the locked input. + * Fetch the entire input into the Nix store, returning the + * location in the Nix store and the locked input. */ - std::pair fetch(ref store) const; + std::pair fetch(ref store) const; Input applyOverrides( std::optional ref, @@ -158,33 +152,4 @@ struct InputScheme void registerInputScheme(std::shared_ptr && fetcher); -struct DownloadFileResult -{ - StorePath storePath; - std::string etag; - std::string effectiveUrl; - std::optional immutableUrl; -}; - -DownloadFileResult downloadFile( - ref store, - const std::string & url, - const std::string & name, - bool locked, - const Headers & headers = {}); - -struct DownloadTarballResult -{ - Tree tree; - time_t lastModified; - std::optional immutableUrl; -}; - -DownloadTarballResult downloadTarball( - ref store, - const std::string & url, - const std::string & name, - bool locked, - const Headers & headers = {}); - } diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index b824140a6..617fc7468 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -7,6 +7,7 @@ #include "git.hh" #include "fetchers.hh" #include "fetch-settings.hh" +#include "tarball.hh" #include #include @@ -213,10 +214,10 @@ struct GitArchiveInputScheme : InputScheme {"rev", rev->gitRev()}, {"lastModified", uint64_t(result.lastModified)} }, - result.tree.storePath, + result.storePath, true); - return {result.tree.storePath, input}; + return {result.storePath, input}; } std::optional experimentalFeature() override diff --git a/src/libfetchers/registry.cc b/src/libfetchers/registry.cc index 43c03beec..a0fff9ceb 100644 --- a/src/libfetchers/registry.cc +++ b/src/libfetchers/registry.cc @@ -1,5 +1,5 @@ #include "registry.hh" -#include "fetchers.hh" +#include "tarball.hh" #include "util.hh" #include "globals.hh" #include "store-api.hh" diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index ba47d59a7..e1ea9b58b 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -1,3 +1,4 @@ +#include "tarball.hh" #include "fetchers.hh" #include "cache.hh" #include "filetransfer.hh" @@ -133,7 +134,7 @@ DownloadTarballResult downloadTarball( if (cached && !cached->expired) return { - .tree = Tree { .actualPath = store->toRealPath(cached->storePath), .storePath = std::move(cached->storePath) }, + .storePath = std::move(cached->storePath), .lastModified = (time_t) getIntAttr(cached->infoAttrs, "lastModified"), .immutableUrl = maybeGetStrAttr(cached->infoAttrs, "immutableUrl"), }; @@ -174,7 +175,7 @@ DownloadTarballResult downloadTarball( locked); return { - .tree = Tree { .actualPath = store->toRealPath(*unpackedStorePath), .storePath = std::move(*unpackedStorePath) }, + .storePath = std::move(*unpackedStorePath), .lastModified = lastModified, .immutableUrl = res.immutableUrl, }; @@ -307,7 +308,7 @@ struct TarballInputScheme : CurlInputScheme if (result.lastModified && !input.attrs.contains("lastModified")) input.attrs.insert_or_assign("lastModified", uint64_t(result.lastModified)); - return {result.tree.storePath, std::move(input)}; + return {result.storePath, std::move(input)}; } }; diff --git a/src/libfetchers/tarball.hh b/src/libfetchers/tarball.hh new file mode 100644 index 000000000..9e6b50b31 --- /dev/null +++ b/src/libfetchers/tarball.hh @@ -0,0 +1,43 @@ +#pragma once + +#include "types.hh" +#include "path.hh" + +#include + +namespace nix { +class Store; +} + +namespace nix::fetchers { + +struct DownloadFileResult +{ + StorePath storePath; + std::string etag; + std::string effectiveUrl; + std::optional immutableUrl; +}; + +DownloadFileResult downloadFile( + ref store, + const std::string & url, + const std::string & name, + bool locked, + const Headers & headers = {}); + +struct DownloadTarballResult +{ + StorePath storePath; + time_t lastModified; + std::optional immutableUrl; +}; + +DownloadTarballResult downloadTarball( + ref store, + const std::string & url, + const std::string & name, + bool locked, + const Headers & headers = {}); + +} diff --git a/src/nix-channel/nix-channel.cc b/src/nix-channel/nix-channel.cc index 95f401441..4504441fa 100755 --- a/src/nix-channel/nix-channel.cc +++ b/src/nix-channel/nix-channel.cc @@ -4,9 +4,9 @@ #include "filetransfer.hh" #include "store-api.hh" #include "legacy.hh" -#include "fetchers.hh" #include "eval-settings.hh" // for defexpr #include "util.hh" +#include "tarball.hh" #include #include diff --git a/src/nix/flake.cc b/src/nix/flake.cc index ceb112c03..b4e4156c0 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -186,7 +186,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON j["revCount"] = *revCount; if (auto lastModified = flake.lockedRef.input.getLastModified()) j["lastModified"] = *lastModified; - j["path"] = store->printStorePath(flake.sourceInfo->storePath); + j["path"] = store->printStorePath(flake.storePath); j["locks"] = lockedFlake.lockFile.toJSON(); logger->cout("%s", j.dump()); } else { @@ -202,7 +202,7 @@ struct CmdFlakeMetadata : FlakeCommand, MixJSON *flake.description); logger->cout( ANSI_BOLD "Path:" ANSI_NORMAL " %s", - store->printStorePath(flake.sourceInfo->storePath)); + store->printStorePath(flake.storePath)); if (auto rev = flake.lockedRef.input.getRev()) logger->cout( ANSI_BOLD "Revision:" ANSI_NORMAL " %s", @@ -976,7 +976,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun StorePathSet sources; - sources.insert(flake.flake.sourceInfo->storePath); + sources.insert(flake.flake.storePath); // FIXME: use graph output, handle cycles. std::function traverse; @@ -988,7 +988,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun auto storePath = dryRun ? (*inputNode)->lockedRef.input.computeStorePath(*store) - : (*inputNode)->lockedRef.input.fetch(store).first.storePath; + : (*inputNode)->lockedRef.input.fetch(store).first; if (json) { auto& jsonObj3 = jsonObj2[inputName]; jsonObj3["path"] = store->printStorePath(storePath); @@ -1005,7 +1005,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun if (json) { nlohmann::json jsonRoot = { - {"path", store->printStorePath(flake.flake.sourceInfo->storePath)}, + {"path", store->printStorePath(flake.flake.storePath)}, {"inputs", traverse(*flake.lockFile.root)}, }; logger->cout("%s", jsonRoot); @@ -1339,12 +1339,12 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON { auto originalRef = getFlakeRef(); auto resolvedRef = originalRef.resolve(store); - auto [tree, lockedRef] = resolvedRef.fetchTree(store); - auto hash = store->queryPathInfo(tree.storePath)->narHash; + auto [storePath, lockedRef] = resolvedRef.fetchTree(store); + auto hash = store->queryPathInfo(storePath)->narHash; if (json) { auto res = nlohmann::json::object(); - res["storePath"] = store->printStorePath(tree.storePath); + res["storePath"] = store->printStorePath(storePath); res["hash"] = hash.to_string(HashFormat::SRI, true); res["original"] = fetchers::attrsToJSON(resolvedRef.toAttrs()); res["locked"] = fetchers::attrsToJSON(lockedRef.toAttrs()); @@ -1352,7 +1352,7 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON } else { notice("Downloaded '%s' to '%s' (hash '%s').", lockedRef.to_string(), - store->printStorePath(tree.storePath), + store->printStorePath(storePath), hash.to_string(HashFormat::SRI, true)); } } From 97a0c08873496dbd6f53adb253e159ecf700d616 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Fri, 20 Oct 2023 21:17:28 +0200 Subject: [PATCH 287/299] Expand derivation examples (#9048) Also use fancier formatting so the example blocks are easier to discern from the description. Co-authored-by: John Ericson --- doc/manual/src/glossary.md | 1 + doc/manual/src/language/derivations.md | 214 +++++++++++++++++++------ 2 files changed, 162 insertions(+), 53 deletions(-) diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index 611d9576f..d49d5e52e 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -207,6 +207,7 @@ - [output]{#gloss-output} A [store object] produced by a [derivation]. + See [the `outputs` argument to the `derivation` function](@docroot@/language/derivations.md#attr-outputs) for details. [output]: #gloss-output diff --git a/doc/manual/src/language/derivations.md b/doc/manual/src/language/derivations.md index d80a7dd48..2aded5527 100644 --- a/doc/manual/src/language/derivations.md +++ b/doc/manual/src/language/derivations.md @@ -8,8 +8,6 @@ It outputs an attribute set, and produces a [store derivation] as a side effect [store derivation]: @docroot@/glossary.md#gloss-store-derivation - - ## Input attributes ### Required @@ -17,11 +15,22 @@ It outputs an attribute set, and produces a [store derivation] as a side effect - [`name`]{#attr-name} ([String](@docroot@/language/values.md#type-string)) A symbolic name for the derivation. - It is added to the [store derivation]'s [path](@docroot@/glossary.md#gloss-store-path) and its [output paths][output path]. + It is added to the [store path] of the corresponding [store derivation] as well as to its [output paths](@docroot@/glossary.md#gloss-output-path). - Example: `name = "hello";` + [store path]: @docroot@/glossary.md#gloss-store-path + + > **Example** + > + > ```nix + > derivation { + > name = "hello"; + > # ... + > } + > ``` + > + > The store derivation's path will be `/nix/store/-hello.drv`. + > The [output](#attr-outputs) paths will be of the form `/nix/store/-hello[-]` - The store derivation's path will be `/nix/store/-hello.drv`, and the output paths will be of the form `/nix/store/-hello[-]` - [`system`]{#attr-system} ([String](@docroot@/language/values.md#type-string)) The system type on which the [`builder`](#attr-builder) executable is meant to be run. @@ -29,77 +38,175 @@ It outputs an attribute set, and produces a [store derivation] as a side effect A necessary condition for Nix to build derivations locally is that the `system` attribute matches the current [`system` configuration option]. It can automatically [build on other platforms](../advanced-topics/distributed-builds.md) by forwarding build requests to other machines. - Examples: - - `system = "x86_64-linux";` - - `system = builtins.currentSystem;` - - [`builtins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) has the value of the [`system` configuration option], and defaults to the system type of the current Nix installation. - [`system` configuration option]: @docroot@/command-ref/conf-file.md#conf-system + > **Example** + > + > Declare a derivation to be built on a specific system type: + > + > ```nix + > derivation { + > # ... + > system = "x86_64-linux"; + > # ... + > } + > ``` + + > **Example** + > + > Declare a derivation to be built on the system type that evaluates the expression: + > + > ```nix + > derivation { + > # ... + > system = builtins.currentSystem; + > # ... + > } + > ``` + > + > [`builtins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) has the value of the [`system` configuration option], and defaults to the system type of the current Nix installation. + - [`builder`]{#attr-builder} ([Path](@docroot@/language/values.md#type-path) | [String](@docroot@/language/values.md#type-string)) Path to an executable that will perform the build. - Examples: + > **Example** + > + > Use the file located at `/bin/bash` as the builder executable: + > + > ```nix + > derivation { + > # ... + > builder = "/bin/bash"; + > # ... + > }; + > ``` - `builder = "/bin/bash";` + - `builder = ./builder.sh;` + > **Example** + > + > Copy a local file to the Nix store for use as the builder executable: + > + > ```nix + > derivation { + > # ... + > builder = ./builder.sh; + > # ... + > }; + > ``` - `builder = "${pkgs.python}/bin/python";` + + + > **Example** + > + > Use a file from another derivation as the builder executable: + > + > ```nix + > let pkgs = import {}; in + > derivation { + > # ... + > builder = "${pkgs.python}/bin/python"; + > # ... + > }; + > ``` ### Optional -- [`args`]{#attr-args} ([List](@docroot@/language/values.md#list) of [String](@docroot@/language/values.md#type-string)) Default: `[ ]` +- [`args`]{#attr-args} ([List](@docroot@/language/values.md#list) of [String](@docroot@/language/values.md#type-string)) + + Default: `[ ]` Command-line arguments to be passed to the [`builder`](#attr-builder) executable. - Example: `args = [ "-c" "echo hello world > $out" ];` + > **Example** + > + > Pass arguments to Bash to interpret a shell command: + > + > ```nix + > derivation { + > # ... + > builder = "/bin/bash"; + > args = [ "-c" "echo hello world > $out" ]; + > # ... + > }; + > ``` -- [`outputs`]{#attr-outputs} ([List](@docroot@/language/values.md#list) of [String](@docroot@/language/values.md#type-string)) Default: `[ "out" ]` +- [`outputs`]{#attr-outputs} ([List](@docroot@/language/values.md#list) of [String](@docroot@/language/values.md#type-string)) + + Default: `[ "out" ]` Symbolic outputs of the derivation. - Each output name is passed to the [`builder`](#attr-builder) executable as an environment variable with its value set to the corresponding [output path]. + Each output name is passed to the [`builder`](#attr-builder) executable as an environment variable with its value set to the corresponding [store path]. - [output path]: @docroot@/glossary.md#gloss-output-path - - By default, a derivation produces a single output path called `out`. - However, derivations can produce multiple output paths. + By default, a derivation produces a single output called `out`. + However, derivations can produce multiple outputs. This allows the associated [store objects](@docroot@/glossary.md#gloss-store-object) and their [closures](@docroot@/glossary.md#gloss-closure) to be copied or garbage-collected separately. - Examples: + > **Example** + > + > Imagine a library package that provides a dynamic library, header files, and documentation. + > A program that links against such a library doesn’t need the header files and documentation at runtime, and it doesn’t need the documentation at build time. + > Thus, the library package could specify: + > + > ```nix + > derivation { + > # ... + > outputs = [ "lib" "dev" "doc" ]; + > # ... + > } + > ``` + > + > This will cause Nix to pass environment variables `lib`, `dev`, and `doc` to the builder containing the intended store paths of each output. + > The builder would typically do something like + > + > ```bash + > ./configure \ + > --libdir=$lib/lib \ + > --includedir=$dev/include \ + > --docdir=$doc/share/doc + > ``` + > + > for an Autoconf-style package. - Imagine a library package that provides a dynamic library, header files, and documentation. - A program that links against such a library doesn’t need the header files and documentation at runtime, and it doesn’t need the documentation at build time. - Thus, the library package could specify: + The name of an output is combined with the name of the derivation to create the name part of the output's store path, unless it is `out`, in which case just the name of the derivation is used. - ```nix - derivation { - # ... - outputs = [ "lib" "dev" "doc" ]; - # ... - } - ``` + > **Example** + > + > + > ```nix + > derivation { + > name = "example"; + > outputs = [ "lib" "dev" "doc" "out" ]; + > # ... + > } + > ``` + > + > The store derivation path will be `/nix/store/-example.drv`. + > The output paths will be + > - `/nix/store/-example-lib` + > - `/nix/store/-example-dev` + > - `/nix/store/-example-doc` + > - `/nix/store/-example` - This will cause Nix to pass environment variables `lib`, `dev`, and `doc` to the builder containing the intended store paths of each output. - The builder would typically do something like + You can refer to each output of a derivation by selecting it as an attribute. + The first element of `outputs` determines the *default output* and ends up at the top-level. - ```bash - ./configure \ - --libdir=$lib/lib \ - --includedir=$dev/include \ - --docdir=$doc/share/doc - ``` - - for an Autoconf-style package. - - You can refer to each output of a derivation by selecting it as an attribute, e.g. `myPackage.lib` or `myPackage.doc`. - - The first element of `outputs` determines the *default output*. - Therefore, in the given example, `myPackage` is equivalent to `myPackage.lib`. + > **Example** + > + > Select an output by attribute name: + > + > ```nix + > let + > myPackage = derivation { + > name = "example"; + > outputs = [ "lib" "dev" "doc" "out" ]; + > # ... + > }; + > in myPackage.dev + > ``` + > + > Since `lib` is the first output, `myPackage` is equivalent to `myPackage.lib`. @@ -123,8 +230,7 @@ It outputs an attribute set, and produces a [store derivation] as a side effect reside in the Nix store. - A *derivation* causes that derivation to be built prior to the - present derivation; its default output path is put in the - environment variable. + present derivation. The environment variable is set to the [store path] of the derivation's default [output](#attr-outputs). - Lists of the previous types are also allowed. They are simply concatenated, separated by spaces. @@ -132,6 +238,8 @@ It outputs an attribute set, and produces a [store derivation] as a side effect - `true` is passed as the string `1`, `false` and `null` are passed as an empty string. + + ## Builder execution The [`builder`](#attr-builder) is executed as follows: From 96c58550b839ca415a2cef0f7a5ae51f3e86f028 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 16 Oct 2023 01:45:06 -0400 Subject: [PATCH 288/299] Test more derived paths --- src/libstore/tests/worker-protocol.cc | 11 ++++++++++- .../libstore/worker-protocol/derived-path.bin | Bin 120 -> 248 bytes 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index 63fecce96..b10192cc1 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -72,10 +72,19 @@ VERSIONED_CHARACTERIZATION_TEST( derivedPath, "derived-path", defaultVersion, - (std::tuple { + (std::tuple { DerivedPath::Opaque { .path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, }, + DerivedPath::Opaque { + .path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo.drv" }, + }, + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }), + .outputs = OutputsSpec::All { }, + }, DerivedPath::Built { .drvPath = makeConstantStorePathRef(StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", diff --git a/unit-test-data/libstore/worker-protocol/derived-path.bin b/unit-test-data/libstore/worker-protocol/derived-path.bin index bb1a81ac6d096cddf510a457e7506e6a81ee66a3..0729b2690e2f99dc8122b013e4ceaa4a51ebd3d6 100644 GIT binary patch delta 39 mcmb>U!8pN!(Rd=G8JAv4Q5gdWm`(%= Date: Fri, 26 May 2023 14:48:11 -0400 Subject: [PATCH 289/299] Systematize the worker protocol derived path serialiser It was some ad-hoc functions to account for versions, while the already factored-out serializer just supported the latest version. Now, we can fold that version-specific logic into the factored out one, and so we do. --- src/libstore/daemon.cc | 18 ++-------- src/libstore/remote-store.cc | 33 ++---------------- src/libstore/tests/worker-protocol.cc | 29 +++++++++++++-- src/libstore/worker-protocol.cc | 26 ++++++++++++-- .../worker-protocol/derived-path-1.29.bin | Bin 0 -> 184 bytes ...derived-path.bin => derived-path-1.30.bin} | Bin 6 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 unit-test-data/libstore/worker-protocol/derived-path-1.29.bin rename unit-test-data/libstore/worker-protocol/{derived-path.bin => derived-path-1.30.bin} (100%) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index e117b507f..27c0b6431 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -261,18 +261,6 @@ struct ClientSettings } }; -static std::vector readDerivedPaths(Store & store, WorkerProto::Version clientVersion, WorkerProto::ReadConn conn) -{ - std::vector reqs; - if (GET_PROTOCOL_MINOR(clientVersion) >= 30) { - reqs = WorkerProto::Serialise>::read(store, conn); - } else { - for (auto & s : readStrings(conn.from)) - reqs.push_back(parsePathWithOutputs(store, s).toDerivedPath()); - } - return reqs; -} - static void performOp(TunnelLogger * logger, ref store, TrustedFlag trusted, RecursiveFlag recursive, WorkerProto::Version clientVersion, Source & from, BufferedSink & to, WorkerProto::Op op) @@ -538,7 +526,7 @@ static void performOp(TunnelLogger * logger, ref store, } case WorkerProto::Op::BuildPaths: { - auto drvs = readDerivedPaths(*store, clientVersion, rconn); + auto drvs = WorkerProto::Serialise::read(*store, rconn); BuildMode mode = bmNormal; if (GET_PROTOCOL_MINOR(clientVersion) >= 15) { mode = (BuildMode) readInt(from); @@ -563,7 +551,7 @@ static void performOp(TunnelLogger * logger, ref store, } case WorkerProto::Op::BuildPathsWithResults: { - auto drvs = readDerivedPaths(*store, clientVersion, rconn); + auto drvs = WorkerProto::Serialise::read(*store, rconn); BuildMode mode = bmNormal; mode = (BuildMode) readInt(from); @@ -938,7 +926,7 @@ static void performOp(TunnelLogger * logger, ref store, } case WorkerProto::Op::QueryMissing: { - auto targets = readDerivedPaths(*store, clientVersion, rconn); + auto targets = WorkerProto::Serialise::read(*store, rconn); logger->startWork(); StorePathSet willBuild, willSubstitute, unknown; uint64_t downloadSize, narSize; diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 1704bfbb0..482c2ae01 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -655,33 +655,6 @@ void RemoteStore::queryRealisationUncached(const DrvOutput & id, } catch (...) { return callback.rethrow(); } } -static void writeDerivedPaths(RemoteStore & store, RemoteStore::Connection & conn, const std::vector & reqs) -{ - if (GET_PROTOCOL_MINOR(conn.daemonVersion) >= 30) { - WorkerProto::write(store, conn, reqs); - } else { - Strings ss; - for (auto & p : reqs) { - auto sOrDrvPath = StorePathWithOutputs::tryFromDerivedPath(p); - std::visit(overloaded { - [&](const StorePathWithOutputs & s) { - ss.push_back(s.to_string(store)); - }, - [&](const StorePath & drvPath) { - throw Error("trying to request '%s', but daemon protocol %d.%d is too old (< 1.29) to request a derivation file", - store.printStorePath(drvPath), - GET_PROTOCOL_MAJOR(conn.daemonVersion), - GET_PROTOCOL_MINOR(conn.daemonVersion)); - }, - [&](std::monostate) { - throw Error("wanted to build a derivation that is itself a build product, but the legacy 'ssh://' protocol doesn't support that. Try using 'ssh-ng://'"); - }, - }, sOrDrvPath); - } - conn.to << ss; - } -} - void RemoteStore::copyDrvsFromEvalStore( const std::vector & paths, std::shared_ptr evalStore) @@ -711,7 +684,7 @@ void RemoteStore::buildPaths(const std::vector & drvPaths, BuildMod auto conn(getConnection()); conn->to << WorkerProto::Op::BuildPaths; assert(GET_PROTOCOL_MINOR(conn->daemonVersion) >= 13); - writeDerivedPaths(*this, *conn, drvPaths); + WorkerProto::write(*this, *conn, drvPaths); if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 15) conn->to << buildMode; else @@ -735,7 +708,7 @@ std::vector RemoteStore::buildPathsWithResults( if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 34) { conn->to << WorkerProto::Op::BuildPathsWithResults; - writeDerivedPaths(*this, *conn, paths); + WorkerProto::write(*this, *conn, paths); conn->to << buildMode; conn.processStderr(); return WorkerProto::Serialise>::read(*this, *conn); @@ -929,7 +902,7 @@ void RemoteStore::queryMissing(const std::vector & targets, // to prevent a deadlock. goto fallback; conn->to << WorkerProto::Op::QueryMissing; - writeDerivedPaths(*this, *conn, targets); + WorkerProto::write(*this, *conn, targets); conn.processStderr(); willBuild = WorkerProto::Serialise::read(*this, *conn); willSubstitute = WorkerProto::Serialise::read(*this, *conn); diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index b10192cc1..b3e857bc4 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -69,9 +69,32 @@ VERSIONED_CHARACTERIZATION_TEST( VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, - derivedPath, - "derived-path", - defaultVersion, + derivedPath_1_29, + "derived-path-1.29", + 1 << 8 | 29, + (std::tuple { + DerivedPath::Opaque { + .path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }), + .outputs = OutputsSpec::All { }, + }, + DerivedPath::Built { + .drvPath = makeConstantStorePathRef(StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }), + .outputs = OutputsSpec::Names { "x", "y" }, + }, + })) + +VERSIONED_CHARACTERIZATION_TEST( + WorkerProtoTest, + derivedPath_1_30, + "derived-path-1.30", + 1 << 8 | 30, (std::tuple { DerivedPath::Opaque { .path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 415e66f16..0f3f20ca5 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -51,12 +51,34 @@ void WorkerProto::Serialise>::write(const Store & sto DerivedPath WorkerProto::Serialise::read(const Store & store, WorkerProto::ReadConn conn) { auto s = readString(conn.from); - return DerivedPath::parseLegacy(store, s); + if (GET_PROTOCOL_MINOR(conn.version) >= 30) { + return DerivedPath::parseLegacy(store, s); + } else { + return parsePathWithOutputs(store, s).toDerivedPath(); + } } void WorkerProto::Serialise::write(const Store & store, WorkerProto::WriteConn conn, const DerivedPath & req) { - conn.to << req.to_string_legacy(store); + if (GET_PROTOCOL_MINOR(conn.version) >= 30) { + conn.to << req.to_string_legacy(store); + } else { + auto sOrDrvPath = StorePathWithOutputs::tryFromDerivedPath(req); + std::visit(overloaded { + [&](const StorePathWithOutputs & s) { + conn.to << s.to_string(store); + }, + [&](const StorePath & drvPath) { + throw Error("trying to request '%s', but daemon protocol %d.%d is too old (< 1.29) to request a derivation file", + store.printStorePath(drvPath), + GET_PROTOCOL_MAJOR(conn.version), + GET_PROTOCOL_MINOR(conn.version)); + }, + [&](std::monostate) { + throw Error("wanted to build a derivation that is itself a build product, but protocols do not support that. Try upgrading the Nix on the other end of this connection"); + }, + }, sOrDrvPath); + } } diff --git a/unit-test-data/libstore/worker-protocol/derived-path-1.29.bin b/unit-test-data/libstore/worker-protocol/derived-path-1.29.bin new file mode 100644 index 0000000000000000000000000000000000000000..05ea7678aa058344a5ca4cb1e9e7333e9cf8fe20 GIT binary patch literal 184 zcmdOAfB^lx%nJSDlKi4n{dB`}^NdR4LR_?NT7Eu*F&X-j5{vXwipsz`&B@oVSfNwN F001Y$HlP3i literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/derived-path.bin b/unit-test-data/libstore/worker-protocol/derived-path-1.30.bin similarity index 100% rename from unit-test-data/libstore/worker-protocol/derived-path.bin rename to unit-test-data/libstore/worker-protocol/derived-path-1.30.bin From ab822af0dfee341be131ad285a2cba5362a3f2dc Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 28 Feb 2021 18:42:46 +0000 Subject: [PATCH 290/299] Factor out serialization for `BuildResult` Worker Protocol: Note that the worker protocol already had a serialization for `BuildResult`; this was added in a4604f19284254ac98f19a13ff7c2216de7fe176. It didn't have any versioning support because at that time reusable seralizers were not away for the protocol version. It could thus only be used for new messages also introduced in that commit. Now that we do support versioning in reusable serializers, we can expand it to support all known versions and use it in many more places. The exist test data becomes the version 1.29 tests: note that those files' contents are unchanged. 1.28 and 1.27 tests are added to cover the older code-paths. The keyered build result test only has 1.29 because the keying was also added in a4604f19284254ac98f19a13ff7c2216de7fe176; the older serializations are always used unkeyed. Serve Protocol: Conversely, no attempt was made to factor out such a serializer for the serve protocol, so our work there in this commit for that protocol proceeds from scratch. --- src/libstore/daemon.cc | 11 +- src/libstore/legacy-ssh-store.cc | 15 +-- src/libstore/remote-store.cc | 15 +-- src/libstore/serve-protocol.cc | 43 +++++++ src/libstore/serve-protocol.hh | 6 + src/libstore/tests/serve-protocol.cc | 113 +++++++++++++++++- src/libstore/tests/worker-protocol.cc | 80 ++++++++++++- src/libstore/worker-protocol.cc | 49 ++++---- src/nix-store/nix-store.cc | 12 +- .../serve-protocol/build-result-2.2.bin | Bin 0 -> 80 bytes .../serve-protocol/build-result-2.3.bin | Bin 0 -> 176 bytes .../build-result-2.6.bin} | Bin .../worker-protocol/build-result-1.27.bin | Bin 0 -> 80 bytes .../worker-protocol/build-result-1.28.bin | Bin 0 -> 648 bytes .../worker-protocol/build-result-1.29.bin | Bin 0 -> 744 bytes ...result.bin => keyed-build-result-1.29.bin} | Bin 16 files changed, 268 insertions(+), 76 deletions(-) create mode 100644 unit-test-data/libstore/serve-protocol/build-result-2.2.bin create mode 100644 unit-test-data/libstore/serve-protocol/build-result-2.3.bin rename unit-test-data/libstore/{worker-protocol/build-result.bin => serve-protocol/build-result-2.6.bin} (100%) create mode 100644 unit-test-data/libstore/worker-protocol/build-result-1.27.bin create mode 100644 unit-test-data/libstore/worker-protocol/build-result-1.28.bin create mode 100644 unit-test-data/libstore/worker-protocol/build-result-1.29.bin rename unit-test-data/libstore/worker-protocol/{keyed-build-result.bin => keyed-build-result-1.29.bin} (100%) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 27c0b6431..19afe4388 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -635,16 +635,7 @@ static void performOp(TunnelLogger * logger, ref store, auto res = store->buildDerivation(drvPath, drv, buildMode); logger->stopWork(); - to << res.status << res.errorMsg; - if (GET_PROTOCOL_MINOR(clientVersion) >= 29) { - to << res.timesBuilt << res.isNonDeterministic << res.startTime << res.stopTime; - } - if (GET_PROTOCOL_MINOR(clientVersion) >= 28) { - DrvOutputs builtOutputs; - for (auto & [output, realisation] : res.builtOutputs) - builtOutputs.insert_or_assign(realisation.id, realisation); - WorkerProto::write(*store, wconn, builtOutputs); - } + WorkerProto::write(*store, wconn, res); break; } diff --git a/src/libstore/legacy-ssh-store.cc b/src/libstore/legacy-ssh-store.cc index d6e24521a..c712f7eb1 100644 --- a/src/libstore/legacy-ssh-store.cc +++ b/src/libstore/legacy-ssh-store.cc @@ -319,20 +319,7 @@ public: conn->to.flush(); - BuildResult status; - status.status = (BuildResult::Status) readInt(conn->from); - conn->from >> status.errorMsg; - - if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 3) - conn->from >> status.timesBuilt >> status.isNonDeterministic >> status.startTime >> status.stopTime; - if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 6) { - auto builtOutputs = ServeProto::Serialise::read(*this, *conn); - for (auto && [output, realisation] : builtOutputs) - status.builtOutputs.insert_or_assign( - std::move(output.outputName), - std::move(realisation)); - } - return status; + return ServeProto::Serialise::read(*this, *conn); } void buildPaths(const std::vector & drvPaths, BuildMode buildMode, std::shared_ptr evalStore) override diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index 482c2ae01..ef648e01b 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -788,20 +788,7 @@ BuildResult RemoteStore::buildDerivation(const StorePath & drvPath, const BasicD writeDerivation(conn->to, *this, drv); conn->to << buildMode; conn.processStderr(); - BuildResult res; - res.status = (BuildResult::Status) readInt(conn->from); - conn->from >> res.errorMsg; - if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 29) { - conn->from >> res.timesBuilt >> res.isNonDeterministic >> res.startTime >> res.stopTime; - } - if (GET_PROTOCOL_MINOR(conn->daemonVersion) >= 28) { - auto builtOutputs = WorkerProto::Serialise::read(*this, *conn); - for (auto && [output, realisation] : builtOutputs) - res.builtOutputs.insert_or_assign( - std::move(output.outputName), - std::move(realisation)); - } - return res; + return WorkerProto::Serialise::read(*this, *conn); } diff --git a/src/libstore/serve-protocol.cc b/src/libstore/serve-protocol.cc index 16a62b5bc..97a0ddf0e 100644 --- a/src/libstore/serve-protocol.cc +++ b/src/libstore/serve-protocol.cc @@ -2,6 +2,7 @@ #include "util.hh" #include "path-with-outputs.hh" #include "store-api.hh" +#include "build-result.hh" #include "serve-protocol.hh" #include "serve-protocol-impl.hh" #include "archive.hh" @@ -12,4 +13,46 @@ namespace nix { /* protocol-specific definitions */ +BuildResult ServeProto::Serialise::read(const Store & store, ServeProto::ReadConn conn) +{ + BuildResult status; + status.status = (BuildResult::Status) readInt(conn.from); + conn.from >> status.errorMsg; + + if (GET_PROTOCOL_MINOR(conn.version) >= 3) + conn.from + >> status.timesBuilt + >> status.isNonDeterministic + >> status.startTime + >> status.stopTime; + if (GET_PROTOCOL_MINOR(conn.version) >= 6) { + auto builtOutputs = ServeProto::Serialise::read(store, conn); + for (auto && [output, realisation] : builtOutputs) + status.builtOutputs.insert_or_assign( + std::move(output.outputName), + std::move(realisation)); + } + return status; +} + +void ServeProto::Serialise::write(const Store & store, ServeProto::WriteConn conn, const BuildResult & status) +{ + conn.to + << status.status + << status.errorMsg; + + if (GET_PROTOCOL_MINOR(conn.version) >= 3) + conn.to + << status.timesBuilt + << status.isNonDeterministic + << status.startTime + << status.stopTime; + if (GET_PROTOCOL_MINOR(conn.version) >= 6) { + DrvOutputs builtOutputs; + for (auto & [output, realisation] : status.builtOutputs) + builtOutputs.insert_or_assign(realisation.id, realisation); + ServeProto::write(store, conn, builtOutputs); + } +} + } diff --git a/src/libstore/serve-protocol.hh b/src/libstore/serve-protocol.hh index a627c6ad6..ba159f6e9 100644 --- a/src/libstore/serve-protocol.hh +++ b/src/libstore/serve-protocol.hh @@ -16,6 +16,9 @@ namespace nix { class Store; struct Source; +// items being serialised +struct BuildResult; + /** * The "serve protocol", used by ssh:// stores. @@ -136,6 +139,9 @@ inline std::ostream & operator << (std::ostream & s, ServeProto::Command op) static void write(const Store & store, ServeProto::WriteConn conn, const T & t); \ }; +template<> +DECLARE_SERVE_SERIALISER(BuildResult); + template DECLARE_SERVE_SERIALISER(std::vector); template diff --git a/src/libstore/tests/serve-protocol.cc b/src/libstore/tests/serve-protocol.cc index ba798ab1c..c8ac87a04 100644 --- a/src/libstore/tests/serve-protocol.cc +++ b/src/libstore/tests/serve-protocol.cc @@ -19,7 +19,7 @@ struct ServeProtoTest : VersionedProtoTest * For serializers that don't care about the minimum version, we * used the oldest one: 1.0. */ - ServeProto::Version defaultVersion = 1 << 8 | 0; + ServeProto::Version defaultVersion = 2 << 8 | 0; }; VERSIONED_CHARACTERIZATION_TEST( @@ -114,6 +114,117 @@ VERSIONED_CHARACTERIZATION_TEST( }, })) +VERSIONED_CHARACTERIZATION_TEST( + ServeProtoTest, + buildResult_2_2, + "build-result-2.2", + 2 << 8 | 2, + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + BuildResult { + .status = BuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::NotDeterministic, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::Built, + }, + }; + t; + })) + +VERSIONED_CHARACTERIZATION_TEST( + ServeProtoTest, + buildResult_2_3, + "build-result-2.3", + 2 << 8 | 3, + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + BuildResult { + .status = BuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::NotDeterministic, + .errorMsg = "no idea why", + .timesBuilt = 3, + .isNonDeterministic = true, + .startTime = 30, + .stopTime = 50, + }, + BuildResult { + .status = BuildResult::Built, + .startTime = 30, + .stopTime = 50, + }, + }; + t; + })) + +VERSIONED_CHARACTERIZATION_TEST( + ServeProtoTest, + buildResult_2_6, + "build-result-2.6", + 2 << 8 | 6, + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + BuildResult { + .status = BuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::NotDeterministic, + .errorMsg = "no idea why", + .timesBuilt = 3, + .isNonDeterministic = true, + .startTime = 30, + .stopTime = 50, + }, + BuildResult { + .status = BuildResult::Built, + .timesBuilt = 1, + .builtOutputs = { + { + "foo", + { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "foo", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + }, + { + "bar", + { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "bar", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar" }, + }, + }, + }, + .startTime = 30, + .stopTime = 50, +#if 0 + // These fields are not yet serialized. + // FIXME Include in next version of protocol or document + // why they are skipped. + .cpuUser = std::chrono::milliseconds(500s), + .cpuSystem = std::chrono::milliseconds(604s), +#endif + }, + }; + t; + })) + VERSIONED_CHARACTERIZATION_TEST( ServeProtoTest, vector, diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index b3e857bc4..e0ce340d8 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -167,9 +167,77 @@ VERSIONED_CHARACTERIZATION_TEST( VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, - buildResult, - "build-result", - defaultVersion, + buildResult_1_27, + "build-result-1.27", + 1 << 8 | 27, + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + BuildResult { + .status = BuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::NotDeterministic, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::Built, + }, + }; + t; + })) + +VERSIONED_CHARACTERIZATION_TEST( + WorkerProtoTest, + buildResult_1_28, + "build-result-1.28", + 1 << 8 | 28, + ({ + using namespace std::literals::chrono_literals; + std::tuple t { + BuildResult { + .status = BuildResult::OutputRejected, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::NotDeterministic, + .errorMsg = "no idea why", + }, + BuildResult { + .status = BuildResult::Built, + .builtOutputs = { + { + "foo", + { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "foo", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" }, + }, + }, + { + "bar", + { + .id = DrvOutput { + .drvHash = Hash::parseSRI("sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="), + .outputName = "bar", + }, + .outPath = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar" }, + }, + }, + }, + }, + }; + t; + })) + +VERSIONED_CHARACTERIZATION_TEST( + WorkerProtoTest, + buildResult_1_29, + "build-result-1.29", + 1 << 8 | 29, ({ using namespace std::literals::chrono_literals; std::tuple t { @@ -226,9 +294,9 @@ VERSIONED_CHARACTERIZATION_TEST( VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, - keyedBuildResult, - "keyed-build-result", - defaultVersion, + keyedBuildResult_1_29, + "keyed-build-result-1.29", + 1 << 8 | 29, ({ using namespace std::literals::chrono_literals; std::tuple t { diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 0f3f20ca5..f69322a61 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -103,17 +103,21 @@ BuildResult WorkerProto::Serialise::read(const Store & store, Worke { BuildResult res; res.status = (BuildResult::Status) readInt(conn.from); - conn.from - >> res.errorMsg - >> res.timesBuilt - >> res.isNonDeterministic - >> res.startTime - >> res.stopTime; - auto builtOutputs = WorkerProto::Serialise::read(store, conn); - for (auto && [output, realisation] : builtOutputs) - res.builtOutputs.insert_or_assign( - std::move(output.outputName), - std::move(realisation)); + conn.from >> res.errorMsg; + if (GET_PROTOCOL_MINOR(conn.version) >= 29) { + conn.from + >> res.timesBuilt + >> res.isNonDeterministic + >> res.startTime + >> res.stopTime; + } + if (GET_PROTOCOL_MINOR(conn.version) >= 28) { + auto builtOutputs = WorkerProto::Serialise::read(store, conn); + for (auto && [output, realisation] : builtOutputs) + res.builtOutputs.insert_or_assign( + std::move(output.outputName), + std::move(realisation)); + } return res; } @@ -121,15 +125,20 @@ void WorkerProto::Serialise::write(const Store & store, WorkerProto { conn.to << res.status - << res.errorMsg - << res.timesBuilt - << res.isNonDeterministic - << res.startTime - << res.stopTime; - DrvOutputs builtOutputs; - for (auto & [output, realisation] : res.builtOutputs) - builtOutputs.insert_or_assign(realisation.id, realisation); - WorkerProto::write(store, conn, builtOutputs); + << res.errorMsg; + if (GET_PROTOCOL_MINOR(conn.version) >= 29) { + conn.to + << res.timesBuilt + << res.isNonDeterministic + << res.startTime + << res.stopTime; + } + if (GET_PROTOCOL_MINOR(conn.version) >= 28) { + DrvOutputs builtOutputs; + for (auto & [output, realisation] : res.builtOutputs) + builtOutputs.insert_or_assign(realisation.id, realisation); + WorkerProto::write(store, conn, builtOutputs); + } } diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index e78720f92..e4dd94585 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -959,17 +959,7 @@ static void opServe(Strings opFlags, Strings opArgs) MonitorFdHup monitor(in.fd); auto status = store->buildDerivation(drvPath, drv); - out << status.status << status.errorMsg; - - if (GET_PROTOCOL_MINOR(clientVersion) >= 3) - out << status.timesBuilt << status.isNonDeterministic << status.startTime << status.stopTime; - if (GET_PROTOCOL_MINOR(clientVersion) >= 6) { - DrvOutputs builtOutputs; - for (auto & [output, realisation] : status.builtOutputs) - builtOutputs.insert_or_assign(realisation.id, realisation); - ServeProto::write(*store, wconn, builtOutputs); - } - + ServeProto::write(*store, wconn, status); break; } diff --git a/unit-test-data/libstore/serve-protocol/build-result-2.2.bin b/unit-test-data/libstore/serve-protocol/build-result-2.2.bin new file mode 100644 index 0000000000000000000000000000000000000000..ae684778bc26addba4bf1b3e49cc30edf5f038fa GIT binary patch literal 80 gcmZQ&fBf0AiU4H~;_u literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/serve-protocol/build-result-2.3.bin b/unit-test-data/libstore/serve-protocol/build-result-2.3.bin new file mode 100644 index 0000000000000000000000000000000000000000..d51e08dfc0d1715210e8a616a5403f03f4b2a46c GIT binary patch literal 176 vcmZQ&fBf0AiU4H~;_u literal 0 HcmV?d00001 diff --git a/unit-test-data/libstore/worker-protocol/build-result-1.28.bin b/unit-test-data/libstore/worker-protocol/build-result-1.28.bin new file mode 100644 index 0000000000000000000000000000000000000000..74bcd5cf98b828fb63a931ef7810f7fabc805ca5 GIT binary patch literal 648 zcmc&wK?=e!5EQ|aK0%e!+`)6%Si?(*`6_8xaxz$KI6Xx-2t!SGa{k;2f8i+RMR2A;`6>RitBjDY7{lnANJD3OVh8WW_c zkTRG+zDX!Yj%68orEsd#Y$KG=*{FoWL-7`MFAQl%7RmZ0!PYe3jk66aF4r+L$O`tu z&uq-x(J#Q)LAOdzsy>VTJDdcofzX)BfXe~O6CwQ^%woR?}>#sX4il$bfs;n zmv%`YOQ~vvTo;t-%xH@l(n4vSK8bRZQHc`kI^B)Ih0TkNGRhXy8rqZN5BnYj(ieFo zAJ+t*u7l`;??iPt&V)lzi91dfGZFf@g4iVAZN4+jUVRU7o>ol_o!fedeM@Pl_mAVf T^ROZOQyyvZZF!s Date: Tue, 8 Mar 2022 23:09:26 +0000 Subject: [PATCH 291/299] Move `ValidPathInfo` serialization code to `worker-protocol.{cc.hh}` It does not belong with the data type itself. This also materializes the fact that `copyPath` does not do any version negotiation just just hard-codes "16". The non-standard interface of these serializers makes it harder to test, but this is fixed in the next commit which then adds those tests. --- src/libstore/daemon.cc | 4 +-- src/libstore/path-info.cc | 54 --------------------------------- src/libstore/path-info.hh | 5 --- src/libstore/remote-store.cc | 11 +++++-- src/libstore/store-api.cc | 11 ++++++- src/libstore/worker-protocol.cc | 45 ++++++++++++++++++++++++++- src/libstore/worker-protocol.hh | 12 ++++++++ 7 files changed, 76 insertions(+), 66 deletions(-) diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 19afe4388..0afaaccba 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -422,7 +422,7 @@ static void performOp(TunnelLogger * logger, ref store, }(); logger->stopWork(); - pathInfo->write(to, *store, GET_PROTOCOL_MINOR(clientVersion)); + WorkerProto::Serialise::write(*store, wconn, *pathInfo); } else { HashType hashAlgo; std::string baseName; @@ -819,7 +819,7 @@ static void performOp(TunnelLogger * logger, ref store, if (info) { if (GET_PROTOCOL_MINOR(clientVersion) >= 17) to << 1; - info->write(to, *store, GET_PROTOCOL_MINOR(clientVersion), false); + WorkerProto::Serialise::write(*store, wconn, *info, false); } else { assert(GET_PROTOCOL_MINOR(clientVersion) >= 17); to << 0; diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index fb507a847..51fb5f02a 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -1,6 +1,4 @@ #include "path-info.hh" -#include "worker-protocol.hh" -#include "worker-protocol-impl.hh" #include "store-api.hh" namespace nix { @@ -99,7 +97,6 @@ Strings ValidPathInfo::shortRefs() const return refs; } - ValidPathInfo::ValidPathInfo( const Store & store, std::string_view name, @@ -128,55 +125,4 @@ ValidPathInfo::ValidPathInfo( }, std::move(ca).raw); } - -ValidPathInfo ValidPathInfo::read(Source & source, const Store & store, unsigned int format) -{ - return read(source, store, format, store.parseStorePath(readString(source))); -} - -ValidPathInfo ValidPathInfo::read(Source & source, const Store & store, unsigned int format, StorePath && path) -{ - auto deriver = readString(source); - auto narHash = Hash::parseAny(readString(source), htSHA256); - ValidPathInfo info(path, narHash); - if (deriver != "") info.deriver = store.parseStorePath(deriver); - info.references = WorkerProto::Serialise::read(store, - WorkerProto::ReadConn { - .from = source, - .version = format, - }); - source >> info.registrationTime >> info.narSize; - if (format >= 16) { - source >> info.ultimate; - info.sigs = readStrings(source); - info.ca = ContentAddress::parseOpt(readString(source)); - } - return info; -} - - -void ValidPathInfo::write( - Sink & sink, - const Store & store, - unsigned int format, - bool includePath) const -{ - if (includePath) - sink << store.printStorePath(path); - sink << (deriver ? store.printStorePath(*deriver) : "") - << narHash.to_string(HashFormat::Base16, false); - WorkerProto::write(store, - WorkerProto::WriteConn { - .to = sink, - .version = format, - }, - references); - sink << registrationTime << narSize; - if (format >= 16) { - sink << ultimate - << sigs - << renderContentAddress(ca); - } -} - } diff --git a/src/libstore/path-info.hh b/src/libstore/path-info.hh index fea6d0e5f..919fe32f1 100644 --- a/src/libstore/path-info.hh +++ b/src/libstore/path-info.hh @@ -121,11 +121,6 @@ struct ValidPathInfo std::string_view name, ContentAddressWithReferences && ca, Hash narHash); virtual ~ValidPathInfo() { } - - static ValidPathInfo read(Source & source, const Store & store, unsigned int format); - static ValidPathInfo read(Source & source, const Store & store, unsigned int format, StorePath && path); - - void write(Sink & sink, const Store & store, unsigned int format, bool includePath = true) const; }; typedef std::map ValidPathInfos; diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index ef648e01b..aaea5c4a2 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -332,7 +332,7 @@ void RemoteStore::queryPathInfoUncached(const StorePath & path, if (!valid) throw InvalidPath("path '%s' is not valid", printStorePath(path)); } info = std::make_shared( - ValidPathInfo::read(conn->from, *this, GET_PROTOCOL_MINOR(conn->daemonVersion), StorePath{path})); + WorkerProto::Serialise::read(*this, *conn, StorePath{path})); } callback(std::move(info)); } catch (...) { callback.rethrow(); } @@ -445,7 +445,7 @@ ref RemoteStore::addCAToStore( } return make_ref( - ValidPathInfo::read(conn->from, *this, GET_PROTOCOL_MINOR(conn->daemonVersion))); + WorkerProto::Serialise::read(*this, *conn)); } else { if (repair) throw Error("repairing is not supported when building through the Nix daemon protocol < 1.25"); @@ -570,7 +570,12 @@ void RemoteStore::addMultipleToStore( auto source = sinkToSource([&](Sink & sink) { sink << pathsToCopy.size(); for (auto & [pathInfo, pathSource] : pathsToCopy) { - pathInfo.write(sink, *this, 16); + WorkerProto::Serialise::write(*this, + WorkerProto::WriteConn { + .to = sink, + .version = 16, + }, + pathInfo); pathSource->drainInto(sink); } }); diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 0c58cca0c..785b26062 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -11,6 +11,9 @@ #include "archive.hh" #include "callback.hh" #include "remote-store.hh" +// FIXME this should not be here, see TODO below on +// `addMultipleToStore`. +#include "worker-protocol.hh" #include #include @@ -357,7 +360,13 @@ void Store::addMultipleToStore( { auto expected = readNum(source); for (uint64_t i = 0; i < expected; ++i) { - auto info = ValidPathInfo::read(source, *this, 16); + // FIXME we should not be using the worker protocol here, let + // alone the worker protocol with a hard-coded version! + auto info = WorkerProto::Serialise::read(*this, + WorkerProto::ReadConn { + .from = source, + .version = 16, + }); info.ultimate = false; addToStore(info, source, repair, checkSigs); } diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index f69322a61..94f8c8811 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -6,7 +6,7 @@ #include "worker-protocol.hh" #include "worker-protocol-impl.hh" #include "archive.hh" -#include "derivations.hh" +#include "path-info.hh" #include @@ -142,4 +142,47 @@ void WorkerProto::Serialise::write(const Store & store, WorkerProto } +ValidPathInfo WorkerProto::Serialise::read(const Store & store, ReadConn conn) +{ + auto path = WorkerProto::Serialise::read(store, conn); + return WorkerProto::Serialise::read(store, conn, std::move(path)); +} + +ValidPathInfo WorkerProto::Serialise::read(const Store & store, ReadConn conn, StorePath && path) +{ + auto deriver = readString(conn.from); + auto narHash = Hash::parseAny(readString(conn.from), htSHA256); + ValidPathInfo info(path, narHash); + if (deriver != "") info.deriver = store.parseStorePath(deriver); + info.references = WorkerProto::Serialise::read(store, conn); + conn.from >> info.registrationTime >> info.narSize; + if (GET_PROTOCOL_MINOR(conn.version) >= 16) { + conn.from >> info.ultimate; + info.sigs = readStrings(conn.from); + info.ca = ContentAddress::parseOpt(readString(conn.from)); + } + return info; +} + +void WorkerProto::Serialise::write( + const Store & store, + WriteConn conn, + const ValidPathInfo & pathInfo, + bool includePath) +{ + if (includePath) + conn.to << store.printStorePath(pathInfo.path); + conn.to + << (pathInfo.deriver ? store.printStorePath(*pathInfo.deriver) : "") + << pathInfo.narHash.to_string(HashFormat::Base16, false); + WorkerProto::write(store, conn, pathInfo.references); + conn.to << pathInfo.registrationTime << pathInfo.narSize; + if (GET_PROTOCOL_MINOR(conn.version) >= 16) { + conn.to + << pathInfo.ultimate + << pathInfo.sigs + << renderContentAddress(pathInfo.ca); + } +} + } diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index 2d55d926a..d35e3c678 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -31,6 +31,7 @@ struct Source; struct DerivedPath; struct BuildResult; struct KeyedBuildResult; +struct ValidPathInfo; enum TrustedFlag : bool; @@ -220,4 +221,15 @@ template DECLARE_WORKER_SERIALISER(std::map); #undef COMMA_ +/* These are a non-standard form for historical reasons. */ + +template<> +struct WorkerProto::Serialise +{ + static ValidPathInfo read(const Store & store, WorkerProto::ReadConn conn); + static ValidPathInfo read(const Store & store, WorkerProto::ReadConn conn, StorePath && path); + + static void write(const Store & store, WriteConn conn, const ValidPathInfo & pathInfo, bool includePath = true); +}; + } From 70f8b96c11af275b5766dca0a49737803d1e0339 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 17 Apr 2023 17:13:44 -0400 Subject: [PATCH 292/299] Factor out `UnkeyedValidPathInfo` and test This makes the path info serialisers ideomatic again, which allows me to test them. --- src/libstore/daemon.cc | 2 +- src/libstore/path-info.cc | 23 ++- src/libstore/path-info.hh | 31 ++-- src/libstore/remote-store.cc | 3 +- src/libstore/tests/worker-protocol.cc | 153 ++++++++++++++++++ src/libstore/worker-protocol.cc | 24 +-- src/libstore/worker-protocol.hh | 16 +- .../unkeyed-valid-path-info-1.15.bin | Bin 0 -> 328 bytes .../worker-protocol/valid-path-info-1.15.bin | Bin 0 -> 488 bytes .../worker-protocol/valid-path-info-1.16.bin | Bin 0 -> 952 bytes 10 files changed, 218 insertions(+), 34 deletions(-) create mode 100644 unit-test-data/libstore/worker-protocol/unkeyed-valid-path-info-1.15.bin create mode 100644 unit-test-data/libstore/worker-protocol/valid-path-info-1.15.bin create mode 100644 unit-test-data/libstore/worker-protocol/valid-path-info-1.16.bin diff --git a/src/libstore/daemon.cc b/src/libstore/daemon.cc index 0afaaccba..007ffc05a 100644 --- a/src/libstore/daemon.cc +++ b/src/libstore/daemon.cc @@ -819,7 +819,7 @@ static void performOp(TunnelLogger * logger, ref store, if (info) { if (GET_PROTOCOL_MINOR(clientVersion) >= 17) to << 1; - WorkerProto::Serialise::write(*store, wconn, *info, false); + WorkerProto::write(*store, wconn, static_cast(*info)); } else { assert(GET_PROTOCOL_MINOR(clientVersion) >= 17); to << 0; diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index 51fb5f02a..ab39e71f4 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -3,6 +3,25 @@ namespace nix { +GENERATE_CMP_EXT( + , + UnkeyedValidPathInfo, + me->deriver, + me->narHash, + me->references, + me->registrationTime, + me->narSize, + //me->id, + me->ultimate, + me->sigs, + me->ca); + +GENERATE_CMP_EXT( + , + ValidPathInfo, + me->path, + static_cast(*me)); + std::string ValidPathInfo::fingerprint(const Store & store) const { if (narSize == 0) @@ -102,8 +121,8 @@ ValidPathInfo::ValidPathInfo( std::string_view name, ContentAddressWithReferences && ca, Hash narHash) - : path(store.makeFixedOutputPathFromCA(name, ca)) - , narHash(narHash) + : UnkeyedValidPathInfo(narHash) + , path(store.makeFixedOutputPathFromCA(name, ca)) { std::visit(overloaded { [this](TextInfo && ti) { diff --git a/src/libstore/path-info.hh b/src/libstore/path-info.hh index 919fe32f1..a82e643ae 100644 --- a/src/libstore/path-info.hh +++ b/src/libstore/path-info.hh @@ -32,9 +32,8 @@ struct SubstitutablePathInfo typedef std::map SubstitutablePathInfos; -struct ValidPathInfo +struct UnkeyedValidPathInfo { - StorePath path; std::optional deriver; /** * \todo document this @@ -72,6 +71,20 @@ struct ValidPathInfo */ std::optional ca; + UnkeyedValidPathInfo(const UnkeyedValidPathInfo & other) = default; + + UnkeyedValidPathInfo(Hash narHash) : narHash(narHash) { }; + + DECLARE_CMP(UnkeyedValidPathInfo); + + virtual ~UnkeyedValidPathInfo() { } +}; + +struct ValidPathInfo : UnkeyedValidPathInfo { + StorePath path; + + DECLARE_CMP(ValidPathInfo); + /** * Return a fingerprint of the store path to be used in binary * cache signatures. It contains the store path, the base-32 @@ -84,11 +97,11 @@ struct ValidPathInfo void sign(const Store & store, const SecretKey & secretKey); - /** - * @return The `ContentAddressWithReferences` that determines the - * store path for a content-addressed store object, `std::nullopt` - * for an input-addressed store object. - */ + /** + * @return The `ContentAddressWithReferences` that determines the + * store path for a content-addressed store object, `std::nullopt` + * for an input-addressed store object. + */ std::optional contentAddressWithReferences() const; /** @@ -114,8 +127,8 @@ struct ValidPathInfo ValidPathInfo(const ValidPathInfo & other) = default; - ValidPathInfo(StorePath && path, Hash narHash) : path(std::move(path)), narHash(narHash) { }; - ValidPathInfo(const StorePath & path, Hash narHash) : path(path), narHash(narHash) { }; + ValidPathInfo(StorePath && path, UnkeyedValidPathInfo info) : UnkeyedValidPathInfo(info), path(std::move(path)) { }; + ValidPathInfo(const StorePath & path, UnkeyedValidPathInfo info) : UnkeyedValidPathInfo(info), path(path) { }; ValidPathInfo(const Store & store, std::string_view name, ContentAddressWithReferences && ca, Hash narHash); diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index aaea5c4a2..7bdc25433 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -332,7 +332,8 @@ void RemoteStore::queryPathInfoUncached(const StorePath & path, if (!valid) throw InvalidPath("path '%s' is not valid", printStorePath(path)); } info = std::make_shared( - WorkerProto::Serialise::read(*this, *conn, StorePath{path})); + StorePath{path}, + WorkerProto::Serialise::read(*this, *conn)); } callback(std::move(info)); } catch (...) { callback.rethrow(); } diff --git a/src/libstore/tests/worker-protocol.cc b/src/libstore/tests/worker-protocol.cc index e0ce340d8..ad5943c69 100644 --- a/src/libstore/tests/worker-protocol.cc +++ b/src/libstore/tests/worker-protocol.cc @@ -329,6 +329,159 @@ VERSIONED_CHARACTERIZATION_TEST( t; })) +VERSIONED_CHARACTERIZATION_TEST( + WorkerProtoTest, + unkeyedValidPathInfo_1_15, + "unkeyed-valid-path-info-1.15", + 1 << 8 | 15, + (std::tuple { + ({ + UnkeyedValidPathInfo info { + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }; + info.registrationTime = 23423; + info.narSize = 34878; + info; + }), + ({ + UnkeyedValidPathInfo info { + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }; + info.deriver = StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }; + info.references = { + StorePath { + "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo.drv", + }, + }; + info.registrationTime = 23423; + info.narSize = 34878; + info; + }), + })) + +VERSIONED_CHARACTERIZATION_TEST( + WorkerProtoTest, + validPathInfo_1_15, + "valid-path-info-1.15", + 1 << 8 | 15, + (std::tuple { + ({ + ValidPathInfo info { + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + UnkeyedValidPathInfo { + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }, + }; + info.registrationTime = 23423; + info.narSize = 34878; + info; + }), + ({ + ValidPathInfo info { + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + UnkeyedValidPathInfo { + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }, + }; + info.deriver = StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }; + info.references = { + // other reference + StorePath { + "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo", + }, + // self reference + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + }; + info.registrationTime = 23423; + info.narSize = 34878; + info; + }), + })) + +VERSIONED_CHARACTERIZATION_TEST( + WorkerProtoTest, + validPathInfo_1_16, + "valid-path-info-1.16", + 1 << 8 | 16, + (std::tuple { + ({ + ValidPathInfo info { + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + UnkeyedValidPathInfo { + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }, + }; + info.registrationTime = 23423; + info.narSize = 34878; + info.ultimate = true; + info; + }), + ({ + ValidPathInfo info { + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + UnkeyedValidPathInfo { + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }, + }; + info.deriver = StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv", + }; + info.references = { + // other reference + StorePath { + "g1w7hyyyy1w7hy3qg1w7hy3qgqqqqy3q-foo", + }, + // self reference + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + }; + info.registrationTime = 23423; + info.narSize = 34878; + info.sigs = { + "fake-sig-1", + "fake-sig-2", + }, + info; + }), + ({ + ValidPathInfo info { + *LibStoreTest::store, + "foo", + FixedOutputInfo { + .method = FileIngestionMethod::Recursive, + .hash = hashString(HashType::htSHA256, "(...)"), + .references = { + .others = { + StorePath { + "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar", + }, + }, + .self = true, + }, + }, + Hash::parseSRI("sha256-FePFYIlMuycIXPZbWi7LGEiMmZSX9FMbaQenWBzm1Sc="), + }; + info.registrationTime = 23423; + info.narSize = 34878; + info; + }), + })) + VERSIONED_CHARACTERIZATION_TEST( WorkerProtoTest, optionalTrustedFlag, diff --git a/src/libstore/worker-protocol.cc b/src/libstore/worker-protocol.cc index 94f8c8811..d618b9bd8 100644 --- a/src/libstore/worker-protocol.cc +++ b/src/libstore/worker-protocol.cc @@ -145,14 +145,24 @@ void WorkerProto::Serialise::write(const Store & store, WorkerProto ValidPathInfo WorkerProto::Serialise::read(const Store & store, ReadConn conn) { auto path = WorkerProto::Serialise::read(store, conn); - return WorkerProto::Serialise::read(store, conn, std::move(path)); + return ValidPathInfo { + std::move(path), + WorkerProto::Serialise::read(store, conn), + }; } -ValidPathInfo WorkerProto::Serialise::read(const Store & store, ReadConn conn, StorePath && path) +void WorkerProto::Serialise::write(const Store & store, WriteConn conn, const ValidPathInfo & pathInfo) +{ + WorkerProto::write(store, conn, pathInfo.path); + WorkerProto::write(store, conn, static_cast(pathInfo)); +} + + +UnkeyedValidPathInfo WorkerProto::Serialise::read(const Store & store, ReadConn conn) { auto deriver = readString(conn.from); auto narHash = Hash::parseAny(readString(conn.from), htSHA256); - ValidPathInfo info(path, narHash); + UnkeyedValidPathInfo info(narHash); if (deriver != "") info.deriver = store.parseStorePath(deriver); info.references = WorkerProto::Serialise::read(store, conn); conn.from >> info.registrationTime >> info.narSize; @@ -164,14 +174,8 @@ ValidPathInfo WorkerProto::Serialise::read(const Store & store, R return info; } -void WorkerProto::Serialise::write( - const Store & store, - WriteConn conn, - const ValidPathInfo & pathInfo, - bool includePath) +void WorkerProto::Serialise::write(const Store & store, WriteConn conn, const UnkeyedValidPathInfo & pathInfo) { - if (includePath) - conn.to << store.printStorePath(pathInfo.path); conn.to << (pathInfo.deriver ? store.printStorePath(*pathInfo.deriver) : "") << pathInfo.narHash.to_string(HashFormat::Base16, false); diff --git a/src/libstore/worker-protocol.hh b/src/libstore/worker-protocol.hh index d35e3c678..dcd54ad16 100644 --- a/src/libstore/worker-protocol.hh +++ b/src/libstore/worker-protocol.hh @@ -32,6 +32,7 @@ struct DerivedPath; struct BuildResult; struct KeyedBuildResult; struct ValidPathInfo; +struct UnkeyedValidPathInfo; enum TrustedFlag : bool; @@ -207,6 +208,10 @@ DECLARE_WORKER_SERIALISER(BuildResult); template<> DECLARE_WORKER_SERIALISER(KeyedBuildResult); template<> +DECLARE_WORKER_SERIALISER(ValidPathInfo); +template<> +DECLARE_WORKER_SERIALISER(UnkeyedValidPathInfo); +template<> DECLARE_WORKER_SERIALISER(std::optional); template @@ -221,15 +226,4 @@ template DECLARE_WORKER_SERIALISER(std::map); #undef COMMA_ -/* These are a non-standard form for historical reasons. */ - -template<> -struct WorkerProto::Serialise -{ - static ValidPathInfo read(const Store & store, WorkerProto::ReadConn conn); - static ValidPathInfo read(const Store & store, WorkerProto::ReadConn conn, StorePath && path); - - static void write(const Store & store, WriteConn conn, const ValidPathInfo & pathInfo, bool includePath = true); -}; - } diff --git a/unit-test-data/libstore/worker-protocol/unkeyed-valid-path-info-1.15.bin b/unit-test-data/libstore/worker-protocol/unkeyed-valid-path-info-1.15.bin new file mode 100644 index 0000000000000000000000000000000000000000..e69ccbe83862b29e3f79810c0c2ff4561a42f86f GIT binary patch literal 328 zcmbu4%?`pK5QOo8D!&WM#q>Qq0R{CUX=_wYUVUsEAs(9a)(w2^a=MH48Q8;TGRcmlTf(6rfXPFjSJ46(yf&k8}#9A2z znmI2!XwnU&AmvqJYIhQk7+x#{!gQkU@$Il_5ceP*O@N zxu!r+#$?P>4g#ry<$$?w`2MnGPahlJ`HLM!?tH6m@;lyRZZ9kI;P`*_d+&94ym9_l z{ZG@h-`{zOWuKw$x?n$F_k7^>Jh*~Rnvu&5}L}Qp+gUA>+#+bF!C(UNw&;S4c literal 0 HcmV?d00001 From 201a4af9a4447a151682de6705a4a74783bf0e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=A2=D1=80=D0=BE?= =?UTF-8?q?=D1=84=D0=B8=D0=BC=D0=BE=D0=B2?= <62882157+trofkm@users.noreply.github.com> Date: Mon, 23 Oct 2023 01:56:46 +0300 Subject: [PATCH 293/299] Clean up `app.cc` (#9201) - Rename `expected` to `expectedType` - Use early `return` and `continue` to reduce nesting --- .gitignore | 2 ++ src/nix/app.cc | 40 ++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 2d3314015..04d96ca2c 100644 --- a/.gitignore +++ b/.gitignore @@ -138,7 +138,9 @@ nix-rust/target result +# IDE .vscode/ +.idea/ # clangd and possibly more .cache/ diff --git a/src/nix/app.cc b/src/nix/app.cc index 34fac9935..935ed18ec 100644 --- a/src/nix/app.cc +++ b/src/nix/app.cc @@ -20,20 +20,24 @@ StringPairs resolveRewrites( const std::vector & dependencies) { StringPairs res; - if (experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { - for (auto & dep : dependencies) { - if (auto drvDep = std::get_if(&dep.path)) { - for (auto & [ outputName, outputPath ] : drvDep->outputs) { - res.emplace( - DownstreamPlaceholder::fromSingleDerivedPathBuilt( - SingleDerivedPath::Built { - .drvPath = make_ref(drvDep->drvPath->discardOutputPath()), - .output = outputName, - }).render(), - store.printStorePath(outputPath) - ); - } - } + if (!experimentalFeatureSettings.isEnabled(Xp::CaDerivations)) { + return res; + } + for (auto &dep: dependencies) { + auto drvDep = std::get_if(&dep.path); + if (!drvDep) { + continue; + } + + for (const auto & [ outputName, outputPath ] : drvDep->outputs) { + res.emplace( + DownstreamPlaceholder::fromSingleDerivedPathBuilt( + SingleDerivedPath::Built { + .drvPath = make_ref(drvDep->drvPath->discardOutputPath()), + .output = outputName, + }).render(), + store.printStorePath(outputPath) + ); } } return res; @@ -58,11 +62,11 @@ UnresolvedApp InstallableValue::toApp(EvalState & state) auto type = cursor->getAttr("type")->getString(); - std::string expected = !attrPath.empty() && + std::string expectedType = !attrPath.empty() && (state.symbols[attrPath[0]] == "apps" || state.symbols[attrPath[0]] == "defaultApp") ? "app" : "derivation"; - if (type != expected) - throw Error("attribute '%s' should have type '%s'", cursor->getAttrPathStr(), expected); + if (type != expectedType) + throw Error("attribute '%s' should have type '%s'", cursor->getAttrPathStr(), expectedType); if (type == "app") { auto [program, context] = cursor->getAttr("program")->getStringWithContext(); @@ -91,7 +95,7 @@ UnresolvedApp InstallableValue::toApp(EvalState & state) }, c.raw)); } - return UnresolvedApp{App { + return UnresolvedApp { App { .context = std::move(context2), .program = program, }}; From 256dfb98e864e474921ac5a8264e33de6978f47d Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Mon, 23 Oct 2023 04:05:02 +0200 Subject: [PATCH 294/299] remove Basic Package Management section (#7974) this is the first thing most beginners see, and it misleads them into assuming `nix-env` is appropriate for doing anything but setting and reverting profile generations. this chapter is the root of most evil around the ecosystem, and today we finally close it for good. --- doc/manual/src/SUMMARY.md.in | 1 - .../package-management/basic-package-mgmt.md | 179 ------------------ 2 files changed, 180 deletions(-) delete mode 100644 doc/manual/src/package-management/basic-package-mgmt.md diff --git a/doc/manual/src/SUMMARY.md.in b/doc/manual/src/SUMMARY.md.in index e49e77cf5..60ebeb138 100644 --- a/doc/manual/src/SUMMARY.md.in +++ b/doc/manual/src/SUMMARY.md.in @@ -17,7 +17,6 @@ - [Upgrading Nix](installation/upgrading.md) - [Uninstalling Nix](installation/uninstall.md) - [Package Management](package-management/package-management.md) - - [Basic Package Management](package-management/basic-package-mgmt.md) - [Profiles](package-management/profiles.md) - [Garbage Collection](package-management/garbage-collection.md) - [Garbage Collector Roots](package-management/garbage-collector-roots.md) diff --git a/doc/manual/src/package-management/basic-package-mgmt.md b/doc/manual/src/package-management/basic-package-mgmt.md deleted file mode 100644 index 07b92fb76..000000000 --- a/doc/manual/src/package-management/basic-package-mgmt.md +++ /dev/null @@ -1,179 +0,0 @@ -# Basic Package Management - -The main command for package management is -[`nix-env`](../command-ref/nix-env.md). You can use it to install, -upgrade, and erase packages, and to query what packages are installed -or are available for installation. - -In Nix, different users can have different “views” on the set of -installed applications. That is, there might be lots of applications -present on the system (possibly in many different versions), but users -can have a specific selection of those active — where “active” just -means that it appears in a directory in the user’s `PATH`. Such a view -on the set of installed applications is called a *user environment*, -which is just a directory tree consisting of symlinks to the files of -the active applications. - -Components are installed from a set of *Nix expressions* that tell Nix -how to build those packages, including, if necessary, their -dependencies. There is a collection of Nix expressions called the -Nixpkgs package collection that contains packages ranging from basic -development stuff such as GCC and Glibc, to end-user applications like -Mozilla Firefox. (Nix is however not tied to the Nixpkgs package -collection; you could write your own Nix expressions based on Nixpkgs, -or completely new ones.) - -You can manually download the latest version of Nixpkgs from -. However, it’s much more -convenient to use the Nixpkgs [*channel*](../command-ref/nix-channel.md), since it makes -it easy to stay up to date with new versions of Nixpkgs. Nixpkgs is -automatically added to your list of “subscribed” channels when you -install Nix. If this is not the case for some reason, you can add it -as follows: - -```console -$ nix-channel --add https://nixos.org/channels/nixpkgs-unstable -$ nix-channel --update -``` - -> **Note** -> -> On NixOS, you’re automatically subscribed to a NixOS channel -> corresponding to your NixOS major release (e.g. -> ). A NixOS channel is identical -> to the Nixpkgs channel, except that it contains only Linux binaries -> and is updated only if a set of regression tests succeed. - -You can view the set of available packages in Nixpkgs: - -```console -$ nix-env --query --available --attr-path -nixpkgs.aterm aterm-2.2 -nixpkgs.bash bash-3.0 -nixpkgs.binutils binutils-2.15 -nixpkgs.bison bison-1.875d -nixpkgs.blackdown blackdown-1.4.2 -nixpkgs.bzip2 bzip2-1.0.2 -… -``` - -The flag `-q` specifies a query operation, `-a` means that you want -to show the “available” (i.e., installable) packages, as opposed to the -installed packages, and `-P` prints the attribute paths that can be used -to unambiguously select a package for installation (listed in the first column). -If you downloaded Nixpkgs yourself, or if you checked it out from GitHub, -then you need to pass the path to your Nixpkgs tree using the `-f` flag: - -```console -$ nix-env --query --available --attr-path --file /path/to/nixpkgs -aterm aterm-2.2 -bash bash-3.0 -… -``` - -where */path/to/nixpkgs* is where you’ve unpacked or checked out -Nixpkgs. - -You can filter the packages by name: - -```console -$ nix-env --query --available --attr-path firefox -nixpkgs.firefox-esr firefox-91.3.0esr -nixpkgs.firefox firefox-94.0.1 -``` - -and using regular expressions: - -```console -$ nix-env --query --available --attr-path 'firefox.*' -``` - -It is also possible to see the *status* of available packages, i.e., -whether they are installed into the user environment and/or present in -the system: - -```console -$ nix-env --query --available --attr-path --status -… --PS nixpkgs.bash bash-3.0 ---S nixpkgs.binutils binutils-2.15 -IPS nixpkgs.bison bison-1.875d -… -``` - -The first character (`I`) indicates whether the package is installed in -your current user environment. The second (`P`) indicates whether it is -present on your system (in which case installing it into your user -environment would be a very quick operation). The last one (`S`) -indicates whether there is a so-called *substitute* for the package, -which is Nix’s mechanism for doing binary deployment. It just means that -Nix knows that it can fetch a pre-built package from somewhere -(typically a network server) instead of building it locally. - -You can install a package using `nix-env --install --attr `. For instance, - -```console -$ nix-env --install --attr nixpkgs.subversion -``` - -will install the package called `subversion` from `nixpkgs` channel (which is, of course, the -[Subversion version management system](http://subversion.tigris.org/)). - -> **Note** -> -> When you ask Nix to install a package, it will first try to get it in -> pre-compiled form from a *binary cache*. By default, Nix will use the -> binary cache ; it contains binaries for most -> packages in Nixpkgs. Only if no binary is available in the binary -> cache, Nix will build the package from source. So if `nix-env -> -iA nixpkgs.subversion` results in Nix building stuff from source, then either -> the package is not built for your platform by the Nixpkgs build -> servers, or your version of Nixpkgs is too old or too new. For -> instance, if you have a very recent checkout of Nixpkgs, then the -> Nixpkgs build servers may not have had a chance to build everything -> and upload the resulting binaries to . The -> Nixpkgs channel is only updated after all binaries have been uploaded -> to the cache, so if you stick to the Nixpkgs channel (rather than -> using a Git checkout of the Nixpkgs tree), you will get binaries for -> most packages. - -Naturally, packages can also be uninstalled. Unlike when installing, you will -need to use the derivation name (though the version part can be omitted), -instead of the attribute path, as `nix-env` does not record which attribute -was used for installing: - -```console -$ nix-env --uninstall subversion -``` - -Upgrading to a new version is just as easy. If you have a new release of -Nix Packages, you can do: - -```console -$ nix-env --upgrade --attr nixpkgs.subversion -``` - -This will *only* upgrade Subversion if there is a “newer” version in the -new set of Nix expressions, as defined by some pretty arbitrary rules -regarding ordering of version numbers (which generally do what you’d -expect of them). To just unconditionally replace Subversion with -whatever version is in the Nix expressions, use `-i` instead of `-u`; -`-i` will remove whatever version is already installed. - -You can also upgrade all packages for which there are newer versions: - -```console -$ nix-env --upgrade -``` - -Sometimes it’s useful to be able to ask what `nix-env` would do, without -actually doing it. For instance, to find out what packages would be -upgraded by `nix-env --upgrade `, you can do - -```console -$ nix-env --upgrade --dry-run -(dry run; not doing anything) -upgrading `libxslt-1.1.0' to `libxslt-1.1.10' -upgrading `graphviz-1.10' to `graphviz-1.12' -upgrading `coreutils-5.0' to `coreutils-5.2.1' -``` From 34a42f0d0ac20a70fdcc3dbf99a212b523c5f2d4 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Mon, 23 Oct 2023 11:05:50 +0200 Subject: [PATCH 295/299] Move PosixSourceAccessor into its own file --- src/libfetchers/fs-input-accessor.cc | 1 + src/libutil/archive.cc | 2 +- src/libutil/posix-source-accessor.cc | 86 ++++++++++++++++++++++++++++ src/libutil/posix-source-accessor.hh | 34 +++++++++++ src/libutil/source-accessor.cc | 81 -------------------------- src/libutil/source-accessor.hh | 27 --------- 6 files changed, 122 insertions(+), 109 deletions(-) create mode 100644 src/libutil/posix-source-accessor.cc create mode 100644 src/libutil/posix-source-accessor.hh diff --git a/src/libfetchers/fs-input-accessor.cc b/src/libfetchers/fs-input-accessor.cc index 3444c4643..7638d2d82 100644 --- a/src/libfetchers/fs-input-accessor.cc +++ b/src/libfetchers/fs-input-accessor.cc @@ -1,4 +1,5 @@ #include "fs-input-accessor.hh" +#include "posix-source-accessor.hh" #include "store-api.hh" namespace nix { diff --git a/src/libutil/archive.cc b/src/libutil/archive.cc index 0cd54e5db..3b1a1e0ef 100644 --- a/src/libutil/archive.cc +++ b/src/libutil/archive.cc @@ -14,7 +14,7 @@ #include "archive.hh" #include "util.hh" #include "config.hh" -#include "source-accessor.hh" +#include "posix-source-accessor.hh" namespace nix { diff --git a/src/libutil/posix-source-accessor.cc b/src/libutil/posix-source-accessor.cc new file mode 100644 index 000000000..48b4fe626 --- /dev/null +++ b/src/libutil/posix-source-accessor.cc @@ -0,0 +1,86 @@ +#include "posix-source-accessor.hh" + +namespace nix { + +void PosixSourceAccessor::readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback) +{ + // FIXME: add O_NOFOLLOW since symlinks should be resolved by the + // caller? + AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (!fd) + throw SysError("opening file '%1%'", path); + + struct stat st; + if (fstat(fd.get(), &st) == -1) + throw SysError("statting file"); + + sizeCallback(st.st_size); + + off_t left = st.st_size; + + std::vector buf(64 * 1024); + while (left) { + checkInterrupt(); + ssize_t rd = read(fd.get(), buf.data(), (size_t) std::min(left, (off_t) buf.size())); + if (rd == -1) { + if (errno != EINTR) + throw SysError("reading from file '%s'", showPath(path)); + } + else if (rd == 0) + throw SysError("unexpected end-of-file reading '%s'", showPath(path)); + else { + assert(rd <= left); + sink({(char *) buf.data(), (size_t) rd}); + left -= rd; + } + } +} + +bool PosixSourceAccessor::pathExists(const CanonPath & path) +{ + return nix::pathExists(path.abs()); +} + +SourceAccessor::Stat PosixSourceAccessor::lstat(const CanonPath & path) +{ + auto st = nix::lstat(path.abs()); + mtime = std::max(mtime, st.st_mtime); + return Stat { + .type = + S_ISREG(st.st_mode) ? tRegular : + S_ISDIR(st.st_mode) ? tDirectory : + S_ISLNK(st.st_mode) ? tSymlink : + tMisc, + .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR + }; +} + +SourceAccessor::DirEntries PosixSourceAccessor::readDirectory(const CanonPath & path) +{ + DirEntries res; + for (auto & entry : nix::readDirectory(path.abs())) { + std::optional type; + switch (entry.type) { + case DT_REG: type = Type::tRegular; break; + case DT_LNK: type = Type::tSymlink; break; + case DT_DIR: type = Type::tDirectory; break; + } + res.emplace(entry.name, type); + } + return res; +} + +std::string PosixSourceAccessor::readLink(const CanonPath & path) +{ + return nix::readLink(path.abs()); +} + +std::optional PosixSourceAccessor::getPhysicalPath(const CanonPath & path) +{ + return path; +} + +} diff --git a/src/libutil/posix-source-accessor.hh b/src/libutil/posix-source-accessor.hh new file mode 100644 index 000000000..608f96ee2 --- /dev/null +++ b/src/libutil/posix-source-accessor.hh @@ -0,0 +1,34 @@ +#pragma once + +#include "source-accessor.hh" + +namespace nix { + +/** + * A source accessor that uses the Unix filesystem. + */ +struct PosixSourceAccessor : SourceAccessor +{ + /** + * The most recent mtime seen by lstat(). This is a hack to + * support dumpPathAndGetMtime(). Should remove this eventually. + */ + time_t mtime = 0; + + void readFile( + const CanonPath & path, + Sink & sink, + std::function sizeCallback) override; + + bool pathExists(const CanonPath & path) override; + + Stat lstat(const CanonPath & path) override; + + DirEntries readDirectory(const CanonPath & path) override; + + std::string readLink(const CanonPath & path) override; + + std::optional getPhysicalPath(const CanonPath & path) override; +}; + +} diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc index 2d03d3d7a..d168a9667 100644 --- a/src/libutil/source-accessor.cc +++ b/src/libutil/source-accessor.cc @@ -55,85 +55,4 @@ std::string SourceAccessor::showPath(const CanonPath & path) return path.abs(); } -void PosixSourceAccessor::readFile( - const CanonPath & path, - Sink & sink, - std::function sizeCallback) -{ - // FIXME: add O_NOFOLLOW since symlinks should be resolved by the - // caller? - AutoCloseFD fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); - if (!fd) - throw SysError("opening file '%1%'", path); - - struct stat st; - if (fstat(fd.get(), &st) == -1) - throw SysError("statting file"); - - sizeCallback(st.st_size); - - off_t left = st.st_size; - - std::vector buf(64 * 1024); - while (left) { - checkInterrupt(); - ssize_t rd = read(fd.get(), buf.data(), (size_t) std::min(left, (off_t) buf.size())); - if (rd == -1) { - if (errno != EINTR) - throw SysError("reading from file '%s'", showPath(path)); - } - else if (rd == 0) - throw SysError("unexpected end-of-file reading '%s'", showPath(path)); - else { - assert(rd <= left); - sink({(char *) buf.data(), (size_t) rd}); - left -= rd; - } - } -} - -bool PosixSourceAccessor::pathExists(const CanonPath & path) -{ - return nix::pathExists(path.abs()); -} - -SourceAccessor::Stat PosixSourceAccessor::lstat(const CanonPath & path) -{ - auto st = nix::lstat(path.abs()); - mtime = std::max(mtime, st.st_mtime); - return Stat { - .type = - S_ISREG(st.st_mode) ? tRegular : - S_ISDIR(st.st_mode) ? tDirectory : - S_ISLNK(st.st_mode) ? tSymlink : - tMisc, - .isExecutable = S_ISREG(st.st_mode) && st.st_mode & S_IXUSR - }; -} - -SourceAccessor::DirEntries PosixSourceAccessor::readDirectory(const CanonPath & path) -{ - DirEntries res; - for (auto & entry : nix::readDirectory(path.abs())) { - std::optional type; - switch (entry.type) { - case DT_REG: type = Type::tRegular; break; - case DT_LNK: type = Type::tSymlink; break; - case DT_DIR: type = Type::tDirectory; break; - } - res.emplace(entry.name, type); - } - return res; -} - -std::string PosixSourceAccessor::readLink(const CanonPath & path) -{ - return nix::readLink(path.abs()); -} - -std::optional PosixSourceAccessor::getPhysicalPath(const CanonPath & path) -{ - return path; -} - } diff --git a/src/libutil/source-accessor.hh b/src/libutil/source-accessor.hh index f3504c9bb..fd823aa39 100644 --- a/src/libutil/source-accessor.hh +++ b/src/libutil/source-accessor.hh @@ -104,31 +104,4 @@ struct SourceAccessor virtual std::string showPath(const CanonPath & path); }; -/** - * A source accessor that uses the Unix filesystem. - */ -struct PosixSourceAccessor : SourceAccessor -{ - /** - * The most recent mtime seen by lstat(). This is a hack to - * support dumpPathAndGetMtime(). Should remove this eventually. - */ - time_t mtime = 0; - - void readFile( - const CanonPath & path, - Sink & sink, - std::function sizeCallback) override; - - bool pathExists(const CanonPath & path) override; - - Stat lstat(const CanonPath & path) override; - - DirEntries readDirectory(const CanonPath & path) override; - - std::string readLink(const CanonPath & path) override; - - std::optional getPhysicalPath(const CanonPath & path) override; -}; - } From b461cac21aa5b9127d230ed3f1a1ca8e8266fb60 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Mon, 23 Oct 2023 09:03:11 -0400 Subject: [PATCH 296/299] Overhaul completions, redo #6693 (#8131) As I complained in https://github.com/NixOS/nix/pull/6784#issuecomment-1421777030 (a comment on the wrong PR, sorry again!), #6693 introduced a second completions mechanism to fix a bug. Having two completion mechanisms isn't so nice. As @thufschmitt also pointed out, it was a bummer to go from `FlakeRef` to `std::string` when collecting flake refs. Now it is `FlakeRefs` again. The underlying issue that sought to work around was that completion of arguments not at the end can still benefit from the information from latter arguments. To fix this better, we rip out that change and simply defer all completion processing until after all the (regular, already-complete) arguments have been passed. In addition, I noticed the original completion logic used some global variables. I do not like global variables, because even if they save lines of code, they also obfuscate the architecture of the code. I got rid of them moved them to a new `RootArgs` class, which now has `parseCmdline` instead of `Args`. The idea is that we have many argument parsers from subcommands and what-not, but only one root args that owns the other per actual parsing invocation. The state that was global is now part of the root args instead. This did, admittedly, add a bunch of new code. And I do feel bad about that. So I went and added a lot of API docs to try to at least make the current state of things clear to the next person. -- This is needed for RFC 134 (tracking issue #7868). It was very hard to modularize `Installable` parsing when there were two completion arguments. I wouldn't go as far as to say it is *easy* now, but at least it is less hard (and the completions test finally passed). Co-authored-by: Valentin Gagarin --- src/libcmd/command.hh | 26 ++++---- src/libcmd/common-eval-args.cc | 4 +- src/libcmd/installables.cc | 114 +++++++++++++++++++-------------- src/libmain/common-args.cc | 9 +-- src/libmain/shared.hh | 3 +- src/libutil/args.cc | 111 ++++++++++++++++++++------------ src/libutil/args.hh | 109 ++++++++++++++++++++----------- src/libutil/args/root.hh | 72 +++++++++++++++++++++ src/libutil/local.mk | 5 ++ src/nix/bundle.cc | 4 +- src/nix/flake.cc | 14 ++-- src/nix/main.cc | 27 ++++---- src/nix/registry.cc | 4 +- src/nix/why-depends.cc | 8 +-- 14 files changed, 329 insertions(+), 181 deletions(-) create mode 100644 src/libutil/args/root.hh diff --git a/src/libcmd/command.hh b/src/libcmd/command.hh index 5c4569001..dafc0db3b 100644 --- a/src/libcmd/command.hh +++ b/src/libcmd/command.hh @@ -97,8 +97,6 @@ struct MixFlakeOptions : virtual Args, EvalCommand { flake::LockFlags lockFlags; - std::optional needsFlakeInputCompletion = {}; - MixFlakeOptions(); /** @@ -109,12 +107,8 @@ struct MixFlakeOptions : virtual Args, EvalCommand * command is operating with (presumably specified via some other * arguments) so that the completions for these flags can use them. */ - virtual std::vector getFlakesForCompletion() + virtual std::vector getFlakeRefsForCompletion() { return {}; } - - void completeFlakeInput(std::string_view prefix); - - void completionHook() override; }; struct SourceExprCommand : virtual Args, MixFlakeOptions @@ -137,7 +131,13 @@ struct SourceExprCommand : virtual Args, MixFlakeOptions /** * Complete an installable from the given prefix. */ - void completeInstallable(std::string_view prefix); + void completeInstallable(AddCompletions & completions, std::string_view prefix); + + /** + * Convenience wrapper around the underlying function to make setting the + * callback easier. + */ + CompleterClosure getCompleteInstallable(); }; /** @@ -170,7 +170,7 @@ struct RawInstallablesCommand : virtual Args, SourceExprCommand bool readFromStdIn = false; - std::vector getFlakesForCompletion() override; + std::vector getFlakeRefsForCompletion() override; private: @@ -199,10 +199,7 @@ struct InstallableCommand : virtual Args, SourceExprCommand void run(ref store) override; - std::vector getFlakesForCompletion() override - { - return {_installable}; - } + std::vector getFlakeRefsForCompletion() override; private: @@ -329,9 +326,10 @@ struct MixEnvironment : virtual Args { void setEnviron(); }; -void completeFlakeRef(ref store, std::string_view prefix); +void completeFlakeRef(AddCompletions & completions, ref store, std::string_view prefix); void completeFlakeRefWithFragment( + AddCompletions & completions, ref evalState, flake::LockFlags lockFlags, Strings attrPathPrefixes, diff --git a/src/libcmd/common-eval-args.cc b/src/libcmd/common-eval-args.cc index e6df49a7a..e53bc4c01 100644 --- a/src/libcmd/common-eval-args.cc +++ b/src/libcmd/common-eval-args.cc @@ -133,8 +133,8 @@ MixEvalArgs::MixEvalArgs() if (to.subdir != "") extraAttrs["dir"] = to.subdir; fetchers::overrideRegistry(from.input, to.input, extraAttrs); }}, - .completer = {[&](size_t, std::string_view prefix) { - completeFlakeRef(openStore(), prefix); + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeRef(completions, openStore(), prefix); }} }); diff --git a/src/libcmd/installables.cc b/src/libcmd/installables.cc index 01c01441c..eff18bbf6 100644 --- a/src/libcmd/installables.cc +++ b/src/libcmd/installables.cc @@ -28,6 +28,20 @@ namespace nix { +static void completeFlakeInputPath( + AddCompletions & completions, + ref evalState, + const std::vector & flakeRefs, + std::string_view prefix) +{ + for (auto & flakeRef : flakeRefs) { + auto flake = flake::getFlake(*evalState, flakeRef, true); + for (auto & input : flake.inputs) + if (hasPrefix(input.first, prefix)) + completions.add(input.first); + } +} + MixFlakeOptions::MixFlakeOptions() { auto category = "Common flake-related options"; @@ -79,8 +93,8 @@ MixFlakeOptions::MixFlakeOptions() .handler = {[&](std::string s) { lockFlags.inputUpdates.insert(flake::parseInputPath(s)); }}, - .completer = {[&](size_t, std::string_view prefix) { - needsFlakeInputCompletion = {std::string(prefix)}; + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeInputPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); }} }); @@ -95,11 +109,12 @@ MixFlakeOptions::MixFlakeOptions() flake::parseInputPath(inputPath), parseFlakeRef(flakeRef, absPath("."), true)); }}, - .completer = {[&](size_t n, std::string_view prefix) { - if (n == 0) - needsFlakeInputCompletion = {std::string(prefix)}; - else if (n == 1) - completeFlakeRef(getEvalState()->store, prefix); + .completer = {[&](AddCompletions & completions, size_t n, std::string_view prefix) { + if (n == 0) { + completeFlakeInputPath(completions, getEvalState(), getFlakeRefsForCompletion(), prefix); + } else if (n == 1) { + completeFlakeRef(completions, getEvalState()->store, prefix); + } }} }); @@ -146,30 +161,12 @@ MixFlakeOptions::MixFlakeOptions() } } }}, - .completer = {[&](size_t, std::string_view prefix) { - completeFlakeRef(getEvalState()->store, prefix); + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeRef(completions, getEvalState()->store, prefix); }} }); } -void MixFlakeOptions::completeFlakeInput(std::string_view prefix) -{ - auto evalState = getEvalState(); - for (auto & flakeRefS : getFlakesForCompletion()) { - auto flakeRef = parseFlakeRefWithFragment(expandTilde(flakeRefS), absPath(".")).first; - auto flake = flake::getFlake(*evalState, flakeRef, true); - for (auto & input : flake.inputs) - if (hasPrefix(input.first, prefix)) - completions->add(input.first); - } -} - -void MixFlakeOptions::completionHook() -{ - if (auto & prefix = needsFlakeInputCompletion) - completeFlakeInput(*prefix); -} - SourceExprCommand::SourceExprCommand() { addFlag({ @@ -226,11 +223,18 @@ Strings SourceExprCommand::getDefaultFlakeAttrPathPrefixes() }; } -void SourceExprCommand::completeInstallable(std::string_view prefix) +Args::CompleterClosure SourceExprCommand::getCompleteInstallable() +{ + return [this](AddCompletions & completions, size_t, std::string_view prefix) { + completeInstallable(completions, prefix); + }; +} + +void SourceExprCommand::completeInstallable(AddCompletions & completions, std::string_view prefix) { try { if (file) { - completionType = ctAttrs; + completions.setType(AddCompletions::Type::Attrs); evalSettings.pureEval = false; auto state = getEvalState(); @@ -265,14 +269,15 @@ void SourceExprCommand::completeInstallable(std::string_view prefix) std::string name = state->symbols[i.name]; if (name.find(searchWord) == 0) { if (prefix_ == "") - completions->add(name); + completions.add(name); else - completions->add(prefix_ + "." + name); + completions.add(prefix_ + "." + name); } } } } else { completeFlakeRefWithFragment( + completions, getEvalState(), lockFlags, getDefaultFlakeAttrPathPrefixes(), @@ -285,6 +290,7 @@ void SourceExprCommand::completeInstallable(std::string_view prefix) } void completeFlakeRefWithFragment( + AddCompletions & completions, ref evalState, flake::LockFlags lockFlags, Strings attrPathPrefixes, @@ -296,9 +302,9 @@ void completeFlakeRefWithFragment( try { auto hash = prefix.find('#'); if (hash == std::string::npos) { - completeFlakeRef(evalState->store, prefix); + completeFlakeRef(completions, evalState->store, prefix); } else { - completionType = ctAttrs; + completions.setType(AddCompletions::Type::Attrs); auto fragment = prefix.substr(hash + 1); std::string prefixRoot = ""; @@ -341,7 +347,7 @@ void completeFlakeRefWithFragment( auto attrPath2 = (*attr)->getAttrPath(attr2); /* Strip the attrpath prefix. */ attrPath2.erase(attrPath2.begin(), attrPath2.begin() + attrPathPrefix.size()); - completions->add(flakeRefS + "#" + prefixRoot + concatStringsSep(".", evalState->symbols.resolve(attrPath2))); + completions.add(flakeRefS + "#" + prefixRoot + concatStringsSep(".", evalState->symbols.resolve(attrPath2))); } } } @@ -352,7 +358,7 @@ void completeFlakeRefWithFragment( for (auto & attrPath : defaultFlakeAttrPaths) { auto attr = root->findAlongAttrPath(parseAttrPath(*evalState, attrPath)); if (!attr) continue; - completions->add(flakeRefS + "#" + prefixRoot); + completions.add(flakeRefS + "#" + prefixRoot); } } } @@ -361,15 +367,15 @@ void completeFlakeRefWithFragment( } } -void completeFlakeRef(ref store, std::string_view prefix) +void completeFlakeRef(AddCompletions & completions, ref store, std::string_view prefix) { if (!experimentalFeatureSettings.isEnabled(Xp::Flakes)) return; if (prefix == "") - completions->add("."); + completions.add("."); - completeDir(0, prefix); + Args::completeDir(completions, 0, prefix); /* Look for registry entries that match the prefix. */ for (auto & registry : fetchers::getRegistries(store)) { @@ -378,10 +384,10 @@ void completeFlakeRef(ref store, std::string_view prefix) if (!hasPrefix(prefix, "flake:") && hasPrefix(from, "flake:")) { std::string from2(from, 6); if (hasPrefix(from2, prefix)) - completions->add(from2); + completions.add(from2); } else { if (hasPrefix(from, prefix)) - completions->add(from); + completions.add(from); } } } @@ -747,9 +753,7 @@ RawInstallablesCommand::RawInstallablesCommand() expectArgs({ .label = "installables", .handler = {&rawInstallables}, - .completer = {[&](size_t, std::string_view prefix) { - completeInstallable(prefix); - }} + .completer = getCompleteInstallable(), }); } @@ -762,6 +766,17 @@ void RawInstallablesCommand::applyDefaultInstallables(std::vector & } } +std::vector RawInstallablesCommand::getFlakeRefsForCompletion() +{ + applyDefaultInstallables(rawInstallables); + std::vector res; + for (auto i : rawInstallables) + res.push_back(parseFlakeRefWithFragment( + expandTilde(i), + absPath(".")).first); + return res; +} + void RawInstallablesCommand::run(ref store) { if (readFromStdIn && !isatty(STDIN_FILENO)) { @@ -775,10 +790,13 @@ void RawInstallablesCommand::run(ref store) run(store, std::move(rawInstallables)); } -std::vector RawInstallablesCommand::getFlakesForCompletion() +std::vector InstallableCommand::getFlakeRefsForCompletion() { - applyDefaultInstallables(rawInstallables); - return rawInstallables; + return { + parseFlakeRefWithFragment( + expandTilde(_installable), + absPath(".")).first + }; } void InstallablesCommand::run(ref store, std::vector && rawInstallables) @@ -794,9 +812,7 @@ InstallableCommand::InstallableCommand() .label = "installable", .optional = true, .handler = {&_installable}, - .completer = {[&](size_t, std::string_view prefix) { - completeInstallable(prefix); - }} + .completer = getCompleteInstallable(), }); } diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc index f92920d18..205b77808 100644 --- a/src/libmain/common-args.cc +++ b/src/libmain/common-args.cc @@ -1,4 +1,5 @@ #include "common-args.hh" +#include "args/root.hh" #include "globals.hh" #include "loggers.hh" @@ -34,21 +35,21 @@ MixCommonArgs::MixCommonArgs(const std::string & programName) .description = "Set the Nix configuration setting *name* to *value* (overriding `nix.conf`).", .category = miscCategory, .labels = {"name", "value"}, - .handler = {[](std::string name, std::string value) { + .handler = {[this](std::string name, std::string value) { try { globalConfig.set(name, value); } catch (UsageError & e) { - if (!completions) + if (!getRoot().completions) warn(e.what()); } }}, - .completer = [](size_t index, std::string_view prefix) { + .completer = [](AddCompletions & completions, size_t index, std::string_view prefix) { if (index == 0) { std::map settings; globalConfig.getSettings(settings); for (auto & s : settings) if (hasPrefix(s.first, prefix)) - completions->add(s.first, fmt("Set the `%s` setting.", s.first)); + completions.add(s.first, fmt("Set the `%s` setting.", s.first)); } } }); diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh index 9415be78a..3159fe479 100644 --- a/src/libmain/shared.hh +++ b/src/libmain/shared.hh @@ -3,6 +3,7 @@ #include "util.hh" #include "args.hh" +#include "args/root.hh" #include "common-args.hh" #include "path.hh" #include "derived-path.hh" @@ -66,7 +67,7 @@ template N getIntArg(const std::string & opt, } -struct LegacyArgs : public MixCommonArgs +struct LegacyArgs : public MixCommonArgs, public RootArgs { std::function parseArg; diff --git a/src/libutil/args.cc b/src/libutil/args.cc index e410c7eec..6bc3cae07 100644 --- a/src/libutil/args.cc +++ b/src/libutil/args.cc @@ -1,4 +1,5 @@ #include "args.hh" +#include "args/root.hh" #include "hash.hh" #include "json-utils.hh" @@ -26,6 +27,11 @@ void Args::removeFlag(const std::string & longName) longFlags.erase(flag); } +void Completions::setType(AddCompletions::Type t) +{ + type = t; +} + void Completions::add(std::string completion, std::string description) { description = trim(description); @@ -37,7 +43,7 @@ void Completions::add(std::string completion, std::string description) if (needs_ellipsis) description.append(" [...]"); } - insert(Completion { + completions.insert(Completion { .completion = completion, .description = description }); @@ -46,12 +52,20 @@ void Completions::add(std::string completion, std::string description) bool Completion::operator<(const Completion & other) const { return completion < other.completion || (completion == other.completion && description < other.description); } -CompletionType completionType = ctNormal; -std::shared_ptr completions; - std::string completionMarker = "___COMPLETE___"; -static std::optional needsCompletion(std::string_view s) +RootArgs & Args::getRoot() +{ + Args * p = this; + while (p->parent) + p = p->parent; + + auto * res = dynamic_cast(p); + assert(res); + return *res; +} + +std::optional RootArgs::needsCompletion(std::string_view s) { if (!completions) return {}; auto i = s.find(completionMarker); @@ -60,7 +74,7 @@ static std::optional needsCompletion(std::string_view s) return {}; } -void Args::parseCmdline(const Strings & _cmdline) +void RootArgs::parseCmdline(const Strings & _cmdline) { Strings pendingArgs; bool dashDash = false; @@ -71,7 +85,7 @@ void Args::parseCmdline(const Strings & _cmdline) size_t n = std::stoi(*s); assert(n > 0 && n <= cmdline.size()); *std::next(cmdline.begin(), n - 1) += completionMarker; - completions = std::make_shared(); + completions = std::make_shared(); verbosity = lvlError; } @@ -125,17 +139,23 @@ void Args::parseCmdline(const Strings & _cmdline) for (auto & f : flagExperimentalFeatures) experimentalFeatureSettings.require(f); + /* Now that all the other args are processed, run the deferred completions. + */ + for (auto d : deferredCompletions) + d.completer(*completions, d.n, d.prefix); } bool Args::processFlag(Strings::iterator & pos, Strings::iterator end) { assert(pos != end); + auto & rootArgs = getRoot(); + auto process = [&](const std::string & name, const Flag & flag) -> bool { ++pos; if (auto & f = flag.experimentalFeature) - flagExperimentalFeatures.insert(*f); + rootArgs.flagExperimentalFeatures.insert(*f); std::vector args; bool anyCompleted = false; @@ -146,10 +166,15 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end) "flag '%s' requires %d argument(s), but only %d were given", name, flag.handler.arity, n); } - if (auto prefix = needsCompletion(*pos)) { + if (auto prefix = rootArgs.needsCompletion(*pos)) { anyCompleted = true; - if (flag.completer) - flag.completer(n, *prefix); + if (flag.completer) { + rootArgs.deferredCompletions.push_back({ + .completer = flag.completer, + .n = n, + .prefix = *prefix, + }); + } } args.push_back(*pos++); } @@ -159,14 +184,14 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end) }; if (std::string(*pos, 0, 2) == "--") { - if (auto prefix = needsCompletion(*pos)) { + if (auto prefix = rootArgs.needsCompletion(*pos)) { for (auto & [name, flag] : longFlags) { if (!hiddenCategories.count(flag->category) && hasPrefix(name, std::string(*prefix, 2))) { if (auto & f = flag->experimentalFeature) - flagExperimentalFeatures.insert(*f); - completions->add("--" + name, flag->description); + rootArgs.flagExperimentalFeatures.insert(*f); + rootArgs.completions->add("--" + name, flag->description); } } return false; @@ -183,12 +208,12 @@ bool Args::processFlag(Strings::iterator & pos, Strings::iterator end) return process(std::string("-") + c, *i->second); } - if (auto prefix = needsCompletion(*pos)) { + if (auto prefix = rootArgs.needsCompletion(*pos)) { if (prefix == "-") { - completions->add("--"); + rootArgs.completions->add("--"); for (auto & [flagName, flag] : shortFlags) if (experimentalFeatureSettings.isEnabled(flag->experimentalFeature)) - completions->add(std::string("-") + flagName, flag->description); + rootArgs.completions->add(std::string("-") + flagName, flag->description); } } @@ -203,6 +228,8 @@ bool Args::processArgs(const Strings & args, bool finish) return true; } + auto & rootArgs = getRoot(); + auto & exp = expectedArgs.front(); bool res = false; @@ -211,15 +238,23 @@ bool Args::processArgs(const Strings & args, bool finish) (exp.handler.arity != ArityAny && args.size() == exp.handler.arity)) { std::vector ss; + bool anyCompleted = false; for (const auto &[n, s] : enumerate(args)) { - if (auto prefix = needsCompletion(s)) { + if (auto prefix = rootArgs.needsCompletion(s)) { + anyCompleted = true; ss.push_back(*prefix); - if (exp.completer) - exp.completer(n, *prefix); + if (exp.completer) { + rootArgs.deferredCompletions.push_back({ + .completer = exp.completer, + .n = n, + .prefix = *prefix, + }); + } } else ss.push_back(s); } - exp.handler.fun(ss); + if (!anyCompleted) + exp.handler.fun(ss); expectedArgs.pop_front(); res = true; } @@ -271,11 +306,11 @@ nlohmann::json Args::toJSON() return res; } -static void hashTypeCompleter(size_t index, std::string_view prefix) +static void hashTypeCompleter(AddCompletions & completions, size_t index, std::string_view prefix) { for (auto & type : hashTypes) if (hasPrefix(type, prefix)) - completions->add(type); + completions.add(type); } Args::Flag Args::Flag::mkHashTypeFlag(std::string && longName, HashType * ht) @@ -287,7 +322,7 @@ Args::Flag Args::Flag::mkHashTypeFlag(std::string && longName, HashType * ht) .handler = {[ht](std::string s) { *ht = parseHashType(s); }}, - .completer = hashTypeCompleter + .completer = hashTypeCompleter, }; } @@ -300,13 +335,13 @@ Args::Flag Args::Flag::mkHashTypeOptFlag(std::string && longName, std::optional< .handler = {[oht](std::string s) { *oht = std::optional { parseHashType(s) }; }}, - .completer = hashTypeCompleter + .completer = hashTypeCompleter, }; } -static void _completePath(std::string_view prefix, bool onlyDirs) +static void _completePath(AddCompletions & completions, std::string_view prefix, bool onlyDirs) { - completionType = ctFilenames; + completions.setType(Completions::Type::Filenames); glob_t globbuf; int flags = GLOB_NOESCAPE; #ifdef GLOB_ONLYDIR @@ -320,20 +355,20 @@ static void _completePath(std::string_view prefix, bool onlyDirs) auto st = stat(globbuf.gl_pathv[i]); if (!S_ISDIR(st.st_mode)) continue; } - completions->add(globbuf.gl_pathv[i]); + completions.add(globbuf.gl_pathv[i]); } } globfree(&globbuf); } -void completePath(size_t, std::string_view prefix) +void Args::completePath(AddCompletions & completions, size_t, std::string_view prefix) { - _completePath(prefix, false); + _completePath(completions, prefix, false); } -void completeDir(size_t, std::string_view prefix) +void Args::completeDir(AddCompletions & completions, size_t, std::string_view prefix) { - _completePath(prefix, true); + _completePath(completions, prefix, true); } Strings argvToStrings(int argc, char * * argv) @@ -368,10 +403,10 @@ MultiCommand::MultiCommand(const Commands & commands_) command = {s, i->second()}; command->second->parent = this; }}, - .completer = {[&](size_t, std::string_view prefix) { + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { for (auto & [name, command] : commands) if (hasPrefix(name, prefix)) - completions->add(name); + completions.add(name); }} }); @@ -393,14 +428,6 @@ bool MultiCommand::processArgs(const Strings & args, bool finish) return Args::processArgs(args, finish); } -void MultiCommand::completionHook() -{ - if (command) - return command->second->completionHook(); - else - return Args::completionHook(); -} - nlohmann::json MultiCommand::toJSON() { auto cmds = nlohmann::json::object(); diff --git a/src/libutil/args.hh b/src/libutil/args.hh index 6457cceed..a5d7cbe4a 100644 --- a/src/libutil/args.hh +++ b/src/libutil/args.hh @@ -15,16 +15,14 @@ enum HashType : char; class MultiCommand; +class RootArgs; + +class AddCompletions; + class Args { public: - /** - * Parse the command line, throwing a UsageError if something goes - * wrong. - */ - void parseCmdline(const Strings & cmdline); - /** * Return a short one-line description of the command. */ @@ -123,6 +121,25 @@ protected: { } }; + /** + * The basic function type of the completion callback. + * + * Used to define `CompleterClosure` and some common case completers + * that individual flags/arguments can use. + * + * The `AddCompletions` that is passed is an interface to the state + * stored as part of the root command + */ + typedef void CompleterFun(AddCompletions &, size_t, std::string_view); + + /** + * The closure type of the completion callback. + * + * This is what is actually stored as part of each Flag / Expected + * Arg. + */ + typedef std::function CompleterClosure; + /** * Description of flags / options * @@ -140,7 +157,7 @@ protected: std::string category; Strings labels; Handler handler; - std::function completer; + CompleterClosure completer; std::optional experimentalFeature; @@ -177,7 +194,7 @@ protected: std::string label; bool optional = false; Handler handler; - std::function completer; + CompleterClosure completer; }; /** @@ -211,13 +228,6 @@ protected: */ virtual void initialFlagsProcessed() {} - /** - * Called after the command line has been processed if we need to generate - * completions. Useful for commands that need to know the whole command line - * in order to know what completions to generate. - */ - virtual void completionHook() { } - public: void addFlag(Flag && flag); @@ -252,24 +262,30 @@ public: }); } + static CompleterFun completePath; + + static CompleterFun completeDir; + virtual nlohmann::json toJSON(); friend class MultiCommand; /** * The parent command, used if this is a subcommand. + * + * Invariant: An Args with a null parent must also be a RootArgs + * + * \todo this would probably be better in the CommandClass. + * getRoot() could be an abstract method that peels off at most one + * layer before recuring. */ MultiCommand * parent = nullptr; -private: - /** - * Experimental features needed when parsing args. These are checked - * after flag parsing is completed in order to support enabling - * experimental features coming after the flag that needs the - * experimental feature. + * Traverse parent pointers until we find the \ref RootArgs "root + * arguments" object. */ - std::set flagExperimentalFeatures; + RootArgs & getRoot(); }; /** @@ -320,8 +336,6 @@ public: bool processArgs(const Strings & args, bool finish) override; - void completionHook() override; - nlohmann::json toJSON() override; }; @@ -333,21 +347,40 @@ struct Completion { bool operator<(const Completion & other) const; }; -class Completions : public std::set { + +/** + * The abstract interface for completions callbacks + * + * The idea is to restrict the callback so it can only add additional + * completions to the collection, or set the completion type. By making + * it go through this interface, the callback cannot make any other + * changes, or even view the completions / completion type that have + * been set so far. + */ +class AddCompletions +{ public: - void add(std::string completion, std::string description = ""); + + /** + * The type of completion we are collecting. + */ + enum class Type { + Normal, + Filenames, + Attrs, + }; + + /** + * Set the type of the completions being collected + * + * \todo it should not be possible to change the type after it has been set. + */ + virtual void setType(Type type) = 0; + + /** + * Add a single completion to the collection + */ + virtual void add(std::string completion, std::string description = "") = 0; }; -extern std::shared_ptr completions; - -enum CompletionType { - ctNormal, - ctFilenames, - ctAttrs -}; -extern CompletionType completionType; - -void completePath(size_t, std::string_view prefix); - -void completeDir(size_t, std::string_view prefix); } diff --git a/src/libutil/args/root.hh b/src/libutil/args/root.hh new file mode 100644 index 000000000..bb98732a1 --- /dev/null +++ b/src/libutil/args/root.hh @@ -0,0 +1,72 @@ +#pragma once + +#include "args.hh" + +namespace nix { + +/** + * The concrete implementation of a collection of completions. + * + * This is exposed so that the main entry point can print out the + * collected completions. + */ +struct Completions final : AddCompletions +{ + std::set completions; + Type type = Type::Normal; + + void setType(Type type) override; + void add(std::string completion, std::string description = "") override; +}; + +/** + * The outermost Args object. This is the one we will actually parse a command + * line with, whereas the inner ones (if they exists) are subcommands (and this + * is also a MultiCommand or something like it). + * + * This Args contains completions state shared between it and all of its + * descendent Args. + */ +class RootArgs : virtual public Args +{ +public: + /** Parse the command line, throwing a UsageError if something goes + * wrong. + */ + void parseCmdline(const Strings & cmdline); + + std::shared_ptr completions; + +protected: + + friend class Args; + + /** + * A pointer to the completion and its two arguments; a thunk; + */ + struct DeferredCompletion { + const CompleterClosure & completer; + size_t n; + std::string prefix; + }; + + /** + * Completions are run after all args and flags are parsed, so completions + * of earlier arguments can benefit from later arguments. + */ + std::vector deferredCompletions; + + /** + * Experimental features needed when parsing args. These are checked + * after flag parsing is completed in order to support enabling + * experimental features coming after the flag that needs the + * experimental feature. + */ + std::set flagExperimentalFeatures; + +private: + + std::optional needsCompletion(std::string_view s); +}; + +} diff --git a/src/libutil/local.mk b/src/libutil/local.mk index f880c0fc5..81efaafec 100644 --- a/src/libutil/local.mk +++ b/src/libutil/local.mk @@ -6,8 +6,13 @@ libutil_DIR := $(d) libutil_SOURCES := $(wildcard $(d)/*.cc) +libutil_CXXFLAGS += -I src/libutil + libutil_LDFLAGS += -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context +$(foreach i, $(wildcard $(d)/args/*.hh), \ + $(eval $(call install-file-in, $(i), $(includedir)/nix/args, 0644))) + ifeq ($(HAVE_LIBCPUID), 1) libutil_LDFLAGS += -lcpuid endif diff --git a/src/nix/bundle.cc b/src/nix/bundle.cc index fbc83b08e..504e35c81 100644 --- a/src/nix/bundle.cc +++ b/src/nix/bundle.cc @@ -21,8 +21,8 @@ struct CmdBundle : InstallableValueCommand .description = fmt("Use a custom bundler instead of the default (`%s`).", bundler), .labels = {"flake-url"}, .handler = {&bundler}, - .completer = {[&](size_t, std::string_view prefix) { - completeFlakeRef(getStore(), prefix); + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeRef(completions, getStore(), prefix); }} }); diff --git a/src/nix/flake.cc b/src/nix/flake.cc index b4e4156c0..0116eff2e 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -36,8 +36,8 @@ public: .label = "flake-url", .optional = true, .handler = {&flakeUrl}, - .completer = {[&](size_t, std::string_view prefix) { - completeFlakeRef(getStore(), prefix); + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeRef(completions, getStore(), prefix); }} }); } @@ -52,9 +52,12 @@ public: return flake::lockFlake(*getEvalState(), getFlakeRef(), lockFlags); } - std::vector getFlakesForCompletion() override + std::vector getFlakeRefsForCompletion() override { - return {flakeUrl}; + return { + // Like getFlakeRef but with expandTilde calld first + parseFlakeRef(expandTilde(flakeUrl), absPath(".")) + }; } }; @@ -762,8 +765,9 @@ struct CmdFlakeInitCommon : virtual Args, EvalCommand .description = "The template to use.", .labels = {"template"}, .handler = {&templateUrl}, - .completer = {[&](size_t, std::string_view prefix) { + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { completeFlakeRefWithFragment( + completions, getEvalState(), lockFlags, defaultTemplateAttrPathsPrefixes, diff --git a/src/nix/main.cc b/src/nix/main.cc index ee3878cc7..ffba10099 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -1,5 +1,6 @@ #include +#include "args/root.hh" #include "command.hh" #include "common-args.hh" #include "eval.hh" @@ -56,7 +57,7 @@ static bool haveInternet() std::string programPath; -struct NixArgs : virtual MultiCommand, virtual MixCommonArgs +struct NixArgs : virtual MultiCommand, virtual MixCommonArgs, virtual RootArgs { bool useNet = true; bool refresh = false; @@ -241,10 +242,7 @@ static void showHelp(std::vector subcommand, NixArgs & toplevel) static NixArgs & getNixArgs(Command & cmd) { - assert(cmd.parent); - MultiCommand * toplevel = cmd.parent; - while (toplevel->parent) toplevel = toplevel->parent; - return dynamic_cast(*toplevel); + return dynamic_cast(cmd.getRoot()); } struct CmdHelp : Command @@ -412,16 +410,16 @@ void mainWrapped(int argc, char * * argv) Finally printCompletions([&]() { - if (completions) { - switch (completionType) { - case ctNormal: + if (args.completions) { + switch (args.completions->type) { + case Completions::Type::Normal: logger->cout("normal"); break; - case ctFilenames: + case Completions::Type::Filenames: logger->cout("filenames"); break; - case ctAttrs: + case Completions::Type::Attrs: logger->cout("attrs"); break; } - for (auto & s : *completions) + for (auto & s : args.completions->completions) logger->cout(s.completion + "\t" + trim(s.description)); } }); @@ -429,7 +427,7 @@ void mainWrapped(int argc, char * * argv) try { args.parseCmdline(argvToStrings(argc, argv)); } catch (UsageError &) { - if (!args.helpRequested && !completions) throw; + if (!args.helpRequested && !args.completions) throw; } if (args.helpRequested) { @@ -446,10 +444,7 @@ void mainWrapped(int argc, char * * argv) return; } - if (completions) { - args.completionHook(); - return; - } + if (args.completions) return; if (args.showVersion) { printVersion(programName); diff --git a/src/nix/registry.cc b/src/nix/registry.cc index cb94bbd31..f509ccae8 100644 --- a/src/nix/registry.cc +++ b/src/nix/registry.cc @@ -175,8 +175,8 @@ struct CmdRegistryPin : RegistryCommand, EvalCommand .label = "locked", .optional = true, .handler = {&locked}, - .completer = {[&](size_t, std::string_view prefix) { - completeFlakeRef(getStore(), prefix); + .completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) { + completeFlakeRef(completions, getStore(), prefix); }} }); } diff --git a/src/nix/why-depends.cc b/src/nix/why-depends.cc index 592de773c..055cf6d0d 100644 --- a/src/nix/why-depends.cc +++ b/src/nix/why-depends.cc @@ -38,17 +38,13 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions expectArgs({ .label = "package", .handler = {&_package}, - .completer = {[&](size_t, std::string_view prefix) { - completeInstallable(prefix); - }} + .completer = getCompleteInstallable(), }); expectArgs({ .label = "dependency", .handler = {&_dependency}, - .completer = {[&](size_t, std::string_view prefix) { - completeInstallable(prefix); - }} + .completer = getCompleteInstallable(), }); addFlag({ From fa9642ec459859557ce29e5f68413cf3b0846fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Wed, 7 Jun 2023 18:12:35 +0200 Subject: [PATCH 297/299] nix-shell: support single quotes in shebangs Single quotes are a basic feature of shell syntax that people expect to work. They are also more convenient for writing literal code expressions with less escaping. --- doc/manual/src/command-ref/nix-shell.md | 6 +++--- doc/manual/src/release-notes/rl-next.md | 2 ++ src/nix-build/nix-build.cc | 26 ++++++++++++++++++------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/doc/manual/src/command-ref/nix-shell.md b/doc/manual/src/command-ref/nix-shell.md index 195b72be5..1eaf3c36a 100644 --- a/doc/manual/src/command-ref/nix-shell.md +++ b/doc/manual/src/command-ref/nix-shell.md @@ -235,14 +235,14 @@ package like Terraform: ```bash #! /usr/bin/env nix-shell -#! nix-shell -i bash --packages "terraform.withPlugins (plugins: [ plugins.openstack ])" +#! nix-shell -i bash --packages 'terraform.withPlugins (plugins: [ plugins.openstack ])' terraform apply ``` > **Note** > -> You must use double quotes (`"`) when passing a simple Nix expression +> You must use single or double quotes (`'`, `"`) when passing a simple Nix expression > in a nix-shell shebang. Finally, using the merging of multiple nix-shell shebangs the following @@ -251,7 +251,7 @@ branch): ```haskell #! /usr/bin/env nix-shell -#! nix-shell -i runghc --packages "haskellPackages.ghcWithPackages (ps: [ps.download-curl ps.tagsoup])" +#! nix-shell -i runghc --packages 'haskellPackages.ghcWithPackages (ps: [ps.download-curl ps.tagsoup])' #! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-20.03.tar.gz import Network.Curl.Download diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md index d0b516dfb..276252c37 100644 --- a/doc/manual/src/release-notes/rl-next.md +++ b/doc/manual/src/release-notes/rl-next.md @@ -12,4 +12,6 @@ - Introduce a new built-in function [`builtins.convertHash`](@docroot@/language/builtins.md#builtins-convertHash). +- `nix-shell` shebang lines now support single-quoted arguments. + - `builtins.fetchTree` is now marked as stable. diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index 2895c5e3c..ae05444b7 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -40,7 +40,8 @@ static std::vector shellwords(const std::string & s) std::string cur; enum state { sBegin, - sQuote + sSingleQuote, + sDoubleQuote }; state st = sBegin; auto it = begin; @@ -56,15 +57,26 @@ static std::vector shellwords(const std::string & s) } } switch (*it) { + case '\'': + if (st != sDoubleQuote) { + cur.append(begin, it); + begin = it + 1; + st = st == sBegin ? sSingleQuote : sBegin; + } + break; case '"': - cur.append(begin, it); - begin = it + 1; - st = st == sBegin ? sQuote : sBegin; + if (st != sSingleQuote) { + cur.append(begin, it); + begin = it + 1; + st = st == sBegin ? sDoubleQuote : sBegin; + } break; case '\\': - /* perl shellwords mostly just treats the next char as part of the string with no special processing */ - cur.append(begin, it); - begin = ++it; + if (st != sSingleQuote) { + /* perl shellwords mostly just treats the next char as part of the string with no special processing */ + cur.append(begin, it); + begin = ++it; + } break; } } From 595010b631b0f92ee6eb4afb8494bc9c73e8513b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Thu, 8 Jun 2023 03:11:04 +0200 Subject: [PATCH 298/299] nix-shell: fix shebang whitespace parsing Leading whitespace after `nix-shell` used to produce an empty argument, while an empty argument at the end of the line was ignored. Fix the first issue by consuming the initial whitespace before calling shellwords; fix the second issue by returning immediately if whitespace is found at the end of the string instead of checking for an empty string. Also throw if quotes aren't terminated. --- src/nix-build/nix-build.cc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/nix-build/nix-build.cc b/src/nix-build/nix-build.cc index ae05444b7..e62c4f6b1 100644 --- a/src/nix-build/nix-build.cc +++ b/src/nix-build/nix-build.cc @@ -34,7 +34,7 @@ extern char * * environ __attribute__((weak)); */ static std::vector shellwords(const std::string & s) { - std::regex whitespace("^(\\s+).*"); + std::regex whitespace("^\\s+"); auto begin = s.cbegin(); std::vector res; std::string cur; @@ -51,9 +51,10 @@ static std::vector shellwords(const std::string & s) if (regex_search(it, s.cend(), match, whitespace)) { cur.append(begin, it); res.push_back(cur); - cur.clear(); - it = match[1].second; + it = match[0].second; + if (it == s.cend()) return res; begin = it; + cur.clear(); } } switch (*it) { @@ -80,8 +81,9 @@ static std::vector shellwords(const std::string & s) break; } } + if (st != sBegin) throw Error("unterminated quote in shebang line"); cur.append(begin, it); - if (!cur.empty()) res.push_back(cur); + res.push_back(cur); return res; } @@ -140,7 +142,7 @@ static void main_nix_build(int argc, char * * argv) for (auto line : lines) { line = chomp(line); std::smatch match; - if (std::regex_match(line, match, std::regex("^#!\\s*nix-shell (.*)$"))) + if (std::regex_match(line, match, std::regex("^#!\\s*nix-shell\\s+(.*)$"))) for (const auto & word : shellwords(match[1].str())) args.push_back(word); } From e053eeb272d2a9115afe7f42077002a3de6b8633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Sat, 17 Jun 2023 14:54:34 +0200 Subject: [PATCH 299/299] tests: test nix-shell shebang quoting --- tests/functional/nix-shell.sh | 5 +++++ tests/functional/shell.shebang.nix | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100755 tests/functional/shell.shebang.nix diff --git a/tests/functional/nix-shell.sh b/tests/functional/nix-shell.sh index edaa1249b..13403fadb 100644 --- a/tests/functional/nix-shell.sh +++ b/tests/functional/nix-shell.sh @@ -84,6 +84,11 @@ chmod a+rx $TEST_ROOT/spaced\ \\\'\"shell.shebang.rb output=$($TEST_ROOT/spaced\ \\\'\"shell.shebang.rb abc ruby) [ "$output" = '-e load(ARGV.shift) -- '"$TEST_ROOT"'/spaced \'\''"shell.shebang.rb abc ruby' ] +# Test nix-shell shebang quoting +sed -e "s|@ENV_PROG@|$(type -P env)|" shell.shebang.nix > $TEST_ROOT/shell.shebang.nix +chmod a+rx $TEST_ROOT/shell.shebang.nix +$TEST_ROOT/shell.shebang.nix + # Test 'nix develop'. nix develop -f "$shellDotNix" shellDrv -c bash -c '[[ -n $stdenv ]]' diff --git a/tests/functional/shell.shebang.nix b/tests/functional/shell.shebang.nix new file mode 100755 index 000000000..08e43d53c --- /dev/null +++ b/tests/functional/shell.shebang.nix @@ -0,0 +1,10 @@ +#! @ENV_PROG@ nix-shell +#! nix-shell -I nixpkgs=shell.nix --no-substitute +#! nix-shell --argstr s1 'foo "bar" \baz'"'"'qux' --argstr s2 "foo 'bar' \"\baz" --argstr s3 \foo\ bar\'baz --argstr s4 '' +#! nix-shell shell.shebang.nix --command true +{ s1, s2, s3, s4 }: +assert s1 == ''foo "bar" \baz'qux''; +assert s2 == "foo 'bar' \"baz"; +assert s3 == "foo bar'baz"; +assert s4 == ""; +(import {}).runCommand "nix-shell" {} ""