#!/usr/bin/env bash
#
# build_module.sh (c) NGINX, Inc. [v0.12 30-Aug-2017] Liam Crilly <liam.crilly@nginx.com>
#
# This script supports apt(8) and yum(8) package managers. Installs the minimum
# necessary prerequisite packages to build 3rd party modules for NGINX Plus.
# Obtains source for module and NGINX OSS, prepares for pkg-oss tool. Inspects
# module configuration and attempts to rewrite for dynamic build if necessary.
# Obtains pkg-oss tool, creates packaging files and copies in module source.
#
# CHANGELOG
# v0.12 [30-Aug-2017] -o option to specify destination for package files
#                     -y (--non-interactive) option for automated builds
# v0.11 [20-Jun-2017] Enforces NGINX versions that support dynamic modules
# v0.10 [27-Apr-2017] Fixed postinstall banner, improved .so filename detection,
#                     -v option for specifying OSS build/version
# v0.9  [10-Apr-2017] @defan patch, improved postinstall banner, added disclaimer
# v0.8  [30-Mar-2017] Package version is now tied to base OSS version instead of 0.01
# v0.7  [29-Mar-2017] Added RPM packaging, flexible command line options with defaults
# v0.6  [16-Feb-2017] Using pkg-oss tool instead of only compiling .so files

cat << __EOF__

DISCLAIMER DISCLAIMER DISCLAIMER DISCLAIMER DISCLAIMER DISCLAIMER DISCLAIMER

 This script is provided as a demonstration of how to use the NGINX pkg-oss
 tooling to compile and package a dynamic module for NGINX and NGINX Plus.
 It will produce an installable package with the correct dependency on the
 NGINX version used for the build so that upgrades will not lead to mismatch
 between NGINX and the module. When using this script please bear in mind:
  - It will not work for every module, check for module prerequisites.
  - The installable packages are not intended for redistribution.
  - Seamless upgrades with dependency matching require a yum/apt repository.

__EOF__

#
# Check command line parameters
#
ME=`basename $0`
if [ $# -eq 0 ]; then
	echo "USAGE: $ME [options] <URL | path to module source>"
	echo ""
	echo " URL may be Github clone or download link, otherwise 'tarball' is assumed."
	echo " Options:"
	echo " -n | --nickname <word>         # Used for packaging, lower case alphanumeric only"
	echo " -s | --skip-depends            # Skip dependecies check/install"
	echo " -y | --non-interactive         # Automatically install dependencies and overwrite files"
	echo " -f | --force-dynamic           # Attempt to convert static configuration to dynamic module"
	echo " -r <NGINX Plus release number> # Build against the corresponding OSS version for this release"
	echo " -v [NGINX OSS version number]  # Build against this OSS version [current mainline] (default)"
	echo " -o <package output directory>  # Create package(s) in this directory"
	echo ""
        exit 1
fi

#
# Process command line options
#
CHECK_DEPENDS=1
SAY_YES=""
COPY_CMD="cp -i"
DO_DYNAMIC_CONVERT=0
MODULE_NAME=""
BUILD_PLATFORM=OSS
OUTPUT_DIR=""
while [ $# -gt 1 ]; do
	case "$1" in
		"-s" | "--skip-depends")
			CHECK_DEPENDS=0
			shift
			;;
		"-y" | "--non-interactive")
			SAY_YES="-y"
			COPY_CMD="cp -f"
			shift
			;;
		"-f" | "--force-dynamic")
			DO_DYNAMIC_CONVERT=1
			shift
			;;
		"-n" | "--nickname" )
			MODULE_NAME=$2
			shift; shift
			;;
		"-r")
			BUILD_PLATFORM=Plus
			if [ `echo -n $2 | tr -d '[0-9p]' | wc -c` -gt 0 ]; then
				echo "$ME: ERROR: NGINX Plus release must be in the format NN[pN] - quitting"
				exit 1
			elif [ "`echo "10^$2" | tr '^' '\n' | sort -nr | head -1`" == "10" ]; then
				echo "$ME: ERROR: NGINX Plus release must be at least 11 to support dynamic modules - quitting"
				exit 1
			fi
			PLUS_REL=$2
			shift; shift
			;;
		"-v")
			BUILD_PLATFORM=OSS
			if [ `echo -n .$2 | tr -d '[0-9\.]' | wc -c` -eq 0 ]; then
				OSS_VER=$2
				shift
			fi
			if [ `echo "1.11.4^$OSS_VER" | tr '^' '\n' | tr '.' ',' | sort -nr | head -1` == "1,11,4" ]; then
				echo "$ME: ERROR: NGINX version must be at least 1.11.5 to support dynamic modules - quitting"
				exit 1
			fi
			shift
			;;
		"-o")
			if [ ! -d $2 ]; then
				echo "$ME: ERROR: Output directory $2 does not exist - quitting"
				exit 1
			fi
			OUTPUT_DIR=$2
			shift; shift
			;;
		*)
			echo "$ME: ERROR: Invalid command line option ($1) - quitting"
			exit 1
			;;
	esac
