Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion doc/hooks/cmake.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,22 @@ You can disable this hook’s behavior by setting `configurePhase` to a custom v

### CMake Exclusive Variables {#cmake-exclusive-variables}

#### `cmakeEntries` {#cmake-entries}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there prior art in Nixpkgs that we can refer to here, such as for another build system?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this PR is the first one that implements structured flags attributes for a build system. The previous pattern is cmakeFlags and cmakeFlagsArray.


Bash associative array of CMake variable cache entries.
Flags like `-D<key>=<value>` will be prepended to the CMake command-line arguments.
One can specify its initial values via the `stdenv.mkDerivation` argument of the same name, given that `__structuredAttrs` is set to `true`.

We choose `ON` and `OFF` as the canonical CMake boolean values for consistency in this setup hook.
Nevertheless, we don't strictly enforce this policy, but accommodate other CMake-supported boolean values with a set of helper Bash functions, such as [`canonicalizeCMakeBool`](#cmake-bash-helper-canonicalizeCMakeBool), [`cmakeBoolToBash`](#cmake-bash-helper-cmakeBoolToBash), and [`testCMakeBool`](#cmake-bash-helper-testCMakeBool).

Boolean values from the `stdenv.mkDerivation` argument are automatically canonicalized when the command `jq` is available.
To enable such canonicalization, non-minimal-build `cmake` packages propagate the `jq` native build inputs.
To opt out such propagation, override as `cmake.override { jq = null; }` or use `cmakeMinimal`.

#### `cmakeFlags` {#cmake-flags}

Controls the flags passed to `cmake setup` during configure phase.
Extra flags to pass to `cmake setup` during configure phase.

#### `cmakeBuildDir` {#cmake-build-dir}

Expand Down Expand Up @@ -59,3 +72,23 @@ it can be combined with `--exclude-regex` option.
#### `ctestFlags` {#cmake-ctest-flags}

Additional options passed to `ctest` together with `checkFlags`.

## Bash helper functions provided by CMake {#cmake-bash-helpers}

### `concatCMakeEntryFlagsTo` {#cmake-bash-helper-concatCMakeEntryFlagsTo}

`concatCMakeEntryFlagsTo` takes the variable names of the flags array and CMake entries associative array, and append the flags array with `-D<key>=<value>` flags constructed with the provided CMake entries.

### `canonicalizeCMakeBool` {#cmake-bash-helper-canonicalizeCMakeBool}

`canonicalizeCMakeBool` takes any supported CMake boolean value and prints either `ON` or `OFF`.
If the input value is not supported, it returns 1 after showing an error message.

### `cmakeBoolToBash` {#cmake-bash-helper-cmakeBoolToBash}

`cmakeBoolToBash` takes any supported CMake boolean value and prints either `1` or an empty string, the latter two are the typical values assigned when a derivation attribute with boolean value is passed into a Bash builder.

### `testCMakeBool` {#cmake-bash-helper-testCMakeBool}

`testCMakeBool` takes any supported CMake boolean value and acts as Bash's `true` or `false` command, handy to construct Bash conditional expressions.
If the input value is not supported, it exits the current process with exit status 1.
18 changes: 18 additions & 0 deletions doc/redirects.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@
"chap-toolchains": [
"index.html#chap-toolchains"
],
"cmake-bash-helper-canonicalizeCMakeBool": [
"index.html#cmake-bash-helper-canonicalizeCMakeBool"
],
"cmake-bash-helper-cmakeBoolToBash": [
"index.html#cmake-bash-helper-cmakeBoolToBash"
],
"cmake-bash-helper-concatCMakeEntryFlagsTo": [
"index.html#cmake-bash-helper-concatCMakeEntryFlagsTo"
],
"cmake-bash-helper-testCMakeBool": [
"index.html#cmake-bash-helper-testCMakeBool"
],
"cmake-bash-helpers": [
"index.html#cmake-bash-helpers"
],
"cmake-ctest": [
"index.html#cmake-ctest"
],
Expand Down Expand Up @@ -2401,6 +2416,9 @@
"cmake-exclusive-variables": [
"index.html#cmake-exclusive-variables"
],
"cmake-entries": [
"index.html#cmake-entries"
],
"cmake-flags": [
"index.html#cmake-flags"
],
Expand Down
6 changes: 6 additions & 0 deletions doc/release-notes/rl-2511.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

- `cmake`'s setup hook now takes `cmakeEntries` for structured CMake entry access.
The Nixpkgs-provided default CMake entry values are now prepended to the Bash associative array `cmakeEntries`.
To detect boolean entries from the Nix derivation attribute `cmakeEntries` for pretty-printing,
The non-minimal-build `cmake` now propagates the `jq` native build inputs.
Such behaviour can be optionally disabled via `cmake.override { jq = null; }` or by using `cmakeMinimal`.

