diff options
-rw-r--r-- | doc/manual/generate-manpage.nix | 22 | ||||
-rw-r--r-- | doc/manual/local.mk | 2 | ||||
-rw-r--r-- | doc/manual/src/contributing/hacking.md | 73 | ||||
-rw-r--r-- | doc/manual/src/glossary.md | 52 | ||||
-rw-r--r-- | src/libcmd/command.cc | 3 | ||||
-rw-r--r-- | src/libcmd/common-eval-args.cc | 2 | ||||
-rw-r--r-- | src/libcmd/common-eval-args.hh | 2 | ||||
-rw-r--r-- | src/libcmd/repl.cc | 2 | ||||
-rw-r--r-- | src/libexpr/primops.cc | 4 | ||||
-rw-r--r-- | src/libmain/common-args.cc | 1 | ||||
-rw-r--r-- | src/libmain/common-args.hh | 1 | ||||
-rw-r--r-- | src/libmain/progress-bar.cc | 2 | ||||
-rw-r--r-- | src/libmain/shared.hh | 20 | ||||
-rw-r--r-- | src/libmain/stack.cc | 12 | ||||
-rw-r--r-- | src/libstore/build/local-derivation-goal.cc | 15 | ||||
-rw-r--r-- | src/libstore/globals.cc | 10 | ||||
-rw-r--r-- | src/libstore/globals.hh | 8 | ||||
-rw-r--r-- | src/libstore/store-api.cc | 4 | ||||
-rw-r--r-- | src/nix/main.cc | 6 | ||||
-rw-r--r-- | tests/lang/eval-okay-versions.nix | 3 |
20 files changed, 207 insertions, 37 deletions
diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix index 18a1a8bfe..057719e34 100644 --- a/doc/manual/generate-manpage.nix +++ b/doc/manual/generate-manpage.nix @@ -1,11 +1,11 @@ -{ command }: +{ toplevel }: with builtins; with import ./utils.nix; let - showCommand = { command, details, filename }: + showCommand = { command, details, filename, toplevel }: let result = '' > **Warning** \ @@ -57,14 +57,15 @@ let maybeOptions = if details.flags == {} then "" else '' # Options - ${showOptions details.flags} + ${showOptions details.flags toplevel.flags} ''; - showOptions = options: + showOptions = options: commonOptions: let + allOptions = options // commonOptions; showCategory = cat: '' ${if cat != "" then "**${cat}:**" else ""} - ${listOptions (filterAttrs (n: v: v.category == cat) options)} + ${listOptions (filterAttrs (n: v: v.category == cat) allOptions)} ''; listOptions = opts: concatStringsSep "\n" (attrValues (mapAttrs showOption opts)); showOption = name: option: @@ -76,30 +77,33 @@ let ${option.description} ''; - categories = sort builtins.lessThan (unique (map (cmd: cmd.category) (attrValues options))); + categories = sort builtins.lessThan (unique (map (cmd: cmd.category) (attrValues allOptions))); in concatStrings (map showCategory categories); in squash result; appendName = filename: name: (if filename == "nix" then "nix3" else filename) + "-" + name; - processCommand = { command, details, filename }: + processCommand = { command, details, filename, toplevel }: let cmd = { inherit command; name = filename + ".md"; - value = showCommand { inherit command details filename; }; + value = showCommand { inherit command details filename toplevel; }; }; subcommand = subCmd: processCommand { command = command + " " + subCmd; details = details.commands.${subCmd}; filename = appendName filename subCmd; + inherit toplevel; }; in [ cmd ] ++ concatMap subcommand (attrNames details.commands or {}); + parsedToplevel = builtins.fromJSON toplevel; manpages = processCommand { command = "nix"; - details = builtins.fromJSON command; + details = parsedToplevel; filename = "nix"; + toplevel = parsedToplevel; }; tableOfContents = let diff --git a/doc/manual/local.mk b/doc/manual/local.mk index 364e02967..486dbd7a2 100644 --- a/doc/manual/local.mk +++ b/doc/manual/local.mk @@ -50,7 +50,7 @@ $(d)/src/SUMMARY.md: $(d)/src/SUMMARY.md.in $(d)/src/command-ref/new-cli $(d)/src/command-ref/new-cli: $(d)/nix.json $(d)/generate-manpage.nix $(bindir)/nix @rm -rf $@ - $(trace-gen) $(nix-eval) --write-to $@ --expr 'import doc/manual/generate-manpage.nix { command = builtins.readFile $<; }' + $(trace-gen) $(nix-eval) --write-to $@ --expr 'import doc/manual/generate-manpage.nix { toplevel = builtins.readFile $<; }' $(d)/src/command-ref/conf-file.md: $(d)/conf-file.json $(d)/generate-options.nix $(d)/src/command-ref/conf-file-prefix.md $(bindir)/nix @cat doc/manual/src/command-ref/conf-file-prefix.md > $@.tmp diff --git a/doc/manual/src/contributing/hacking.md b/doc/manual/src/contributing/hacking.md index e97b40cdd..9f7d5057b 100644 --- a/doc/manual/src/contributing/hacking.md +++ b/doc/manual/src/contributing/hacking.md @@ -86,9 +86,7 @@ by: $ nix develop ``` -## Testing - -Nix comes with three different flavors of tests: unit, functional and integration. +## Running tests ### Unit-tests @@ -111,3 +109,72 @@ These tests include everything that needs to interact with external services or Because these tests are expensive and require more than what the standard github-actions setup provides, they only run on the master branch (on <https://hydra.nixos.org/jobset/nix/master>). You can run them manually with `nix build .#hydraJobs.tests.{testName}` or `nix-build -A hydraJobs.tests.{testName}` + +### Installer tests + +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): + +- The `installer` job generates installers for the platforms below and uploads them to your Cachix cache: + - `x86_64-linux` + - `armv6l-linux` + - `armv7l-linux` + - `x86_64-darwin` + +- The `installer_test` job (which runs on `ubuntu-latest` and `macos-latest`) will try to install Nix with the cached installer and run a trivial Nix command. + +#### One-time setup + +1. Have a GitHub account with a fork of the [Nix repository](https://github.com/NixOS/nix). +2. At cachix.org: + - Create or log in to an account. + - Create a Cachix cache using the format `<github-username>-nix-install-tests`. + - Navigate to the new cache > Settings > Auth Tokens. + - Generate a new Cachix auth token and copy the generated value. +3. At github.com: + - Navigate to your Nix fork > Settings > Secrets > Actions > New repository secret. + - Name the secret `CACHIX_AUTH_TOKEN`. + - Paste the copied value of the Cachix cache auth token. + +#### Using the CI-generated installer for manual testing + +After the CI run completes, you can check the output to extract the installer URL: +1. Click into the detailed view of the CI run. +2. Click into any `installer_test` run (the URL you're here to extract will be the same in all of them). +3. Click into the `Run cachix/install-nix-action@v...` step and click the detail triangle next to the first log line (it will also be `Run cachix/install-nix-action@v...`) +4. Copy the value of `install_url` +5. To generate an install command, plug this `install_url` and your GitHub username into this template: + + ```console + sh <(curl -L <install_url>) --tarball-url-prefix https://<github-username>-nix-install-tests.cachix.org/serve + ``` + +<!-- #### Manually generating test installers + +There's obviously a manual way to do this, and it's still the only way for +platforms that lack GA runners. + +I did do this back in Fall 2020 (before the GA approach encouraged here). I'll +sketch what I recall in case it encourages someone to fill in detail, but: I +didn't know what I was doing at the time and had to fumble/ask around a lot-- +so I don't want to uphold any of it as "right". It may have been dumb or +the _hard_ way from the getgo. Fundamentals may have changed since. + +Here's the build command I used to do this on and for x86_64-darwin: +nix build --out-link /tmp/foo ".#checks.x86_64-darwin.binaryTarball" + +I used the stable out-link to make it easier to script the next steps: +link=$(readlink /tmp/foo) +cp $link/*-darwin.tar.xz ~/somewheres + +I've lost the last steps and am just going from memory: + +From here, I think I had to extract and modify the `install` script to point +it at this tarball (which I scped to my own site, but it might make more sense +to just share them locally). I extracted this script once and then just +search/replaced in it for each new build. + +The installer now supports a `--tarball-url-prefix` flag which _may_ have +solved this need? +--> diff --git a/doc/manual/src/glossary.md b/doc/manual/src/glossary.md index aa0ac78cb..70a0eb994 100644 --- a/doc/manual/src/glossary.md +++ b/doc/manual/src/glossary.md @@ -7,10 +7,44 @@ translated into low-level *store derivations* (implicitly by `nix-env` and `nix-build`, or explicitly by `nix-instantiate`). + - [content-addressed derivation]{#gloss-content-addressed-derivation}\ + A derivation which has the + [`__contentAddressed`](language/advanced-attributes.md#adv-attr-__contentAddressed) + attribute set to `true`. + + - [fixed-output derivation]{#gloss-fixed-output-derivation}\ + A derivation which includes the + [`outputHash`](language/advanced-attributes.md#adv-attr-outputHash) attribute. + - [store]{#gloss-store}\ The location in the file system where store objects live. Typically `/nix/store`. + From the perspective of the location where Nix is + invoked, the Nix store can be referred to + as a "_local_" or a "_remote_" one: + + + A *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. + + + 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. + + - [chroot store]{#gloss-chroot-store}\ + A local store whose canonical path is anything other than `/nix/store`. + + - [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 path]{#gloss-store-path}\ The location in the file system of a store object, i.e., an immediate child of the Nix store directory. @@ -22,6 +56,19 @@ derivation outputs (objects produced by running a build action), or derivations (files describing a build action). + - [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. + + - [output-addressed store object]{#gloss-output-addressed-store-object}\ + A store object whose store path hashes its content. This + includes derivations, the outputs of + [content-addressed derivations](#gloss-content-addressed-derivation), + and the outputs of + [fixed-output derivations](#gloss-fixed-output-derivation). + - [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 @@ -29,6 +76,11 @@ store object by downloading a pre-built version of the store object from some server. + - [substituter]{#gloss-substituter}\ + A *substituter* is an additional store from which Nix will + copy store objects it doesn't have. For details, see the + [`substituters` option](command-ref/conf-file.html#conf-substituters). + - [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 diff --git a/src/libcmd/command.cc b/src/libcmd/command.cc index 14bb27936..1fdd9e0bd 100644 --- a/src/libcmd/command.cc +++ b/src/libcmd/command.cc @@ -88,7 +88,8 @@ EvalCommand::EvalCommand() { addFlag({ .longName = "debugger", - .description = "start an interactive environment if evaluation fails", + .description = "Start an interactive environment if evaluation fails.", + .category = MixEvalArgs::category, .handler = {&startReplOnEvalErrors, true}, }); } diff --git a/src/libcmd/common-eval-args.cc b/src/libcmd/common-eval-args.cc index 5b6e82388..140ed3b88 100644 --- a/src/libcmd/common-eval-args.cc +++ b/src/libcmd/common-eval-args.cc @@ -13,8 +13,6 @@ namespace nix { MixEvalArgs::MixEvalArgs() { - auto category = "Common evaluation options"; - addFlag({ .longName = "arg", .description = "Pass the value *expr* as the argument *name* to Nix functions.", diff --git a/src/libcmd/common-eval-args.hh b/src/libcmd/common-eval-args.hh index 03fa226aa..1ec800613 100644 --- a/src/libcmd/common-eval-args.hh +++ b/src/libcmd/common-eval-args.hh @@ -10,6 +10,8 @@ class Bindings; struct MixEvalArgs : virtual Args { + static constexpr auto category = "Common evaluation options"; + MixEvalArgs(); Bindings * getAutoArgs(EvalState & state); diff --git a/src/libcmd/repl.cc b/src/libcmd/repl.cc index 61c05050f..df8932087 100644 --- a/src/libcmd/repl.cc +++ b/src/libcmd/repl.cc @@ -1050,7 +1050,7 @@ struct CmdRepl : InstallablesCommand evalSettings.pureEval = false; } - void prepare() + void prepare() override { if (!settings.isExperimentalFeatureEnabled(Xp::ReplFlake) && !(file) && this->_installables.size() >= 1) { warn("future versions of Nix will require using `--file` to load a file"); diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 28b998474..840bfecef 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -3821,8 +3821,8 @@ static RegisterPrimOp primop_parseDrvName({ .args = {"s"}, .doc = R"( Split the string *s* into a package name and version. The package - name is everything up to but not including the first dash followed - by a digit, and the version is everything following that dash. The + name is everything up to but not including the first dash not followed + by a letter, and the version is everything following that dash. The result is returned in a set `{ name, version }`. Thus, `builtins.parseDrvName "nix-0.12pre12876"` returns `{ name = "nix"; version = "0.12pre12876"; }`. diff --git a/src/libmain/common-args.cc b/src/libmain/common-args.cc index 12f5403ea..f92920d18 100644 --- a/src/libmain/common-args.cc +++ b/src/libmain/common-args.cc @@ -32,6 +32,7 @@ MixCommonArgs::MixCommonArgs(const std::string & programName) addFlag({ .longName = "option", .description = "Set the Nix configuration setting *name* to *value* (overriding `nix.conf`).", + .category = miscCategory, .labels = {"name", "value"}, .handler = {[](std::string name, std::string value) { try { diff --git a/src/libmain/common-args.hh b/src/libmain/common-args.hh index 25453b8c6..f180d83ce 100644 --- a/src/libmain/common-args.hh +++ b/src/libmain/common-args.hh @@ -6,6 +6,7 @@ namespace nix { //static constexpr auto commonArgsCategory = "Miscellaneous common options"; static constexpr auto loggingCategory = "Logging-related options"; +static constexpr auto miscCategory = "Miscellaneous global options"; class MixCommonArgs : public virtual Args { diff --git a/src/libmain/progress-bar.cc b/src/libmain/progress-bar.cc index 0bbeaff8d..961f4e18a 100644 --- a/src/libmain/progress-bar.cc +++ b/src/libmain/progress-bar.cc @@ -503,7 +503,7 @@ public: return s[0]; } - virtual void setPrintBuildLogs(bool printBuildLogs) + void setPrintBuildLogs(bool printBuildLogs) override { this->printBuildLogs = printBuildLogs; } diff --git a/src/libmain/shared.hh b/src/libmain/shared.hh index 0cc56d47d..3c37fd627 100644 --- a/src/libmain/shared.hh +++ b/src/libmain/shared.hh @@ -113,5 +113,25 @@ struct PrintFreed /* Install a SIGSEGV handler to detect stack overflows. */ void detectStackOverflow(); +/* Pluggable behavior to run in case of a stack overflow. + + Default value: defaultStackOverflowHandler. + + This is called by the handler installed by detectStackOverflow(). + + This gives Nix library consumers a limit opportunity to report the error + condition. The handler should exit the process. + See defaultStackOverflowHandler() for a reference implementation. + + NOTE: Use with diligence, because this runs in the signal handler, with very + limited stack space and a potentially a corrupted heap, all while the failed + thread is blocked indefinitely. All functions called must be reentrant. */ +extern std::function<void(siginfo_t * info, void * ctx)> stackOverflowHandler; + +/* The default, robust implementation of stackOverflowHandler. + + Prints an error message directly to stderr using a syscall instead of the + logger. Exits the process immediately after. */ +void defaultStackOverflowHandler(siginfo_t * info, void * ctx); } diff --git a/src/libmain/stack.cc b/src/libmain/stack.cc index b0a4a4c5d..10f71c1dc 100644 --- a/src/libmain/stack.cc +++ b/src/libmain/stack.cc @@ -1,4 +1,5 @@ #include "error.hh" +#include "shared.hh" #include <cstring> #include <cstddef> @@ -29,9 +30,7 @@ static void sigsegvHandler(int signo, siginfo_t * info, void * ctx) ptrdiff_t diff = (char *) info->si_addr - sp; if (diff < 0) diff = -diff; if (diff < 4096) { - char msg[] = "error: stack overflow (possible infinite recursion)\n"; - [[gnu::unused]] auto res = write(2, msg, strlen(msg)); - _exit(1); // maybe abort instead? + nix::stackOverflowHandler(info, ctx); } } @@ -67,5 +66,12 @@ void detectStackOverflow() #endif } +std::function<void(siginfo_t * info, void * ctx)> stackOverflowHandler(defaultStackOverflowHandler); + +void defaultStackOverflowHandler(siginfo_t * info, void * ctx) { + char msg[] = "error: stack overflow (possible infinite recursion)\n"; + [[gnu::unused]] auto res = write(2, msg, strlen(msg)); + _exit(1); // maybe abort instead? +} } diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 18b682e13..5cea3b590 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -1594,6 +1594,8 @@ void LocalDerivationGoal::runChild() /* Warning: in the child we should absolutely not make any SQLite calls! */ + bool sendException = true; + try { /* child */ commonChildInit(builderOut); @@ -2050,6 +2052,8 @@ void LocalDerivationGoal::runChild() /* Indicate that we managed to set up the build environment. */ writeFull(STDERR_FILENO, std::string("\2\n")); + sendException = false; + /* Execute the program. This should not return. */ if (drv->isBuiltin()) { try { @@ -2103,10 +2107,13 @@ void LocalDerivationGoal::runChild() throw SysError("executing '%1%'", drv->builder); } catch (Error & e) { - writeFull(STDERR_FILENO, "\1\n"); - FdSink sink(STDERR_FILENO); - sink << e; - sink.flush(); + if (sendException) { + writeFull(STDERR_FILENO, "\1\n"); + FdSink sink(STDERR_FILENO); + sink << e; + sink.flush(); + } else + std::cerr << e.msg(); _exit(1); } } diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index d724897bb..ff658c428 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -154,13 +154,9 @@ StringSet Settings::getDefaultExtraPlatforms() // machines. Note that we can’t force processes from executing // x86_64 in aarch64 environments or vice versa since they can // always exec with their own binary preferences. - if (pathExists("/Library/Apple/System/Library/LaunchDaemons/com.apple.oahd.plist") || - pathExists("/System/Library/LaunchDaemons/com.apple.oahd.plist")) { - if (std::string{SYSTEM} == "x86_64-darwin") - extraPlatforms.insert("aarch64-darwin"); - else if (std::string{SYSTEM} == "aarch64-darwin") - extraPlatforms.insert("x86_64-darwin"); - } + if (std::string{SYSTEM} == "aarch64-darwin" && + runProgram(RunOptions {.program = "arch", .args = {"-arch", "x86_64", "/usr/bin/true"}, .mergeStderrToStdout = true}).first == 0) + extraPlatforms.insert("x86_64-darwin"); #endif return extraPlatforms; diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 75927d395..3dcf3d479 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -619,6 +619,14 @@ public: are tried based on their Priority value, which each substituter can set independently. Lower value means higher priority. The default is `https://cache.nixos.org`, with a Priority of 40. + + Nix will copy a store path from a remote store only if one + of the following is true: + + - the store object is signed by one of the [`trusted-public-keys`](#conf-trusted-public-keys) + - the substituter is in the [`trusted-substituters`](#conf-trusted-substituters) list + - the [`require-sigs`](#conf-require-sigs) option has been set to `false` + - the store object is [output-addressed](glossary.md#gloss-output-addressed-store-object) )", {"binary-caches"}}; diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 86b12257a..06a9758fc 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -1363,9 +1363,9 @@ std::shared_ptr<Store> openFromNonUri(const std::string & uri, const Store::Para } catch (Error & e) { return std::make_shared<LocalStore>(params); } - warn("'/nix' does not exist, so Nix will use '%s' as a chroot store", chrootStore); + warn("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore); } else - debug("'/nix' does not exist, so Nix will use '%s' as a chroot store", chrootStore); + debug("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore); Store::Params params2; params2["root"] = chrootStore; return std::make_shared<LocalStore>(params2); diff --git a/src/nix/main.cc b/src/nix/main.cc index e0155cd5d..d78312944 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -74,6 +74,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs addFlag({ .longName = "help", .description = "Show usage information.", + .category = miscCategory, .handler = {[&]() { throw HelpRequested(); }}, }); @@ -88,6 +89,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs addFlag({ .longName = "version", .description = "Show version information.", + .category = miscCategory, .handler = {[&]() { showVersion = true; }}, }); @@ -95,12 +97,14 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs .longName = "offline", .aliases = {"no-net"}, // FIXME: remove .description = "Disable substituters and consider all previously downloaded files up-to-date.", + .category = miscCategory, .handler = {[&]() { useNet = false; }}, }); addFlag({ .longName = "refresh", .description = "Consider all previously downloaded files out-of-date.", + .category = miscCategory, .handler = {[&]() { refresh = true; }}, }); } @@ -187,7 +191,7 @@ static void showHelp(std::vector<std::string> subcommand, MultiCommand & topleve *vUtils); auto attrs = state.buildBindings(16); - attrs.alloc("command").mkString(toplevel.toJSON().dump()); + attrs.alloc("toplevel").mkString(toplevel.toJSON().dump()); auto vRes = state.allocValue(); state.callFunction(*vGenerateManpage, state.allocValue()->mkAttrs(attrs), *vRes, noPos); diff --git a/tests/lang/eval-okay-versions.nix b/tests/lang/eval-okay-versions.nix index e63c36586..e9111f5f4 100644 --- a/tests/lang/eval-okay-versions.nix +++ b/tests/lang/eval-okay-versions.nix @@ -4,6 +4,7 @@ let name2 = "hello"; name3 = "915resolution-0.5.2"; name4 = "xf86-video-i810-1.7.4"; + name5 = "name-that-ends-with-dash--1.0"; eq = 0; lt = builtins.sub 0 1; @@ -23,6 +24,8 @@ let ((builtins.parseDrvName name3).version == "0.5.2") ((builtins.parseDrvName name4).name == "xf86-video-i810") ((builtins.parseDrvName name4).version == "1.7.4") + ((builtins.parseDrvName name5).name == "name-that-ends-with-dash") + ((builtins.parseDrvName name5).version == "-1.0") (versionTest "1.0" "2.3" lt) (versionTest "2.1" "2.3" lt) (versionTest "2.3" "2.3" eq) |