done

#
# Locate/select package manager and configure
#
if [ `whereis yum | grep -c "^yum: /"` -eq 1 ]; then
        PKG_MGR=yum
	PKG_FMT=rpm
	NGINX_PACKAGES="pcre-devel zlib-devel openssl-devel"
	DEVEL_PACKAGES="rpm-build"
	PACKAGING_ROOT=${HOME}/rpmbuild/
	PACKAGING_DIR=rpm/SPECS
	PACKAGE_SOURCES_DIR=../SOURCES
	PACKAGE_OUTPUT_DIR=RPMS
elif [ `whereis apt-get | grep -c "^apt-get: /"` -eq 1 ]; then
        PKG_MGR=apt-get
	PKG_FMT=deb
	NGINX_PACKAGES="libpcre3-dev zlib1g-dev libssl-dev"
	DEVEL_PACKAGES="devscripts debhelper dpkg-dev quilt lsb-release"
	PACKAGING_ROOT=${HOME}/debuild/
	PACKAGING_DIR=debian
	PACKAGE_SOURCES_DIR=extra
	PACKAGE_OUTPUT_DIR="*/debian"
else
        echo "$ME: ERROR: Could not locate a supported package manager - quitting"
        exit 1
fi

if [ $CHECK_DEPENDS = 1 ]; then
	echo "$ME: INFO: testing sudo"
	sudo pwd > /dev/null
	if [ $? -ne 0 ]; then
	        echo "ERROR: sudo failed. If you do not have sudo credentials then try using the '--skip-depends' option. Quitting."
	        exit 1
	fi

	echo "$ME: INFO: checking for dependent packages"
	CORE_PACKAGES="gcc make unzip wget"
	if [ "$BUILD_PLATFORM" = "OSS" ]; then
		CORE_PACKAGES="$CORE_PACKAGES mercurial"
	fi
	if [ "${1##*.}" == "git" ]; then
		CORE_PACKAGES="$CORE_PACKAGES git"
	fi
	sudo $PKG_MGR install $SAY_YES $CORE_PACKAGES $NGINX_PACKAGES $DEVEL_PACKAGES
fi

#
# Ask for a nickname if we did't get one on the command line
#
if [ "$MODULE_NAME" = "" ]; then
	#
	# Construct a reasonable nickname from the module source location
	#
	MODULE_NAME=`basename $1 | tr '[:blank:][:punct:]' '\n' | tr '[A-Z]' '[a-z]' | grep -ve nginx -e ngx -e http -e stream -e module -e plus -e tar -e zip -e gz -e git | tr -d '\n'`
	if [ -z "$SAY_YES" ]; then
		read -p "$ME: INPUT: Enter module nickname [$MODULE_NAME]: "
		if [ "$REPLY" != "" ]; then
			MODULE_NAME=$REPLY
		fi
	else
		echo "$ME: INFO: using \"$MODULE_NAME\" as module nickname"
	fi
fi

#
# Sanitize module nickname (this is a debbuild requirement, probably needs to check for more characters)
#
while true; do
	MODULE_NAME_CLEAN=`echo $MODULE_NAME | tr '[A-Z]' '[a-z]' | tr -d '[/_\-\.\t ]'`
	if [ "$MODULE_NAME_CLEAN" != "$MODULE_NAME" ] || [ -z $MODULE_NAME ]; then
		echo "$ME: WARNING: Removed illegal characters from module nickname - using \"$MODULE_NAME_CLEAN\""
		if [ -z $SAY_YES ]; then
			read -p "$ME: INPUT: Confirm module nickname [$MODULE_NAME_CLEAN]: " MODULE_NAME
			if [ "$MODULE_NAME" = "" ]; then
				MODULE_NAME=$MODULE_NAME_CLEAN
			fi
		else
			MODULE_NAME=$MODULE_NAME_CLEAN
			break
		fi
	else
		break
	fi
done

#
# Create temporary build area, with working copy of module source
#
BUILD_DIR=/tmp/$ME.$$
MODULE_DIR=$BUILD_DIR/$MODULE_NAME
echo "$ME: INFO: Creating $BUILD_DIR build area"
mkdir $BUILD_DIR

