2 #------------------------------------------------------------------------------#
3 # build.sh (DarkPlaces Build Script) #
5 # Copyright (c) 2019-2020 David Knapp #
7 # Permission is hereby granted, free of charge, to any person obtaining a copy #
8 # of this software and associated documentation files (the "Software"), to #
9 # deal in the Software without restriction, including without limitation the #
10 # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or #
11 # sell copies of the Software, and to permit persons to whom the Software is #
12 # furnished to do so, subject to the following conditions: #
14 # The above copyright notice and this permission notice shall be included in #
15 # all copies or substantial portions of the Software. #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING #
22 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS #
24 #------------------------------------------------------------------------------#
25 if [ -z "${BASH_VERSION:-}" ]; then
26 echo "This script requires bash."
29 #------------------------------------------------------------------------------#
30 ### HELPER FUNCTIONS ###
32 printf -- "\e[31m%b\e[0m" "$1"
36 printf -- "\e[33m%b\e[0m" "$1"
40 printf -- "\e[32m%b\e[0m" "$1"
46 Usage: %s [OPTIONS] PROJECT" "$me"
50 For more information, run again with --help.
56 --cc= specify a C compiler
57 --cxx= specify a C++ compiler
58 --threads= | --jN set how many threads to compile with
59 --generator= cmake generator to use. Run 'cmake --help' for a list
60 --cmake-options= pass additional options to cmake
61 --config-dir= override the location of the config.cmake file
62 --build-dir= override the location cmake will write build files
63 --reset-build delete build files of PROJECT
64 --reset-cache delete the cache
65 --new-options specify a new set of options
66 --nocache do not read from, or write to the cache
67 --auto do not display any prompts, even with --reset
68 --help print this message then exit
70 Unless you set --config-dir, the PROJECT you specify is assumed to be a
71 correspondingly named subdirectory of the 'game' directory. The build directory
72 is also relative to the config directory by default, unless --build-dir is set.
73 If the directory (assumed or specified) doesn't exist, a new project can be
74 created for you from a template.
76 The script maintains a cache of your build settings so you don't have to input
77 the same settings for a specified PROJECT more than once. You can simply run
78 the script with PROJECT, and optionally --build, and it will configure and/or
79 build PROJECT with the cached settings automatically.
81 This script will run in auto mode if ran from a non-interactive shell.
89 ! command -v ls -A "$1" >/dev/null; then return 0; fi
92 #------------------------------------------------------------------------------#
93 check_env() { # Make sure the environment is sane before continuing.
96 if ! (( "$(id -u)" )); then
97 if (( ! option_asroot )); then
98 perror "*** This script cannot be run as root. Use --jackass to override\n\n"
100 else pwarn "* Running as root as you requested. Welcome to Jackass!\n\n"; fi
103 if [[ $- == *i* ]]; then
105 pwarn "* Shell is non-interactive. Prompts are impossible/pointless. --auto enabled.\n\n"
109 if [ ! -d "$(pwd)/engine" ]; then perror "*** Required directory '\"$(pwd)\"/engine' was not found.\n" ; failed+=1; fi
110 if [ ! -d "$(pwd)/game" ] || [ ! -w "$(pwd)/game" ]; then perror "*** Required directory '\"$(pwd)\"/game' was not found or is not writable.\n" ; failed+=1; fi
111 if [ ! -d "$(pwd)/game/default" ]; then perror "*** Required directory '\"$(pwd)\"/game/default' was not found.\n"; failed+=1; fi
112 if [ ! -f "$(pwd)/CMakeLists.txt" ]; then perror "*** Required file '\"$(pwd)\"/engine/CMakeLists.txt' was not found.\n"; failed+=1; fi
113 if [ ! -f "$(pwd)/game/default/dpconfig.cmake" ]; then perror "*** Required file '\"$(pwd)\"/game/default/config.cmake' was not found.\n"; failed+=1; fi
115 if ! command -v cmake >/dev/null; then perror "*** Could not find cmake. Please install it and make sure it's in your PATH.\n"; failed+=1; fi
117 if (( failed )); then
118 perror "*** The script failed to initialize. Please check the output for more information.\n\n"
122 #------------------------------------------------------------------------------#
123 option_cache_read() {
124 cache_file="${cache_dir}/${option_project}"
126 if [ -f "${cache_file}" ]; then
127 if grep -q "cache_" "${cache_file}" ; then
129 psuccess "* Loaded cached settings for '$option_project'\n\n"
132 perror "* Could not load '$option_project' from the cache. Please check if this is a\nvalid cache file.\n\n"
140 option_cache_write() {
141 if [ ! -d "${cache_dir}" ]; then
142 if [ -f "${cache_dir}" ]; then
143 perror "* The cache directory cannot be created because a file of the same name exists.\nThe cache cannot function. Please rename or delete this file.\n\n"
149 if (( cache_changed )); then
150 if (( ! option_cache_off )); then
153 cache_project_dir=\"${option_project_dir}\"
154 cache_build_dir=\"${option_build_dir}\"
155 cache_build_cc=\"${option_build_cc}\"
156 cache_build_cxx=\"${option_build_cxx}\"
157 cache_build_threads=\"${option_build_threads}\"
158 cache_build_cmake_generator=\"${option_build_cmake_generator}\"
159 cache_build_cmake_options=\"${option_build_cmake_options}\"
163 Your build options for \"%s\" has been written to the cache. You only have to
164 run '%s %s' to build the same project again.
166 If you wish to change any setting later, you can pass --new-options to the
169 " "$option_project" "$me" "$option_project"
170 else pwarn "* The cache is disabled. Skipping write.\n\n"; fi
174 option_cache_compare() {
175 local option=$1; local cache=$2
177 if ! [ "${option}" = "${cache}" ]; then cache_changed=1; return 1; fi
181 option_cache_list() {
182 local cache_list; cache_list="$(pwd)/.temp"
185 if (( option_auto )) || check_empty "${cache_dir}" || [ "$option_project" ]; then
188 printf "Please select a project."
190 if (( ! option_run_reset_build )); then
191 printf " Leave blank to create a new one."
196 while [ ! "$option_project" ]; do
200 for i in "${cache_dir}"/*; do
201 printf "%s. %s\n" "$j" "$(basename "$i")" >> "$cache_list"
207 if [ ! "$cache_select" ]; then break; fi
208 option_project="$(sed -n -e "s/^.*$cache_select. //p" "$cache_list")"
212 #------------------------------------------------------------------------------#
213 option_get_prompt() { # Generic prompt function
214 local -n option=$1; local default=$2
215 local message=$3; local required=$4
216 local error=$5; local default_text="[$default]"
218 if ! [ "$default" ]; then default_text=""; fi
220 if (( option_auto )); then # No prompts in auto mode.
221 if (( required )); then
230 printf -- "%b\n" "$message"
231 read -rp "$default_text: " option
232 option=${option:-$default}
237 option_get_cmdline() { # Iterate over any args.
238 for (( i=0; i<${#args[@]}; i++)); do
239 if [[ "${args[$i]}" == "-"* ]]; then
240 case "${args[$i]}" in
244 option_run_reset_build=1 ;;
246 option_run_reset_cache=1 ;;
250 option_build_dir=${args[$i]##--build-dir=} ;;
252 option_project_dir=${args[$i]##--config-dir=} ;;
254 option_build_cc=${args[$i]##--cc=} ;;
256 option_build_cxx=${args[$i]##--cxx=} ;;
258 option_build_threads=${args[$i]##--threads=} ;;
260 option_build_threads=${args[$i]##--j} ;;
262 option_build_threads=${args[$i]##-j} ;;
264 option_build_cmake_generator=${args[$i]##--generator=} ;;
265 "--cmake-options="* )
266 option_build_cmake_options=${args[$i]##--cmake-options=} ;;
268 option_cache_off=1 ;;
270 option_from_cmake=1 ;;
276 pwarn "Unknown option '${args[$i]}'\n"
279 # Last arg should be the build config, but not the first arg.
280 else option_project=${args[$i]}; fi
283 #------------------------------------------------------------------------------#
285 if (( option_run_reset_cache )); then reset_cache; fi
287 if (( option_auto )); then
288 pwarn "* --auto is set. Prompts will not appear.\n\n"
289 if (( option_new )); then
291 pwarn "* --new-options is useless in auto mode. Ignoring.\n\n"
296 option_get_check_config
298 if ! (( option_cache_off )); then option_cache_read
299 else pwarn "* The cache is disabled. Skipping read.\n\n"; fi
301 if (( option_run_reset_build )); then reset_build; exit 0; fi
303 option_get_check_config_dir
304 option_get_check_build_dir
305 if (( ! option_from_cmake )); then
306 option_get_check_build_cc
307 option_get_check_build_cxx
308 option_get_check_build_threads
309 option_get_check_build_cmake_generator
310 option_get_check_build_cmake_options
314 option_get_check_config() { # If the user didn't give us anything, ask.
315 if (( option_run_reset_build )); then pwarn "* Resetting build files...\n\n"; fi
318 if [ "$option_project" ]; then
319 if [ "${option_project}" == "default" ]; then
320 pwarn "* H-hey...! Get your own project! That's the template. You can't use that!\n\n"
323 else option_get_prompt \
326 "Please specify the name of your project" \
333 option_get_check_config_dir() {
336 if [ ! "$cache_project_dir" ]; then # Set the default
337 cache_project_dir="$(pwd)/game/$option_project"; fi
339 # Don't prompt for this unless something is wrong. Assume the default.
340 if [ ! "$option_project_dir" ]; then
341 if (( cache_new )) || (( ! option_new )); then
342 option_project_dir="$cache_project_dir"
347 if [ "$option_project_dir" ]; then
348 if [ -d "${option_project_dir}" ]; then
349 option_cache_compare "$option_project_dir" "$cache_project_dir"
350 return # We're good. Proceed.
351 else printf "* The directory of the specified project does not exist.\n\n"
352 if [ -w "$(dirname "$option_project_dir")" ]; then
356 "Would you like to create a new project from the template in:\n$option_project_dir?" \
359 if [[ "$new_config" =~ ^(Y|y)$ ]]; then
360 cp -rv "$config_template" "$option_project_dir"
363 elif [ -f "${option_project_dir}" ]; then
364 pwarn "* The directory of the specified project is a file. Cannot create a new project\nfrom the template here.\n\n"
366 pwarn "* The parent directory is also not writable or doesn't exist. Cannot create a new\nproject from the template here.\n\n"
369 elif (( ! option_new )); then
370 pwarn "* No config directory has been specified and --new-options isn't set. Something is wrong with your configuration, or there's\na bug in the script.\n\n"
373 # Get our answer unless --auto is set.
376 "$cache_project_dir" \
377 "Specify the location of '${option_project}'. If it doesn't exist, it can be created\nfrom a template." \
379 "You must provide a valid project directory with --auto set\n\n"
383 option_get_check_build_dir() {
384 local finished=0; local force=0; local ask=0
386 if [ ! "$cache_build_dir" ]; then
387 cache_build_dir="$(pwd)/output/$option_project/"; fi
389 # Don't prompt for this unless something is wrong. Assume the default.
390 if [ ! "$option_build_dir" ]; then
391 if (( cache_new )) || (( ! option_new )); then
392 option_build_dir="$cache_build_dir"
396 while ! (( finished )); do
397 if [ "$option_build_dir" ]; then
398 # Check if it even exists first.
399 if [ -d "$option_build_dir" ]; then # Exists
400 # Check if writable. Permissions change.
401 if ! [ -w "$option_build_dir" ]; then
402 pwarn "* The directory '$option_build_dir' is NOT writable.\n\n"
404 elif ! check_empty "$option_build_dir" &&
405 [[ $cache_build_dir != "$option_build_dir" ]]; then
406 if (( ! option_from_cmake )) && [ ! -f "$option_build_dir/CMakeCache.txt" ]; then
407 pwarn "* The directory '$option_build_dir' is NOT empty.\n\n"
411 "Would you like to build here anyway?" \
413 "*** You must specify an empty directory when --auto is set."
414 if [[ "$force" =~ ^(Y|y)$ ]]; then ask=1; fi
418 pwarn "* Build directory doesn't exist. CMake will create the directory for you.\n\n"
426 "Please provide an empty and writable directory for the build files" \
428 "*** You must provide a valid build directory with --auto set\n\n"
434 option_get_check_build_cc() {
435 if [ ! "$option_build_cc" ] && (( option_new )); then
439 "Which C compiler would you like to use? Leave this default to use the compiler\nspecified in the cache, or if blank, for CMake to autodetect the compiler,\nunless you have something specific in mind (like clang)" \
444 export CC="$option_build_cc"
446 if ! option_cache_compare "$option_build_cc" "$cache_build_cc" && (( ! cache_new )); then
447 pwarn "* The cmake cache must be deleted and regenerated when changing compilers.\n\n"
452 option_get_check_build_cxx() {
453 if [ ! "$option_build_cxx" ] && (( option_new )); then
457 "Which C++ compiler would you like to use? Leave this default to use the compiler\nspecified in the cache, or if blank, for CMake to autodetect the compiler,\nunless you have something specific in mind (like clang++)" \
462 export CXX="$option_build_cxx"
464 if ! option_cache_compare "$option_build_cxx" "$cache_build_cxx" && (( ! cache_new )); then
465 pwarn "* The cmake cache must be deleted and regenerated when changing compilers.\n\n"
470 option_get_check_build_threads() {
471 if [ ! "$option_build_threads" ] && (( ! option_new )); then
472 option_build_threads="$cache_build_threads"
476 if [ "$option_build_threads" ]; then
477 if (( ! option_build_threads )); then
478 pwarn "* Threads must be a number, silly!\n\n"
479 elif (( option_build_threads < 0 )); then
480 pwarn "* Threads can't be a negative number, silly!\n\n"
484 option_build_threads \
485 "$cache_build_threads" \
486 "How many threads would you like to compile with? Enter 0 to run the configure\nstep only." \
490 option_cache_compare "$option_build_threads" "$cache_build_threads"
493 option_get_check_build_cmake_generator() {
494 if [ ! "$option_build_cmake_generator" ]; then
495 if (( ! option_new )); then
496 option_build_cmake_generator="$cache_build_cmake_generator"
499 option_build_cmake_generator \
500 "$cache_build_cmake_generator" \
501 "What CMake generator would you like to use?" \
506 if ! option_cache_compare "$option_build_cmake_generator" "$cache_build_cmake_generator" && (( ! cache_new )); then
507 pwarn "* The cmake cache must be deleted and regenerated when changing generators.\n\n"
512 option_get_check_build_cmake_options() {
513 if (( option_new )); then
515 option_build_cmake_options \
516 "$cache_build_cmake_options" \
517 "Specify additional command-line options for CMake" \
521 option_build_cmake_options="$cache_build_cmake_options"
523 option_cache_compare "$option_build_cmake_options" "$cache_build_cmake_options"
525 #------------------------------------------------------------------------------#
526 build_start_config() {
527 local cmd_cmake_config="cmake -G\"${option_build_cmake_generator}\" -B$option_build_dir -DGAME_PROJECT=\"${option_project}\" -DGAME_PROJECT_DIR=\"${option_project_dir}\" $option_build_cmake_options"
529 printf "* Running CMake...\n\n"
530 printf "* Using \"%s\" build config.\n\n" "$option_project"
532 # Try to configure. If that's successful, go ahead and build.
533 printf "* CMake config commandline: %s\n\n" "$cmd_cmake_config"
534 if ! eval "$cmd_cmake_config"; then
535 perror "*** Configure failed. Please check the output for more information.\n\n"
538 psuccess "* Configure completed successfully.\n\n"
541 build_start_compile() {
542 local cmd_cmake_build="cmake --build $option_build_dir -- -j$option_build_threads"
544 printf "* CMake build commandline: %s\n\n" "$cmd_cmake_build"
545 if ! eval "$cmd_cmake_build"; then
546 pwarn "* Build failed, but configure was successful. Please check the output for more information.\n\n"
549 psuccess "* Build completed successfully.\n\n"
556 #------------------------------------------------------------------------------#
558 local reset="Y" # Defaults to Y in case --auto is set
559 if ! [ "$cache_build_dir" ]; then # Not set?
560 pwarn "* --reset-build: No build directory for '$option_project' was specified or\nfound in the cache, or the cache is disabled.\n\n"
561 elif ! [ -d "$cache_build_dir" ]; then # Doesn't exist?
562 pwarn "* --reset-build: The build directory of '$option_project' doesn't exist.\nNothing to delete.\n\n"
567 "Do you wish to delete all build files under\n'$cache_build_dir'?"
568 if [[ "$reset" =~ ^(Y|y)$ ]]; then
569 if ! rm -rfv "$cache_build_dir" ; then # Can't delete?
570 perror "*** --reset-build: Failed to delete build files under '$cache_build_dir'\n\n"
572 pwarn "* --reset-build: Deleted the build directory of '$option_project'.\n\n"
578 reset_cache() { # It resets the cache.
579 local reset="Y" # Defaults to Y in case --auto is set
581 if [ -d "$cache_dir" ]; then
585 "Do you wish to delete the entire build cache?"
586 if [[ "$reset" =~ ^(Y|y)$ ]]; then
588 pwarn "* --reset-cache: Deleted the build cache.\n\n"
591 pwarn "* --reset-cache: The build cache doesn't exist. Nothing to delete.\n\n"
595 #------------------------------------------------------------------------------#
596 printf "\n\e[1;34m---Darkplaces Build Wizard---\e[0m\n\n"
598 declare -g args=("$@") # Put cmdline in separate array to be read in functions
599 declare -g me=$0 # Script can refer to itself regardless of filename in global scope
600 declare -g cache_dir; cache_dir="./.cache"
601 declare -g cache_file # Current cache file handle.
602 declare -g config_template; config_template="$(pwd)/game/default"
604 ### Default options ###
605 # These are changed by the cache (if it exists) and compared with user input.
606 declare -g cache_project="darkplaces"
607 declare -g cache_project_dir="" # Defined later
608 declare -g cache_build_dir="" # Defined later
609 declare -g cache_build_cc=""
610 declare -g cache_build_cxx=""
611 declare -g cache_build_threads=1
612 declare -g cache_build_cmake_options=""
613 declare -g cache_build_cmake_generator="Unix Makefiles"
616 # If any of these don't match the cache, write to it.
617 declare -g option_project
618 declare -g option_project_dir
619 declare -g option_build_dir
620 declare -g option_build_cc
621 declare -g option_build_cxx
622 declare -g option_build_threads
623 declare -g option_build_cmake_options
624 declare -g option_build_cmake_generator
626 declare -g option_auto=0
627 declare -g option_new=0
628 declare -g option_asroot=0
629 declare -g option_run_reset_build=0
630 declare -g option_run_reset_cache=0
631 declare -g option_cache_off=0
632 declare -g option_from_cmake=0
634 # State tracking variables
635 declare -g cache_new=0
636 declare -g cache_loaded=0
637 declare -g cache_changed=0 # Set to 1 if any options don't match the cache.
639 check_env # Make sure the environment is sane first
640 option_get_cmdline # Get input from command-line
641 option_get_check # Check that input and ask for new input
642 build_start # Start build with options
643 option_cache_write # Write to cache
645 psuccess "* The Darkplaces Build Wizard has completed the requested operations\n successfully.\n\n"