#!/bin/bash

#
# Copyright 2018 Jesse Haber-Kucharsky
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#
# This is cmake-cooking v0.10.0
# The home of cmake-cooking is https://github.com/hakuch/CMakeCooking
#

set -e

CMAKE=${CMAKE:-cmake}

invoked_args=("$@")
source_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
initial_wd=$(pwd)
memory_file="${initial_wd}/.cooking_memory"

recipe="${source_dir}/cooking_recipe.cmake"
declare -a excluded_ingredients
declare -a included_ingredients
build_dir="${initial_wd}/build"
build_type="Debug"
# Depends on `build_dir`.
ingredients_dir=""
generator="Ninja"
list_only=""
nested=""

usage() {
    cat <<EOF

Fetch, configure, build, and install dependencies ("ingredients") for a CMake project
in a local and repeatable development environment.

Usage: $0 [OPTIONS]

where OPTIONS are:

-a
-r RECIPE
-e INGREDIENT
-i INGREDIENT
-d BUILD_DIR (=${build_dir})
-p INGREDIENTS_DIR (=${build_dir}/_cooking/installed)
-t BUILD_TYPE (=${build_type})
-g GENERATOR (=${generator})
-s VAR=VALUE
-f EXPORT_DIR
-l
-h

By default, cmake-cooking reads a file called 'cooking_recipe.cmake'.

If neither [-i] nor [-e] are specified with a recipe ([-r]), then all ingredients of the recipe
will be fetched and built.

[-i] and [-e] are mutually-exclusive options: only provide one.

Option details:

-a

    Invoke 'cooking.sh' with the arguments that were provided to it last time, instead
    of the arguments provided.

-r RECIPE

    Instead of reading the recipe in a file called 'cooking_recipe.cmake', follow the recipe
    in the named file.

    If the recipe file is a relative path, it is interpretted relative to the source directory
    of the project.

-e INGREDIENT

    Exclude an ingredient from a recipe. This option can be supplied many times.

    For example, if a recipe consists of 'apple', 'banana', 'carrot', and 'donut', then

        ./cooking.sh -r dev -e apple -e donut

    will prepare 'banana' and 'carrot' but not prepare 'apple' and 'donut'.

    If an ingredient is excluded, then it is assumed that all ingredients that depend on it
    can satisfy that dependency in some other way from the system (ie, the dependency is
    removed internally).

-i INGREDIENT

   Include an ingredient from a recipe, ignoring the others. This option can be supplied
   many times.

   Similar to [-e], but the opposite.

   For example, if a recipe consists of 'apple', 'banana', 'carrot', and 'donut' then

       ./cooking.sh -r dev -i apple -i donut

   will prepare 'apple' and 'donut' but not prepare 'banana' and 'carrot'.

   If an ingredient is not in the "include-list", then it is assumed that all
   ingredients that are in the list and which depend on it can satisfy that dependency
   in some other way from the system.

-d BUILD_DIR (=${build_dir})

   Configure the project and build it in the named directory.

-p INGREDIENTS_DIR (=${build_dir}/_cooking/installed)

   Install compiled ingredients into this directory.

-t BUILD_TYPE (=${build_type})

   Configure all ingredients and the project with the named CMake build-type.
   An example build type is "Release".

-g GENERATOR (=${generator})

    Use the named CMake generator for building all ingredients and the project.
    An example generator is "Unix Makfiles".

-s VAR=VALUE

   Set an environmental variable 'VAR' to the value 'VALUE' during the invocation of CMake.

-f EXPORT_DIR

   If provided, and the project is successfully configured, then the tree of installed ingredients
   is exported to this directory (the actual files: not symbolic links).

   This option requires rsync.

   This may be useful for preparing continuous integration environments, but it is not
   recommended for distribution or release purposes (since this would be counter
   to the goal of cmake-cooking).

-l

    Only list available ingredients for a given recipe, and don't do anything else.

-h

    Show this help information and exit.

EOF
}

parse_assignment() {
    local var
    local value
    IFS='=' read -r var value <<< "${1}"
    export "${var}"="${value}"
}