- Added `rewriteURL` attribute to the nixpkgs `config`, to allow for rewriting the URLs downloaded by `fetchurl`.
- Added `hashedMirrors` attribute to the nixpkgs `config`, to allow for customization of the hashed mirrors used by `fetchurl`.

Expand Down
5 changes: 5 additions & 0 deletions pkgs/by-name/cm/cmake/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
bzip2,
curlMinimal,
expat,
jq,
libarchive,
libuv,
ncurses,
Expand Down Expand Up @@ -109,6 +110,10 @@ stdenv.mkDerivation (finalAttrs: {
++ lib.optionals buildDocs [ texinfo ]
++ lib.optionals qt5UI [ wrapQtAppsHook ];

propagatedNativeBuildInputs =
# For boolean cmakeEntries attribute canonicalization.
lib.optional (!isMinimalBuild) jq;

buildInputs =
lib.optionals useSharedLibraries [
bzip2
Expand Down
206 changes: 169 additions & 37 deletions pkgs/by-name/cm/cmake/setup-hook.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,146 @@
# The no-op Bash function that preserves the return status of its inputs
#
# NOTE:
# If nop is called as the first command inside the else-block,
# and the inputs of nop don't have return status,
# it will return non-zero exit status from the if-condition.
nop() {
local ret="$?"
if [[ "$ret" != 0 ]]; then
echo "nop: Get nonzero exit status $ret from the previous command." >&2
return "$ret"
fi
return 0
}

addCMakeParams() {
# NIXPKGS_CMAKE_PREFIX_PATH is like CMAKE_PREFIX_PATH except cmake
# will not search it for programs
addToSearchPath NIXPKGS_CMAKE_PREFIX_PATH $1
}

# https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/user-interaction/index.html#setting-variables-on-the-command-line
# https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#cmake-language-variables
# https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html
# https://cmake.org/cmake/help/book/mastering-cmake/chapter/CMake%20Cache.html
declare -gA cmakeEntries

# https://cmake.org/cmake/help/latest/command/if.html#constant
canonicalizeCMakeBool() {
local valIn="$1"
case "${valIn^^}" in
1|ON|YES|TRUE|Y)
echo ON
;;
""|0|OFF|NO|FALSE|N|IGNORE|NOTFOUND|*-NOTFOUND)
echo OFF
;;
*[!0-9]*)
echo "ERROR: cmakeBoolToBash: unsupported value ${valIn}" >&2
return 1
;;
*)
echo 1
;;
esac
}

canonicalizeCMakeEntryAttrs() {
local key typeOrig
for key in "${!cmakeEntries[@]}"; do
if ! jq -e "has(\"$key\")" "$NIX_ATTRS_JSON_FILE" > /dev/null; then
continue
fi
typeOrig="$(jq ".cmakeEntries.$key|type" "$NIX_ATTRS_JSON_FILE")"
if [[ "$typeOrig" == "null" ]]; then
{
echo 'ERROR: canonicalizeCMakeEntryAttrs: `cmakeEntries.'"$key"'` has `null` value.'
echo ' `null` values in structured sub-attributes are not ignored but translated to empty strings.'
echo ' To exclude a cmakeEntries attribute, simply remove it from `cmakeEntries`'
} >&2
return 1
fi
if [[ "$typeOrig" == "boolean" ]]; then
cmakeEntries[$key]="$(canonicalizeCMakeBool "${cmakeEntries[$key]}")"
fi
done
}

canonicalizeCMakeEntryAttrsIfAvailable() {
local reasonToSkip=""
if ! [[ -e "${NIX_ATTRS_JSON_FILE-}" ]]; then
reasonToSkip="File \$NIX_ATTRS_JSON_FILE not available."
elif ! command -v jq; then
reasonToSkip="Command jq not available."
elif ! jq -e "has(\"cmakeEntries\")" "$NIX_ATTRS_JSON_FILE"; then
reasonToSkip="Attribute cmakeEntries not set."
fi
if [[ -n "$reasonToSkip" ]]; then
echo "cmake: Skipping entry attribute canonicalization." >&2
echo "$reasonToSkip" >&2
return
fi
canonicalizeCMakeEntryAttrs
}

canonicalizeCMakeEntryAttrsIfAvailable

cmakeBoolToBash() {
local valIn="$1"
local valCanonical
valCanonical="$(canonicalizeCMakeBool "$valIn")"
if [[ "$valCanonical" == ON ]]; then
echo 1
fi
}

