diff --git a/src/libmain/include/nix/main/shared.hh b/src/libmain/include/nix/main/shared.hh index 47d08a05042..ab58b7c7e06 100644 --- a/src/libmain/include/nix/main/shared.hh +++ b/src/libmain/include/nix/main/shared.hh @@ -39,9 +39,10 @@ void printGCWarning(); class Store; struct MissingPaths; -void printMissing(ref store, const std::vector & paths, Verbosity lvl = lvlInfo); +void printMissing( + ref store, const std::vector & paths, Verbosity lvl = lvlInfo, bool intendToRealise = true); -void printMissing(ref store, const MissingPaths & missing, Verbosity lvl = lvlInfo); +void printMissing(ref store, const MissingPaths & missing, Verbosity lvl = lvlInfo, bool intendToRealise = true); std::string getArg(const std::string & opt, Strings::iterator & i, const Strings::iterator & end); diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 7187e972059..b125ffb135d 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -45,18 +45,21 @@ void printGCWarning() "the result might be removed by the garbage collector"); } -void printMissing(ref store, const std::vector & paths, Verbosity lvl) +void printMissing(ref store, const std::vector & paths, Verbosity lvl, bool intendToRealise) { - printMissing(store, store->queryMissing(paths), lvl); + printMissing(store, store->queryMissing(paths), lvl, intendToRealise); } -void printMissing(ref store, const MissingPaths & missing, Verbosity lvl) +void printMissing(ref store, const MissingPaths & missing, Verbosity lvl, bool intendToRealise) { if (!missing.willBuild.empty()) { if (missing.willBuild.size() == 1) - printMsg(lvl, "this derivation will be built:"); + printMsg(lvl, intendToRealise ? "this derivation will be built:" : "this derivation would be built:"); else - printMsg(lvl, "these %d derivations will be built:", missing.willBuild.size()); + printMsg( + lvl, + intendToRealise ? "these %d derivations will be built:" : "these %d derivations would be built:", + missing.willBuild.size()); auto sorted = store->topoSortPaths(missing.willBuild); reverse(sorted.begin(), sorted.end()); for (auto & i : sorted) @@ -68,11 +71,16 @@ void printMissing(ref store, const MissingPaths & missing, Verbosity lvl) const float narSizeMiB = missing.narSize / (1024.f * 1024.f); if (missing.willSubstitute.size() == 1) { printMsg( - lvl, "this path will be fetched (%.2f MiB download, %.2f MiB unpacked):", downloadSizeMiB, narSizeMiB); + lvl, + intendToRealise ? "this path will be fetched (%.2f MiB download, %.2f MiB unpacked):" + : "this path can be fetched (%.2f MiB download, %.2f MiB unpacked):", + downloadSizeMiB, + narSizeMiB); } else { printMsg( lvl, - "these %d paths will be fetched (%.2f MiB download, %.2f MiB unpacked):", + intendToRealise ? "these %d paths will be fetched (%.2f MiB download, %.2f MiB unpacked):" + : "these %d paths can be fetched (%.2f MiB download, %.2f MiB unpacked):", missing.willSubstitute.size(), downloadSizeMiB, narSizeMiB); diff --git a/src/nix/check.cc b/src/nix/check.cc new file mode 100644 index 00000000000..191c7889b7e --- /dev/null +++ b/src/nix/check.cc @@ -0,0 +1,109 @@ +#include "nix/cmd/command.hh" +#include "nix/cmd/installable-flake.hh" +#include "nix/main/common-args.hh" +#include "nix/main/shared.hh" +#include "nix/store/store-api.hh" +#include "nix/store/globals.hh" + +using namespace nix; + +struct CmdCheck : InstallablesCommand, MixDryRun +{ + std::string description() override + { + return "check that a derivation can be built or substituted"; + } + + std::string doc() override + { + return +#include "check.md" + ; + } + + Strings getDefaultFlakeAttrPaths() override + { + return {}; + } + + Strings getDefaultFlakeAttrPathPrefixes() override + { + return { + "checks." + settings.thisSystem.get() + ".", + "packages." + settings.thisSystem.get() + ".", + "legacyPackages." + settings.thisSystem.get() + "."}; + } + + void applyDefaultInstallables(std::vector & rawInstallables) override + { + if (rawInstallables.empty()) + throw UsageError( + "'nix check' requires at least one installable argument.\n\nDid you mean 'nix flake check'?"); + } + + void run(ref store, Installables && installables) override + { + // Detect bare flake references without fragments + for (auto & installable : installables) { + if (auto installableFlake = installable.dynamic_pointer_cast()) { + if (installableFlake->attrPaths.empty()) { + throw Error( + "Installable '%s' does not specify which outputs to check.\n" + "Use '%s#' to check a specific output, or 'nix flake check %s' to check all outputs.", + installableFlake->flakeRef.to_string(), + installableFlake->flakeRef.to_string(), + installableFlake->flakeRef.to_string()); + } + } + } + + std::vector pathsToCheck; + + for (auto & i : installables) + for (auto & b : i->toDerivedPaths()) + pathsToCheck.push_back(b.path); + + // Query what needs to be built vs what can be substituted + auto missing = store->queryMissing(pathsToCheck); + + if (dryRun) { + // Use lvlError to always show the output + printMissing(store, missing, lvlError, /* intendToRealise = */ false); + return; + } + + // Only build what cannot be substituted + std::vector toBuild; + // Convert derivation store paths to DerivedPath::Built (same pattern as nix flake check) + for (auto & path : missing.willBuild) { + toBuild.emplace_back( + DerivedPath::Built{ + .drvPath = makeConstantStorePathRef(path), + .outputs = OutputsSpec::All{}, + }); + } + + if (!toBuild.empty()) { + // TODO: Add a Realise mode that performs the build but does not copy + // the outputs to the local store (for remote builders). + store->buildPaths(toBuild); + } + + // Report success for all checked paths + for (auto & path : pathsToCheck) { + std::visit( + overloaded{ + [&](const DerivedPath::Opaque & bo) { + logger->log(lvlNotice, fmt("%s: OK (opaque path)", store->printStorePath(bo.path))); + }, + [&](const DerivedPath::Built & bfd) { + auto drvPath = resolveDerivedPath(*store, *bfd.drvPath); + logger->log(lvlNotice, fmt("%s: OK (available)", store->printStorePath(drvPath))); + }, + }, + path.raw()); + } + } +}; + +static auto rCmdCheck = registerCommand("check"); diff --git a/src/nix/check.md b/src/nix/check.md new file mode 100644 index 00000000000..d09d8b21263 --- /dev/null +++ b/src/nix/check.md @@ -0,0 +1,52 @@ +R""( + +# Examples + +* Check a package from nixpkgs: + + ```console + # nix check nixpkgs#hello + ``` + +* Check a flake check attribute: + + ```console + # nix check .#checks.x86_64-linux.integration-test + ``` + +* Check multiple derivations: + + ```console + # nix check .#package-a .#package-b + ``` + +# Description + +`nix check` verifies that the specified *installables* can be realised. +[Installables](./nix.md#installables) that resolve to derivations are +checked by building them from source, or verifying they can be fetched +from a substituter. + +This command requires at least one installable argument with a specific +attribute path (e.g., `nixpkgs#hello` or `.#checks.x86_64-linux.foo`). Bare +flake references without an attribute path are not accepted. If you want to +check all outputs of a flake, use `nix flake check` instead. + +Unlike `nix build`, this command: +- Does not create result symlinks +- Does not download outputs that are available in substituters +- Only builds derivations that cannot be substituted + +**Note**: When checking substitutability, only metadata is queried - no full +substitution is performed. A broken or unreliable binary cache could still +fail to provide the contents even after `nix check` reports success. + +When looking up flake attributes, this command first searches in +`checks.`, then falls back to `packages.` and +`legacyPackages.` (like `nix build`). + +This command is useful for CI systems and development workflows where you +want to verify that derivations are buildable without creating local store +contents and result symlinks. + +)"" diff --git a/src/nix/meson.build b/src/nix/meson.build index e989e80164f..24437bafc6c 100644 --- a/src/nix/meson.build +++ b/src/nix/meson.build @@ -64,6 +64,7 @@ nix_sources = [ config_priv_h ] + files( 'build.cc', 'bundle.cc', 'cat.cc', + 'check.cc', 'config-check.cc', 'config.cc', 'copy.cc', diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 6f649c8360b..3f6393f44f6 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -129,6 +129,7 @@ suites = [ 'linux-sandbox.sh', 'supplementary-groups.sh', 'build-dry.sh', + 'nix-check.sh', 'structured-attrs.sh', 'shell.sh', 'brotli.sh', diff --git a/tests/functional/nix-check.sh b/tests/functional/nix-check.sh new file mode 100755 index 00000000000..9eec7d26383 --- /dev/null +++ b/tests/functional/nix-check.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +source common.sh + +TODO_NixOS + +################################################### +# Test that nix check requires explicit installables + +output=$(nix check 2>&1) && fail "nix check with no arguments should fail" +echo "$output" | grepQuiet "requires at least one installable" +echo "$output" | grepQuiet "Did you mean 'nix flake check'?" + +################################################### +# Test that nix check rejects bare flake references + +# Create a minimal flake for testing +emptyFlake=$TEST_ROOT/empty-flake +mkdir -p "$emptyFlake" +cat > "$emptyFlake/flake.nix" << 'EOF' +{ + outputs = { self }: { }; +} +EOF +git -C "$emptyFlake" init +git -C "$emptyFlake" add flake.nix + +output=$(nix check "$emptyFlake" 2>&1) && fail "nix check with bare flake reference should fail" +echo "$output" | grepQuiet "does not specify which outputs to check" +echo "$output" | grepQuiet "#" +echo "$output" | grepQuiet "nix flake check" + +################################################### +# Test basic functionality: check derivations and report paths + +clearStore +clearCache + +input1_drvPath=$(nix eval -f dependencies.nix input1_drv.drvPath --raw) +input2_drvPath=$(nix eval -f dependencies.nix input2_drv.drvPath --raw) + +# Single derivation +output=$(nix check -f dependencies.nix input1_drv 2>&1) +echo "$output" | grepQuiet "$input1_drvPath: OK (available)" + +# Multiple derivations +output=$(nix check -f dependencies.nix input1_drv input2_drv 2>&1) +echo "$output" | grepQuiet "$input1_drvPath: OK (available)" +echo "$output" | grepQuiet "$input2_drvPath: OK (available)" + +################################################### +# Test dry-run doesn't build + +clearStore +clearCache + +# With no substituters, this needs to be built +output=$(nix check -f dependencies.nix input1_drv --dry-run 2>&1) +echo "$output" | grepQuiet "would be built" +# Verify nothing was actually built +! nix path-info -f dependencies.nix input1_drv 2>/dev/null || fail "dry-run built the derivation" + +################################################### +# Test no result symlinks created + +RESULT=$TEST_ROOT/result +rm -f "$RESULT"* +nix check -f dependencies.nix input1_drv input2_drv +[[ ! -h $RESULT ]] || fail "nix check created result symlink" + +################################################### +# Test opaque store paths + +clearStore +clearCache + +storePath=$(nix build -f dependencies.nix input1_drv --no-link --print-out-paths) +output=$(nix check "$storePath" 2>&1) +echo "$output" | grepQuiet "OK (opaque path)" + +################################################### +# Test failing derivation + +clearStore +clearCache + +# Create a derivation that will fail +cat > "$TEST_ROOT/failing.nix" <&1 || fail "nix check succeeded on failing derivation" + +################################################### +# Test optimization: +# don't download substitutable paths + +if [[ -n "${NIX_REMOTE:-}" ]]; then + echo "Skipping substituter test with daemon" +else + clearStore + clearCacheCache + + # Set up binary cache with a built derivation + outPath=$(nix-build dependencies.nix --no-out-link -A input1_drv) + input1_drvPath=$(nix eval -f dependencies.nix input1_drv.drvPath --raw) + nix copy --to "file://$cacheDir" "$outPath" + + # Keep the .drv in the store so queryMissing can determine output paths + # but delete the actual output + nix-store --delete "$outPath" + + # Dry-run should report it can be fetched + clearCacheCache + + output=$(nix check -f dependencies.nix input1_drv --dry-run --substituters "file://$cacheDir" --no-require-sigs --option substitute true 2>&1) + echo "$output" | grepQuiet "can be fetched" + + # Check should succeed without downloading + output=$(nix check -f dependencies.nix input1_drv --substituters "file://$cacheDir" --no-require-sigs --option substitute true 2>&1) + echo "$output" | grepQuiet "$input1_drvPath: OK (available)" + + # Verify it didn't download + echo "$output" | grepQuietInverse "copying path.*$outPath" || fail "downloaded substitutable path" + ! nix path-info "$outPath" 2>/dev/null || fail "path exists in local store after check" +fi