From 995f7635f86ffe5f4837303d99c530cfad1d6c05 Mon Sep 17 00:00:00 2001 From: Connor Baker Date: Fri, 28 Feb 2025 22:29:08 +0000 Subject: [PATCH] {makeSetupHook',sourceGuard}: init --- doc/build-helpers/special.md | 1 + .../special/makeSetupHookPrime.section.md | 83 ++++++++++ doc/hooks/index.md | 1 + doc/hooks/sourceGuard.section.md | 8 + doc/redirects.json | 12 ++ pkgs/by-name/ma/makeSetupHook'/package.nix | 96 +++++++++++ pkgs/by-name/ma/makeSetupHook'/tests.nix | 4 + pkgs/by-name/so/sourceGuard/package.nix | 17 ++ pkgs/by-name/so/sourceGuard/sourceGuard.bash | 151 ++++++++++++++++++ pkgs/by-name/so/sourceGuard/tests.nix | 12 ++ pkgs/test/default.nix | 4 + 11 files changed, 389 insertions(+) create mode 100644 doc/build-helpers/special/makeSetupHookPrime.section.md create mode 100644 doc/hooks/sourceGuard.section.md create mode 100644 pkgs/by-name/ma/makeSetupHook'/package.nix create mode 100644 pkgs/by-name/ma/makeSetupHook'/tests.nix create mode 100644 pkgs/by-name/so/sourceGuard/package.nix create mode 100755 pkgs/by-name/so/sourceGuard/sourceGuard.bash create mode 100644 pkgs/by-name/so/sourceGuard/tests.nix diff --git a/doc/build-helpers/special.md b/doc/build-helpers/special.md index 9da278f094dd8e..1502dad048c45a 100644 --- a/doc/build-helpers/special.md +++ b/doc/build-helpers/special.md @@ -6,6 +6,7 @@ This chapter describes several special build helpers. special/fakenss.section.md special/fhs-environments.section.md special/makesetuphook.section.md +special/makeSetupHookPrime.section.md special/mkshell.section.md special/vm-tools.section.md special/checkpoint-build.section.md diff --git a/doc/build-helpers/special/makeSetupHookPrime.section.md b/doc/build-helpers/special/makeSetupHookPrime.section.md new file mode 100644 index 00000000000000..98887c17384b50 --- /dev/null +++ b/doc/build-helpers/special/makeSetupHookPrime.section.md @@ -0,0 +1,83 @@ +# makeSetupHook' {#build-helpers-special-makeSetupHookPrime} + +`makeSetupHook'` is a build helper which produces hooks which may be added to a derivation's `nativeBuildInputs`. + +This helper differs from `makeSetupHook`[#sec-pkgs.makesetuphook] in several ways: + +- it uses `pkgs.replaceVars` instead of the bash function `substituteAll` +- it uses `sourceGuard`[#setup-hook-sourceGuard] to source the script +- `strictDeps` is set to `true` +- script dependencies are provided using the `nativeBuildInputs` and `buildInputs` arguments rather than `propagatedBuildInputs` and `depsTargetTargetPropagated` +- the script argument must be a path and is interpolated into a string, causing Nix to create a store path for only it, enforcing isolation + + +:::{.example #ex-makeSetupHookPrime-doc-example} + +# Usage example of makeSetupHook' + +Re-using the example from [`makeSetupHook`](#sec-pkgs.makeSetupHook-usage-example): + +```nix +pkgs.makeSetupHook' { + name = "run-hello-hook"; + script = writeScript "run-hello-hook.sh" '' + #!@shell@ + # the direct path to the executable has to be here because + # this will be run when the file is sourced + # at which point '$PATH' has not yet been populated with inputs + @cowsay@ cow + + _printHelloHook() { + hello + } + preConfigureHooks+=(_printHelloHook) + ''; + nativeBuildInputs = [ + pkgs.cowsay + pkgs.hello + ]; + replacements = { + cowsay = lib.getExe pkgs.cowsay; + shell = lib.getExe pkgs.bash; + }; +} +``` + +::: + +## Inputs {#build-helpers-special-makeSetupHookPrime-inputs} + +`name` (string) + +: The name of the hook. + When `useSourceGuard` is enabled, `name` is used as the `guardName` argument to `sourceGuard`. + +`script` (path-like) + +: The derivation or store path to make into a hook. + The script path is interpolated into a string, causing Nix to create a store path for only it, enforcing isolation. + Values in the script may be replaced using the `replacements` argument. + +`nativeBuildInputs` (array of path-like values, optional) +: A list of derivations or store paths which should be added to the `nativeBuildInputs` of derivations which include this hook in their `nativeBuildInputs`. + When not provided, this value defaults to `[ ]`. + +`buildInputs` (array of path-like values, optional) +: A list of derivations or store paths which should be added to the `buildInputs` of derivations which include this hook in their `nativeBuildInputs`. + When not provided, this value defaults to `[ ]`. + +`useSourceGuard` (boolean, optional) +: Whether to use `sourceGuard`[#setup-hook-sourceGuard] to source the hook. + When not provided, this value defaults to `true`. + +`replacements` (attribute set of string-like values, optional) +: A map of string-like values which are used to replace variables in `script`. + When not provided, this value defaults to `{ }`. + +`passthru` (attribute set, optional) +: A map of values which are passed to the `passthru` attribute of the hook derivation. + When not provided, this value defaults to `{ }`. + +`meta` (attribute set, optional) +: A map of values which are passed to the `meta` attribute of the hook derivation. + When not provided, this value defaults to `{ }`. diff --git a/doc/hooks/index.md b/doc/hooks/index.md index e4b744056c5e4f..3ed6b4247ba33d 100644 --- a/doc/hooks/index.md +++ b/doc/hooks/index.md @@ -32,6 +32,7 @@ postgresql-test-hook.section.md premake.section.md python.section.md scons.section.md +sourceGuard.section.md tauri.section.md tetex-tex-live.section.md unzip.section.md diff --git a/doc/hooks/sourceGuard.section.md b/doc/hooks/sourceGuard.section.md new file mode 100644 index 00000000000000..f4b8ace4ce4618 --- /dev/null +++ b/doc/hooks/sourceGuard.section.md @@ -0,0 +1,8 @@ +# sourceGuard {#setup-hook-sourceGuard} + +This hook provides the `sourceGuard` bash function. + +Using `sourceGuard` to source scripts ensures two things: + +- the script is only sourced if it is a build-time dependency (which is to say it has a `hostOffset` of `-1`) +- the script is only sourced once, even if `sourceGuard` is called multiple times diff --git a/doc/redirects.json b/doc/redirects.json index 4757c26720578a..6dafa9d2fd8471 100644 --- a/doc/redirects.json +++ b/doc/redirects.json @@ -1,4 +1,10 @@ { + "build-helpers-special-makeSetupHookPrime": [ + "index.html#build-helpers-special-makeSetupHookPrime" + ], + "build-helpers-special-makeSetupHookPrime-inputs": [ + "index.html#build-helpers-special-makeSetupHookPrime-inputs" + ], "chap-build-helpers-finalAttrs": [ "index.html#chap-build-helpers-finalAttrs" ], @@ -11,6 +17,9 @@ "ex-shfmt": [ "index.html#ex-shfmt" ], + "ex-makeSetupHookPrime-doc-example": [ + "index.html#ex-makeSetupHookPrime-doc-example" + ], "ex-testBuildFailurePrime-doc-example": [ "index.html#ex-testBuildFailurePrime-doc-example" ], @@ -317,6 +326,9 @@ "sec-tools-of-stdenv": [ "index.html#sec-tools-of-stdenv" ], + "setup-hook-sourceGuard": [ + "index.html#setup-hook-sourceGuard" + ], "ssec-stdenv-dependencies": [ "index.html#ssec-stdenv-dependencies" ], diff --git a/pkgs/by-name/ma/makeSetupHook'/package.nix b/pkgs/by-name/ma/makeSetupHook'/package.nix new file mode 100644 index 00000000000000..ac957753e2db74 --- /dev/null +++ b/pkgs/by-name/ma/makeSetupHook'/package.nix @@ -0,0 +1,96 @@ +{ + lib, + replaceVarsWith, + sourceGuard, + stdenvNoCC, + testers, + writeTextFile, +}: +# Docs in doc/build-helpers/special/makeSetupHookPrime.section.md +# See https://nixos.org/manual/nixpkgs/unstable/#build-helpers-special-makeSetupHookPrime +lib.makeOverridable ( + { + name, + script, + nativeBuildInputs ? [ ], + buildInputs ? [ ], + useSourceGuard ? true, + replacements ? { }, + passthru ? { }, + meta ? { }, + }: + # NOTE: To enforce isolation, interpolating the path in `script` causes Nix to copy the file to its own store path, + # containing nothing else. + assert lib.assertMsg (lib.isPath script) "makeSetupHook': script must be a path"; + let + templatedScriptName = if replacements == { } then name else "templated-${name}"; + templatedScript = + if replacements == { } then + "${script}" + else + replaceVarsWith { + # Boilerplate + __structuredAttrs = true; + strictDeps = true; + + name = templatedScriptName; + src = "${script}"; + inherit replacements; + }; + in + stdenvNoCC.mkDerivation { + # Boilerplate + __structuredAttrs = true; + allowSubstitutes = false; + preferLocalBuild = true; + strictDeps = true; + + inherit name meta; + + src = null; + dontUnpack = true; + + # Perhaps due to the order in which Nix loads dependencies (current node, then dependencies), we need to add sourceGuard + # as a dependency in with a slightly earlier dependency offset. + # Adding sourceGuard to `propagatedBuildInputs` causes our `setupHook` to fail to run with a `sourceGuard: command not found` + # error. + # See https://github.com/NixOS/nixpkgs/pull/31414. + depsHostHostPropagated = lib.optionals useSourceGuard [ sourceGuard ]; + + # Since we're producing a setup hook which will be used in nativeBuildInputs, all of our dependency propagation is + # understood to be shifted by one to the right -- that is, the script's nativeBuildInputs correspond to this + # derivation's propagatedBuildInputs, and the script's buildInputs correspond to this derivation's + # depsTargetTargetPropagated. + propagatedBuildInputs = nativeBuildInputs; + depsTargetTargetPropagated = buildInputs; + + setupHook = + if useSourceGuard then + writeTextFile { + name = "sourceGuard-${templatedScriptName}"; + text = '' + sourceGuard ${lib.escapeShellArg name} ${lib.escapeShellArg templatedScript} + ''; + derivationArgs = { + # Boilerplate + __structuredAttrs = true; + strictDeps = true; + }; + } + else + templatedScript; + + passthru = passthru // { + tests = passthru.tests or { } // { + shellcheck = testers.shellcheck { + name = templatedScriptName; + src = templatedScript; + }; + shfmt = testers.shfmt { + name = templatedScriptName; + src = templatedScript; + }; + }; + }; + } +) diff --git a/pkgs/by-name/ma/makeSetupHook'/tests.nix b/pkgs/by-name/ma/makeSetupHook'/tests.nix new file mode 100644 index 00000000000000..e72db20cd38b48 --- /dev/null +++ b/pkgs/by-name/ma/makeSetupHook'/tests.nix @@ -0,0 +1,4 @@ +{ lib }: +lib.recurseIntoAttrs { + # TODO(@connorbaker) +} diff --git a/pkgs/by-name/so/sourceGuard/package.nix b/pkgs/by-name/so/sourceGuard/package.nix new file mode 100644 index 00000000000000..6b53f906780cfe --- /dev/null +++ b/pkgs/by-name/so/sourceGuard/package.nix @@ -0,0 +1,17 @@ +{ + callPackages, + lib, + makeSetupHook', +}: +# Docs in doc/hooks/sourceGuard.section.md +# See https://nixos.org/manual/nixpkgs/unstable/#setup-hook-sourceGuard +makeSetupHook' { + name = "sourceGuard"; + script = ./sourceGuard.bash; + useSourceGuard = false; # Avoid self-reference + passthru.tests = callPackages ./tests.nix { }; + meta = { + description = "Ensure files are sourced at most once and are build-time dependencies"; + maintainers = [ lib.maintainers.connorbaker ]; + }; +} diff --git a/pkgs/by-name/so/sourceGuard/sourceGuard.bash b/pkgs/by-name/so/sourceGuard/sourceGuard.bash new file mode 100755 index 00000000000000..0946c44acc32a0 --- /dev/null +++ b/pkgs/by-name/so/sourceGuard/sourceGuard.bash @@ -0,0 +1,151 @@ +# shellcheck shell=bash + +# Early return without logging attempting to source this file if we've already sourced it because this +# script is used in a number of places and we don't want to spam the log. +((${sourceGuardSourced:-0} == 1)) && return 0 + +# sourceGuardArgumentCheck checks the arguments accepted by the sourceGuard family of functions. +sourceGuardArgumentCheck() { + local -ir numArgsExpected=$1 + shift + if ((numArgsExpected < 1 || 2 < numArgsExpected)); then + nixErrorLog "numArgsExpected must be 1 or 2, but got $numArgsExpected" + exit 1 + fi + + if (($# != numArgsExpected)); then + nixErrorLog "${FUNCNAME[1]} expected $numArgsExpected arguments, but got $#!" + if ((numArgsExpected == 1)); then + nixErrorLog "usage: ${FUNCNAME[1]} guardName" + elif ((numArgsExpected == 2)); then + nixErrorLog "usage: ${FUNCNAME[1]} guardName script" + fi + exit 1 + fi + + local -r guardName="$1" + if [[ -z $guardName ]]; then + nixErrorLog "${FUNCNAME[1]}: guardName argument for script $script must not be empty" + exit 1 + fi + ((numArgsExpected == 1)) && return 0 + + local -r script="$2" + if [[ ! -f $script || ! -r $script ]]; then + nixErrorLog "${FUNCNAME[1]}: guardName $guardName supplied script $script which is not a readable file" + exit 1 + fi + + return 0 +} + +# Returns zero if the guard has been sourced, one otherwise. +sourceGuardHasSourced() { + sourceGuardArgumentCheck 1 "$@" + local -r guardName="$1" + local -nr guardNameSourcedRef="${guardName}Sourced" + return $((1 - ${guardNameSourcedRef:-0})) +} + +# sourceGuardPrintCurrent prints the current guard name and script. +sourceGuardPrintCurrent() { + sourceGuardArgumentCheck 2 "$@" + local -r guardName="$1" + local -r script="$2" + echo -n \ + "guardName=$guardName" \ + "script=$script" \ + "hostOffset=${hostOffset:-0}" \ + "targetOffset=${targetOffset:-0}" + return 0 +} + +# sourceGuardPrintSourced prints the sourced guard name and script. +# It is an error to call this function if the script has not been sourced. +sourceGuardPrintSourced() { + sourceGuardArgumentCheck 1 "$@" + local -r guardName="$1" + local -nr guardNameSourcedRef="${guardName}Sourced" + + if ((${guardNameSourcedRef:-0} == 0)); then + nixErrorLog "guardName $guardName has not been sourced" + exit 1 + fi + + local -nr guardNameSourcedScriptRef="${!guardNameSourcedRef}Script" + local -nr guardNameSourcedHostOffsetRef="${!guardNameSourcedRef}HostOffset" + local -nr guardNameSourcedTargetOffsetRef="${!guardNameSourcedRef}TargetOffset" + + echo -n \ + "guardName=$guardName" \ + "script=${guardNameSourcedScriptRef:?}" \ + "hostOffset=${guardNameSourcedHostOffsetRef:?}" \ + "targetOffset=${guardNameSourcedTargetOffsetRef:?}" + + return 0 +} + +# sourceGuardSetSourced sets the sourced guard name and script. +# It is an error to call this function if the script has already been sourced. +sourceGuardSetSourced() { + sourceGuardArgumentCheck 2 "$@" + local -r guardName="$1" + local -r script="$2" + + if sourceGuardHasSourced "$guardName"; then + nixErrorLog "guardName $guardName has already been sourced" + exit 1 + fi + + declare -gir "${guardName}Sourced"=1 + declare -gr "${guardName}SourcedScript"="$script" + declare -gir "${guardName}SourcedHostOffset"="${hostOffset:-0}" + declare -gir "${guardName}SourcedTargetOffset"="${targetOffset:-0}" + + return 0 +} + +# sourceGuard ensures: +# +# - the script is sourced at most once per build +# - the script must be in a dependency array such that the script is a build-time dependency +# - the script exists and is readable +sourceGuard() { + sourceGuardArgumentCheck 2 "$@" + local -r guardName="$1" + local -r script="$2" + + # Check if we have already sourced the script + if sourceGuardHasSourced "$guardName"; then + nixInfoLog "skipping sourcing $(sourceGuardPrintCurrent "$guardName" "$script")" \ + "because we have already sourced $(sourceGuardPrintSourced "$guardName")" + elif [[ -n ${strictDeps:-} && ${hostOffset:?} -ge 0 ]]; then + nixInfoLog "skipping sourcing $(sourceGuardPrintCurrent "$guardName" "$script")" \ + "because it is not a build-time dependency" + else + sourceGuardSetSourced "$guardName" "$script" + nixInfoLog "sourcing $(sourceGuardPrintSourced "$guardName")" + # shellcheck disable=SC1090 + source "$script" || { + nixErrorLog "failed to source $(sourceGuardPrintSourced "$guardName")" + exit 1 + } + fi + + return 0 +} + +# If we've not already sourced this file, try to source it, and make sourceGuard readonly if we were successfull. +if ! sourceGuardHasSourced "sourceGuard"; then + sourceGuardSetSourced "sourceGuard" "${BASH_SOURCE[0]}" + if sourceGuardHasSourced "sourceGuard"; then + readonly -f sourceGuardArgumentCheck + readonly -f sourceGuardHasSourced + readonly -f sourceGuardPrintCurrent + readonly -f sourceGuardPrintSourced + readonly -f sourceGuardSetSourced + readonly -f sourceGuard + fi +fi + +return 0 diff --git a/pkgs/by-name/so/sourceGuard/tests.nix b/pkgs/by-name/so/sourceGuard/tests.nix new file mode 100644 index 00000000000000..82418511ad8641 --- /dev/null +++ b/pkgs/by-name/so/sourceGuard/tests.nix @@ -0,0 +1,12 @@ +{ lib, testers }: +lib.recurseIntoAttrs { + shellcheck = testers.shellcheck { + name = "sourceGuard"; + src = ./sourceGuard.bash; + }; + shfmt = testers.shfmt { + name = "sourceGuard"; + src = ./sourceGuard.bash; + }; + # TODO(@connorbaker): More tests. +} diff --git a/pkgs/test/default.nix b/pkgs/test/default.nix index dfeeef7fcf4f67..fceea9cdbba5c8 100644 --- a/pkgs/test/default.nix +++ b/pkgs/test/default.nix @@ -201,6 +201,10 @@ with pkgs; auto-patchelf-hook = callPackage ./auto-patchelf-hook { }; + makeSetupHook' = callPackages (../pkgs/by-name/ma + "/makeSetupHook'/tests.nix") { }; + + sourceGuard = pkgs.sourceGuard.passthru.tests; + srcOnly = callPackage ../build-support/src-only/tests.nix { }; systemd = callPackage ./systemd { };