testCMakeBool() {
local valIn="$1"
local valCanonical
nop "${valCanonical=$(canonicalizeCMakeBool "$valIn")}" || exit 1
if [[ "$valCanonical" != ON ]]; then
false
fi
}

concatCMakeEntryFlagsTo() {
if [[ "$1" != flagsArray ]]; then
local -n flagsArray="$1"
fi
if [[ "$2" != cmakeEntries ]]; then
local -n cmakeEntries="$2"
fi
local key
for key in "${!cmakeEntries[@]}"; do
flagsArray+=("-D$key=${cmakeEntries[$key]}")
done
}

# The docdir flag needs to include PROJECT_NAME as per GNU guidelines,
# try to extract it from CMakeLists.txt.
parseShareDocName() {
local cmakeLists="$cmakeDir/CMakeLists.txt"
if [[ -f "$cmakeLists" ]]; then
local shareDocName
shareDocName="$(grep --only-matching --perl-regexp --ignore-case '\bproject\s*\(\s*"?\K([^[:space:]")]+)' <"$cmakeLists" | head -n1)"
fi
# The argument sometimes contains garbage or variable interpolation.
# When that is the case, let’s fall back to the derivation name.
if [[ -z "$shareDocName" ]] || echo "$shareDocName" | grep -q '[^a-zA-Z0-9_+-]'; then
if [[ -n "${pname-}" ]]; then
shareDocName="$pname"
else
shareDocName="$(echo "$name" | sed 's/-[^a-zA-Z].*//')"
fi
fi
echo "$shareDocName"
}

cmakeConfigurePhase() {
runHook preConfigure

# default to CMake defaults if unset
: ${cmakeBuildDir:=build}
nop "${cmakeBuildDir:=build}"

export CTEST_OUTPUT_ON_FAILURE=1
if [ -n "${enableParallelChecking-1}" ]; then
Expand All @@ -18,13 +150,14 @@ cmakeConfigurePhase() {
if [ -z "${dontUseCmakeBuildDir-}" ]; then
mkdir -p "$cmakeBuildDir"
cd "$cmakeBuildDir"
: ${cmakeDir:=..}
nop "${cmakeDir:=..}"
else
: ${cmakeDir:=.}
true # Zeroize the previous exit status.
nop "${cmakeDir:=.}"
fi

if [ -z "${dontAddPrefix-}" ]; then
prependToVar cmakeFlags "-DCMAKE_INSTALL_PREFIX=$prefix"
nop "${cmakeEntries[CMAKE_INSTALL_PREFIX]=$prefix}"
fi

# We should set the proper `CMAKE_SYSTEM_NAME`.
Expand All @@ -33,11 +166,11 @@ cmakeConfigurePhase() {
# Unfortunately cmake seems to expect absolute paths for ar, ranlib, and
# strip. Otherwise they are taken to be relative to the source root of the
# package being built.
prependToVar cmakeFlags "-DCMAKE_CXX_COMPILER=$CXX"
prependToVar cmakeFlags "-DCMAKE_C_COMPILER=$CC"
prependToVar cmakeFlags "-DCMAKE_AR=$(command -v $AR)"
prependToVar cmakeFlags "-DCMAKE_RANLIB=$(command -v $RANLIB)"
prependToVar cmakeFlags "-DCMAKE_STRIP=$(command -v $STRIP)"
nop "${cmakeEntries[CMAKE_CXX_COMPILER]=$CXX}"
nop "${cmakeEntries[CMAKE_C_COMPILER]=$CC}"
nop "${cmakeEntries[CMAKE_AR]=$(command -v "$AR")}"
nop "${cmakeEntries[CMAKE_RANLIB]=$(command -v "$RANLIB")}"
nop "${cmakeEntries[CMAKE_STRIP]=$(command -v "$STRIP")}"

# This installs shared libraries with a fully-specified install
# name. By default, cmake installs shared libraries with just the
Expand All @@ -46,61 +179,60 @@ cmakeConfigurePhase() {
# libraries are in a system path or in the same directory as the
# executable. This flag makes the shared library accessible from its
# nix/store directory.
prependToVar cmakeFlags "-DCMAKE_INSTALL_NAME_DIR=${!outputLib}/lib"
nop "${cmakeEntries[CMAKE_INSTALL_NAME_DIR]=${!outputLib}/lib}"

# The docdir flag needs to include PROJECT_NAME as per GNU guidelines,
# try to extract it from CMakeLists.txt.
if [[ -z "$shareDocName" ]]; then
local cmakeLists="${cmakeDir}/CMakeLists.txt"
if [[ -f "$cmakeLists" ]]; then
local shareDocName="$(grep --only-matching --perl-regexp --ignore-case '\bproject\s*\(\s*"?\K([^[:space:]")]+)' < "$cmakeLists" | head -n1)"
fi
# The argument sometimes contains garbage or variable interpolation.
# When that is the case, let’s fall back to the derivation name.
if [[ -z "$shareDocName" ]] || echo "$shareDocName" | grep -q '[^a-zA-Z0-9_+-]'; then
if [[ -n "${pname-}" ]]; then
shareDocName="$pname"
else
shareDocName="$(echo "$name" | sed 's/-[^a-zA-Z].*//')"
fi
fi
shareDocName=$(parseShareDocName)
fi

# This ensures correct paths with multiple output derivations
# It requires the project to use variables from GNUInstallDirs module
# https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html
prependToVar cmakeFlags "-DCMAKE_INSTALL_BINDIR=${!outputBin}/bin"
prependToVar cmakeFlags "-DCMAKE_INSTALL_SBINDIR=${!outputBin}/sbin"
prependToVar cmakeFlags "-DCMAKE_INSTALL_INCLUDEDIR=${!outputInclude}/include"
prependToVar cmakeFlags "-DCMAKE_INSTALL_MANDIR=${!outputMan}/share/man"
prependToVar cmakeFlags "-DCMAKE_INSTALL_INFODIR=${!outputInfo}/share/info"
prependToVar cmakeFlags "-DCMAKE_INSTALL_DOCDIR=${!outputDoc}/share/doc/${shareDocName}"
prependToVar cmakeFlags "-DCMAKE_INSTALL_LIBDIR=${!outputLib}/lib"
prependToVar cmakeFlags "-DCMAKE_INSTALL_LIBEXECDIR=${!outputLib}/libexec"
prependToVar cmakeFlags "-DCMAKE_INSTALL_LOCALEDIR=${!outputLib}/share/locale"
nop "${cmakeEntries[CMAKE_INSTALL_BINDIR]=${!outputBin}/bin}"
nop "${cmakeEntries[CMAKE_INSTALL_SBINDIR]=${!outputBin}/sbin}"
nop "${cmakeEntries[CMAKE_INSTALL_INCLUDEDIR]=${!outputInclude}/include}"
nop "${cmakeEntries[CMAKE_INSTALL_MANDIR]=${!outputMan}/share/man}"
nop "${cmakeEntries[CMAKE_INSTALL_INFODIR]=${!outputInfo}/share/info}"
nop "${cmakeEntries[CMAKE_INSTALL_DOCDIR]=${!outputDoc}/share/doc/${shareDocName}}"
nop "${cmakeEntries[CMAKE_INSTALL_LIBDIR]=${!outputLib}/lib}"
nop "${cmakeEntries[CMAKE_INSTALL_LIBEXECDIR]=${!outputLib}/libexec}"
nop "${cmakeEntries[CMAKE_INSTALL_LOCALEDIR]=${!outputLib}/share/locale}"

# Don’t build tests when doCheck = false
if [ -z "${doCheck-}" ]; then
prependToVar cmakeFlags "-DBUILD_TESTING=OFF"
nop "${BUILD_TESTING=OFF}"
fi

# Always build Release, to ensure optimisation flags
prependToVar cmakeFlags "-DCMAKE_BUILD_TYPE=${cmakeBuildType:-Release}"
nop "${cmakeEntries[CMAKE_BUILD_TYPE]=${cmakeBuildType:-Release}}"

# Disable user package registry to avoid potential side effects
# and unecessary attempts to access non-existent home folder
# https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#disabling-the-package-registry
prependToVar cmakeFlags "-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON"
prependToVar cmakeFlags "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF"
prependToVar cmakeFlags "-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF"
nop "${CMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON}"
nop "${CMAKE_FIND_USE_PACKAGE_REGISTRY=OFF}"
nop "${CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF}"

if [ "${buildPhase-}" = ninjaBuildPhase ]; then
prependToVar cmakeFlags "-GNinja"
fi

local flagsArray=()
concatCMakeEntryFlagsTo flagsArray cmakeEntries
concatTo flagsArray cmakeFlags cmakeFlagsArray

# Redefine cmakeFlags for backward compatibility purposes.
# Some packages may still expect cmakeConfigurePhase
# to add the full list of flags into cmakeFlags.
if [[ "$(declare -p cmakeFlags)" =~ "^declare -a" ]]; then
cmakeFlags=()
else
cmakeFlags=""
fi
appendToVar cmakeFlags "${flagsArray[@]}"

echoCmd 'cmake flags' "${flagsArray[@]}"

cmake "$cmakeDir" "${flagsArray[@]}"
Expand Down
Loading
Loading