diff options
33 files changed, 669 insertions, 248 deletions
diff --git a/.clang-tidy b/.clang-tidy index 3b5dcd91a..0cc1f2520 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -16,3 +16,6 @@ Checks: - -bugprone-unchecked-optional-access # many warnings, seems like a questionable lint - -bugprone-branch-clone + +CheckOptions: + bugprone-reserved-identifier.AllowedIdentifiers: '__asan_default_options' diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index 775149180..630af29ff 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -70,10 +70,17 @@ horrors: iFreilicht: github: iFreilicht +isabelroses: + forgejo: isabelroses + github: isabelroses + jade: forgejo: jade github: lf- +kjeremy: + github: kjeremy + kloenk: forgejo: kloenk github: kloenk diff --git a/doc/manual/rl-next/nix-flake-show-description.md b/doc/manual/rl-next/nix-flake-show-description.md new file mode 100644 index 000000000..e819a3a29 --- /dev/null +++ b/doc/manual/rl-next/nix-flake-show-description.md @@ -0,0 +1,37 @@ +--- +synopsis: "Lix will now show the package descriptions in when running `nix flake show`." +cls: [1540] +issues: [] +credits: [kjeremy, isabelroses] +category: Improvements +--- + +When running `nix flake show`, Lix will now show the package descriptions, if they exist. + +Before: + +```shell +$ nix flake show +path:/home/isabel/dev/lix-show?lastModified=1721736108&narHash=sha256-Zo8HP1ur7Q2b39hKUEG8EAh/opgq8xJ2jvwQ/htwO4Q%3D +└───packages + └───x86_64-linux + ├───aNoDescription: package 'simple' + ├───bOneLineDescription: package 'simple' + ├───cMultiLineDescription: package 'simple' + ├───dLongDescription: package 'simple' + └───eEmptyDescription: package 'simple' +``` + +After: + +```shell +$ nix flake show +path:/home/isabel/dev/lix-show?lastModified=1721736108&narHash=sha256-Zo8HP1ur7Q2b39hKUEG8EAh/opgq8xJ2jvwQ/htwO4Q%3D +└───packages + └───x86_64-linux + ├───aNoDescription: package 'simple' + ├───bOneLineDescription: package 'simple' - 'one line' + ├───cMultiLineDescription: package 'simple' - 'line one' + ├───dLongDescription: package 'simple' - 'abcdefghijklmnopqrstuvwxyz' + └───eEmptyDescription: package 'simple' +``` @@ -33,7 +33,7 @@ # This notice gets echoed as a dev shell hook, and can be turned off with # `touch .nocontribmsg` - sgr = ''[''; + sgr = builtins.fromJSON ''"\u001b["''; freezePage = "https://wiki.lix.systems/books/lix-contributors/page/freezes-and-recommended-contributions"; codebaseOverview = "https://wiki.lix.systems/books/lix-contributors/page/codebase-overview"; contribNotice = builtins.toFile "lix-contrib-notice" '' @@ -59,7 +59,8 @@ (Run `touch .nocontribmsg` to hide this message.) ''; - officialRelease = false; + versionJson = builtins.fromJSON (builtins.readFile ./version.json); + officialRelease = versionJson.official_release; # Set to true to build the release notes for the next release. buildUnreleasedNotes = true; @@ -275,6 +276,19 @@ # System tests. tests = import ./tests/nixos { inherit lib nixpkgs nixpkgsFor; } // { + # This is x86_64-linux only, just because we have significantly + # cheaper x86_64-linux compute in CI. + # It is clangStdenv because clang's sanitizers are nicer. + asanBuild = self.packages.x86_64-linux.nix-clangStdenv.override { + sanitize = [ + "address" + "undefined" + ]; + # it is very hard to make *every* CI build use this option such + # that we don't wind up building Lix twice, so we do it here where + # we are already doing so. + werror = true; + }; # Make sure that nix-env still produces the exact same result # on a particular version of Nixpkgs. @@ -406,7 +420,7 @@ pkgs: stdenv: let nix = pkgs.callPackage ./package.nix { - inherit stdenv officialRelease versionSuffix; + inherit stdenv versionSuffix; busybox-sandbox-shell = pkgs.busybox-sandbox-shell or pkgs.default-busybox-sandbox; internalApiDocs = false; }; diff --git a/meson.build b/meson.build index 56f447501..ed50dff78 100644 --- a/meson.build +++ b/meson.build @@ -199,7 +199,11 @@ configdata = { } # Dependencies # -boehm = dependency('bdw-gc', required : get_option('gc'), version : '>=8.2.6') +gc_opt = get_option('gc').disable_if( + 'address' in get_option('b_sanitize'), + error_message: 'gc does far too many memory crimes for ASan' +) +boehm = dependency('bdw-gc', required : gc_opt, version : '>=8.2.6') configdata += { 'HAVE_BOEHMGC': boehm.found().to_int(), } @@ -482,7 +486,14 @@ if cxx.get_id() == 'clang' and get_option('b_sanitize') != '' add_project_link_arguments('-shared-libsan', language : 'cpp') endif +# Clang gets grumpy about missing libasan symbols if -shared-libasan is not +# passed when building shared libs, at least on Linux +if cxx.get_id() == 'clang' and 'address' in get_option('b_sanitize') + add_project_link_arguments('-shared-libasan', language : 'cpp') +endif + add_project_link_arguments('-pthread', language : 'cpp') + if cxx.get_linker_id() in ['ld.bfd', 'ld.gold'] add_project_link_arguments('-Wl,--no-copy-dt-needed-entries', language : 'cpp') endif @@ -497,7 +508,7 @@ endif # maintainers/buildtime_report.sh BUILD-DIR to simply work in clang builds. # # They can also be manually viewed at https://ui.perfetto.dev -if get_option('profile-build').require(meson.get_compiler('cpp').get_id() == 'clang').enabled() +if get_option('profile-build').require(cxx.get_id() == 'clang').enabled() add_project_arguments('-ftime-trace', language: 'cpp') endif diff --git a/package.nix b/package.nix index 06f1107e0..7ebe2721b 100644 --- a/package.nix +++ b/package.nix @@ -20,6 +20,7 @@ doxygen, editline-lix ? __forDefaults.editline-lix, editline, + expect, git, gtest, jq, @@ -52,16 +53,24 @@ pname ? "lix", versionSuffix ? "", - officialRelease ? false, + officialRelease ? __forDefaults.versionJson.official_release, # Set to true to build the release notes for the next release. buildUnreleasedNotes ? true, internalApiDocs ? false, + # List of Meson sanitize options. Accepts values of b_sanitize, e.g. + # "address", "undefined", "thread". + sanitize ? null, + # Turn compiler warnings into errors. + werror ? false, + # Not a real argument, just the only way to approximate let-binding some # stuff for argument defaults. __forDefaults ? { canRunInstalled = stdenv.buildPlatform.canExecute stdenv.hostPlatform; + versionJson = builtins.fromJSON (builtins.readFile ./version.json); + boehmgc-nix = boehmgc.override { enableLargeConfig = true; }; editline-lix = editline.overrideAttrs (prev: { @@ -77,8 +86,7 @@ let inherit (lib) fileset; inherit (stdenv) hostPlatform buildPlatform; - versionJson = builtins.fromJSON (builtins.readFile ./version.json); - version = versionJson.version + versionSuffix; + version = __forDefaults.versionJson.version + versionSuffix; aws-sdk-cpp-nix = if aws-sdk-cpp == null then @@ -170,6 +178,12 @@ stdenv.mkDerivation (finalAttrs: { dontBuild = false; mesonFlags = + let + sanitizeOpts = lib.optionals (sanitize != null) ( + [ "-Db_sanitize=${builtins.concatStringsSep "," sanitize}" ] + ++ lib.optional (builtins.elem "address" sanitize) "-Dgc=disabled" + ); + in lib.optionals hostPlatform.isLinux [ # You'd think meson could just find this in PATH, but busybox is in buildInputs, # which don't actually get added to PATH. And buildInputs is correct over @@ -185,8 +199,10 @@ stdenv.mkDerivation (finalAttrs: { (lib.mesonEnable "internal-api-docs" internalApiDocs) (lib.mesonBool "enable-tests" finalAttrs.finalPackage.doCheck) (lib.mesonBool "enable-docs" canRunInstalled) + (lib.mesonBool "werror" werror) ] - ++ lib.optional (hostPlatform != buildPlatform) "--cross-file=${mesonCrossFile}"; + ++ lib.optional (hostPlatform != buildPlatform) "--cross-file=${mesonCrossFile}" + ++ sanitizeOpts; # We only include CMake so that Meson can locate toml11, which only ships CMake dependency metadata. dontUseCmakeConfigure = true; @@ -243,6 +259,8 @@ stdenv.mkDerivation (finalAttrs: { ++ lib.optional (hostPlatform.canExecute buildPlatform) aws-sdk-cpp-nix ++ lib.optionals (finalAttrs.dontBuild) maybePropagatedInputs; + nativeCheckInputs = [ expect ]; + checkInputs = [ gtest rapidcheck @@ -371,8 +389,6 @@ stdenv.mkDerivation (finalAttrs: { pegtl ; - inherit officialRelease; - # The collection of dependency logic for this derivation is complicated enough that # it's easier to parameterize the devShell off an already called package.nix. mkDevShell = diff --git a/releng/README.md b/releng/README.md index cfacf4b8e..2aa3b959f 100644 --- a/releng/README.md +++ b/releng/README.md @@ -30,7 +30,7 @@ First, we prepare the release. `python -m releng prepare` is used for this. Then we tag the release with `python -m releng tag`: * Git HEAD is detached. -* `officialRelease = true` is set in `flake.nix`, this is committed, and a +* `"official_release": true` is set in `version.json`, this is committed, and a release is tagged. * The tag is merged back into the last branch (either `main` for new releases or `release-MAJOR` for maintenance releases) with `git merge -s ours VERSION` diff --git a/releng/create_release.xsh b/releng/create_release.xsh index 358124359..62114350b 100644 --- a/releng/create_release.xsh +++ b/releng/create_release.xsh @@ -11,7 +11,7 @@ from . import environment from .environment import RelengEnvironment from . import keys from . import docker -from .version import VERSION, RELEASE_NAME, MAJOR +from .version import VERSION, RELEASE_NAME, MAJOR, OFFICIAL_RELEASE from .gitutils import verify_are_on_tag, git_preconditions from . import release_notes @@ -39,12 +39,18 @@ def setup_creds(env: RelengEnvironment): def official_release_commit_tag(force_tag=False): - print('[+] Setting officialRelease in flake.nix and tagging') + print('[+] Setting officialRelease in version.json and tagging') prev_branch = $(git symbolic-ref --short HEAD).strip() git switch --detach - sed -i 's/officialRelease = false/officialRelease = true/' flake.nix - git add flake.nix + + # Must be done in two parts due to buffering (opening the file immediately + # would truncate it). + new_version_json = $(jq --indent 4 '.official_release = true' version.json) + with open('version.json', 'w') as fh: + fh.write(new_version_json) + git add version.json + message = f'release: {VERSION} "{RELEASE_NAME}"\n\nRelease produced with releng/create_release.xsh' git commit -m @(message) git tag @(['-f'] if force_tag else []) -a -m @(message) @(VERSION) @@ -250,15 +256,14 @@ def build_manual(eval_result): def upload_manual(env: RelengEnvironment): - stable = json.loads($(nix eval --json '.#nix.officialRelease')) - if stable: + if OFFICIAL_RELEASE: version = MAJOR else: version = 'nightly' print('[+] aws s3 sync manual') aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/@(version)/ - if stable: + if OFFICIAL_RELEASE: aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/stable/ diff --git a/releng/version.py b/releng/version.py index 47ef23504..4ad188d46 100644 --- a/releng/version.py +++ b/releng/version.py @@ -4,3 +4,4 @@ version_json = json.load(open('version.json')) VERSION = version_json['version'] MAJOR = '.'.join(VERSION.split('.')[:2]) RELEASE_NAME = version_json['release_name'] +OFFICIAL_RELEASE = version_json['official_release'] diff --git a/src/asan-options/asan-options.cc b/src/asan-options/asan-options.cc new file mode 100644 index 000000000..c4cf360af --- /dev/null +++ b/src/asan-options/asan-options.cc @@ -0,0 +1,17 @@ +/// @file This is very bothersome code that has to be included in every +/// executable to get the correct default ASan options. I am so sorry. + +extern "C" [[gnu::retain]] const char *__asan_default_options() +{ + // We leak a bunch of memory knowingly on purpose. It's not worthwhile to + // diagnose that memory being leaked for now. + // + // Instruction bytes are useful for finding the actual code that + // corresponds to an ASan report. + // + // TODO: setting log_path=asan.log or not: neither works, since you can't + // write to the fs in certain places in the testsuite, but you also cannot + // write arbitrarily to stderr in other places so the reports get eaten. + // pain 🥖 + return "halt_on_error=1:abort_on_error=1:detect_leaks=0:print_summary=1:dump_instruction_bytes=1"; +} diff --git a/src/libcmd/installable-attr-path.cc b/src/libcmd/installable-attr-path.cc index eb15fecc3..9891b263c 100644 --- a/src/libcmd/installable-attr-path.cc +++ b/src/libcmd/installable-attr-path.cc @@ -1,23 +1,11 @@ -#include "globals.hh" #include "installable-attr-path.hh" #include "outputs-spec.hh" #include "command.hh" #include "attr-path.hh" #include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" #include "eval.hh" #include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" #include "flake/flake.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" - -#include <regex> -#include <queue> #include <nlohmann/json.hpp> diff --git a/src/libcmd/installable-attr-path.hh b/src/libcmd/installable-attr-path.hh index 86c2f8219..edc9c2906 100644 --- a/src/libcmd/installable-attr-path.hh +++ b/src/libcmd/installable-attr-path.hh @@ -1,25 +1,11 @@ #pragma once ///@file -#include "globals.hh" #include "installable-value.hh" #include "outputs-spec.hh" #include "command.hh" -#include "attr-path.hh" #include "common-eval-args.hh" -#include "derivations.hh" -#include "eval-inline.hh" #include "eval.hh" -#include "get-drvs.hh" -#include "store-api.hh" -#include "shared.hh" -#include "eval-cache.hh" -#include "url.hh" -#include "registry.hh" -#include "build-result.hh" - -#include <regex> -#include <queue> #include <nlohmann/json.hpp> diff --git a/src/libexpr/flake/flakeref.cc b/src/libexpr/flake/flakeref.cc index a95df04ba..3be4ea550 100644 --- a/src/libexpr/flake/flakeref.cc +++ b/src/libexpr/flake/flakeref.cc @@ -169,14 +169,13 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( 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", "")), + FlakeRef(Input::fromURL(parsedURL, isFlake), subdir), fragment); } @@ -204,7 +203,13 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment( std::string fragment; std::swap(fragment, parsedURL.fragment); - auto input = Input::fromURL(parsedURL, isFlake); + // This has a special meaning for flakes and must not be passed to libfetchers. + // Of course this means that libfetchers cannot have fetchers + // expecting an argument `dir` 🫠 + ParsedURL urlForFetchers(parsedURL); + urlForFetchers.query.erase("dir"); + + auto input = Input::fromURL(urlForFetchers, isFlake); input.parent = baseDir; return std::make_pair( diff --git a/src/libexpr/get-drvs.cc b/src/libexpr/get-drvs.cc index 8e3969aac..d7869d09b 100644 --- a/src/libexpr/get-drvs.cc +++ b/src/libexpr/get-drvs.cc @@ -1,6 +1,7 @@ #include "get-drvs.hh" #include "eval-inline.hh" #include "derivations.hh" +#include "eval.hh" #include "store-api.hh" #include "path-with-outputs.hh" @@ -101,66 +102,134 @@ StorePath DrvInfo::queryOutPath() return *outPath; } +void DrvInfo::fillOutputs(bool withPaths) +{ + auto fillDefault = [&]() { + std::optional<StorePath> outPath = std::nullopt; + if (withPaths) { + outPath.emplace(this->queryOutPath()); + } + this->outputs.emplace("out", outPath); + }; + + // lol. lmao even. + if (this->attrs == nullptr) { + fillDefault(); + return; + } + + Attr * outputs = this->attrs->get(this->state->sOutputs); + if (outputs == nullptr) { + fillDefault(); + return; + } + + // NOTE(Qyriad): I don't think there is any codepath that can cause this to error. + this->state->forceList( + *outputs->value, + outputs->pos, + "while evaluating the 'outputs' attribute of a derivation" + ); + + for (auto [idx, elem] : enumerate(outputs->value->listItems())) { + // NOTE(Qyriad): This error should be *extremely* rare in practice. + // It is impossible to construct with `stdenv.mkDerivation`, + // `builtins.derivation`, or even `derivationStrict`. As far as we can tell, + // it is only possible by overriding a derivation attrset already created by + // one of those with `//` to introduce the failing `outputs` entry. + auto errMsg = fmt("while evaluating output %d of a derivation", idx); + std::string_view outputName = state->forceStringNoCtx( + *elem, + outputs->pos, + errMsg + ); + + if (withPaths) { + // Find the attr with this output's name... + Attr * out = this->attrs->get(this->state->symbols.create(outputName)); + if (out == nullptr) { + // FIXME: throw error? + continue; + } + + // Meanwhile we couldn't figure out any circumstances + // that cause this to error. + state->forceAttrs(*out->value, outputs->pos, errMsg); + + // ...and evaluate its `outPath` attribute. + Attr * outPath = out->value->attrs->get(this->state->sOutPath); + if (outPath == nullptr) { + continue; + // FIXME: throw error? + } + + NixStringContext context; + // And idk what could possibly cause this one to error + // that wouldn't error before here. + auto storePath = state->coerceToStorePath( + outPath->pos, + *outPath->value, + context, + errMsg + ); + this->outputs.emplace(outputName, storePath); + } else { + this->outputs.emplace(outputName, std::nullopt); + } + } +} DrvInfo::Outputs DrvInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall) { + // If we haven't already cached the outputs set, then do so now. if (outputs.empty()) { - /* Get the ‘outputs’ list. */ - Bindings::iterator i; - if (attrs && (i = attrs->find(state->sOutputs)) != attrs->end()) { - state->forceList(*i->value, i->pos, "while evaluating the 'outputs' attribute of a derivation"); - - /* For each output... */ - for (auto elem : i->value->listItems()) { - std::string output(state->forceStringNoCtx(*elem, i->pos, "while evaluating the name of an output of a derivation")); - - if (withPaths) { - /* Evaluate the corresponding set. */ - Bindings::iterator out = attrs->find(state->symbols.create(output)); - if (out == attrs->end()) continue; // FIXME: throw error? - state->forceAttrs(*out->value, i->pos, "while evaluating an output of a derivation"); - - /* And evaluate its ‘outPath’ attribute. */ - Bindings::iterator outPath = out->value->attrs->find(state->sOutPath); - if (outPath == out->value->attrs->end()) continue; // FIXME: throw error? - NixStringContext context; - outputs.emplace(output, state->coerceToStorePath(outPath->pos, *outPath->value, context, "while evaluating an output path of a derivation")); - } else - outputs.emplace(output, std::nullopt); - } - } else - outputs.emplace("out", withPaths ? std::optional{queryOutPath()} : std::nullopt); + // FIXME: this behavior seems kind of busted, since whether or not this + // DrvInfo will have paths is forever determined by the *first* call to + // this function?? + fillOutputs(withPaths); } + // Things that operate on derivations like packages, like `nix-env` and `nix build`, + // allow derivations to specify which outputs should be used in those user-facing + // cases if the user didn't specify an output explicitly. + // If the caller just wanted all the outputs for this derivation, though, + // then we're done here. if (!onlyOutputsToInstall || !attrs) return outputs; - Bindings::iterator i; - if (attrs && (i = attrs->find(state->sOutputSpecified)) != attrs->end() && state->forceBool(*i->value, i->pos, "while evaluating the 'outputSpecified' attribute of a derivation")) { - Outputs result; - auto out = outputs.find(queryOutputName()); - if (out == outputs.end()) - throw Error("derivation does not have output '%s'", queryOutputName()); - result.insert(*out); - return result; + // Regardless of `meta.outputsToInstall`, though, you can select into a derivation + // output by its attribute, e.g. `pkgs.lix.dev`, which (lol?) sets the magic + // attribute `outputSpecified = true`, and changes the `outputName` attr to the + // explicitly selected-into output. + if (Attr * outSpecAttr = attrs->get(state->sOutputSpecified)) { + bool outputSpecified = this->state->forceBool( + *outSpecAttr->value, + outSpecAttr->pos, + "while evaluating the 'outputSpecified' attribute of a derivation" + ); + if (outputSpecified) { + auto maybeOut = outputs.find(queryOutputName()); + if (maybeOut == outputs.end()) { + throw Error("derivation does not have output '%s'", queryOutputName()); + } + return Outputs{*maybeOut}; + } } - else { - /* Check for `meta.outputsToInstall` and return `outputs` reduced to that. */ - const Value * outTI = queryMeta("outputsToInstall"); - if (!outTI) return outputs; - auto errMsg = Error("this derivation has bad 'meta.outputsToInstall'"); - /* ^ this shows during `nix-env -i` right under the bad derivation */ - if (!outTI->isList()) throw errMsg; - Outputs result; - for (auto elem : outTI->listItems()) { - if (elem->type() != nString) throw errMsg; - auto out = outputs.find(elem->string.s); - if (out == outputs.end()) throw errMsg; - result.insert(*out); - } - return result; + /* Check for `meta.outputsToInstall` and return `outputs` reduced to that. */ + const Value * outTI = queryMeta("outputsToInstall"); + if (!outTI) return outputs; + auto errMsg = Error("this derivation has bad 'meta.outputsToInstall'"); + /* ^ this shows during `nix-env -i` right under the bad derivation */ + if (!outTI->isList()) throw errMsg; + Outputs result; + for (auto elem : outTI->listItems()) { + if (elem->type() != nString) throw errMsg; + auto out = outputs.find(elem->string.s); + if (out == outputs.end()) throw errMsg; + result.insert(*out); } + return result; } @@ -350,56 +419,95 @@ static void getDerivations(EvalState & state, Value & vIn, Value v; state.autoCallFunction(autoArgs, vIn, v); - /* Process the expression. */ - if (!getDerivation(state, v, pathPrefix, drvs, ignoreAssertionFailures)) ; - - else if (v.type() == nAttrs) { - - /* Dont consider sets we've already seen, e.g. y in - `rec { x.d = derivation {...}; y = x; }`. */ - if (!done.insert(v.attrs).second) return; - - /* !!! undocumented hackery to support combining channels in - nix-env.cc. */ - bool combineChannels = v.attrs->find(state.symbols.create("_combineChannels")) != v.attrs->end(); - - /* Consider the attributes in sorted order to get more - deterministic behaviour in nix-env operations (e.g. when - there are names clashes between derivations, the derivation - bound to the attribute with the "lower" name should take - precedence). */ - for (auto & i : v.attrs->lexicographicOrder(state.symbols)) { - debug("evaluating attribute '%1%'", state.symbols[i->name]); - if (!std::regex_match(std::string(state.symbols[i->name]), attrRegex)) - continue; - std::string pathPrefix2 = addToPath(pathPrefix, state.symbols[i->name]); - if (combineChannels) - getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); - else if (getDerivation(state, *i->value, pathPrefix2, drvs, ignoreAssertionFailures)) { - /* If the value of this attribute is itself a set, - should we recurse into it? => Only if it has a - `recurseForDerivations = true' attribute. */ - if (i->value->type() == nAttrs) { - Bindings::iterator j = i->value->attrs->find(state.sRecurseForDerivations); - if (j != i->value->attrs->end() && state.forceBool(*j->value, j->pos, "while evaluating the attribute `recurseForDerivations`")) - getDerivations(state, *i->value, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); - } - } - } + bool shouldRecurse = getDerivation(state, v, pathPrefix, drvs, ignoreAssertionFailures); + if (!shouldRecurse) { + // We're done here. + return; } - else if (v.type() == nList) { + if (v.type() == nList) { // NOTE we can't really deduplicate here because small lists don't have stable addresses // and can cause spurious duplicate detections due to v being on the stack. for (auto [n, elem] : enumerate(v.listItems())) { - std::string pathPrefix2 = addToPath(pathPrefix, fmt("%d", n)); - if (getDerivation(state, *elem, pathPrefix2, drvs, ignoreAssertionFailures)) - getDerivations(state, *elem, pathPrefix2, autoArgs, drvs, done, ignoreAssertionFailures); + std::string joinedAttrPath = addToPath(pathPrefix, fmt("%d", n)); + bool shouldRecurse = getDerivation(state, *elem, joinedAttrPath, drvs, ignoreAssertionFailures); + if (shouldRecurse) { + getDerivations( + state, + *elem, + joinedAttrPath, + autoArgs, + drvs, + done, + ignoreAssertionFailures + ); + } } + + return; + } else if (v.type() != nAttrs) { + state.error<TypeError>( + "expression was expected to be a derivation or collection of derivations, but instead was %s", + showType(v.type(), true) + ).debugThrow(); + } + + /* Dont consider sets we've already seen, e.g. y in + `rec { x.d = derivation {...}; y = x; }`. */ + auto const &[_, didInsert] = done.insert(v.attrs); + if (!didInsert) { + return; } - else - state.error<TypeError>("expression does not evaluate to a derivation (or a set or list of those)").debugThrow(); + // FIXME: what the fuck??? + /* !!! undocumented hackery to support combining channels in + nix-env.cc. */ + bool combineChannels = v.attrs->find(state.symbols.create("_combineChannels")) != v.attrs->end(); + + /* Consider the attributes in sorted order to get more + deterministic behaviour in nix-env operations (e.g. when + there are names clashes between derivations, the derivation + bound to the attribute with the "lower" name should take + precedence). */ + for (auto & attr : v.attrs->lexicographicOrder(state.symbols)) { + debug("evaluating attribute '%1%'", state.symbols[attr->name]); + // FIXME: only consider attrs with identifier-like names?? Why??? + if (!std::regex_match(std::string(state.symbols[attr->name]), attrRegex)) { + continue; + } + std::string joinedAttrPath = addToPath(pathPrefix, state.symbols[attr->name]); + if (combineChannels) { + getDerivations(state, *attr->value, joinedAttrPath, autoArgs, drvs, done, ignoreAssertionFailures); + } else if (getDerivation(state, *attr->value, joinedAttrPath, drvs, ignoreAssertionFailures)) { + /* If the value of this attribute is itself a set, + should we recurse into it? => Only if it has a + `recurseForDerivations = true' attribute. */ + if (attr->value->type() == nAttrs) { + Attr * recurseForDrvs = attr->value->attrs->get(state.sRecurseForDerivations); + if (recurseForDrvs == nullptr) { + continue; + } + bool shouldRecurse = state.forceBool( + *recurseForDrvs->value, + attr->pos, + fmt("while evaluating the '%s' attribute", Magenta("recurseForDerivations")) + ); + if (!shouldRecurse) { + continue; + } + + getDerivations( + state, + *attr->value, + joinedAttrPath, + autoArgs, + drvs, + done, + ignoreAssertionFailures + ); + } + } + } } diff --git a/src/libexpr/get-drvs.hh b/src/libexpr/get-drvs.hh index 44aac3015..fd927b9e5 100644 --- a/src/libexpr/get-drvs.hh +++ b/src/libexpr/get-drvs.hh @@ -37,13 +37,14 @@ private: bool checkMeta(Value & v); + void fillOutputs(bool withPaths = true); + public: /** * path towards the derivation */ std::string attrPath; - DrvInfo(EvalState & state) : state(&state) { }; DrvInfo(EvalState & state, std::string attrPath, Bindings * attrs); DrvInfo(EvalState & state, ref<Store> store, const std::string & drvPathWithOutputs); diff --git a/src/libfetchers/fetchers.hh b/src/libfetchers/fetchers.hh index 2bb4248be..40f2b6294 100644 --- a/src/libfetchers/fetchers.hh +++ b/src/libfetchers/fetchers.hh @@ -159,6 +159,37 @@ struct InputScheme std::optional<std::string> commitMsg) const; virtual std::pair<StorePath, Input> fetch(ref<Store> store, const Input & input) = 0; + +protected: + void emplaceURLQueryIntoAttrs( + const ParsedURL & parsedURL, + Attrs & attrs, + const StringSet & numericParams, + const StringSet & booleanParams) const + { + for (auto &[name, value] : parsedURL.query) { + if (name == "url") { + throw BadURL( + "URL '%s' must not override url via query param!", + parsedURL.to_string() + ); + } else if (numericParams.count(name) != 0) { + if (auto n = string2Int<uint64_t>(value)) { + attrs.insert_or_assign(name, *n); + } else { + throw BadURL( + "URL '%s' has non-numeric parameter '%s'", + parsedURL.to_string(), + name + ); + } + } else if (booleanParams.count(name) != 0) { + attrs.emplace(name, Explicit<bool> { value == "1" }); + } else { + attrs.emplace(name, value); + } + } + } }; void registerInputScheme(std::shared_ptr<InputScheme> && fetcher); diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 9fd8d7bbf..8e3165ff6 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -273,18 +273,15 @@ struct GitInputScheme : InputScheme Attrs attrs; attrs.emplace("type", "git"); - - for (auto & [name, value] : url.query) { - if (name == "rev" || name == "ref") - attrs.emplace(name, value); - else if (name == "shallow" || name == "submodules" || name == "allRefs") - attrs.emplace(name, Explicit<bool> { value == "1" }); - else - url2.query.emplace(name, value); - } - attrs.emplace("url", url2.to_string()); + emplaceURLQueryIntoAttrs( + url, + attrs, + {"lastModified", "revCount"}, + {"shallow", "submodules", "allRefs"} + ); + return inputFromAttrs(attrs); } diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc index 60fefd1f3..b971781ae 100644 --- a/src/libfetchers/github.cc +++ b/src/libfetchers/github.cc @@ -1,3 +1,4 @@ +#include "attrs.hh" #include "filetransfer.hh" #include "cache.hh" #include "globals.hh" @@ -36,18 +37,11 @@ struct GitArchiveInputScheme : InputScheme auto path = tokenizeString<std::vector<std::string>>(url.path, "/"); - std::optional<Hash> rev; - std::optional<std::string> ref; - std::optional<std::string> host_url; + std::optional<std::string> refOrRev; auto size = path.size(); if (size == 3) { - if (std::regex_match(path[2], revRegex)) - rev = Hash::parseAny(path[2], htSHA1); - else if (std::regex_match(path[2], refRegex)) - ref = path[2]; - else - throw BadURL("in URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[2]); + refOrRev = path[2]; } else if (size > 3) { std::string rs; for (auto i = std::next(path.begin(), 2); i != path.end(); i++) { @@ -58,61 +52,91 @@ struct GitArchiveInputScheme : InputScheme } if (std::regex_match(rs, refRegex)) { - ref = rs; + refOrRev = rs; } else { throw BadURL("in URL '%s', '%s' is not a branch/tag name", url.url, rs); } } else if (size < 2) throw BadURL("URL '%s' is invalid", url.url); + Attrs attrs; + attrs.emplace("type", type()); + attrs.emplace("owner", path[0]); + attrs.emplace("repo", path[1]); + for (auto &[name, value] : url.query) { - if (name == "rev") { - if (rev) - throw BadURL("URL '%s' contains multiple commit hashes", url.url); - rev = Hash::parseAny(value, htSHA1); - } - else if (name == "ref") { - if (!std::regex_match(value, refRegex)) - throw BadURL("URL '%s' contains an invalid branch/tag name", url.url); - if (ref) - throw BadURL("URL '%s' contains multiple branch/tag names", url.url); - ref = value; - } - else if (name == "host") { - if (!std::regex_match(value, hostRegex)) - throw BadURL("URL '%s' contains an invalid instance host", url.url); - host_url = value; + if (name == "rev" || name == "ref") { + if (refOrRev) { + throw BadURL("URL '%s' already contains a ref or rev", url.url); + } else { + refOrRev = value; + } + } else if (name == "lastModified") { + if (auto n = string2Int<uint64_t>(value)) { + attrs.emplace(name, *n); + } else { + throw Error( + "Attribute 'lastModified' in URL '%s' must be an integer", + url.to_string() + ); + } + } else { + attrs.emplace(name, value); } - // FIXME: barf on unsupported attributes } - if (ref && rev) - throw BadURL("URL '%s' contains both a commit hash and a branch/tag name %s %s", url.url, *ref, rev->gitRev()); + if (refOrRev) attrs.emplace("refOrRev", *refOrRev); - Input input; - input.attrs.insert_or_assign("type", type()); - input.attrs.insert_or_assign("owner", path[0]); - input.attrs.insert_or_assign("repo", path[1]); - if (rev) input.attrs.insert_or_assign("rev", rev->gitRev()); - if (ref) input.attrs.insert_or_assign("ref", *ref); - if (host_url) input.attrs.insert_or_assign("host", *host_url); - - return input; + return inputFromAttrs(attrs); } std::optional<Input> inputFromAttrs(const Attrs & attrs) const override { - if (maybeGetStrAttr(attrs, "type") != type()) return {}; + // Attributes can contain refOrRev and it needs to be figured out + // which one it is (see inputFromURL for when that may happen). + // The correct one (ref or rev) will be written into finalAttrs and + // it needs to be mutable for that. + Attrs finalAttrs(attrs); + auto type_ = maybeGetStrAttr(finalAttrs, "type"); + if (type_ != type()) return {}; + + auto owner = getStrAttr(finalAttrs, "owner"); + auto repo = getStrAttr(finalAttrs, "repo"); + + auto url = fmt("%s:%s/%s", *type_, owner, repo); + if (auto host = maybeGetStrAttr(finalAttrs, "host")) { + if (!std::regex_match(*host, hostRegex)) { + throw BadURL("URL '%s' contains an invalid instance host", url); + } + } - for (auto & [name, value] : attrs) - if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev" && name != "narHash" && name != "lastModified" && name != "host") - throw Error("unsupported input attribute '%s'", name); + if (auto refOrRev = maybeGetStrAttr(finalAttrs, "refOrRev")) { + finalAttrs.erase("refOrRev"); + if (std::regex_match(*refOrRev, revRegex)) { + finalAttrs.emplace("rev", *refOrRev); + } else if (std::regex_match(*refOrRev, refRegex)) { + finalAttrs.emplace("ref", *refOrRev); + } else { + throw Error( + "in URL '%s', '%s' is not a commit hash or a branch/tag name", + url, + *refOrRev + ); + } + } else if (auto ref = maybeGetStrAttr(finalAttrs, "ref")) { + if (!std::regex_match(*ref, refRegex)) { + throw BadURL("URL '%s' contains an invalid branch/tag name", url); + } + } - getStrAttr(attrs, "owner"); - getStrAttr(attrs, "repo"); + for (auto & [name, value] : finalAttrs) { + if (name != "type" && name != "owner" && name != "repo" && name != "ref" && name != "rev" && name != "narHash" && name != "lastModified" && name != "host") { + throw Error("unsupported input attribute '%s'", name); + } + } Input input; - input.attrs = attrs; + input.attrs = finalAttrs; return input; } diff --git a/src/libfetchers/indirect.cc b/src/libfetchers/indirect.cc index c73505b31..8c0176e84 100644 --- a/src/libfetchers/indirect.cc +++ b/src/libfetchers/indirect.cc @@ -17,6 +17,8 @@ struct IndirectInputScheme : InputScheme std::optional<Hash> rev; std::optional<std::string> ref; + Attrs attrs; + if (path.size() == 1) { } else if (path.size() == 2) { if (std::regex_match(path[1], revRegex)) @@ -26,29 +28,21 @@ struct IndirectInputScheme : InputScheme else throw BadURL("in flake URL '%s', '%s' is not a commit hash or branch/tag name", url.url, path[1]); } else if (path.size() == 3) { - if (!std::regex_match(path[1], refRegex)) - throw BadURL("in flake URL '%s', '%s' is not a branch/tag name", url.url, path[1]); ref = path[1]; - if (!std::regex_match(path[2], revRegex)) - throw BadURL("in flake URL '%s', '%s' is not a commit hash", url.url, path[2]); rev = Hash::parseAny(path[2], htSHA1); } else throw BadURL("GitHub URL '%s' is invalid", url.url); std::string id = path[0]; - if (!std::regex_match(id, flakeRegex)) - throw BadURL("'%s' is not a valid flake ID", id); - // FIXME: forbid query params? + attrs.emplace("type", "indirect"); + attrs.emplace("id", id); + if (rev) attrs.emplace("rev", rev->gitRev()); + if (ref) attrs.emplace("ref", *ref); - 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()); - if (ref) input.attrs.insert_or_assign("ref", *ref); + emplaceURLQueryIntoAttrs(url, attrs, {}, {}); - return input; + return inputFromAttrs(attrs); } std::optional<Input> inputFromAttrs(const Attrs & attrs) const override @@ -63,6 +57,18 @@ struct IndirectInputScheme : InputScheme if (!std::regex_match(id, flakeRegex)) throw BadURL("'%s' is not a valid flake ID", id); + // TODO come up with a nicer error message for those two. + if (auto rev = maybeGetStrAttr(attrs, "rev")) { + if (!std::regex_match(*rev, revRegex)) { + throw BadURL("in flake '%s', '%s' is not a commit hash", id, *rev); + } + } + if (auto ref = maybeGetStrAttr(attrs, "ref")) { + if (!std::regex_match(*ref, refRegex)) { + throw BadURL("in flake '%s', '%s' is not a valid branch/tag name", id, *ref); + } + } + Input input; input.direct = false; input.attrs = attrs; diff --git a/src/libfetchers/mercurial.cc b/src/libfetchers/mercurial.cc index 23cf7b51d..b4150e9df 100644 --- a/src/libfetchers/mercurial.cc +++ b/src/libfetchers/mercurial.cc @@ -56,12 +56,7 @@ struct MercurialInputScheme : InputScheme Attrs attrs; attrs.emplace("type", "hg"); - for (auto &[name, value] : url.query) { - if (name == "rev" || name == "ref") - attrs.emplace(name, value); - else - url2.query.emplace(name, value); - } + emplaceURLQueryIntoAttrs(url, attrs, {"revCount"}, {}); attrs.emplace("url", url2.to_string()); diff --git a/src/libfetchers/tarball.cc b/src/libfetchers/tarball.cc index 6ce35aeb2..b11665805 100644 --- a/src/libfetchers/tarball.cc +++ b/src/libfetchers/tarball.cc @@ -201,29 +201,17 @@ struct CurlInputScheme : InputScheme if (!isValidURL(_url, requireTree)) return std::nullopt; - Input input; - auto url = _url; - url.scheme = parseUrlScheme(url.scheme).transport; - - auto narHash = url.query.find("narHash"); - if (narHash != url.query.end()) - input.attrs.insert_or_assign("narHash", narHash->second); + Attrs attrs; + attrs.emplace("type", inputType()); - if (auto i = get(url.query, "rev")) - input.attrs.insert_or_assign("rev", *i); - - if (auto i = get(url.query, "revCount")) - if (auto n = string2Int<uint64_t>(*i)) - input.attrs.insert_or_assign("revCount", *n); + url.scheme = parseUrlScheme(url.scheme).transport; - url.query.erase("rev"); - url.query.erase("revCount"); + emplaceURLQueryIntoAttrs(url, attrs, {"revCount"}, {}); - input.attrs.insert_or_assign("type", inputType()); - input.attrs.insert_or_assign("url", url.to_string()); - return input; + attrs.emplace("url", url.to_string()); + return inputFromAttrs(attrs); } std::optional<Input> inputFromAttrs(const Attrs & attrs) const override @@ -235,7 +223,7 @@ struct CurlInputScheme : InputScheme std::set<std::string> 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); + throw Error("unsupported %s input attribute '%s'. If you wanted to fetch a tarball with a query parameter, please use '{ type = \"tarball\"; url = \"...\"; }'", *type, name); Input input; input.attrs = attrs; diff --git a/src/meson.build b/src/meson.build index 3fc5595b8..e918ae392 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,10 +12,19 @@ subdir('libmain') # libcmd depends on everything subdir('libcmd') - # The rest of the subdirectories aren't separate components, # just source files in another directory, so we process them here. +# Static library that just sets default ASan options. It needs to be included +# in every executable. +asanoptions = static_library( + 'libasanoptions', + files('asan-options/asan-options.cc'), +) +libasanoptions = declare_dependency( + link_whole: asanoptions +) + build_remote_sources = files( 'build-remote/build-remote.cc', ) diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 63b250d13..9d18b81b8 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -15,6 +15,7 @@ #include "registry.hh" #include "eval-cache.hh" #include "markdown.hh" +#include "terminal.hh" #include <nlohmann/json.hpp> #include <queue> @@ -1225,25 +1226,42 @@ struct CmdFlakeShow : FlakeCommand, MixJSON auto showDerivation = [&]() { auto name = visitor.getAttr(state->sName)->getString(); + std::optional<std::string> description; + if (auto aMeta = visitor.maybeGetAttr(state->sMeta)) { + if (auto aDescription = aMeta->maybeGetAttr(state->sDescription)) + description = aDescription->getString(); + } + if (json) { - std::optional<std::string> description; - if (auto aMeta = visitor.maybeGetAttr(state->sMeta)) { - if (auto aDescription = aMeta->maybeGetAttr(state->sDescription)) - description = aDescription->getString(); - } j.emplace("type", "derivation"); j.emplace("name", name); if (description) j.emplace("description", *description); } else { - logger->cout("%s: %s '%s'", - headerPrefix, + auto type = attrPath.size() == 2 && attrPathS[0] == "devShell" ? "development environment" : attrPath.size() >= 2 && attrPathS[0] == "devShells" ? "development environment" : attrPath.size() == 3 && attrPathS[0] == "checks" ? "derivation" : attrPath.size() >= 1 && attrPathS[0] == "hydraJobs" ? "derivation" : - "package", - name); + "package"; + if (description && !description->empty()) { + // Trim the string and only display the first line of the description. + auto desc = nix::trim(*description); + auto firstLineDesc = desc.substr(0, desc.find('\n')); + + std::string output = fmt("%s: %s '%s' - '%s'", headerPrefix, type, name, firstLineDesc); + if (output.size() > getWindowSize().second) { + // we resize to 4 less then the window size to account for the "...'" we append to + // the final string, we also include the ' since that is removed when we truncate the string + output.resize(getWindowSize().second - 4); + output.append("...'"); + } + + logger->cout("%s", output.c_str()); + } + else { + logger->cout("%s: %s '%s'", headerPrefix, type, name); + } } }; diff --git a/src/nix/meson.build b/src/nix/meson.build index 22f148fcb..97387e402 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -80,6 +80,7 @@ nix = executable( profiles_md_gen, nix2_commands_sources, dependencies : [ + libasanoptions, liblixcmd, liblixutil_mstatic, liblixstore_mstatic, diff --git a/tests/functional/fetchers.sh b/tests/functional/fetchers.sh new file mode 100644 index 000000000..0f888dc33 --- /dev/null +++ b/tests/functional/fetchers.sh @@ -0,0 +1,91 @@ +source common.sh + +requireGit + +clearStore + +testFetchTreeError() { + rawFetchTreeArg="${1?fetchTree arg missing}" + messageSubstring="${2?messageSubstring missing}" + + output="$(nix eval --impure --raw --expr "(builtins.fetchTree $rawFetchTreeArg).outPath" 2>&1)" && status=0 || status=$? + grepQuiet "$messageSubstring" <<<"$output" + test "$status" -ne 0 +} + +# github/gitlab/sourcehut fetcher input validation +for provider in github gitlab sourcehut; do + # ref/rev validation + testFetchTreeError \ + "{ type = \"$provider\"; owner = \"foo\"; repo = \"bar\"; ref = \",\"; }" \ + "URL '$provider:foo/bar' contains an invalid branch/tag name" + + testFetchTreeError \ + "\"$provider://host/foo/bar/,\"" \ + "URL '$provider:foo/bar', ',' is not a commit hash or a branch/tag name" + + testFetchTreeError \ + "\"$provider://host/foo/bar/f16d8f43dd0998cdb315a2cccf2e4d10027e7ca4?rev=abc\"" \ + "URL '$provider://host/foo/bar/f16d8f43dd0998cdb315a2cccf2e4d10027e7ca4?rev=abc' already contains a ref or rev" + + testFetchTreeError \ + "\"$provider://host/foo/bar/ref?ref=ref2\"" \ + "URL '$provider://host/foo/bar/ref?ref=ref2' already contains a ref or rev" + + # host validation + testFetchTreeError \ + "{ type = \"$provider\"; owner = \"foo\"; repo = \"bar\"; host = \"git_hub.com\"; }" \ + "URL '$provider:foo/bar' contains an invalid instance host" + + testFetchTreeError \ + "\"$provider://host/foo/bar/ref?host=git_hub.com\"" \ + "URL '$provider:foo/bar' contains an invalid instance host" + + # invalid attributes + testFetchTreeError \ + "{ type = \"$provider\"; owner = \"foo\"; repo = \"bar\"; wrong = true; }" \ + "unsupported input attribute 'wrong'" + + testFetchTreeError \ + "\"$provider://host/foo/bar/ref?wrong=1\"" \ + "unsupported input attribute 'wrong'" +done + +# unsupported attributes w/ tarball fetcher +testFetchTreeError \ + "\"https://host/foo?wrong=1\"" \ + "unsupported tarball input attribute 'wrong'. If you wanted to fetch a tarball with a query parameter, please use '{ type = \"tarball\"; url = \"...\"; }" + +# test for unsupported attributes / validation in git fetcher +testFetchTreeError \ + "\"git+https://github.com/owner/repo?invalid=1\"" \ + "unsupported Git input attribute 'invalid'" + +testFetchTreeError \ + "\"git+https://github.com/owner/repo?url=foo\"" \ + "URL 'git+https://github.com/owner/repo?url=foo' must not override url via query param!" + +testFetchTreeError \ + "\"git+https://github.com/owner/repo?ref=foo.lock\"" \ + "invalid Git branch/tag name 'foo.lock'" + +testFetchTreeError \ + "{ type = \"git\"; url =\"https://github.com/owner/repo\"; ref = \"foo.lock\"; }" \ + "invalid Git branch/tag name 'foo.lock'" + +# same for mercurial +testFetchTreeError \ + "\"hg+https://forge.tld/owner/repo?invalid=1\"" \ + "unsupported Mercurial input attribute 'invalid'" + +testFetchTreeError \ + "{ type = \"hg\"; url = \"https://forge.tld/owner/repo\"; invalid = 1; }" \ + "unsupported Mercurial input attribute 'invalid'" + +testFetchTreeError \ + "\"hg+https://forge.tld/owner/repo?ref=,\"" \ + "invalid Mercurial branch/tag name ','" + +testFetchTreeError \ + "{ type = \"hg\"; url = \"https://forge.tld/owner/repo\"; ref = \",\"; }" \ + "invalid Mercurial branch/tag name ','" diff --git a/tests/functional/flakes/show.sh b/tests/functional/flakes/show.sh index a3d300552..25f481575 100644 --- a/tests/functional/flakes/show.sh +++ b/tests/functional/flakes/show.sh @@ -85,3 +85,31 @@ assert show_output.legacyPackages.${builtins.currentSystem}.AAAAAASomeThingsFail assert show_output.legacyPackages.${builtins.currentSystem}.simple.name == "simple"; true ' + +cat >flake.nix<<EOF +{ + outputs = inputs: { + packages.$system = { + aNoDescription = import ./simple.nix; + bOneLineDescription = import ./simple.nix // { meta.description = "one line"; }; + cMultiLineDescription = import ./simple.nix // { meta.description = '' + line one + line two + ''; }; + dLongDescription = import ./simple.nix // { meta.description = '' + abcdefghijklmnopqrstuvwxyz + ''; }; + eEmptyDescription = import ./simple.nix // { meta.description = ""; }; + }; + }; +} +EOF +unbuffer sh -c ' + stty rows 20 cols 100 + nix flake show > show-output.txt +' +test "$(awk -F '[:] ' '/aNoDescription/{print $NF}' ./show-output.txt)" = "package 'simple'" +test "$(awk -F '[:] ' '/bOneLineDescription/{print $NF}' ./show-output.txt)" = "package 'simple' - 'one line'" +test "$(awk -F '[:] ' '/cMultiLineDescription/{print $NF}' ./show-output.txt)" = "package 'simple' - 'line one'" +test "$(awk -F '[:] ' '/dLongDescription/{print $NF}' ./show-output.txt)" = "package 'simple' - 'abcdefghijklmnopqrs...'" +test "$(awk -F '[:] ' '/eEmptyDescription/{print $NF}' ./show-output.txt)" = "package 'simple'" diff --git a/tests/functional/flakes/subdir-flake.sh b/tests/functional/flakes/subdir-flake.sh new file mode 100644 index 000000000..399518502 --- /dev/null +++ b/tests/functional/flakes/subdir-flake.sh @@ -0,0 +1,20 @@ +source common.sh +requireGit +clearStore + +container="$TEST_HOME"/flake-container +flake_dir="$container"/flake-dir + +createGitRepo "$container" +mkdir -p "$flake_dir" +writeSimpleFlake "$flake_dir" +git -C "$container" add flake-dir + +pushd "$flake_dir" &>/dev/null + info="$(nix flake info --json)" + [[ "$(jq -r '.resolvedUrl' <<<"$info")" == git+file://*/flake-container?dir=flake-dir ]] + [[ "$(jq -r '.url' <<<"$info")" == git+file://*/flake-container?dir=flake-dir ]] + + # Make sure we can actually access & build stuff in this flake. + nix build "path:$flake_dir#foo" -L +popd &>/dev/null diff --git a/tests/functional/meson.build b/tests/functional/meson.build index cbf6a1563..7a9c7182f 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -71,6 +71,7 @@ functional_tests_scripts = [ 'flakes/build-paths.sh', 'flakes/flake-registry.sh', 'flakes/flake-in-submodule.sh', + 'flakes/subdir-flake.sh', 'gc.sh', 'nix-collect-garbage-d.sh', 'nix-collect-garbage-dry-run.sh', @@ -94,6 +95,7 @@ functional_tests_scripts = [ 'fetchGitRefs.sh', 'gc-runtime.sh', 'tarball.sh', + 'fetchers.sh', 'fetchGit.sh', 'fetchurl.sh', 'fetchPath.sh', diff --git a/tests/functional/repl_characterization/meson.build b/tests/functional/repl_characterization/meson.build index 56410cfd2..79de9a5f5 100644 --- a/tests/functional/repl_characterization/meson.build +++ b/tests/functional/repl_characterization/meson.build @@ -7,6 +7,7 @@ repl_characterization_tester = executable( 'test-repl-characterization', repl_characterization_tester_sources, dependencies : [ + libasanoptions, liblixutil, liblixutil_test_support, sodium, diff --git a/tests/functional/test-libstoreconsumer/meson.build b/tests/functional/test-libstoreconsumer/meson.build index ad96aac12..63d0c97ac 100644 --- a/tests/functional/test-libstoreconsumer/meson.build +++ b/tests/functional/test-libstoreconsumer/meson.build @@ -2,6 +2,7 @@ libstoreconsumer_tester = executable( 'test-libstoreconsumer', 'main.cc', dependencies : [ + libasanoptions, liblixutil, liblixstore, sodium, diff --git a/tests/nixos/tarball-flakes.nix b/tests/nixos/tarball-flakes.nix index ca7627bd1..5deba4a12 100644 --- a/tests/nixos/tarball-flakes.nix +++ b/tests/nixos/tarball-flakes.nix @@ -69,7 +69,7 @@ in # Check that we got redirected to the immutable URL. locked_url = info["locked"]["url"] - assert locked_url == "http://localhost/stable/${nixpkgs.rev}.tar.gz", f"{locked_url=} != http://localhost/stable/${nixpkgs.rev}.tar.gz" + assert locked_url == "http://localhost/stable/${nixpkgs.rev}.tar.gz?rev=${nixpkgs.rev}&revCount=1234", f"{locked_url=} != http://localhost/stable/${nixpkgs.rev}.tar.gz" # Check that we got the rev and revCount attributes. revision = info["revision"] diff --git a/tests/unit/meson.build b/tests/unit/meson.build index c449b2276..55c7566bd 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -11,6 +11,10 @@ # functions, the result would be way less readable than just a bit of copypasta. # It's only ~200 lines; better to just refactor the tests themselves which we'll want to do anyway. +default_test_env = { + 'ASAN_OPTIONS': 'detect_leaks=0:halt_on_error=1:abort_on_error=1:print_summary=1:dump_instruction_bytes=1' +} + libutil_test_support_sources = files( 'libutil-support/tests/cli-literate-parser.cc', 'libutil-support/tests/hash.cc', @@ -63,6 +67,7 @@ libutil_tester = executable( 'liblixutil-tests', libutil_tests_sources, dependencies : [ + libasanoptions, rapidcheck, gtest, boehm, @@ -78,7 +83,7 @@ test( 'libutil-unit-tests', libutil_tester, args : tests_args, - env : { + env : default_test_env + { '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libutil/data', }, suite : 'check', @@ -132,6 +137,7 @@ libstore_tester = executable( 'liblixstore-tests', libstore_tests_sources, dependencies : [ + libasanoptions, liblixstore_test_support, liblixutil_test_support, liblixstore_mstatic, @@ -147,7 +153,7 @@ test( 'libstore-unit-tests', libstore_tester, args : tests_args, - env : { + env : default_test_env + { '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libstore/data', }, suite : 'check', @@ -196,6 +202,7 @@ libexpr_tester = executable( 'liblixexpr-tests', libexpr_tests_sources, dependencies : [ + libasanoptions, liblixexpr_test_support, liblixstore_test_support, liblixstore_mstatic, @@ -214,7 +221,7 @@ test( 'libexpr-unit-tests', libexpr_tester, args : tests_args, - env : { + env : default_test_env + { '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libexpr/data', }, suite : 'check', @@ -226,6 +233,7 @@ libcmd_tester = executable( 'liblixcmd-tests', files('libcmd/args.cc'), dependencies : [ + libasanoptions, liblixcmd, liblixutil, liblixmain, @@ -241,7 +249,7 @@ test( 'libcmd-unit-tests', libcmd_tester, args : tests_args, - env : { + env : default_test_env + { # No special meaning here, it's just a file laying around that is unlikely to go anywhere # any time soon. '_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'src/nix-env/buildenv.nix', @@ -272,6 +280,7 @@ test( 'libmain-unit-tests', libmain_tester, args : tests_args, + env : default_test_env, suite : 'check', protocol : 'gtest', ) diff --git a/version.json b/version.json index 48db2994f..809358e6d 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,5 @@ { "version": "2.91.0-dev", + "official_release": false, "release_name": "TBA" } |