#!/bin/sh set -euf print_usage() { cat < [options] [arguments] Example: tool build --portmidi orca Commands: build Compiles the livecoding environment or the CLI tool. Targets: orca, cli Output: build/ clean Removes build/ info Prints information about the detected build environment. help Prints this message and exits. Options: -c Use a specific compiler binary. Default: \$CC, or cc -d Build with debug features. Output changed to: build/debug/ --harden Enable compiler safeguards like -fstack-protector. You should probably do this if you plan to give the compiled binary to other people. --static Build static binary. --pie Enable PIE (ASLR). Note: --pie and --static cannot be mixed. -s Print statistics about compile time and binary size. -v Print important commands as they're executed. -h or --help Print this message and exit. Optional Features: --portmidi Enable or disable hardware MIDI output support with --no-portmidi PortMidi. Note: PortMidi has memory leaks and bugs. Default: disabled. --mouse Enable or disable mouse features in the livecoding --no-mouse environment. Default: enabled. EOF } warn() { printf 'Warning: %s\n' "$*" >&2; } fatal() { printf 'Error: %s\n' "$*" >&2; exit 1; } script_error() { printf 'Script error: %s\n' "$*" >&2; exit 1; } if [ -z "${1:-}" ]; then printf 'Error: Command required\n' >&2 print_usage >&2 exit 1 fi cmd=$1 shift case $(uname -s | awk '{print tolower($0)}') in linux*) os=linux;; darwin*) os=mac;; cygwin*) os=cygwin;; *bsd*) os=bsd;; *) os=unknown; warn "Build script not tested on this platform";; esac cc_exe="${CC:-cc}" if [ $os = cygwin ]; then # Under cygwin, specifically ignore the mingw compilers if they're set as the # CC environment variable. This may be the default from the cygwin installer. # But we want to use 'gcc' from the cygwin gcc-core package (probably aliased # to cc), *not* the mingw compiler, because otherwise lots of POSIX stuff # will break. (Note that the 'cli' target might be fine, because it doesn't # uses curses or networking, but the 'orca' target almost certainly won't # be.) # # I'm worried about ambiguity with 'cc' being still aliased to mingw if the # user doesn't have gcc-core installed. I have no idea if that actually # happens. So we'll just explicitly set it to gcc. This might mess up people # who have clang installed but not gcc, I guess? Is that even possible? case $cc_exe in i686-w64-mingw32-gcc.exe|x86_64-w64-mingw32-gcc.exe) cc_exe=gcc;; esac fi verbose=0 protections_enabled=0 stats_enabled=0 pie_enabled=0 static_enabled=0 portmidi_enabled=0 mouse_disabled=0 config_mode=release while getopts c:dhsv-: opt_val; do case $opt_val in -) case $OPTARG in harden) protections_enabled=1;; help) print_usage; exit 0;; static) static_enabled=1;; pie) pie_enabled=1;; portmidi) portmidi_enabled=1;; no-portmidi|noportmidi) portmidi_enabled=0;; mouse) mouse_disabled=0;; no-mouse|nomouse) mouse_disabled=1;; *) printf 'Unknown option --%s\n' "$OPTARG" >&2; exit 1;; esac;; c) cc_exe=$OPTARG;; d) config_mode=debug;; h) print_usage; exit 0;; s) stats_enabled=1;; v) verbose=1;; \?) print_usage >&2; exit 1;; esac done case $(uname -m) in x86_64) arch=x86_64;; *) arch=unknown;; esac verbose_echo() { # Don't print 'timed_stats' if it's the first part of the command if [ $verbose = 1 ] && [ $# -gt 1 ]; then printf '%s ' "$@" | sed -E -e 's/^timed_stats[[:space:]]+//' -e 's/ $//' \ | tr -d '\n' printf '\n' fi "$@" } file_size() { wc -c < "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } timed_stats_result= timed_stats() { if [ $stats_enabled = 1 ] && command -v time >/dev/null 2>&1; then TIMEFORMAT='%3R' { timed_stats_result=$( { time "$@" 1>&3- 2>&4-; } 2>&1 ); } 3>&1 4>&2 else "$@" fi } normalized_version() { printf '%s\n' "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } cc_id= cc_vers= lld_detected=0 lld_name=lld if preproc_result=$( \ ("$cc_exe" -E -xc - 2>/dev/null | tail -n 2 | tr -d '\040') </dev/null 2>&1; then lld_detected=1; fi ;; esac fi test -z "$cc_id" && warn "Failed to detect compiler type" test -z "$cc_vers" && warn "Failed to detect compiler version" cc_vers_normalized=$(normalized_version "$cc_vers") cc_vers_is_gte() { test "$cc_vers_normalized" -ge "$(normalized_version "$1")" } cc_id_and_vers_gte() { test "$cc_id" = "$1" && cc_vers_is_gte "$2" } # Append arguments to a string, separated by newlines. Like a bad array. add() { if [ -z "${1:-}" ]; then script_error "At least one argument required for add" fi _add_name=${1} shift while [ -n "${1+x}" ]; do # shellcheck disable=SC2034 _add_hidden=$1 eval "$_add_name"'=$(printf '"'"'%s\n%s.'"' "'"$'"$_add_name"'" "$_add_hidden")' eval "$_add_name"'=${'"$_add_name"'%.}' shift done } try_make_dir() { if ! [ -e "$1" ]; then verbose_echo mkdir "$1" elif ! [ -d "$1" ]; then fatal "File $1 already exists but is not a directory" fi } build_dir=build build_target() { cc_flags= libraries= source_files= out_exe= add cc_flags -std=c99 -pipe -finput-charset=UTF-8 -Wall -Wpedantic -Wextra \ -Wwrite-strings if cc_id_and_vers_gte gcc 6.0.0 || cc_id_and_vers_gte clang 3.9.0; then add cc_flags -Wconversion -Wshadow -Wstrict-prototypes \ -Werror=implicit-function-declaration -Werror=implicit-int \ -Werror=incompatible-pointer-types -Werror=int-conversion fi if [ "$cc_id" = tcc ]; then add cc_flags -Wunsupported fi if [ $os = mac ] && [ "$cc_id" = clang ]; then # The clang that's shipped with Mac 10.12 has bad behavior for issuing # warnings for structs initialed with {0} in C99. We have to disable this # warning, or it will issue a bunch of useless warnings. It might be fixed # in later versions, but Apple makes the version of clang/LLVM # indecipherable, so we'll just always turn it off. add cc_flags -Wno-missing-field-initializers fi if [ $lld_detected = 1 ]; then add cc_flags "-fuse-ld=$lld_name" fi if [ $protections_enabled = 1 ]; then add cc_flags -D_FORTIFY_SOURCE=2 -fstack-protector-strong fi if [ $pie_enabled = 1 ]; then add cc_flags -pie -fpie -Wl,-pie # Only explicitly specify no-pie if cc version is new enough elif cc_id_and_vers_gte gcc 6.0.0 || cc_id_and_vers_gte clang 6.0.0; then add cc_flags -no-pie -fno-pie fi if [ $static_enabled = 1 ]; then add cc_flags -static fi case $config_mode in debug) add cc_flags -DDEBUG -ggdb # cygwin gcc doesn't seem to have this stuff, so just elide for now if [ $os != cygwin ]; then if cc_id_and_vers_gte gcc 6.0.0 || cc_id_and_vers_gte clang 3.9.0; then add cc_flags -fsanitize=address -fsanitize=undefined \ -fsanitize=float-divide-by-zero fi if cc_id_and_vers_gte clang 7.0.0; then add cc_flags -fsanitize=implicit-conversion \ -fsanitize=unsigned-integer-overflow fi fi case $os in mac) add cc_flags -O1;; # Our Mac clang does not have -Og *) add cc_flags -Og;; esac case $cc_id in tcc) add cc_flags -g -bt10;; esac ;; release) add cc_flags -DNDEBUG -O2 -g0 if [ $protections_enabled != 1 ]; then add cc_flags -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 case $cc_id in gcc|clang) add cc_flags -fno-stack-protector esac fi # -flto is good on both clang and gcc on Linux and Cygwin. Not supported # on BSD, and no improvement on Mac. -s gives an obsolescence warning on # Mac. For tcc, -flto gives and unsupported warning, and -s is ignored. case $cc_id in gcc|clang) case $os in linux|cygwin) add cc_flags -flto -s;; bsd) add cc_flags -s;; esac esac ;; *) fatal "Unknown build config \"$config_mode\"";; esac case $arch in x86_64) case $cc_id in # 'nehalem' tuning actually produces faster code for orca than later # archs, for both gcc and clang, even if it's running on a later arch # CPU. This is likely due to smaller emitted code size. gcc earlier # than 4.9 does not recognize the arch flag for it it, though, and I # haven't tested a compiler that old, so I don't know what optimization # behavior we get with it is. Just leave it at default, in that case. gcc) if cc_vers_is_gte 4.9; then add cc_flags -march=nehalem fi ;; clang) add cc_flags -march=nehalem;; esac ;; esac add source_files gbuffer.c field.c vmio.c sim.c case $1 in cli) add source_files cli_main.c out_exe=cli ;; orca|tui) add source_files osc_out.c term_util.c sysmisc.c thirdparty/oso.c tui_main.c add cc_flags -D_XOPEN_SOURCE_EXTENDED=1 # thirdparty headers (like sokol_time.h) should get -isystem for their # include dir so that any warnings they generate with our warning flags # are ignored. (sokol_time.h may generate sign conversion warning on # mac.) add cc_flags -isystem thirdparty out_exe=orca case $os in mac) if ! brew_prefix=$(printenv HOMEBREW_PREFIX); then brew_prefix=/usr/local fi ncurses_dir="$brew_prefix/opt/ncurses" if ! [ -d "$ncurses_dir" ]; then printf 'Error: ncurses directory not found at %s\n' \ "$ncurses_dir" >&2 printf 'Install with: brew install ncurses\n' >&2 exit 1 fi # prefer homebrew version of ncurses if installed. Will give us # better terminfo, so we can use A_DIM in Terminal.app, etc. add libraries "-L$ncurses_dir/lib" add cc_flags "-I$ncurses_dir/include" # todo mach time stuff for mac? if [ $portmidi_enabled = 1 ]; then portmidi_dir="$brew_prefix/opt/portmidi" if ! [ -d "$portmidi_dir" ]; then printf 'Error: PortMidi directory not found at %s\n' \ "$portmidi_dir" >&2 printf 'Install with: brew install portmidi\n' >&2 exit 1 fi add libraries "-L$portmidi_dir/lib" add cc_flags "-I$portmidi_dir/include" fi # needed for using pbpaste instead of xclip add cc_flags -DORCA_OS_MAC ;; bsd) if [ $portmidi_enabled = 1 ]; then add libraries "-L/usr/local/lib" add cc_flags "-I/usr/local/include" fi ;; *) # librt and high-res posix timers on Linux add libraries -lrt add cc_flags -D_POSIX_C_SOURCE=200809L ;; esac # Depending on the Linux distro, ncurses might have been built with tinfo # as a separate library that explicitly needs to be linked, or it might # not. And if it does, it might need to be either -ltinfo or -ltinfow. # Yikes. If this is Linux, let's try asking pkg-config what it thinks. curses_flags=0 if [ $os = linux ]; then if curses_flags=$(pkg-config --libs ncursesw formw 2>/dev/null); then # Split by spaces intentionall # shellcheck disable=SC2086 IFS=' ' add libraries $curses_flags curses_flags=1 else curses_flags=0 fi fi # If we didn't get the flags by pkg-config, just guess. (This will work # most of the time, including on Mac with Homebrew, and cygwin.) if [ $curses_flags = 0 ]; then add libraries -lncursesw -lformw fi if [ $portmidi_enabled = 1 ]; then add libraries -lportmidi add cc_flags -DFEAT_PORTMIDI if [ $config_mode = debug ]; then cat >&2 <&2 exit 1 ;; esac try_make_dir "$build_dir" if [ $config_mode = debug ]; then build_dir=$build_dir/debug try_make_dir "$build_dir" fi out_path=$build_dir/$out_exe IFS=' ' # shellcheck disable=SC2086 verbose_echo timed_stats "$cc_exe" $cc_flags -o "$out_path" $source_files $libraries compile_ok=$? if [ $stats_enabled = 1 ]; then if [ -n "$timed_stats_result" ]; then printf '%s\n' "time: $timed_stats_result" else printf '%s\n' "time: unavailable (missing 'time' command)" fi if [ $compile_ok = 0 ]; then printf '%s\n' "size: $(file_size "$out_path")" fi fi } print_info() { if [ $lld_detected = 1 ]; then linker_name=LLD # Not sure if we should always print the specific LLD name or not. Or never # print it. if [ "$lld_name" != lld ]; then linker_name="$linker_name ($lld_name)" fi else linker_name=default fi cat <&2 <&2 <