if [ -d $1 ]; then
	mkdir -v $MODULE_DIR
	echo "$ME: INFO: Building $MODULE_NAME from $MODULE_DIR"
	cp -a $1/* $MODULE_DIR
else
        #
        # Module sources string is not a local directory so assume it is a URL.
        # Obtain the sources in the best way for the suffix provided.
        #
	case "${1##*.}" in
		"git")
			echo "$ME: INFO: Cloning module source"
			git clone --recursive $1 $MODULE_DIR
			;;
		"zip")
			echo "$ME: INFO Downloading module source"
			wget -O $BUILD_DIR/module.zip $1
			ARCHIVE_DIR=`zipinfo -1 $BUILD_DIR/module.zip | head --lines=1 | cut -f1 -d/`
			unzip $BUILD_DIR/module.zip -d $BUILD_DIR
			mv $BUILD_DIR/$ARCHIVE_DIR $MODULE_DIR
			;;
		*)
			echo "$ME: INFO Downloading module source"
			# Assume tarball of some kind
			wget -O $BUILD_DIR/module.tgz $1
			ARCHIVE_DIR=`tar tfz $BUILD_DIR/module.tgz | head --lines=1 | cut -f1 -d/`
			cd $BUILD_DIR
			tar xfz module.tgz
			mv $ARCHIVE_DIR $MODULE_DIR
			cd -
			;;
	esac
fi

#
# Check the module sources look OK
#
if [ ! -f $MODULE_DIR/config ]; then
	echo "$ME: ERROR: Cannot locate module config file - quitting"
	exit 1
fi

#
# Check/convert module config
#
if [ `grep -c "\.[[:space:]]auto/module" $MODULE_DIR/config` -eq 0 ]; then
	if [ $DO_DYNAMIC_CONVERT = 1 ]; then
		echo "$ME: WARNING: This is a static module, attempting to convert to dynamic (experimental)"
		grep -ve HTTP_MODULES -e STREAM_MODULES -e NGX_ADDON_SRCS $MODULE_DIR/config > $MODULE_DIR/config.dynamic
		echo "ngx_module_name=`grep ngx_addon_name= $MODULE_DIR/config | cut -f2 -d=`" >> $MODULE_DIR/config.dynamic
		if [ `grep -c "HTTP_AUX_FILTER_MODULES=" $MODULE_DIR/config` -gt 0 ]; then
			echo "ngx_module_type=HTTP_AUX_FILTER" >> $MODULE_DIR/config.dynamic
		elif [ `grep -c "STREAM_MODULES=" $MODULE_DIR/config` -gt 0 ]; then
			echo "ngx_module_type=Stream" >> $MODULE_DIR/config.dynamic
		else
			echo "ngx_module_type=HTTP" >> $MODULE_DIR/config.dynamic
		fi
		echo "ngx_module_srcs=\"`grep NGX_ADDON_SRCS= $MODULE_DIR/config | cut -f2 -d\\" | sed -e 's/^\$NGX_ADDON_SRCS \(\$ngx_addon_dir\/.*$\)/\1/'`\"" >> $MODULE_DIR/config.dynamic
		echo ". auto/module" >> $MODULE_DIR/config.dynamic
		mv $MODULE_DIR/config $MODULE_DIR/config.static
		cp $MODULE_DIR/config.dynamic $MODULE_DIR/config
	else
		echo "$ME: ERROR: This is a static module and should be updated to dynamic configuration. To attempt automatic conversion to dynamic module configuration use the '--force-dynamic' option. This will not modify the original configuration. Quitting."
		exit 1
	fi
fi

#
# Get the internal module name(s) from the module config so we can write
# the .so files into the postinstall banner.
#
touch $BUILD_DIR/postinstall.txt
for MODULE_SO_NAME in $(grep ngx_module_name= $MODULE_DIR/config | cut -f2 -d= | cut -f2 -d\"); do
	if [ "`echo $MODULE_SO_NAME | cut -c1`" = "$" ]; then
		# Dereference variable
		SOURCE_VAR=`echo $MODULE_SO_NAME | cut -f2 -d\$`
		MODULE_SO_NAME=`grep $SOURCE_VAR= $MODULE_DIR/config | cut -f2 -d= | cut -f2 -d\"`
	fi
	# Only write load_module line when no backslash present (can't cope with multi-line values)
	echo $MODULE_SO_NAME | grep -c '\\' > /dev/null
	if [ $? -eq 1 ]; then
		echo "    load_module modules/$MODULE_SO_NAME.so;" >> $BUILD_DIR/postinstall.txt
	fi
done
if [ ! -s $BUILD_DIR/postinstall.txt ]; then
	# Didn't find any .so names so this is a final attempt to extract from config file
	MODULE_SO_NAME=`grep ngx_addon_name= $MODULE_DIR/config | cut -f2 -d= | cut -f2 -d\"`
	echo "    load_module modules/$MODULE_SO_NAME.so;" >> $BUILD_DIR/postinstall.txt
fi

#
# Get NGINX OSS packaging tool
#
echo "$ME: INFO: Downloading NGINX packaging tool"
cd $BUILD_DIR
if [ "$BUILD_PLATFORM" = "OSS" ]; then
	if [ "$OSS_VER" != "" ]; then
		MERCURIAL_TAG="-r $OSS_VER-1"
	fi
	hg clone $MERCURIAL_TAG http://hg.nginx.org/pkg-oss
	cd pkg-oss/$PACKAGING_DIR
else
	wget -O - http://hg.nginx.org/pkg-oss/archive/target-plus-r$PLUS_REL.tar.gz  | tar xfz -
	cd pkg-oss-target-plus-r$PLUS_REL/$PACKAGING_DIR
fi
if [ $? -ne 0 ]; then
	echo "$ME: ERROR: Unable to obtain NGINX packaging tool - quitting"
	exit 1
fi

#
# Archive the module source for use with packaging tool using the base OSS version
#
VERSION=`grep "^BASE_VERSION=" Makefile | cut -f2 -d= | tr -d "[:blank:]"`
echo "$ME: INFO: Archiving module source for $VERSION"
cd $BUILD_DIR
mv $MODULE_NAME $MODULE_NAME-$VERSION
tar cf - $MODULE_NAME-$VERSION | gzip -1 > $OLDPWD/$PACKAGE_SOURCES_DIR/$MODULE_NAME-$VERSION.tar.gz
cd -

echo "$ME: INFO: Creating changelog"
if [ "$PKG_MGR" = "yum" ]; then
	echo "* `date '+%a %b %d %Y'` Build Script <build.script@example.com>" > nginx-module-$MODULE_NAME.changelog.in
	echo "- initial version of $MODULE_NAME module" >> nginx-module-$MODULE_NAME.changelog.in
else
	cat << __EOF__ > nginx-module-$MODULE_NAME.changelog.in
nginx-module-$MODULE_NAME (${VERSION}-1~%%CODENAME%%) %%CODENAME%%; urgency=low

  * initial release of $MODULE_NAME module for nginx

 -- Build Script <build.script@example.com>  `date -R`
__EOF__
fi

echo "$ME: INFO: Creating module Makefile"
cat << __EOF__ > Makefile.module-$MODULE_NAME
MODULES=$MODULE_NAME

MODULE_PACKAGE_VENDOR=	Build Script <build.script@example.com>
MODULE_PACKAGE_URL=	https://www.nginx.com/blog/compiling-dynamic-modules-nginx-plus/

MODULE_SUMMARY_$MODULE_NAME=		$MODULE_NAME dynamic module
MODULE_VERSION_$MODULE_NAME=		$VERSION
MODULE_RELEASE_$MODULE_NAME=		1
MODULE_CONFARGS_$MODULE_NAME=		--add-dynamic-module=\$(MODSRC_PREFIX)$MODULE_NAME-$VERSION
MODULE_SOURCES_$MODULE_NAME=		$MODULE_NAME-$VERSION.tar.gz

define MODULE_POST_$MODULE_NAME
cat <<BANNER
----------------------------------------------------------------------

The \$(MODULE_SUMMARY_$MODULE_NAME) for nginx has been installed.
To enable this module, add the following to /etc/nginx/nginx.conf
and reload nginx:

`uniq $BUILD_DIR/postinstall.txt`

----------------------------------------------------------------------
BANNER
endef
export MODULE_POST_$MODULE_NAME
__EOF__

#
# Build!
#
echo "$ME: INFO: Building"
if [ -d $PACKAGING_ROOT -a "$SAY_YES" = "-y" ]; then
        rm -fr $PACKAGING_ROOT
fi
make prepare-build-env
if [ $? -ne 0 ]; then
	echo "$ME: ERROR: Unable to prepare build environment - quitting"
	exit 1
fi

if [ "$PKG_MGR" = "yum" ]; then
	cd ~/rpmbuild/SPECS
else
	cd ~/debuild/nginx-$VERSION/debian
fi

if [ "$BUILD_PLATFORM" = "Plus" ]; then
	MODULE_TARGET=plus make module-$MODULE_NAME
else
	make module-$MODULE_NAME
fi
if [ $? -ne 0 ]; then
	echo "$ME: ERROR: Build failed"
else
	echo ""
	echo "$ME: INFO: Module binaries created"
	find $PACKAGING_ROOT -type f -name "*.so" -print

	echo "$ME: INFO: Module packages created"
	if [ "$OUTPUT_DIR" = "" ]; then
		find $PACKAGING_ROOT$PACKAGE_OUTPUT_DIR -type f -name "*.$PKG_FMT" -print
	else
		find $PACKAGING_ROOT$PACKAGE_OUTPUT_DIR -type f -name "*.$PKG_FMT" -exec $COPY_CMD -v {} $OUTPUT_DIR \;
	fi
	echo "$ME: INFO: Removing $BUILD_DIR"
	rm -fr $BUILD_DIR
fi