yell_include_exclude_mutually_exclusive() {
    echo "Cooking: [-e] and [-i] are mutually exclusive options!" >&2
}

while getopts "ar:e:i:d:p:t:g:s:f:lhx" arg; do
    case "${arg}" in
        a)
            if [ ! -f "${memory_file}" ]; then
                echo "No previous invocation found to recall!" >&2
                exit 1
            fi

            source "${memory_file}"
            run_previous && exit 0
            ;;
        r)
            if [[ "${OPTARG}" = /* ]]; then
                recipe=${OPTARG}
            else
                recipe="${source_dir}/${OPTARG}"
            fi
            ;;
        e)
            if [[ ${#included_ingredients[@]} -ne 0 ]]; then
                yell_include_exclude_mutually_exclusive
                exit 1
            fi

            excluded_ingredients+=(${OPTARG})
            ;;
        i)
            if [[ ${#excluded_ingredients[@]} -ne 0 ]]; then
                yell_include_exclude_mutually_exclusive
                exit 1
            fi

            included_ingredients+=(${OPTARG})
            ;;
        d) build_dir=$(realpath "${OPTARG}") ;;
        p) ingredients_dir=$(realpath "${OPTARG}") ;;
        t) build_type=${OPTARG} ;;
        g) generator=${OPTARG} ;;
        s) parse_assignment "${OPTARG}" ;;
        f) export_dir=$(realpath "${OPTARG}") ;;
        l) list_only="1" ;;
        h) usage; exit 0 ;;
        x) nested="1" ;;
        *) usage; exit 1 ;;
    esac
done

shift $((OPTIND - 1))

cooking_dir="${build_dir}/_cooking"
cache_file="${build_dir}/CMakeCache.txt"
ingredients_ready_file="${cooking_dir}/ready.txt"

if [ -z "${ingredients_dir}" ]; then
    ingredients_dir="${cooking_dir}/installed"
fi

mkdir -p "${build_dir}"

cat <<'EOF' > "${build_dir}/Cooking.cmake"
#
# Copyright 2018 Jesse Haber-Kucharsky
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#
# This file was generated by cmake-cooking v0.10.0
# The home of cmake-cooking is https://github.com/hakuch/CMakeCooking
#

macro (project name)
  set (_cooking_dir ${CMAKE_CURRENT_BINARY_DIR}/_cooking)

  if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
    set (_cooking_root ON)
  else ()
    set (_cooking_root OFF)
  endif ()

  find_program (Cooking_STOW_EXECUTABLE
    stow
    "Executable path of GNU Stow.")

  if (NOT Cooking_STOW_EXECUTABLE)
    message (FATAL_ERROR "Cooking: GNU Stow is required!")
  endif ()

  set (Cooking_INGREDIENTS_DIR
    ${_cooking_dir}/installed
    CACHE
    PATH
    "Directory where ingredients will be installed.")

  set (Cooking_EXCLUDED_INGREDIENTS
   ""
   CACHE
   STRING
   "Semicolon-separated list of ingredients that are not provided by Cooking.")

  set (Cooking_INCLUDED_INGREDIENTS
   ""
   CACHE
   STRING
   "Semicolon-separated list of ingredients that are provided by Cooking.")

  option (Cooking_LIST_ONLY
    "Available ingredients will be listed and nothing will be installed."
    OFF)

  set (Cooking_RECIPE "" CACHE STRING "Configure ${name}'s dependencies according to the named recipe.")

  if ((NOT DEFINED Cooking_EXCLUDED_INGREDIENTS) OR (Cooking_EXCLUDED_INGREDIENTS STREQUAL ""))
    set (_cooking_is_excluding OFF)
  else ()
    set (_cooking_is_excluding ON)
  endif ()

  if ((NOT DEFINED Cooking_INCLUDED_INGREDIENTS) OR (Cooking_INCLUDED_INGREDIENTS STREQUAL ""))
    set (_cooking_is_including OFF)
  else ()
    set (_cooking_is_including ON)
  endif ()

  if (_cooking_is_excluding AND _cooking_is_including)
    message (
      FATAL_ERROR
      "Cooking: The EXCLUDED_INGREDIENTS and INCLUDED_INGREDIENTS lists are mutually exclusive options!")
  endif ()

  if (_cooking_root)
    _project (${name} ${ARGN})

    if (NOT ("${Cooking_RECIPE}" STREQUAL ""))
      add_custom_target (_cooking_ingredients)

      set (_cooking_ready_marker_file ${_cooking_dir}/ready.txt)

      add_custom_command (
        OUTPUT ${_cooking_ready_marker_file}
        DEPENDS _cooking_ingredients
        COMMAND ${CMAKE_COMMAND} -E touch ${_cooking_ready_marker_file})

      add_custom_target (_cooking_ingredients_ready
        DEPENDS ${_cooking_ready_marker_file})

      set (_cooking_local_synchronize_marker_file ${Cooking_INGREDIENTS_DIR}/.cooking_local_synchronize)

      add_custom_command (
        OUTPUT ${_cooking_local_synchronize_marker_file}
        COMMAND ${CMAKE_COMMAND} -E touch ${_cooking_local_synchronize_marker_file})

      add_custom_target (_cooking_marked_for_local_synchronization
        DEPENDS ${_cooking_local_synchronize_marker_file})

      list (APPEND CMAKE_PREFIX_PATH ${Cooking_INGREDIENTS_DIR})
      include (${Cooking_RECIPE})

      if (NOT EXISTS ${_cooking_ready_marker_file})
        return ()
      endif ()
    endif ()
  endif ()
endmacro ()

function (_cooking_set_union x y var)
  set (r ${${x}})

  foreach (e ${${y}})
    list (APPEND r ${e})
  endforeach ()

  list (REMOVE_DUPLICATES r)
  set (${var} ${r} PARENT_SCOPE)
endfunction ()

function (_cooking_set_difference x y var)
  set (r ${${x}})

  foreach (e ${${y}})
    if (${e} IN_LIST ${x})
       list (REMOVE_ITEM r ${e})
    endif ()
  endforeach ()

  set (${var} ${r} PARENT_SCOPE)
endfunction ()

function (_cooking_set_intersection x y var)
  set (r "")

  foreach (e ${${y}})
    if (${e} IN_LIST ${x})
      list (APPEND r ${e})
    endif ()
  endforeach ()

  list (REMOVE_DUPLICATES r)
  set (${var} ${r} PARENT_SCOPE)
endfunction ()

function (_cooking_query_by_key list key var)
  list (FIND ${list} ${key} index)

  if (${index} EQUAL "-1")
    set (value NOTFOUND)
  else ()
    math (EXPR value_index "${index} + 1")
    list (GET ${list} ${value_index} value)
  endif ()

  set (${var} ${value} PARENT_SCOPE)
endfunction ()

function (_cooking_populate_ep_parameter)
  cmake_parse_arguments (
    pa
    ""
    "EXTERNAL_PROJECT_ARGS_LIST;PARAMETER;DEFAULT_VALUE"
    ""
    ${ARGN})

  string (TOLOWER ${pa_PARAMETER} parameter_lower)
  _cooking_query_by_key (${pa_EXTERNAL_PROJECT_ARGS_LIST} ${pa_PARAMETER} ${parameter_lower})
  set (value ${${parameter_lower}})
  set (var_name _cooking_${parameter_lower})
  set (ep_var_name _cooking_ep_${parameter_lower})

  if (NOT value)
    set (${var_name} ${pa_DEFAULT_VALUE} PARENT_SCOPE)
    set (${ep_var_name} ${pa_PARAMETER} ${pa_DEFAULT_VALUE} PARENT_SCOPE)
  else ()
    set (${var_name} ${value} PARENT_SCOPE)
    set (${ep_var_name} "" PARENT_SCOPE)
  endif ()
endfunction ()

function (_cooking_define_listing_targets)
  cmake_parse_arguments (
    pa
    ""
    "NAME;SOURCE_DIR;RECIPE"
    "REQUIRES"
    ${ARGN})

  set (stale_file ${Cooking_INGREDIENTS_DIR}/.cooking_stale_ingredient_${pa_NAME})

  add_custom_command (
    OUTPUT ${stale_file}
    COMMAND ${CMAKE_COMMAND} -E touch ${stale_file})

  add_custom_target (_cooking_ingredient_${pa_NAME}_stale
    DEPENDS ${stale_file})

  set (commands
    COMMAND
    ${CMAKE_COMMAND} -E touch ${Cooking_INGREDIENTS_DIR}/.cooking_ingredient_${pa_NAME})

  if (pa_RECIPE)
    if (pa_RECIPE STREQUAL <DEFAULT>)
      set (recipe_args "")
    else ()
      set (recipe_args -r ${pa_RECIPE})
    endif ()

    list (INSERT commands 0
      COMMAND
      ${pa_SOURCE_DIR}/cooking.sh
      ${recipe_args}
      -p ${Cooking_INGREDIENTS_DIR}
      -g ${CMAKE_GENERATOR}
      -x
      -l)
  endif ()

  add_custom_command (
    OUTPUT ${Cooking_INGREDIENTS_DIR}/.cooking_ingredient_${pa_NAME}
    DEPENDS
      _cooking_ingredient_${pa_NAME}_stale
      ${stale_file}
    ${commands})

  add_custom_target (_cooking_ingredient_${pa_NAME}_listed
    DEPENDS ${Cooking_INGREDIENTS_DIR}/.cooking_ingredient_${pa_NAME})

  foreach (d ${pa_REQUIRES})
    add_dependencies (_cooking_ingredient_${pa_NAME}_listed _cooking_ingredient_${d}_listed)
  endforeach ()

  add_dependencies (_cooking_ingredients _cooking_ingredient_${pa_NAME}_listed)
endfunction ()

function (_cooking_adjust_requirements)
  cmake_parse_arguments (
    pa
    ""
    "IS_EXCLUDING;IS_INCLUDING;OUTPUT_LIST"
    "REQUIREMENTS"
    ${ARGN})

  if (pa_IS_EXCLUDING)
    # Strip out any dependencies that are excluded.
    _cooking_set_difference (
      pa_REQUIREMENTS
      Cooking_EXCLUDED_INGREDIENTS
      pa_REQUIREMENTS)
  elseif (_cooking_is_including)
    # Eliminate dependencies that have not been included.
    _cooking_set_intersection (
      pa_REQUIREMENTS
      Cooking_INCLUDED_INGREDIENTS
      pa_REQUIREMENTS)
  endif ()

  set (${pa_OUTPUT_LIST} ${pa_REQUIREMENTS} PARENT_SCOPE)
endfunction ()

function (_cooking_populate_ep_depends)
  cmake_parse_arguments (
    pa
    ""
    ""
    "REQUIREMENTS"
    ${ARGN})

  if (pa_REQUIREMENTS)
    set (value DEPENDS)

    foreach (d ${pa_REQUIREMENTS})
      list (APPEND value ingredient_${d})
    endforeach ()
  else ()
    set (value "")
  endif ()

  set (_cooking_ep_depends ${value} PARENT_SCOPE)
endfunction ()

function (_cooking_prepare_restrictions_arguments)
  cmake_parse_arguments (
    pa
    ""
    "IS_EXCLUDING;IS_INCLUDING;OUTPUT_LIST"
    "REQUIREMENTS"
    ${ARGN})

  set (args "")

  if (pa_IS_INCLUDING)
    _cooking_set_difference (
      Cooking_INCLUDED_INGREDIENTS
      pa_REQUIREMENTS
      included)

    foreach (x ${included})
      list (APPEND args -i ${x})
    endforeach ()
  elseif (pa_IS_EXCLUDING)
    _cooking_set_union (
      Cooking_EXCLUDED_INGREDIENTS
      pa_REQUIREMENTS
      excluded)

    foreach (x ${excluded})
      list (APPEND args -e ${x})
    endforeach ()
  else ()
    foreach (x ${pa_REQUIREMENTS})
      list (APPEND args -e ${x})
    endforeach ()
  endif ()

  set (${pa_OUTPUT_LIST} ${args} PARENT_SCOPE)
endfunction ()

function (_cooking_determine_common_cmake_args output)
  string (REPLACE ";" ":::" prefix_path_with_colons "${CMAKE_PREFIX_PATH}")

  set (cmake_args "-G" "${CMAKE_GENERATOR}")

  if (CMAKE_CXX_FLAGS)
    list(APPEND cmake_args -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS})
  endif ()
  if (CMAKE_C_FLAGS)
    list(APPEND cmake_args -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS})
  endif ()

  list (APPEND cmake_args
    -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
    -DCMAKE_PREFIX_PATH=${prefix_path_with_colons}
    -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
    -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}
    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
    -DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS})

  set (${output} ${cmake_args}
    PARENT_SCOPE)
endfunction ()

function (_cooking_populate_ep_configure_command)
  cmake_parse_arguments (
    pa
    ""
    "IS_EXCLUDING;IS_INCLUDING;RECIPE;EXTERNAL_PROJECT_ARGS_LIST"
    "REQUIREMENTS;CMAKE_ARGS;COOKING_CMAKE_ARGS"
    ${ARGN})

  if (pa_RECIPE)
    if (pa_RECIPE STREQUAL <DEFAULT>)
      set (recipe_args "")
    else ()
      set (recipe_args -r ${pa_RECIPE})
    endif ()

    _cooking_prepare_restrictions_arguments (
      IS_EXCLUDING ${pa_IS_EXCLUDING}
      IS_INCLUDING ${pa_IS_INCLUDING}
      REQUIREMENTS ${pa_REQUIREMENTS}
      OUTPUT_LIST restrictions_args)

    set (value
      CONFIGURE_COMMAND
      <SOURCE_DIR>/cooking.sh
      ${recipe_args}
      -d <BINARY_DIR>
      -p ${Cooking_INGREDIENTS_DIR}
      -g ${CMAKE_GENERATOR}
      -x
      ${restrictions_args}
      --
      ${pa_COOKING_CMAKE_ARGS})
  elseif (NOT (CONFIGURE_COMMAND IN_LIST ${pa_EXTERNAL_PROJECT_ARGS_LIST}))
    set (value
      CONFIGURE_COMMAND
      ${CMAKE_COMMAND}
      ${pa_CMAKE_ARGS}
      <SOURCE_DIR>)
  else ()
    set (value "")
  endif ()

  set (_cooking_ep_configure_command ${value} PARENT_SCOPE)
endfunction ()

function (_cooking_populate_ep_build_command ep_args_list)
  if (NOT (BUILD_COMMAND IN_LIST ${ep_args_list}))
    set (value BUILD_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR>)
  else ()
    set (value "")
  endif ()

  set (_cooking_ep_build_command ${value} PARENT_SCOPE)
endfunction ()

function (_cooking_populate_ep_install_command ep_args_list)
  if (NOT (INSTALL_COMMAND IN_LIST ${ep_args_list}))
    set (value INSTALL_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR> --target install)
  else ()
    set (value "")
  endif ()

  set (_cooking_ep_install_command ${value} PARENT_SCOPE)
endfunction ()

function (_cooking_define_ep)
  cmake_parse_arguments (
    pa
    ""
    "NAME;SOURCE_DIR;BINARY_DIR;EXTERNAL_PROJECT_ARGS_LIST;RECIPE;INGREDIENT_DIR;STOW_DIR;LOCAL_RECONFIGURE;LOCAL_REBUILD"
    "DEPENDS;CONFIGURE_COMMAND;BUILD_COMMAND;INSTALL_COMMAND;CMAKE_ARGS"
    ${ARGN})

  string (REPLACE "<DISABLE>" "" forwarded_ep_args "${${pa_EXTERNAL_PROJECT_ARGS_LIST}}")
  set (ep_name ingredient_${pa_NAME})
  include (ExternalProject)

  set (stamp_dir ${pa_INGREDIENT_DIR}/stamp)

  ExternalProject_add (${ep_name}
    DEPENDS ${pa_DEPENDS}
    SOURCE_DIR ${pa_SOURCE_DIR}
    BINARY_DIR ${pa_BINARY_DIR}
    CONFIGURE_COMMAND ${pa_CONFIGURE_COMMAND}
    BUILD_COMMAND ${pa_BUILD_COMMAND}
    INSTALL_COMMAND ${pa_INSTALL_COMMAND}
    PREFIX ${pa_INGREDIENT_DIR}
    STAMP_DIR ${stamp_dir}
    INSTALL_DIR ${pa_STOW_DIR}/${pa_NAME}
    CMAKE_ARGS ${pa_CMAKE_ARGS}
    LIST_SEPARATOR :::
    STEP_TARGETS install
    "${forwarded_ep_args}")

  set (stow_marker_file ${Cooking_INGREDIENTS_DIR}/.cooking_ingredient_${pa_NAME})
  set (lock_file ${Cooking_INGREDIENTS_DIR}/.cooking_stow.lock)

  add_custom_command (
    OUTPUT ${stow_marker_file}
    DEPENDS
      ${ep_name}-install
      ${stamp_dir}/ingredient_${pa_NAME}-install
    COMMAND
      flock
      --wait 30
      ${lock_file}
      ${Cooking_STOW_EXECUTABLE}
      -t ${Cooking_INGREDIENTS_DIR}
      -d ${pa_STOW_DIR}
      ${pa_NAME}
    COMMAND ${CMAKE_COMMAND} -E touch ${stow_marker_file})

  add_custom_target (_cooking_ingredient_${pa_NAME}_stowed
    DEPENDS ${stow_marker_file})

  if (pa_RECIPE)
    set (reconfigure_marker_file ${Cooking_INGREDIENTS_DIR}/.cooking_reconfigure_ingredient_${pa_NAME})

    add_custom_command (
      OUTPUT ${reconfigure_marker_file}
      COMMAND ${CMAKE_COMMAND} -E touch ${reconfigure_marker_file})

    add_custom_target (_cooking_ingredient_${pa_NAME}_marked_for_reconfigure
      DEPENDS ${reconfigure_marker_file})

    ExternalProject_add_step (${ep_name}
      cooking-reconfigure
      DEPENDERS configure
      DEPENDS ${reconfigure_marker_file}
      COMMAND ${CMAKE_COMMAND} -E echo_append)

    ExternalProject_add_stepdependencies (${ep_name}
      cooking-reconfigure
      _cooking_ingredient_${pa_NAME}_marked_for_reconfigure)
  endif ()

  foreach (d ${pa_DEPENDS})
    ExternalProject_add_stepdependencies (${ep_name}
      configure
      _cooking_${d}_stowed)
  endforeach ()

  add_dependencies (_cooking_ingredients _cooking_ingredient_${pa_NAME}_stowed)

  if (pa_LOCAL_RECONFIGURE OR pa_LOCAL_REBUILD)
    if (pa_LOCAL_RECONFIGURE)
      set (step configure)
    else ()
      set (step build)
    endif ()

    ExternalProject_add_step (${ep_name}
      cooking-local-${step}
      DEPENDERS ${step}
      DEPENDS ${_cooking_local_synchronize_marker_file}
      COMMAND ${CMAKE_COMMAND} -E echo_append)

    ExternalProject_add_stepdependencies (${ep_name}
      cooking-local-${step}
      _cooking_marked_for_local_synchronization)
  endif ()
endfunction ()

macro (cooking_ingredient name)
  set (_cooking_args "${ARGN}")

  if ((_cooking_is_excluding AND (${name} IN_LIST Cooking_EXCLUDED_INGREDIENTS))
      OR (_cooking_is_including AND (NOT (${name} IN_LIST Cooking_INCLUDED_INGREDIENTS))))
    # Nothing.
  else ()
    set (_cooking_ingredient_dir ${_cooking_dir}/ingredient/${name})

    cmake_parse_arguments (
      _cooking_pa
      "LOCAL_RECONFIGURE;LOCAL_REBUILD"
      "COOKING_RECIPE"
      "CMAKE_ARGS;COOKING_CMAKE_ARGS;EXTERNAL_PROJECT_ARGS;REQUIRES"
      ${_cooking_args})

    _cooking_populate_ep_parameter (
      EXTERNAL_PROJECT_ARGS_LIST _cooking_pa_EXTERNAL_PROJECT_ARGS
      PARAMETER SOURCE_DIR
      DEFAULT_VALUE ${_cooking_ingredient_dir}/src)

    _cooking_populate_ep_parameter (
      EXTERNAL_PROJECT_ARGS_LIST _cooking_pa_EXTERNAL_PROJECT_ARGS
      PARAMETER BINARY_DIR
      DEFAULT_VALUE ${_cooking_ingredient_dir}/build)

    _cooking_populate_ep_parameter (
      EXTERNAL_PROJECT_ARGS_LIST _cooking_pa_EXTERNAL_PROJECT_ARGS
      PARAMETER BUILD_IN_SOURCE
      DEFAULT_VALUE OFF)

    if (_cooking_build_in_source)
       set (_cooking_ep_binary_dir "")
    endif ()

    if (Cooking_LIST_ONLY)
      _cooking_define_listing_targets (
        NAME ${name}
        SOURCE_DIR ${_cooking_source_dir}
        RECIPE ${_cooking_pa_COOKING_RECIPE}
        REQUIRES ${_cooking_pa_REQUIRES})
    else ()
      _cooking_adjust_requirements (
        IS_EXCLUDING ${_cooking_is_excluding}
        IS_INCLUDING ${_cooking_is_including}
        REQUIREMENTS ${_cooking_pa_REQUIRES}
        OUTPUT_LIST _cooking_pa_REQUIRES)

      _cooking_populate_ep_depends (
        REQUIREMENTS ${_cooking_pa_REQUIRES})

      _cooking_determine_common_cmake_args (_cooking_common_cmake_args)

      _cooking_populate_ep_configure_command (
        IS_EXCLUDING ${_cooking_is_excluding}
        IS_INCLUDING ${_cooking_is_including}
        RECIPE ${_cooking_pa_COOKING_RECIPE}
        REQUIREMENTS ${_cooking_pa_REQUIRES}
        EXTERNAL_PROJECT_ARGS_LIST _cooking_pa_EXTERNAL_PROJECT_ARGS
        CMAKE_ARGS
          ${_cooking_common_cmake_args}
          ${_cooking_pa_CMAKE_ARGS}
        COOKING_CMAKE_ARGS
          ${_cooking_common_cmake_args}
          ${_cooking_pa_COOKING_CMAKE_ARGS})

      _cooking_populate_ep_build_command (_cooking_pa_EXTERNAL_PROJECT_ARGS)
      _cooking_populate_ep_install_command (_cooking_pa_EXTERNAL_PROJECT_ARGS)

      _cooking_define_ep (
        NAME ${name}
        RECIPE ${_cooking_pa_COOKING_RECIPE}
        DEPENDS ${_cooking_ep_depends}
        SOURCE_DIR ${_cooking_ep_source_dir}
        BINARY_DIR ${_cooking_ep_binary_dir}
        CONFIGURE_COMMAND ${_cooking_ep_configure_command}
        BUILD_COMMAND ${_cooking_ep_build_command}
        INSTALL_COMMAND ${_cooking_ep_install_command}
        INGREDIENT_DIR ${_cooking_ingredient_dir}
        STOW_DIR ${_cooking_dir}/stow
        CMAKE_ARGS ${_cooking_common_cmake_args}
        EXTERNAL_PROJECT_ARGS_LIST _cooking_pa_EXTERNAL_PROJECT_ARGS
        LOCAL_RECONFIGURE ${_cooking_pa_LOCAL_RECONFIGURE}
        LOCAL_REBUILD ${_cooking_pa_LOCAL_REBUILD})
    endif ()
  endif ()
endmacro ()
EOF

cmake_cooking_args=(
    "-DCooking_INGREDIENTS_DIR=${ingredients_dir}"
    "-DCooking_RECIPE=${recipe}"
)

#
# Remove any `Cooking.cmake` file from the source directory. We now generate this file in the build directory, and old
# copies will cause conflicts.
#

old_cooking_file="${source_dir}/cmake/Cooking.cmake"

if [ -f "${old_cooking_file}" ]; then
    grep 'This file was generated by cmake-cooking' "${old_cooking_file}" > /dev/null && rm "${old_cooking_file}"
fi

#
# Clean-up from a previous run.
#

if [ -e "${ingredients_ready_file}" ]; then
    rm "${ingredients_ready_file}"
fi

if [ -e "${cache_file}" ]; then
    rm "${cache_file}"
fi

if [ -d "${ingredients_dir}" -a -z "${nested}" ]; then
    rm -r --preserve-root "${ingredients_dir}"
fi

mkdir -p "${ingredients_dir}"

#
# Validate recipe.
#

if [ -n "${recipe}" ]; then
    if [ ! -f "${recipe}" ]; then
        echo "Cooking: The '${recipe}' recipe does not exist!" >&2
        exit 1
    fi
fi

#
# Prepare lists of included and excluded ingredients.
#

if [ -n "${excluded_ingredients}" ] && [ -z "${list_only}" ]; then
    cmake_cooking_args+=(
        -DCooking_EXCLUDED_INGREDIENTS=$(printf "%s;" "${excluded_ingredients[@]}")
        -DCooking_INCLUDED_INGREDIENTS=
    )
fi

if [ -n "${included_ingredients}" ] && [ -z "${list_only}" ]; then
    cmake_cooking_args+=(
        -DCooking_EXCLUDED_INGREDIENTS=
        -DCooking_INCLUDED_INGREDIENTS=$(printf "%s;" "${included_ingredients[@]}")
    )
fi

#
# Configure and build ingredients.
#

mkdir -p "${cooking_dir}"/stow
touch "${cooking_dir}"/stow/.stow
cd "${build_dir}"

declare -a build_args

if [ "${generator}" == "Ninja" ]; then
    build_args+=(-v)
fi

if [ -n "${list_only}" ]; then
    cmake_cooking_args+=("-DCooking_LIST_ONLY=ON")
fi

${CMAKE} -DCMAKE_BUILD_TYPE="${build_type}" "${cmake_cooking_args[@]}" -G "${generator}" "${source_dir}" "${@}"

if [ -n "${recipe}" ]; then
    ${CMAKE} --build . --target _cooking_ingredients_ready -- "${build_args[@]}"

    #
    # Report what we've done (if we're not nested).
    #

    if [ -z "${nested}" ]; then
        ingredients=($(find "${ingredients_dir}" -name '.cooking_ingredient_*' -printf '%f\n' | sed -r 's/\.cooking_ingredient_(.+)/\1/'))

        if [ -z "${list_only}" ]; then
            printf "\nCooking: Installed the following ingredients:\n"
        else
            printf "\nCooking: The following ingredients are necessary for this recipe:\n"
        fi

        for ingredient in "${ingredients[@]}"; do
            echo "  - ${ingredient}"
        done

        printf '\n'
    fi

    if [ -n "${list_only}" ]; then
        exit 0
    fi

    #
    # Configure the project, expecting all requirements satisfied.
    #

    ${CMAKE} -DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON "${@}" .

    #
    # Optionally export the installed files.
    #

    if [ -n "${export_dir}" ]; then
        rsync "${ingredients_dir}/" "${export_dir}" -a --copy-links
        printf "\nCooking: Exported ingredients to ${export_dir}\n"
    fi
fi

#
# Save invocation information.
#

cd "${initial_wd}"

cat <<EOF > "${memory_file}"
run_previous() {
    "${0}" ${invoked_args[@]@Q}
}
EOF
