#!/usr/bin/env bash # # Your build script should simply source this script, optionally override any build hooks and then invoke `build`. # The build product should be available under `build-~/out`, under the library path. # # Hook lifecycle: # - build # - initialize # - needs # - clean & exit (only if script was ran with "clean" argument) # - check & exit (only if target has already been successfully built) # - prepare # - create # - config # - target # - prepare # - configure # - build # - finalize # - merge # - clean # # You can override any of these hooks to provide a custom implementation or call their underscore variant to delegate to the default implementation. # For example: # target_prepare() { make -s distclean; } # target_configure() { _target_configure "$@" --enable-minimal; } # set -e PATH+=:/usr/local/bin # needs ... # # Utility for ensuring all tools needed by the script are installed prior to starting. # needs() { _needs "$@"; } _needs() { local failed=0 for spec; do IFS=: read pkg tools <<< "$spec" IFS=, read -a tools <<< "${tools:-$pkg}" for tool in "${tools[@]}"; do hash "$tool" 2>/dev/null && continue 2 done echo >&2 "Missing: $pkg. Please install this package." (( failed++ )) done return $failed } # initialize # # The build script invokes this once prior to all other actions. # initialize() { _initialize "$@"; } _initialize() { initialize_needs "$@" } # initialize_needs # # Check if all tools required to configure and build for the platform are available. # # By default, this will check for: # - Windows: MSBuild # - Other: `libtool` (for libtoolize), `automake` (for aclocal), `autoconf` (for autoreconf) and make # initialize_needs() { _initialize_needs "$@"; } _initialize_needs() { if [[ $platform = windows ]]; then needs cmd for dir in "$VSINSTALLDIR" "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/..; do dir=$( [[ $dir ]] && cd "$dir" && [[ -e "Common7/Tools/VsMSBuildCmd.bat" ]] && cygpath -w "$PWD" ) && \ export VSINSTALLDIR=$dir && echo "Using MSBuild: $VSINSTALLDIR" && return done echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'. See https://visualstudio.microsoft.com/downloads/?q=build+tools" return 1 else needs libtool:libtoolize,glibtoolize automake autoconf make fi } # clean # # Fully clean up the library code, restoring it to a pristine state. # # By default, this will: # - Windows: `msbuild /t:Clean`, or # - Makefile: run `make distclean`, or # - GIT: `git clean -fdx` # # Finally, it will wipe the prefix. # clean() { _clean "$@"; } _clean() { if [[ $platform = windows ]]; then PATH="$(cygpath "$VSINSTALLDIR")/Common7/Tools:$PATH" \ cmd /v /c 'VsMSBuildCmd && for %s in (*.sln) do msbuild /t:Clean %s' elif [[ -e Makefile ]] && make -s distclean; then : elif [[ -e .git ]] && git clean -fdx; then : fi rm -rf "$prefix" } # prepare [ ... ] # # Initialize the prefix in anticipation for building the s on this machine. # The build script invokes this once prior to building each of its targets. # prepare() { _prepare "$@"; } _prepare() { prepare_create "$@" prepare_config "$@" } # prepare_create [ ... ] # # Perform any necessary clean-up of the library code prior to building. # # By default, this will wipe the build configuration and re-create the prefix. # TODO: Should this differ from clean()? # prepare_create() { _prepare_create "$@"; } _prepare_create() { local prefix=$1 platform=$2; shift 2 if [[ $platform = windows ]]; then : else [[ ! -e Makefile ]] || make -s distclean || git clean -fdx fi rm -rf "$prefix" install -d "$prefix/out" } # prepare_config [ ... ] # # Generate build solution for configuring a build on this machine. # The has been newly created. # # TODO: cmake support? # By default, this will: # - Windows: do nothing # - Other: run `autoreconf`. # prepare_config() { _prepare_config "$@"; } _prepare_config() { local prefix=$1 platform=$2; shift 2 [[ -e "$prefix/out/.prepared" ]] && return if [[ $platform = windows ]]; then : else # autoreconf installs a useless INSTALL documentation stub that can overwrite repo docs. [[ -e INSTALL ]] && mv INSTALL{,~} autoreconf --verbose --install --force 2> >(sed 's/^\([^:]*\):[0-9]\{1,\}: /\1: /') [[ -e INSTALL~ ]] && mv INSTALL{~,} fi touch "$prefix/out/.prepared" } # target # # Build the library to the binary for the architecture on into the given . # The build script invokes this function when it's ready to build the library's code. # target() { _target "$@"; } _target() { target_prepare "$@" target_configure "$@" target_build "$@" } # target_prepare # # Any build-related work to be done in the prefix prior to building. # # By default, this will: # - Windows: do nothing # - macOS/iOS: Discover SDKROOT & build flags # - Android: Prepare an NDK toolchain & build flags # - Makefile: run `make clean` # target_prepare() { _target_prepare "$@"; } _target_prepare() { local prefix=$1 platform=$2 arch=$3 host=$4; shift 3 case "$platform" in 'windows') ;; 'macos') SDKROOT="$(xcrun --show-sdk-path --sdk macosx)" export PATH="$(xcrun --show-sdk-platform-path --sdk macosx)/usr/bin:$PATH" export CPPFLAGS="-arch $host -flto -O2 -g -isysroot $SDKROOT -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET:-10.8} $CPPFLAGS" export LDFLAGS="-arch $host -flto -isysroot $SDKROOT -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET:-10.8} $LDFLAGS" ;; 'ios') case "$arch" in *'arm'*) SDKROOT="$(xcrun --show-sdk-path --sdk iphoneos)" export PATH="$(xcrun --show-sdk-platform-path --sdk iphoneos)/usr/bin:$PATH" export CPPFLAGS="-arch $host -mthumb -fembed-bitcode -flto -O2 -g -isysroot $SDKROOT -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET:-8.0} $CPPFLAGS" export LDFLAGS="-arch $host -mthumb -fembed-bitcode -flto -isysroot $SDKROOT -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET:-8.0} $LDFLAGS" ;; *) SDKROOT="$(xcrun --show-sdk-path --sdk iphonesimulator)" export PATH="$(xcrun --show-sdk-platform-path --sdk iphonesimulator)/usr/bin:$PATH" export CPPFLAGS="-arch $host -flto -O2 -g -isysroot $SDKROOT -mios-simulator-version-min=${IPHONEOS_DEPLOYMENT_TARGET:-8.0} $CPPFLAGS" export LDFLAGS="-arch $host -flto -isysroot $SDKROOT -mios-simulator-version-min=${IPHONEOS_DEPLOYMENT_TARGET:-8.0} $LDFLAGS" ;; esac ;; 'android') [[ -x $ANDROID_NDK_HOME/build/ndk-build ]] || { echo >&2 "Android NDK not found. Please set ANDROID_NDK_HOME."; return 1; } SDKROOT="$prefix/$arch/ndk" # Platform 21 is lowest that supports x86_64 "$ANDROID_NDK_HOME/build/tools/make-standalone-toolchain.sh" --force --install-dir="$SDKROOT" --platform='android-21' --arch="$arch" export PATH="$SDKROOT/bin:$PATH" export CPPFLAGS="-O2 -g $CPPFLAGS" export LDFLAGS="-avoid-version $LDFLAGS" export CC='clang' ;; *) case "$arch" in x86) export CPPFLAGS="-m32 $CPPFLAGS" LDFLAGS="-m32 $LDFLAGS" ;; x86_64) export CPPFLAGS="-m64 $CPPFLAGS" LDFLAGS="-m64 $LDFLAGS" ;; esac ;; esac if [[ $platform = windows ]]; then : else [[ ! -e Makefile ]] || make -s clean fi } # target_configure [ ... ] # # Configure the library for building the target. This generates the compiler configuration. # # By default, this will: # - Windows: do nothing # - Other: run `./configure --host= --prefix=/ `. # # Some platform-specific configure arguments will be passed in as well. # --enable-pic --disable-pie to ensure the resulting library can be linked again. # target_configure() { _target_configure "$@"; } _target_configure() { local prefix=$1 platform=$2 arch=$3 host=$4; shift 4 local build= [[ -x config.guess ]] && build=$(./config.guess) [[ -x build-aux/config.guess ]] && build=$(build-aux/config.guess) case "$platform" in 'windows') # doesn't use ./configure return 0 ;; 'ios'|'macos') host+=-apple set -- --enable-static --disable-shared "$@" ;; 'android') host=( "$SDKROOT/$host"*-android* ) host=${host##*/} set -- --disable-static --enable-shared --with-sysroot="$SDKROOT/sysroot" "$@" ;; *) set -- --enable-static --disable-shared "$@" ;; esac ./configure ${build:+--build="$build"} ${host:+--host="$host"} --prefix="$prefix/$arch" --enable-pic --disable-pie "$@" } # target_build # # Build the library code for the target. This runs the compiler per the previous configuration. # # By default, this will: # - Windows: run `msbuild /t:Rebuild /p:Configuration:Release;Platform=` # - Other: run `make check install`. # target_build() { _target_build "$@"; } _target_build() { local prefix=$1 platform=$2 arch=$3 host=$4; shift 4 if [[ $platform = windows ]]; then if [[ -e CMakeLists.txt ]]; then ( projdir=$PWD; mkdir -p "$prefix/$arch/"; cd "$prefix/$arch/" PATH="$(cygpath "$VSINSTALLDIR")/Common7/Tools:$(cygpath "$VSINSTALLDIR")/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH" \ cmd /v /c "$(printf 'VsMSBuildCmd && cmake -A %s %s && for %%s in (*.sln) do msbuild /m /t:Rebuild /p:Configuration=Release;Platform=%s;OutDir=. %%s' \ "$host" "$(cygpath -w "$projdir")" "$host")" ) else PATH="$(cygpath "$VSINSTALLDIR")/Common7/Tools:$PATH" \ cmd /v /c "$(printf 'VsMSBuildCmd && for %%s in (*.sln) do msbuild /m /t:Rebuild /p:Configuration=Release;Platform=%s;OutDir=%s %%s' \ "$host" "$(cygpath -w "${prefix##$PWD/}/$arch/")")" fi else local cores=$(getconf NPROCESSORS_ONLN 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null ||:) #make -j"${cores:-3}" check install # TODO: libjson-c breaks on parallel build for check and install #make check install # TODO: libjson-c has a failing test atm make install fi } # finalize [ ... ] # # Prepare the final build product. # The build script invokes this once after a successful build of all targets. # finalize() { _finalize "$@"; } _finalize() { finalize_merge "$@" finalize_clean "$@" } # finalize_merge [ ... ] # # Merge all targets into a product the application can use, available at `/out`. # # By default, this will copy the headers to `/out/include`, install libraries into `/out/lib` and mark the output product as successful. # finalize_merge() { _finalize_merge "$@"; } _finalize_merge() { local prefix=$1 platform=$2; shift 2 local archs=( "$@" ) [[ -e "$prefix/$archs/include" ]] && cp -a -- "$prefix/$archs/include" "$prefix/out/" install -d "$prefix/out/lib" case "$platform" in 'linux') for arch in "${archs[@]}"; do install -d "$prefix/out/lib/$arch" install -p "$prefix/$arch/lib/"*.a "$prefix/out/lib/$arch/" done ;; 'windows') for arch in "${archs[@]}"; do install -d "$prefix/out/lib/$arch" install -p "$prefix/$arch/"*.lib "$prefix/out/lib/$arch/" done ;; 'macos'|'ios') for arch in "${archs[@]}"; do install -d "$prefix/out/lib/$arch" install -p "$prefix/$arch/lib/"*.a "$prefix/out/lib/$arch/" done local libs=( "$prefix/out/lib/"*/* ) lipo -create "${libs[@]}" -output "$prefix/out/lib/${libs##*/}" ;; 'android') for arch in "${archs[@]}"; do local abi=$arch case "$arch" in 'arm') abi='armeabi-v7a' ;; 'arm64') abi='arm64-v8a' ;; esac install -d "$prefix/out/lib/$abi" install -p "$prefix/$arch/lib/"*.so "$prefix/out/lib/$abi/" done ;; esac touch "$prefix/out/.success" } # finalize_clean [ ... ] # # Clean up the library after a successful build (eg. housekeeping of temporary files). # # By default, this will run `make clean`. # finalize_clean() { _finalize_clean "$@"; } _finalize_clean() { if [[ $platform = windows ]]; then : else [[ ! -e Makefile ]] || make -s clean fi } # build [] # # Build the library (found at ../) for platform (or "host" if unspecified). # build() { _build "$@"; } _build() { local name=$1 platform=${2:-host} local path="../$name" [[ $path = /* ]] || path="${BASH_SOURCE%/*}/$path" cd "$path" if [[ $platform = host ]]; then case "$(uname -s)" in 'Darwin') platform='macos' archs=( "$(uname -m)" ) ;; esac fi if (( ! ${#archs[@]} )); then case "$platform" in 'macos') archs=( 'x86_64' ) ;; 'ios') archs=( 'x86:i386' 'x86_64' 'armv7' 'armv7s' 'arm64' ) ;; 'android') archs=( 'arm' 'arm64:aarch64' 'x86:i686' 'x86_64' ) ;; 'windows') archs=( 'x86:Win32' 'x86_64:x64' ) ;; *) archs=( 'x86:i686' 'x86_64' ) ;; esac fi local prefix="$PWD/build-$platform~" echo echo " # $name ($platform: ${archs[*]}) into $prefix ..." initialize "$prefix" "$platform" # "clean" argument wipes the lib clean and exits. If .success exists in prefix output, skip build. if [[ ${BASH_ARGV[@]:(-1)} = clean ]]; then clean "$prefix" "$platform" exit elif [[ -e "$prefix"/out/.success ]]; then echo >&2 "Skipping build for $platform: output product already built successfully." exit fi # Prepare the output location and build configuration. prepare "$prefix" "$platform" "${archs[@]}" # Repeat the build for each individual architecture. for arch in "${archs[@]}"; do ( local host=${arch#*:} arch=${arch%%:*} echo echo " # $name [$platform: $arch ($host)] ..." target "$prefix" "$platform" "$arch" "$host" ); done finalize "$prefix" "$platform" "${archs[@]%%:*}" }