diff --git a/.version b/.version index 544fe5d43..5506598e0 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.32.2 +2.32.3 diff --git a/doc/manual/generate-store-types.nix b/doc/manual/generate-store-types.nix index a03d3d621..4e06c7f60 100644 --- a/doc/manual/generate-store-types.nix +++ b/doc/manual/generate-store-types.nix @@ -24,9 +24,9 @@ let in concatStringsSep "\n" (map showEntry storesList); - "index.md" = - replaceStrings [ "@store-types@" ] [ index ] - (readFile ./source/store/types/index.md.in); + "index.md" = replaceStrings [ "@store-types@" ] [ index ] ( + readFile ./source/store/types/index.md.in + ); tableOfContents = let diff --git a/flake.lock b/flake.lock index cc2b2f27e..63290ef86 100644 --- a/flake.lock +++ b/flake.lock @@ -63,16 +63,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1756178832, - "narHash": "sha256-O2CIn7HjZwEGqBrwu9EU76zlmA5dbmna7jL1XUmAId8=", + "lastModified": 1761597516, + "narHash": "sha256-wxX7u6D2rpkJLWkZ2E932SIvDJW8+ON/0Yy8+a5vsDU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d98ce345cdab58477ca61855540999c86577d19d", + "rev": "daf6dc47aa4b44791372d6139ab7b25269184d55", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.05-small", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index a2bdeb0e5..93502d71e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "The purely functional package manager"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05-small"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; inputs.nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2"; inputs.nixpkgs-23-11.url = "github:NixOS/nixpkgs/a62e6edd6d5e1fa0329b8653c801147986f8d446"; diff --git a/misc/zsh/completion.zsh b/misc/zsh/completion.zsh index eb26a16cb..6146455fe 100644 --- a/misc/zsh/completion.zsh +++ b/misc/zsh/completion.zsh @@ -1,5 +1,5 @@ -# shellcheck disable=all #compdef nix +# shellcheck disable=all function _nix() { local ifs_bk="$IFS" diff --git a/nix-meson-build-support/common/meson.build b/nix-meson-build-support/common/meson.build index 2944a733b..595503f61 100644 --- a/nix-meson-build-support/common/meson.build +++ b/nix-meson-build-support/common/meson.build @@ -42,7 +42,7 @@ if cxx.get_id() == 'clang' add_project_arguments('-fpch-instantiate-templates', language : 'cpp') endif -# Darwin ld doesn't like "X.Y.Zpre" -nix_soversion = meson.project_version().split('pre')[0] +# Darwin ld doesn't like "X.Y.ZpreABCD+W" +nix_soversion = meson.project_version().split('+')[0].split('pre')[0] subdir('assert-fail') diff --git a/src/libexpr/include/nix/expr/attr-set.hh b/src/libexpr/include/nix/expr/attr-set.hh index 46eecd9bd..f57302c42 100644 --- a/src/libexpr/include/nix/expr/attr-set.hh +++ b/src/libexpr/include/nix/expr/attr-set.hh @@ -5,6 +5,7 @@ #include "nix/expr/symbol-table.hh" #include +#include #include #include @@ -463,12 +464,48 @@ private: return bindings->baseLayer; } + /** + * If the bindings gets "layered" on top of another we need to recalculate + * the number of unique attributes in the chain. + * + * This is done by either iterating over the base "layer" and the newly added + * attributes and counting duplicates. If the base "layer" is big this approach + * is inefficient and we fall back to doing per-element binary search in the base + * "layer". + */ void finishSizeIfNecessary() { - if (hasBaseLayer()) - /* NOTE: Do not use std::ranges::distance, since Bindings is a sized - range, but we are calculating this size here. */ - bindings->numAttrsInChain = std::distance(bindings->begin(), bindings->end()); + if (!hasBaseLayer()) + return; + + auto & base = *bindings->baseLayer; + auto attrs = std::span(bindings->attrs, bindings->numAttrs); + + Bindings::size_type duplicates = 0; + + /* If the base bindings is smaller than the newly added attributes + iterate using std::set_intersection to run in O(|base| + |attrs|) = + O(|attrs|). Otherwise use an O(|attrs| * log(|base|)) per-attr binary + search to check for duplicates. Note that if we are in this code path then + |attrs| <= bindingsUpdateLayerRhsSizeThreshold, which 16 by default. We are + optimizing for the case when a small attribute set gets "layered" on top of + a much larger one. When attrsets are already small it's fine to do a linear + scan, but we should avoid expensive iterations over large "base" attrsets. */ + if (attrs.size() > base.size()) { + std::set_intersection( + base.begin(), + base.end(), + attrs.begin(), + attrs.end(), + boost::make_function_output_iterator([&]([[maybe_unused]] auto && _) { ++duplicates; })); + } else { + for (const auto & attr : attrs) { + if (base.get(attr.name)) + ++duplicates; + } + } + + bindings->numAttrsInChain = base.numAttrsInChain + attrs.size() - duplicates; } public: diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 9334dc1cb..c8311c17f 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -164,8 +164,7 @@ struct GitInputScheme : InputScheme { std::optional inputFromURL(const Settings & settings, const ParsedURL & url, bool requireTree) const override { - auto parsedScheme = parseUrlScheme(url.scheme); - if (parsedScheme.application != "git") + if (url.scheme != "git" && parseUrlScheme(url.scheme).application != "git") return {}; auto url2(url); diff --git a/src/libflake-tests/flakeref.cc b/src/libflake-tests/flakeref.cc index e2cb91bb8..3879a5ba1 100644 --- a/src/libflake-tests/flakeref.cc +++ b/src/libflake-tests/flakeref.cc @@ -199,6 +199,28 @@ INSTANTIATE_TEST_SUITE_P( .description = "flake_id_ref_branch_ignore_empty_segments_ref_rev", .expectedUrl = "flake:nixpkgs/branch/2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", }, + InputFromURLTestCase{ + .url = "git://somewhere/repo?ref=branch", + .attrs = + { + {"type", Attr("git")}, + {"ref", Attr("branch")}, + {"url", Attr("git://somewhere/repo")}, + }, + .description = "plain_git_with_ref", + .expectedUrl = "git://somewhere/repo?ref=branch", + }, + InputFromURLTestCase{ + .url = "git+https://somewhere.aaaaaaa/repo?ref=branch", + .attrs = + { + {"type", Attr("git")}, + {"ref", Attr("branch")}, + {"url", Attr("https://somewhere.aaaaaaa/repo")}, + }, + .description = "git_https_with_ref", + .expectedUrl = "git+https://somewhere.aaaaaaa/repo?ref=branch", + }, InputFromURLTestCase{ // Note that this is different from above because the "flake id" shorthand // doesn't allow this. diff --git a/src/libflake/flakeref.cc b/src/libflake/flakeref.cc index 38979783d..2474c2cc4 100644 --- a/src/libflake/flakeref.cc +++ b/src/libflake/flakeref.cc @@ -80,7 +80,8 @@ std::pair parsePathFlakeRefWithFragment( std::smatch match; auto succeeds = std::regex_match(url, match, pathFlakeRegex); - assert(succeeds); + if (!succeeds) + throw Error("invalid flakeref '%s'", url); auto path = match[1].str(); auto query = decodeQuery(match[3].str(), /*lenient=*/true); auto fragment = percentDecode(match[5].str()); diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt new file mode 100644 index 000000000..e093c3f30 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt @@ -0,0 +1 @@ +ssh://userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0]?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt new file mode 100644 index 000000000..8375d3c6d --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt @@ -0,0 +1 @@ +ssh://userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt new file mode 100644 index 000000000..f5a09c2f7 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt @@ -0,0 +1 @@ +ssh://userinfo@fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt new file mode 100644 index 000000000..3bef5e73f --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt @@ -0,0 +1 @@ +ssh://userinfo@fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt new file mode 100644 index 000000000..3db9f9910 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt @@ -0,0 +1 @@ +ssh://fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt new file mode 100644 index 000000000..ad199cfde --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt @@ -0,0 +1 @@ +ssh://fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0 \ No newline at end of file diff --git a/src/libstore-tests/store-reference.cc b/src/libstore-tests/store-reference.cc index 7b42b45a2..a52b92b78 100644 --- a/src/libstore-tests/store-reference.cc +++ b/src/libstore-tests/store-reference.cc @@ -183,4 +183,64 @@ static StoreReference sshIPv6AuthorityWithUserinfoAndParams{ URI_TEST_READ(ssh_unbracketed_ipv6_3, sshIPv6AuthorityWithUserinfoAndParams) +static const StoreReference sshIPv6AuthorityWithUserinfoAndParamsAndZoneId{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_4, sshIPv6AuthorityWithUserinfoAndParamsAndZoneId) +URI_TEST_READ(ssh_unbracketed_ipv6_5, sshIPv6AuthorityWithUserinfoAndParamsAndZoneId) + +static const StoreReference sshIPv6AuthorityWithUserinfoAndParamsAndZoneIdTricky{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%2525]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +// Non-standard syntax where the IPv6 literal appears without brackets. In +// this case don't considering %25 to be a pct-encoded % and just take it as a +// literal value. 25 is a perfectly legal ZoneId value in theory. +URI_TEST_READ(ssh_unbracketed_ipv6_6, sshIPv6AuthorityWithUserinfoAndParamsAndZoneIdTricky) +URI_TEST_READ(ssh_unbracketed_ipv6_7, sshIPv6AuthorityWithUserinfoAndParamsAndZoneId) + +static const StoreReference sshIPv6AuthorityWithParamsAndZoneId{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_8, sshIPv6AuthorityWithParamsAndZoneId) + +static const StoreReference sshIPv6AuthorityWithZoneId{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]", + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_9, sshIPv6AuthorityWithZoneId) + } // namespace nix diff --git a/src/libstore/build/derivation-building-goal.cc b/src/libstore/build/derivation-building-goal.cc index 001816ca0..310b2bf84 100644 --- a/src/libstore/build/derivation-building-goal.cc +++ b/src/libstore/build/derivation-building-goal.cc @@ -860,7 +860,15 @@ Goal::Co DerivationBuildingGoal::tryToBuild() { builder.reset(); StorePathSet outputPaths; - for (auto & [_, output] : builtOutputs) { + /* In the check case we install no store objects, and so + `builtOutputs` is empty. However, per issue #14287, there is + an expectation that the post-build hook is still executed. + (This is useful for e.g. logging successful deterministic rebuilds.) + + In order to make that work, in the check case just load the + (preexisting) infos from scratch, rather than relying on what + `DerivationBuilder` returned to us. */ + for (auto & [_, output] : buildMode == bmCheck ? checkPathValidity(initialOutputs).second : builtOutputs) { // for sake of `bmRepair` worker.markContentsGood(output.outPath); outputPaths.insert(output.outPath); diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 2e57c1708..cd4a2df6f 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -182,7 +182,19 @@ Goal::Co DerivationGoal::haveDerivation() } } - assert(success.builtOutputs.count(wantedOutput) > 0); + /* If the wanted output is not in builtOutputs (e.g., because it + was already valid and therefore not re-registered), we need to + add it ourselves to ensure we return the correct information. */ + if (success.builtOutputs.count(wantedOutput) == 0) { + debug( + "BUG! wanted output '%s' not in builtOutputs, working around by adding it manually", wantedOutput); + auto realisation = assertPathValidity(); + realisation.id = DrvOutput{ + .drvHash = outputHash, + .outputName = wantedOutput, + }; + success.builtOutputs.emplace(wantedOutput, std::move(realisation)); + } } } diff --git a/src/libstore/store-reference.cc b/src/libstore/store-reference.cc index 96ee829d0..01e197be7 100644 --- a/src/libstore/store-reference.cc +++ b/src/libstore/store-reference.cc @@ -121,7 +121,27 @@ StoreReference StoreReference::parse(const std::string & uri, const StoreReferen * greedily assumed to be the part of the host address. */ auto authorityString = schemeAndAuthority->authority; auto userinfo = splitPrefixTo(authorityString, '@'); - auto maybeIpv6 = boost::urls::parse_ipv6_address(authorityString); + /* Back-compat shim for ZoneId specifiers. Technically this isn't + * standard, but the expectation is this works with the old syntax + * for ZoneID specifiers. For the full story behind the fiasco that + * is ZoneID in URLs look at [^]. + * [^]: https://datatracker.ietf.org/doc/html/draft-schinazi-httpbis-link-local-uri-bcp-03 + */ + + /* Fish out the internals from inside square brackets. It might be that the pct-sign is unencoded and that's + * why we failed to parse it previously. */ + if (authorityString.starts_with('[') && authorityString.ends_with(']')) { + authorityString.remove_prefix(1); + authorityString.remove_suffix(1); + } + + auto maybeBeforePct = splitPrefixTo(authorityString, '%'); + bool hasZoneId = maybeBeforePct.has_value(); + auto maybeZoneId = hasZoneId ? std::optional{authorityString} : std::nullopt; + + std::string_view maybeIpv6S = maybeBeforePct.value_or(authorityString); + auto maybeIpv6 = boost::urls::parse_ipv6_address(maybeIpv6S); + if (maybeIpv6) { std::string fixedAuthority; if (userinfo) { @@ -129,7 +149,11 @@ StoreReference StoreReference::parse(const std::string & uri, const StoreReferen fixedAuthority += '@'; } fixedAuthority += '['; - fixedAuthority += authorityString; + fixedAuthority += maybeIpv6S; + if (maybeZoneId) { + fixedAuthority += "%25"; // pct-encoded percent character + fixedAuthority += *maybeZoneId; + } fixedAuthority += ']'; return { .variant = diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 8c9e1f1eb..6c76659dd 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -64,7 +64,7 @@ boost = dependency( 'url', ], include_type : 'system', - version : '>=1.82.0', + version : '>=1.87.0', ) # boost is a public dependency, but not a pkg-config dependency unfortunately, so we # put in `deps_other`. diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 18be64bba..f4850fa92 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -798,8 +798,6 @@ struct CmdFlakeCheck : FlakeCommand // via substitution, as `nix flake check` only needs to verify buildability, // not actually produce the outputs. auto missing = store->queryMissing(drvPaths); - // Only occurs if `drvPaths` contains a `DerivedPath::Opaque`, which should never happen - assert(missing.unknown.empty()); std::vector toBuild; for (auto & path : missing.willBuild) { diff --git a/tests/functional/build-hook-list-paths.sh b/tests/functional/build-hook-list-paths.sh new file mode 100755 index 000000000..03691c2d2 --- /dev/null +++ b/tests/functional/build-hook-list-paths.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -x +set -e + +[ -n "$OUT_PATHS" ] +[ -n "$DRV_PATH" ] +[ -n "$HOOK_DEST" ] + +for o in $OUT_PATHS; do + echo "$o" >> "$HOOK_DEST" +done diff --git a/tests/functional/post-hook.sh b/tests/functional/post-hook.sh index 67bb46377..b16d8ab84 100755 --- a/tests/functional/post-hook.sh +++ b/tests/functional/post-hook.sh @@ -29,6 +29,18 @@ nix-build -o "$TEST_ROOT"/result dependencies.nix --post-build-hook "$pushToStor export BUILD_HOOK_ONLY_OUT_PATHS=$([ ! "$NIX_TESTS_CA_BY_DEFAULT" ]) nix-build -o "$TEST_ROOT"/result-mult multiple-outputs.nix -A a.first --post-build-hook "$pushToStore" +if isDaemonNewer "2.33.0pre20251029"; then + # Regression test for issue #14287: `--check` should re-run post build + # hook, even though nothing is getting newly registered. + export HOOK_DEST=$TEST_ROOT/listing + # Needed so the hook will get the above environment variable. + restartDaemon + nix-build -o "$TEST_ROOT"/result-mult multiple-outputs.nix --check -A a.first --post-build-hook "$PWD/build-hook-list-paths.sh" + grepQuiet a-first "$HOOK_DEST" + grepQuiet a-second "$HOOK_DEST" + unset HOOK_DEST +fi + clearStore # Ensure that the remote store contains both the runtime and build-time