#!/bin/bash -e # Copyright (c) 2023 Project CHIP Authors # # 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. # Usage: configure [OPTIONS] [--project=... [PROJECT OPTIONS]] # # Configures a stand-alone build for a CHIP application in the current # directory and creates a `ninja-build` wrapper script to build it. Should # generally be run from an empty build directory (i.e. out-of-tree). # # This is intended to be used in the context of an external build system and # represents a light-weight alternative to bootstrapping the full Pigweed build # environment (via scripts/activate.sh). The pigweed git sub-module must still # be present though. # # External tool dependencies: bash, python3, gn, ninja # # The zap-cli code generator and a small number of Python modules are # downloaded if necessary (see scripts/setup/requirements.build.txt) and # installed in a build environment directory. By default this is local to # the build directory, but an external directory can be specified using the # --build-env-dir option. The build environment directory can be shared by any # number of build directories, independently of target / tool chain. set -o pipefail shopt -s extglob function usage() { # status info "Usage: $0 [OPTIONS] [--project=... [PROJECT OPTIONS]]" info "Options:" info " --build-env-dir=DIR Directory to create (host) build environment in" info " --project=DIR Sub-directory to build, eg examples/lighting-app/linux" info "" info "Project options (mapped to GN build args):" info " --enable-[=no] Enables (or disables with '=no') a bool build arg" info " --= Sets a (non-bool) build arg to the given value" info " GN argument names can be specified with '-' instead of '_' and prefixes" info " like 'chip_' can be ommitted from names. For the full list of available" info " build arguments, see the generated args.configured file." info "" info " By default, the toolchain for the GN build will be configured from the usual" info " environment variables (CC, CXX, AR, CFLAGS, CXXFLAGS, ...), falling back to" info " default tool names (CC=cc, ...). When using this script within an external" info " build system, toolchain environment variables should be populated." exit "$1" } function main() { # ... CHIP_ROOT=$(cd "$(dirname "$0")/.." && pwd) BUILD_ENV_DEPS=( "${CHIP_ROOT}/scripts/setup/requirements.build.txt" "${CHIP_ROOT}/scripts/setup/constraints.txt" "${CHIP_ROOT}/scripts/setup/zap.version" ) # Parse global options, process VAR=VALUE style arguments, and collect project options BUILD_ENV_DIR= PROJECT= PROJECT_ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --help) usage 0 ;; --build-env-dir=*) BUILD_ENV_DIR="${1#*=}" ;; --project=*) PROJECT="${1#*=}" ;; +([A-Z_])=*) export "$1" ;; *) [[ -n "$PROJECT" ]] || fail "Invalid argument: '$1'" PROJECT_ARGS+=("$1") ;; esac shift done # Ensure we have something to do [[ -n "$PROJECT" || -n "$BUILD_ENV_DIR" ]] || usage 1 if [[ -n "$PROJECT" ]]; then local subdir="$(cd "${CHIP_ROOT}/${PROJECT}" 2>/dev/null && pwd)" [[ -n "$subdir" && -r "${subdir}/.gn" ]] || fail "Invalid project '${PROJECT}'" PROJECT="${subdir#${CHIP_ROOT}/}" [[ "$subdir" == "${CHIP_ROOT}/${PROJECT}" ]] || fail "Unable to determine project path" fi check_binary gn GN check_binary ninja NINJA # Work out build and environment directories if [[ "$PWD" == "$CHIP_ROOT" ]]; then BUILD_DIR="out/configured" NINJA_HINT="ninja -C ${BUILD_DIR}" else BUILD_DIR="." NINJA_HINT="ninja" fi if [[ -n "$BUILD_ENV_DIR" ]]; then mkdir -p "$BUILD_ENV_DIR" BUILD_ENV_PATH="$(cd "$BUILD_ENV_DIR" && pwd)" [[ -n "$BUILD_ENV_PATH" ]] || fail "Invalid build-env-dir '${BUILD_ENV_DIR}'" BUILD_ENV_DIR="$BUILD_ENV_PATH" # absolute else BUILD_ENV_DIR="build-env" # relative to BUILD_DIR BUILD_ENV_PATH="${BUILD_DIR}/${BUILD_ENV_DIR}" fi # Create the build environment if necessary if ! check_build_env; then check_python configure_python_env if ! check_binary zap-cli; then download_zap fi finalize_build_env fi # Configure the project (if requested) if [[ -z "$PROJECT" ]]; then info "Build environment created. (Specify --project=DIR to configure a build.)" return fi [[ "$BUILD_DIR" != "." ]] && info "Configuring in-tree, will build in ${BUILD_DIR}" create_empty_pw_env guess_toolchain gn_generate "${PROJECT_ARGS[@]}" create_ninja_wrapper info "You can now run ./ninja-build (or $NINJA_HINT)" } function create_empty_pw_env() { # The Pigweed environment ("//build_overrides/pigweed_environment.gni") is # imported unconditionally in various build files, so ensure it exists. local gni="build_overrides/pigweed_environment.gni" if [[ -d "${CHIP_ROOT}/$(dirname "$gni")" ]]; then if safe_to_clobber "$gni"; then info "Creating empty $gni in source tree" echo "# ${CONFIGURE_MARKER}" >"${CHIP_ROOT}/${gni}" else info "Warning: Leaving existing $gni in place, this might affect the build configuration." fi fi } function guess_toolchain() { # There is no widely used standard command for the C++ compiler (analogous to # `cc` for the C compiler), so if neither CC nor CXX are defined try to guess. if [[ -z "$CC" && -z "$CXX" ]] && have_binary cc; then local probe="$(cc -E - <<<'gnu=__GNUC__ clang=__clang__' 2>/dev/null)" # Check for clang first because it also defines __GNUC__ if [[ "$probe" =~ clang=[1-9] ]] && have_binary clang && have_binary clang++; then info "Guessing CC=clang CXX=clang++ because cc appears to be clang" export CC=clang CXX=clang++ elif [[ "$probe" =~ gnu=[1-9] ]] && have_binary gcc && have_binary g++; then info "Guessing CC=gcc CXX=g++ because cc appears to be gcc" export CC=gcc CXX=g++ else info "Unable to guess c++ compiler: $probe" fi fi } function gn_generate() { # [project options] mkdir -p "${BUILD_DIR}" ensure_no_clobber "${BUILD_DIR}/args.gn" # Pass --script-executable to all `gn` calls so scripts run in our venv local gn=(gn --script-executable="${BUILD_ENV_DIR}/bin/python" --root="${CHIP_ROOT}/${PROJECT}") # Run gn gen with an empty args.gn first so we can list all arguments info "Configuring gn build arguments (see $BUILD_DIR/args.configured for full list)" { echo "# ${CONFIGURE_MARKER}" echo "# project root: ${PROJECT}" } >"${BUILD_DIR}/args.gn" "${gn[@]}" -q gen "$BUILD_DIR" # Use the argument list to drive the mapping of our command line options to GN args call_impl process_project_args <("${gn[@]}" args "$BUILD_DIR" --list --json) "$@" >>"${BUILD_DIR}/args.gn" "${gn[@]}" args "$BUILD_DIR" --list >"${BUILD_DIR}/args.configured" # Now gn gen with the arguments we have configured. info "Running gn gen to generate ninja files" "${gn[@]}" -q gen "$BUILD_DIR" } function create_ninja_wrapper() { local wrapper="ninja-build" ensure_no_clobber "$wrapper" { echo "#!/bin/bash -e" echo "# ${CONFIGURE_MARKER}" if [[ "$BUILD_DIR" != "." ]]; then echo 'args=(-C "$(dirname "$0")/'"${BUILD_DIR}"'")' else echo 'args=() dir="$(dirname "$0")"' echo '[[ "$dir" != "." ]] && args=(-C "$dir")' fi echo 'exec ninja "${args[@]}" "$@"' } >"$wrapper" chmod a+x "$wrapper" } function check_build_env() { generate_build_env_cksum # re-used by finalize_build_env [[ -r "${BUILD_ENV_PATH}/.cksum" ]] || return 1 read -r <"${BUILD_ENV_PATH}/.cksum" || true [[ "$REPLY" == "$CURRENT_ENV_CKSUM" ]] || return 1 [[ -r "${BUILD_ENV_PATH}/bin/activate" ]] || return 1 info "Using existing build environment: ${BUILD_ENV_PATH}" PYTHON="${BUILD_ENV_PATH}/bin/python" } function configure_python_env() { progress "Setting up Python venv" # Debian and Ubuntu ship python3 with a broken venv module unless the # python3-venv package is installed (https://bugs.launchpad.net/bugs/1290847) local withoutpip=() pip="${BUILD_ENV_PATH}/bin/pip" if ! "$PYTHON" -m ensurepip --version >/dev/null 2>&1; then withoutpip=(--without-pip) pip="${pip}.pyz" # bootstrapped below fi "$PYTHON" -m venv --clear "${withoutpip[@]}" "$BUILD_ENV_PATH" info "$BUILD_ENV_PATH" # Download a standalone pip.pyz from pypa.io if necessary if [[ -n "$withoutpip" ]]; then progress "Bootstrapping pip via pypa.io (venv module is missing ensurepip dependency)" call_impl download https://bootstrap.pypa.io/pip/pip.pyz "$pip" info "ok" fi # Install our auto-loading venvactivate module so that running scripts via # the venv python has the side-effect of fully activating the environment. local sitepkgs=("${BUILD_ENV_PATH}/lib/python"*"/site-packages") [[ -d "$sitepkgs" ]] || fail "Failed to locate venv site-packages" cp "${CHIP_ROOT}/scripts/configure.venv/venvactivate".{pth,py} "${sitepkgs}/" progress "Installing Python build dependencies" # Ensure pip and wheel are up to date first (using pip.pyz if necessary) "${BUILD_ENV_PATH}/bin/python3" "$pip" install --require-virtualenv --quiet --upgrade pip wheel "${BUILD_ENV_PATH}/bin/pip" install --require-virtualenv --quiet \ -r "${CHIP_ROOT}/scripts/setup/requirements.build.txt" \ -c "${CHIP_ROOT}/scripts/setup/constraints.txt" info "ok" } function generate_build_env_cksum() { # Conservatively assume that any change to this script or BUILD_ENV_DEPS invalidates the environment CURRENT_ENV_CKSUM="$(cat "$0" "${BUILD_ENV_DEPS[@]}" | cksum)" [[ -n "$CURRENT_ENV_CKSUM" ]] || fail "Failed to generate build environment checksum" } function finalize_build_env() { echo "$CURRENT_ENV_CKSUM" >"${BUILD_ENV_PATH}/.cksum" } function download_zap() { local version read -r version <"${CHIP_ROOT}/scripts/setup/zap.version" local platform="$(uname -sm)" flavor case "$platform" in Linux\ x86_64) flavor=linux-x64 ;; Linux\ arm64) flavor=linux-arm64 ;; Darwin\ *) flavor=mac-x64 ;; # there is no mac arm build of zap (can run x64 via Rosetta) *) fail "Unable to determine zap flavor for $platform" ;; esac local url="https://github.com/project-chip/zap/releases/download/${version}/zap-${flavor}.zip" progress "Installing zap-cli from $url" call_impl download_and_extract_zip "$url" "${BUILD_ENV_PATH}/bin" zap-cli chmod a+x "${BUILD_ENV_PATH}/bin/zap-cli" # ZipFile.extract() does not handle permissions info "ok" } function call_impl() { # func ... "$PYTHON" "${CHIP_ROOT}/scripts/configure.impl.py" "$@" } function check_python() { progress "Checking for Python 3" if have_binary python3; then PYTHON="$(hash -t python3)" elif have_binary python; then PYTHON="$(hash -t python)" local ver="$("$PYTHON" --version)" if [[ "$ver" != "Python 3."* ]]; then info "need Python 3 but found $ver" return 1 fi else info "not found" return 1 fi info "$PYTHON" } function check_binary() { # binary [VAR] progress "Checking for $1" if ! have_binary "$1"; then info "not found" return 1 fi local path="$(hash -t "$1")" [[ -n "$2" ]] && eval "$2=\$path" info "$path" } function have_binary() { # binary hash "$1" 2>/dev/null } function ensure_no_clobber() { # file safe_to_clobber "$1" || fail "Won't overwrite file not generated by configure: $1" } function safe_to_clobber() { # file CONFIGURE_MARKER="Auto-generated by configure, do not edit" [[ -s "$1" ]] || return 0 read -r -n 512 -d '' <"$1" || true [[ "${REPLY/$CONFIGURE_MARKER/}" != "$REPLY" ]] && return 0 return 1 } function info() { # message echo "$*" >&2 } function progress() { # message echo -n "$*... " >&2 } function fail() { # message echo "Error: $*" >&2 exit 1 } main "$@"