configure 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. #!/bin/bash -e
  2. # Copyright (c) 2023 Project CHIP Authors
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. # Usage: configure [OPTIONS] [--project=... [PROJECT OPTIONS]]
  16. #
  17. # Configures a stand-alone build for a CHIP application in the current
  18. # directory and creates a `ninja-build` wrapper script to build it. Should
  19. # generally be run from an empty build directory (i.e. out-of-tree).
  20. #
  21. # This is intended to be used in the context of an external build system and
  22. # represents a light-weight alternative to bootstrapping the full Pigweed build
  23. # environment (via scripts/activate.sh). The pigweed git sub-module must still
  24. # be present though.
  25. #
  26. # External tool dependencies: bash, python3, gn, ninja
  27. #
  28. # The zap-cli code generator and a small number of Python modules are
  29. # downloaded if necessary (see scripts/setup/requirements.build.txt) and
  30. # installed in a build environment directory. By default this is local to
  31. # the build directory, but an external directory can be specified using the
  32. # --build-env-dir option. The build environment directory can be shared by any
  33. # number of build directories, independently of target / tool chain.
  34. set -o pipefail
  35. shopt -s extglob
  36. function usage() { # status
  37. info "Usage: $0 [OPTIONS] [--project=... [PROJECT OPTIONS]]"
  38. info "Options:"
  39. info " --build-env-dir=DIR Directory to create (host) build environment in"
  40. info " --project=DIR Sub-directory to build, eg examples/lighting-app/linux"
  41. info ""
  42. info "Project options (mapped to GN build args):"
  43. info " --enable-<ARG>[=no] Enables (or disables with '=no') a bool build arg"
  44. info " --<ARG>=<VALUE> Sets a (non-bool) build arg to the given value"
  45. info " GN argument names can be specified with '-' instead of '_' and prefixes"
  46. info " like 'chip_' can be ommitted from names. For the full list of available"
  47. info " build arguments, see the generated args.configured file."
  48. info ""
  49. info " By default, the toolchain for the GN build will be configured from the usual"
  50. info " environment variables (CC, CXX, AR, CFLAGS, CXXFLAGS, ...), falling back to"
  51. info " default tool names (CC=cc, ...). When using this script within an external"
  52. info " build system, toolchain environment variables should be populated."
  53. exit "$1"
  54. }
  55. function main() { # ...
  56. CHIP_ROOT=$(cd "$(dirname "$0")/.." && pwd)
  57. BUILD_ENV_DEPS=(
  58. "${CHIP_ROOT}/scripts/setup/requirements.build.txt"
  59. "${CHIP_ROOT}/scripts/setup/constraints.txt"
  60. "${CHIP_ROOT}/scripts/setup/zap.version"
  61. )
  62. # Parse global options, process VAR=VALUE style arguments, and collect project options
  63. BUILD_ENV_DIR=
  64. PROJECT=
  65. PROJECT_ARGS=()
  66. while [[ $# -gt 0 ]]; do
  67. case "$1" in
  68. --help) usage 0 ;;
  69. --build-env-dir=*) BUILD_ENV_DIR="${1#*=}" ;;
  70. --project=*) PROJECT="${1#*=}" ;;
  71. +([A-Z_])=*) export "$1" ;;
  72. *)
  73. [[ -n "$PROJECT" ]] || fail "Invalid argument: '$1'"
  74. PROJECT_ARGS+=("$1")
  75. ;;
  76. esac
  77. shift
  78. done
  79. # Ensure we have something to do
  80. [[ -n "$PROJECT" || -n "$BUILD_ENV_DIR" ]] || usage 1
  81. if [[ -n "$PROJECT" ]]; then
  82. local subdir="$(cd "${CHIP_ROOT}/${PROJECT}" 2>/dev/null && pwd)"
  83. [[ -n "$subdir" && -r "${subdir}/.gn" ]] || fail "Invalid project '${PROJECT}'"
  84. PROJECT="${subdir#${CHIP_ROOT}/}"
  85. [[ "$subdir" == "${CHIP_ROOT}/${PROJECT}" ]] || fail "Unable to determine project path"
  86. fi
  87. check_binary gn GN
  88. check_binary ninja NINJA
  89. # Work out build and environment directories
  90. if [[ "$PWD" == "$CHIP_ROOT" ]]; then
  91. BUILD_DIR="out/configured"
  92. NINJA_HINT="ninja -C ${BUILD_DIR}"
  93. else
  94. BUILD_DIR="."
  95. NINJA_HINT="ninja"
  96. fi
  97. if [[ -n "$BUILD_ENV_DIR" ]]; then
  98. mkdir -p "$BUILD_ENV_DIR"
  99. BUILD_ENV_PATH="$(cd "$BUILD_ENV_DIR" && pwd)"
  100. [[ -n "$BUILD_ENV_PATH" ]] || fail "Invalid build-env-dir '${BUILD_ENV_DIR}'"
  101. BUILD_ENV_DIR="$BUILD_ENV_PATH" # absolute
  102. else
  103. BUILD_ENV_DIR="build-env" # relative to BUILD_DIR
  104. BUILD_ENV_PATH="${BUILD_DIR}/${BUILD_ENV_DIR}"
  105. fi
  106. # Create the build environment if necessary
  107. if ! check_build_env; then
  108. check_python
  109. configure_python_env
  110. if ! check_binary zap-cli; then
  111. download_zap
  112. fi
  113. finalize_build_env
  114. fi
  115. # Configure the project (if requested)
  116. if [[ -z "$PROJECT" ]]; then
  117. info "Build environment created. (Specify --project=DIR to configure a build.)"
  118. return
  119. fi
  120. [[ "$BUILD_DIR" != "." ]] && info "Configuring in-tree, will build in ${BUILD_DIR}"
  121. create_empty_pw_env
  122. guess_toolchain
  123. gn_generate "${PROJECT_ARGS[@]}"
  124. create_ninja_wrapper
  125. info "You can now run ./ninja-build (or $NINJA_HINT)"
  126. }
  127. function create_empty_pw_env() {
  128. # The Pigweed environment ("//build_overrides/pigweed_environment.gni") is
  129. # imported unconditionally in various build files, so ensure it exists.
  130. local gni="build_overrides/pigweed_environment.gni"
  131. if [[ -d "${CHIP_ROOT}/$(dirname "$gni")" ]]; then
  132. if safe_to_clobber "$gni"; then
  133. info "Creating empty $gni in source tree"
  134. echo "# ${CONFIGURE_MARKER}" >"${CHIP_ROOT}/${gni}"
  135. else
  136. info "Warning: Leaving existing $gni in place, this might affect the build configuration."
  137. fi
  138. fi
  139. }
  140. function guess_toolchain() {
  141. # There is no widely used standard command for the C++ compiler (analogous to
  142. # `cc` for the C compiler), so if neither CC nor CXX are defined try to guess.
  143. if [[ -z "$CC" && -z "$CXX" ]] && have_binary cc; then
  144. local probe="$(cc -E - <<<'gnu=__GNUC__ clang=__clang__' 2>/dev/null)"
  145. # Check for clang first because it also defines __GNUC__
  146. if [[ "$probe" =~ clang=[1-9] ]] && have_binary clang && have_binary clang++; then
  147. info "Guessing CC=clang CXX=clang++ because cc appears to be clang"
  148. export CC=clang CXX=clang++
  149. elif [[ "$probe" =~ gnu=[1-9] ]] && have_binary gcc && have_binary g++; then
  150. info "Guessing CC=gcc CXX=g++ because cc appears to be gcc"
  151. export CC=gcc CXX=g++
  152. else
  153. info "Unable to guess c++ compiler: $probe"
  154. fi
  155. fi
  156. }
  157. function gn_generate() { # [project options]
  158. mkdir -p "${BUILD_DIR}"
  159. ensure_no_clobber "${BUILD_DIR}/args.gn"
  160. # Pass --script-executable to all `gn` calls so scripts run in our venv
  161. local gn=(gn --script-executable="${BUILD_ENV_DIR}/bin/python" --root="${CHIP_ROOT}/${PROJECT}")
  162. # Run gn gen with an empty args.gn first so we can list all arguments
  163. info "Configuring gn build arguments (see $BUILD_DIR/args.configured for full list)"
  164. {
  165. echo "# ${CONFIGURE_MARKER}"
  166. echo "# project root: ${PROJECT}"
  167. } >"${BUILD_DIR}/args.gn"
  168. "${gn[@]}" -q gen "$BUILD_DIR"
  169. # Use the argument list to drive the mapping of our command line options to GN args
  170. call_impl process_project_args <("${gn[@]}" args "$BUILD_DIR" --list --json) "$@" >>"${BUILD_DIR}/args.gn"
  171. "${gn[@]}" args "$BUILD_DIR" --list >"${BUILD_DIR}/args.configured"
  172. # Now gn gen with the arguments we have configured.
  173. info "Running gn gen to generate ninja files"
  174. "${gn[@]}" -q gen "$BUILD_DIR"
  175. }
  176. function create_ninja_wrapper() {
  177. local wrapper="ninja-build"
  178. ensure_no_clobber "$wrapper"
  179. {
  180. echo "#!/bin/bash -e"
  181. echo "# ${CONFIGURE_MARKER}"
  182. if [[ "$BUILD_DIR" != "." ]]; then
  183. echo 'args=(-C "$(dirname "$0")/'"${BUILD_DIR}"'")'
  184. else
  185. echo 'args=() dir="$(dirname "$0")"'
  186. echo '[[ "$dir" != "." ]] && args=(-C "$dir")'
  187. fi
  188. echo 'exec ninja "${args[@]}" "$@"'
  189. } >"$wrapper"
  190. chmod a+x "$wrapper"
  191. }
  192. function check_build_env() {
  193. generate_build_env_cksum # re-used by finalize_build_env
  194. [[ -r "${BUILD_ENV_PATH}/.cksum" ]] || return 1
  195. read -r <"${BUILD_ENV_PATH}/.cksum" || true
  196. [[ "$REPLY" == "$CURRENT_ENV_CKSUM" ]] || return 1
  197. [[ -r "${BUILD_ENV_PATH}/bin/activate" ]] || return 1
  198. info "Using existing build environment: ${BUILD_ENV_PATH}"
  199. PYTHON="${BUILD_ENV_PATH}/bin/python"
  200. }
  201. function configure_python_env() {
  202. progress "Setting up Python venv"
  203. # Debian and Ubuntu ship python3 with a broken venv module unless the
  204. # python3-venv package is installed (https://bugs.launchpad.net/bugs/1290847)
  205. local withoutpip=() pip="${BUILD_ENV_PATH}/bin/pip"
  206. if ! "$PYTHON" -m ensurepip --version >/dev/null 2>&1; then
  207. withoutpip=(--without-pip) pip="${pip}.pyz" # bootstrapped below
  208. fi
  209. "$PYTHON" -m venv --clear "${withoutpip[@]}" "$BUILD_ENV_PATH"
  210. info "$BUILD_ENV_PATH"
  211. # Download a standalone pip.pyz from pypa.io if necessary
  212. if [[ -n "$withoutpip" ]]; then
  213. progress "Bootstrapping pip via pypa.io (venv module is missing ensurepip dependency)"
  214. call_impl download https://bootstrap.pypa.io/pip/pip.pyz "$pip"
  215. info "ok"
  216. fi
  217. # Install our auto-loading venvactivate module so that running scripts via
  218. # the venv python has the side-effect of fully activating the environment.
  219. local sitepkgs=("${BUILD_ENV_PATH}/lib/python"*"/site-packages")
  220. [[ -d "$sitepkgs" ]] || fail "Failed to locate venv site-packages"
  221. cp "${CHIP_ROOT}/scripts/configure.venv/venvactivate".{pth,py} "${sitepkgs}/"
  222. progress "Installing Python build dependencies"
  223. # Ensure pip and wheel are up to date first (using pip.pyz if necessary)
  224. "${BUILD_ENV_PATH}/bin/python3" "$pip" install --require-virtualenv --quiet --upgrade pip wheel
  225. "${BUILD_ENV_PATH}/bin/pip" install --require-virtualenv --quiet \
  226. -r "${CHIP_ROOT}/scripts/setup/requirements.build.txt" \
  227. -c "${CHIP_ROOT}/scripts/setup/constraints.txt"
  228. info "ok"
  229. }
  230. function generate_build_env_cksum() {
  231. # Conservatively assume that any change to this script or BUILD_ENV_DEPS invalidates the environment
  232. CURRENT_ENV_CKSUM="$(cat "$0" "${BUILD_ENV_DEPS[@]}" | cksum)"
  233. [[ -n "$CURRENT_ENV_CKSUM" ]] || fail "Failed to generate build environment checksum"
  234. }
  235. function finalize_build_env() {
  236. echo "$CURRENT_ENV_CKSUM" >"${BUILD_ENV_PATH}/.cksum"
  237. }
  238. function download_zap() {
  239. local version
  240. read -r version <"${CHIP_ROOT}/scripts/setup/zap.version"
  241. local platform="$(uname -sm)" flavor
  242. case "$platform" in
  243. Linux\ x86_64) flavor=linux-x64 ;;
  244. Linux\ arm64) flavor=linux-arm64 ;;
  245. Darwin\ *) flavor=mac-x64 ;; # there is no mac arm build of zap (can run x64 via Rosetta)
  246. *) fail "Unable to determine zap flavor for $platform" ;;
  247. esac
  248. local url="https://github.com/project-chip/zap/releases/download/${version}/zap-${flavor}.zip"
  249. progress "Installing zap-cli from $url"
  250. call_impl download_and_extract_zip "$url" "${BUILD_ENV_PATH}/bin" zap-cli
  251. chmod a+x "${BUILD_ENV_PATH}/bin/zap-cli" # ZipFile.extract() does not handle permissions
  252. info "ok"
  253. }
  254. function call_impl() { # func ...
  255. "$PYTHON" "${CHIP_ROOT}/scripts/configure.impl.py" "$@"
  256. }
  257. function check_python() {
  258. progress "Checking for Python 3"
  259. if have_binary python3; then
  260. PYTHON="$(hash -t python3)"
  261. elif have_binary python; then
  262. PYTHON="$(hash -t python)"
  263. local ver="$("$PYTHON" --version)"
  264. if [[ "$ver" != "Python 3."* ]]; then
  265. info "need Python 3 but found $ver"
  266. return 1
  267. fi
  268. else
  269. info "not found"
  270. return 1
  271. fi
  272. info "$PYTHON"
  273. }
  274. function check_binary() { # binary [VAR]
  275. progress "Checking for $1"
  276. if ! have_binary "$1"; then
  277. info "not found"
  278. return 1
  279. fi
  280. local path="$(hash -t "$1")"
  281. [[ -n "$2" ]] && eval "$2=\$path"
  282. info "$path"
  283. }
  284. function have_binary() { # binary
  285. hash "$1" 2>/dev/null
  286. }
  287. function ensure_no_clobber() { # file
  288. safe_to_clobber "$1" || fail "Won't overwrite file not generated by configure: $1"
  289. }
  290. function safe_to_clobber() { # file
  291. CONFIGURE_MARKER="Auto-generated by configure, do not edit"
  292. [[ -s "$1" ]] || return 0
  293. read -r -n 512 -d '' <"$1" || true
  294. [[ "${REPLY/$CONFIGURE_MARKER/}" != "$REPLY" ]] && return 0
  295. return 1
  296. }
  297. function info() { # message
  298. echo "$*" >&2
  299. }
  300. function progress() { # message
  301. echo -n "$*... " >&2
  302. }
  303. function fail() { # message
  304. echo "Error: $*" >&2
  305. exit 1
  306. }
  307. main "$@"