diff --git a/src/libcmd/builtin-flake-schemas.nix b/src/libcmd/builtin-flake-schemas.nix index f326b413c9e6..a8b78b167eb9 100644 --- a/src/libcmd/builtin-flake-schemas.nix +++ b/src/libcmd/builtin-flake-schemas.nix @@ -82,12 +82,12 @@ inventory = self.lib.derivationsInventory "package" false; }; - dockerImagesSchema = { + ociImagesSchema = { version = 1; doc = '' - The `dockerImages` flake output contains derivations that build valid Docker images. + The `ociImages` flake output contains derivations that build valid Open Container Initiative images. ''; - inventory = self.lib.derivationsInventory "Docker image" false; + inventory = self.lib.derivationsInventory "OCI image" false; }; legacyPackagesSchema = { @@ -432,7 +432,7 @@ schemas.homeModules = homeModulesSchema; schemas.darwinConfigurations = darwinConfigurationsSchema; schemas.darwinModules = darwinModulesSchema; - schemas.dockerImages = dockerImagesSchema; + schemas.ociImages = ociImagesSchema; schemas.bundlers = bundlersSchema; }; } diff --git a/src/libcmd/flake-schemas.cc b/src/libcmd/flake-schemas.cc index 725326c0fd75..44fb4d22fe2b 100644 --- a/src/libcmd/flake-schemas.cc +++ b/src/libcmd/flake-schemas.cc @@ -3,6 +3,7 @@ #include "nix/fetchers/fetch-to-store.hh" #include "nix/util/memory-source-accessor.hh" #include "nix/util/mounted-source-accessor.hh" +#include "nix/flake/provenance.hh" namespace nix::flake_schemas { @@ -162,12 +163,18 @@ void forEachOutput( void visit( std::optional system, ref node, + std::shared_ptr provenance, std::function visitLeaf, std::function)> visitNonLeaf, std::function node, const std::vector & systems)> visitFiltered) { Activity act(*logger, lvlInfo, actUnknown, fmt("evaluating '%s'", node->getAttrPathStr())); + PushProvenance pushedProvenance( + node->root->state, + provenance ? std::make_shared(provenance, node->getAttrPathStr(), evalSettings.pureEval) + : nullptr); + /* Apply the system type filter. */ if (system) { if (auto forSystems = Node(node).forSystems()) { diff --git a/src/libcmd/include/nix/cmd/flake-schemas.hh b/src/libcmd/include/nix/cmd/flake-schemas.hh index c1ceefdc35aa..32653d910588 100644 --- a/src/libcmd/include/nix/cmd/flake-schemas.hh +++ b/src/libcmd/include/nix/cmd/flake-schemas.hh @@ -65,6 +65,7 @@ typedef std::function attr, bool isLast)> void visit( std::optional system, ref node, + std::shared_ptr provenance, std::function visitLeaf, std::function)> visitNonLeaf, std::function node, const std::vector & systems)> visitFiltered); diff --git a/src/libcmd/include/nix/cmd/installable-attr-path.hh b/src/libcmd/include/nix/cmd/installable-attr-path.hh index 474bb358ec91..ef9dac813346 100644 --- a/src/libcmd/include/nix/cmd/installable-attr-path.hh +++ b/src/libcmd/include/nix/cmd/installable-attr-path.hh @@ -21,8 +21,6 @@ #include #include -#include - namespace nix { class InstallableAttrPath : public InstallableValue diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index 4acfd91969ae..982a4f0b6928 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -422,6 +422,22 @@ std::string BaseSetting::to_string() const return nlohmann::json(value).dump(); } +template<> +std::map BaseSetting>::parse(const std::string & str) const +{ + try { + return nlohmann::json::parse(str).template get>(); + } catch (std::exception & e) { + throw UsageError("parsing setting '%s': %s", name, e.what()); + } +} + +template<> +std::string BaseSetting>::to_string() const +{ + return nlohmann::json(value).dump(); +} + template<> void BaseSetting::appendOrSet(PathsInChroot newValue, bool append) { diff --git a/src/libstore/include/nix/store/globals.hh b/src/libstore/include/nix/store/globals.hh index 23ffc0d49f8c..d0a134b9c4e1 100644 --- a/src/libstore/include/nix/store/globals.hh +++ b/src/libstore/include/nix/store/globals.hh @@ -1462,6 +1462,15 @@ public: )"}; std::optional getHostName(); + + Setting> buildProvenanceTags{ + this, + {}, + "build-provenance-tags", + R"( + Arbitrary name/value pairs that are recorded in the build provenance of store paths built by this machine. + This can be used to tag builds with metadata such as the CI job URL, build cluster name, etc. + )"}; }; // FIXME: don't use a global variable. diff --git a/src/libstore/include/nix/store/provenance.hh b/src/libstore/include/nix/store/provenance.hh index f742888b362a..c3ae4f8a6b23 100644 --- a/src/libstore/include/nix/store/provenance.hh +++ b/src/libstore/include/nix/store/provenance.hh @@ -23,6 +23,11 @@ struct BuildProvenance : Provenance */ std::optional buildHost; + /** + * User-defined tags from the build host. + */ + std::map tags; + /** * The system type of the derivation. */ @@ -39,15 +44,9 @@ struct BuildProvenance : Provenance const StorePath & drvPath, const OutputName & output, std::optional buildHost, + std::map tags, std::string system, - std::shared_ptr next) - : drvPath(drvPath) - , output(output) - , buildHost(std::move(buildHost)) - , system(std::move(system)) - , next(std::move(next)) - { - } + std::shared_ptr next); nlohmann::json to_json() const override; }; diff --git a/src/libstore/provenance.cc b/src/libstore/provenance.cc index 0fa38658d769..9fde815b4d06 100644 --- a/src/libstore/provenance.cc +++ b/src/libstore/provenance.cc @@ -1,8 +1,35 @@ #include "nix/store/provenance.hh" #include "nix/util/json-utils.hh" +#include + namespace nix { +static void checkProvenanceTagName(std::string_view name) +{ + static const std::regex tagNameRegex("^[A-Za-z_][A-Za-z0-9_+\\-]*$"); + if (!std::regex_match(name.begin(), name.end(), tagNameRegex)) + throw Error("tag name '%s' is invalid", name); +} + +BuildProvenance::BuildProvenance( + const StorePath & drvPath, + const OutputName & output, + std::optional buildHost, + std::map tags, + std::string system, + std::shared_ptr next) + : drvPath(drvPath) + , output(output) + , buildHost(std::move(buildHost)) + , tags(std::move(tags)) + , system(std::move(system)) + , next(std::move(next)) +{ + for (const auto & [name, value] : this->tags) + checkProvenanceTagName(name); +} + nlohmann::json BuildProvenance::to_json() const { return { @@ -12,6 +39,7 @@ nlohmann::json BuildProvenance::to_json() const {"buildHost", buildHost}, {"system", system}, {"next", next ? next->to_json() : nlohmann::json(nullptr)}, + {"tags", tags}, }; } @@ -23,10 +51,14 @@ Provenance::Register registerBuildProvenance("build", [](nlohmann::json json) { std::optional buildHost; if (auto p = optionalValueAt(obj, "buildHost")) buildHost = p->get>(); + std::map tags; + if (auto p = optionalValueAt(obj, "tags"); p && !p->is_null()) + tags = p->get>(); auto buildProv = make_ref( StorePath(getString(valueAt(obj, "drv"))), getString(valueAt(obj, "output")), - buildHost, + std::move(buildHost), + std::move(tags), getString(valueAt(obj, "system")), next); return buildProv; diff --git a/src/libstore/unix/build/derivation-builder.cc b/src/libstore/unix/build/derivation-builder.cc index a0790d7523b4..d47776655e5d 100644 --- a/src/libstore/unix/build/derivation-builder.cc +++ b/src/libstore/unix/build/derivation-builder.cc @@ -1873,7 +1873,12 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs() newInfo.ultimate = true; if (experimentalFeatureSettings.isEnabled(Xp::Provenance)) newInfo.provenance = std::make_shared( - drvPath, outputName, settings.getHostName(), drv.platform, drvProvenance); + drvPath, + outputName, + settings.getHostName(), + settings.buildProvenanceTags.get(), + drv.platform, + drvProvenance); store.signPathInfo(newInfo); finish(newInfo.path); diff --git a/src/nix/flake.cc b/src/nix/flake.cc index 454e10d4ecf2..c0587742691f 100644 --- a/src/nix/flake.cc +++ b/src/nix/flake.cc @@ -383,6 +383,7 @@ struct CmdFlakeCheck : FlakeCommand, MixFlakeSchemas flake_schemas::visit( checkAllSystems ? std::optional() : localSystem, node, + flake->flake.provenance, [&](const flake_schemas::Leaf & leaf) { try { @@ -899,6 +900,7 @@ struct CmdFlakeShow : FlakeCommand, MixJSON, MixFlakeSchemas flake_schemas::visit( showAllSystems ? std::optional() : localSystem, node, + flake->flake.provenance, [&](const flake_schemas::Leaf & leaf) { if (auto what = leaf.what()) diff --git a/src/nix/provenance.cc b/src/nix/provenance.cc index 2df68128f458..3fddb42e789a 100644 --- a/src/nix/provenance.cc +++ b/src/nix/provenance.cc @@ -79,6 +79,8 @@ struct CmdProvenanceShow : StorePathsCommand build->output, build->buildHost.value_or("unknown host").c_str(), build->system); + for (auto & [tagName, tagValue] : build->tags) + logger->cout(" tag " ANSI_BOLD "%s" ANSI_NORMAL ": %s", tagName, tagValue); provenance = build->next; } diff --git a/tests/functional/common/init.sh b/tests/functional/common/init.sh index 41e1851160a7..6d98b7ae357e 100755 --- a/tests/functional/common/init.sh +++ b/tests/functional/common/init.sh @@ -53,6 +53,7 @@ substituters = flake-registry = $TEST_ROOT/registry.json show-trace = true host-name = test-host +build-provenance-tags = {"pr": "1234", "branch": "main"} include nix.conf.extra trusted-users = $(whoami) ${_NIX_TEST_EXTRA_CONFIG:-} diff --git a/tests/functional/flakes/provenance.sh b/tests/functional/flakes/provenance.sh index 933436983f38..2004ccd5e693 100644 --- a/tests/functional/flakes/provenance.sh +++ b/tests/functional/flakes/provenance.sh @@ -58,6 +58,10 @@ builder=$(nix eval --raw "$flake1Dir#packages.$system.default._builder") }, "output": "out", "system": "$system", + "tags": { + "branch": "main", + "pr": "1234" + }, "type": "build" } EOF @@ -172,6 +176,10 @@ nix copy --from "file://$binaryCache" "$outPath" --no-check-sigs }, "output": "out", "system": "$system", + "tags": { + "branch": "main", + "pr": "1234" + }, "type": "build" }, "type": "copied" @@ -186,6 +194,8 @@ unset _NIX_FORCE_HTTP $outPath ← copied from file://$binaryCache ← built from derivation $drvPath (output out) on test-host for $system + tag branch: main + tag pr: 1234 ← with derivation metadata { "license": [ @@ -258,6 +268,8 @@ nix build --impure --print-out-paths --no-link "$flake1Dir#packages.$system.defa [[ "$(nix provenance show "$outPath")" = "$(cat < "$TEST_ROOT/hello.txt" expectStderr 1 nix provenance verify "$path" | grepQuiet "hash mismatch for URL" + +# Test invalid tag names +for name in "123-invalid" "invalid tag" "invalid@tag" "-invalid" " foo"; do + expectStderr 1 nix build --build-provenance-tags "{\"$name\": \"value\"}" --no-link "$flake1Dir#packages.$system.default" 2>&1 | grepQuiet "tag name '$name' is invalid" +done