#!/bin/bash
#*****************************************************************************#
# release - Create a Yocto Linux BSP release                                  #
# Author: Tim Jaacks <tim.jaacks@garz-fricke.com>                             #
# Copyright 2014 Garz & Fricke GmbH                                           #
#*****************************************************************************#

#*****************************************************************************#
# Variables                                                                   #
#*****************************************************************************#
configuration="$1"
image=""
dont_clean=""
assume_yes=""
skip_classified=""
continue=""
verbose=""
svnrev=""
rep_sub_path="trunk"
version=""
builddir=""
ext_kernel_rev=""
ext_local_src_rev=""
minor_version=""
append_conf=""
errors=0

#*****************************************************************************#
# Print usage                                                                 #
#*****************************************************************************#
usage()
{
	echo "Syntax: release <configuration> [options] [list of platforms]"
	echo "Options:"
	echo "  -r <revision>   Check out specific svn revision (default: HEAD)"
	echo "  -p <svnpath>    Use SVN sub path (default: trunk)"
	echo "  -m <minor>      Specify minor release version number (default: 0)"
	echo "  -a <file>       Append file to local.conf"
	echo "  -n              Do not delete build trees after release"
	echo "  -c              Skip build of classified sources for unselected platforms"
	echo "  -y              Do not ask questions, assume yes"
	echo "  -k              Continue even if a platform fails to be built"
	echo "  -v              Print verbose log in case of an error"
	exit 1
}

#*****************************************************************************#
# Setup platform
#*****************************************************************************#
setup_platform() 
{
	builddir=$1
	platform=$2

	cd $builddir
	# Build BSP
	EULA=1 MACHINE=$platform DISTRO=guf DISTRO_CONFIGURATION=$configuration source ./setup-environment build-$platform 2>&1 > /dev/null

	if [ $? -ne 0 ]
	then
		echo -n "ERROR: Platform "
		echo -n "${platform}" | tr "[:lower:]" "[:upper:]"
		echo " could not be selected"

		if [ "$continue" = "-k" ]; then
			errors=$((errors + 1))
			return 1
		else
			echo "Exiting..."
			exit 1
		fi
	fi
	return 0
}

#*****************************************************************************#
# Bitbake with error check
#*****************************************************************************#
call_bitbake()
{
	bitbake $@ $continue
	if [ $? -ne 0 ]
	then
		echo -n "ERROR: 'bitbake $@' failed."
		if [ "$continue" = "-k" ]; then
			echo " Continue anyway..."
			errors=$((errors + 1))
			return 1
		else
			echo " Exiting..."
			exit 1
		fi
	fi
}

#*****************************************************************************#
# Find out image name for platform
#*****************************************************************************#
# check for available custom images for the current platform
image_for_platform()
{
	platform=$1
	if [ -e $builddir/sources/meta-guf/meta-$configuration/recipes-bsp/images/guf-image-$platform.bb ]
	then
		image="guf-image-$platform"
	else
		image="guf-image"
	fi
	echo $image	
}

#*****************************************************************************#
# Find out receipe name for classified bb file
#*****************************************************************************#
recipe_from_classified()
{
	file=$1
	recipe=$(basename $file .bb.classified)
	# drop version part of the file name
	recipe=$( echo $recipe | sed 's/\([^_]*\)_\(.*\)/\1/')
	echo $recipe
}

#*****************************************************************************#
# Check first parameter                                                       #
#*****************************************************************************#
if [ -z "$configuration" -o "$configuration" = "-n" -o "$configuration" = "-r" ]
then
	echo "Please specify a configuration to build a release of."
	usage
fi

#*****************************************************************************#
# Check remaining parameters                                                  #
#*****************************************************************************#
OPTIND=`expr $OPTIND + 1`
while getopts :p:r:m:a:yhnckvs arg
do case $arg in
	h)  usage;;
	n)  dont_clean="-n"
	    ;;

	c)  skip_classified="-c"
	    ;;

	y)  assume_yes="-y"
		;;

	k)  continue="-k"
		;;

	v)  verbose="-v"
		;;

	r)  svnrev=$OPTARG
		if [ svnrev = "" ]
		then
			usage
		fi
		;;

	p)  rep_sub_path=$OPTARG
		if [ rep_sub_path = "" ]
		then
			usage
		fi
		;;

	m)  minor_version=$OPTARG
		if [ minor_version = "" ]
		then
			usage
		fi
		;;

	a)  append_conf=$OPTARG
		if [ append_conf = "" ]
		then
			usage
		fi
		;;

	\?) usage;;
	esac
done

shift $(($OPTIND - 1))

platforms=$*

#*****************************************************************************#
# Print header                                                                #
#*****************************************************************************#
echo ""
echo "*************************************"
echo "* Garz & Fricke - Yocto BSP Release *"
echo "*************************************"
echo ""
echo "Preparing release for configuration $configuration"

#*****************************************************************************#
# Check for local modifications                                               #
#*****************************************************************************#
if [ -z $svnrev ] && [ "$assume_yes" != "-y" ]
then
	if [ "$rep_sub_path" = "trunk" ]
	then
		if [ $(svn status | grep ^[^?XIP] | wc -l) != 0 ]
		then
			echo "Your repository has local modifications. Are you sure you want to"
			read -p "create a release for the HEAD revision? (y/n) " answer
			if [ "$answer" != "y" ]
			then
				echo "Release aborted"
				exit 1
			fi
		fi
	fi
fi

#*****************************************************************************#
# Check if external revision is fixed                                         #
#*****************************************************************************#
svnurl="svn://gfweb/sw/som/linux-mx/$rep_sub_path/yocto"
meta_kernel_path="${svnurl}/sources/meta-guf/meta/recipes-kernel/linux"
meta_conf_kernel_path="${svnurl}/sources/meta-guf/meta-$configuration/recipes-kernel/linux"
meta_kernel_recipes=$(svn list ${meta_kernel_path} 2>/dev/null | grep -x -e ".*\.bb" | awk '{ print "'${meta_kernel_path}'/" $0 }')
meta_conf_kernel_recipes=$(svn list ${meta_conf_kernel_path} 2>/dev/null | grep -x -e ".*\.bb" | awk '{ print "'${meta_conf_kernel_path}'/" $0 }')
OLDIFS=$IFS
IFS=$'\n'
for kernel_recipe in ${meta_kernel_recipes} ${meta_conf_kernel_recipes}; do
	ext_kernel_rev=$(svn cat $kernel_recipe | grep "SRCREV = ")
	ext_kernel_rev=${ext_kernel_rev##SRCREV = \"}
	ext_kernel_rev=${ext_kernel_rev%%\"}
	if [ "$ext_kernel_rev" = "HEAD" ]
	then
		echo -n "WARNING: external svn kernel revision is set to HEAD in "
		echo ${kernel_recipe##$svnurl/}
		if [ "$assume_yes" != "-y" ]
		then
			read -p "Are you sure you want to continue? (y/n) " answer
			if [ "$answer" != "y" ]
			then
				echo "Release aborted"
				exit 1
			fi
		fi
	fi
done
svnrev_param=""
if [ ! -z "$svnrev" ]; then
	svnrev_param="-r$svnrev"
fi
for external in $(svn propget $svnrev_param svn:externals -R $svnurl | grep -v '^$'); do
	local_path=$(echo $external | awk '{ printf "%s/%s", $1, $4 }')
	external_path=$(echo $external | awk '{ print $3 }')
	external_revision=$(echo $external_path | awk -F@ '{ print $2 }')
	if [ -z "$external_revision" ] || [ "$external_revision" = "HEAD" ]
	then
		echo "WARNING: svn externals revision is set to HEAD on path"
		echo $local_path
		if [ "$assume_yes" != "-y" ]
		then
			read -p "Are you sure you want to continue? (y/n) " answer
			if [ "$answer" != "y" ]
			then
				echo "Release aborted"
				exit 1
			fi
		fi
	fi
done
IFS=$OLDIFS

#*****************************************************************************#
# Export SVN repository to temporary folder                                   #
#*****************************************************************************#
if [ -z $svnrev ]; then
	svnrev=$(svn info $svnurl | grep "Last Changed Rev: " | cut -b 19-)
fi

rm releases/temp -rf

echo "Checkout SVN repository... (revision $svnrev)"
svn checkout $svnurl@$svnrev releases/temp/yocto > /dev/null
if [ $? -ne 0 ]
then
	echo "ERROR: svn export failed"
	exit 1
fi
# generate "version" from svn information
lastdir="$PWD"
cd releases/temp/yocto
current_platform=$(for i in $platforms; do echo $i; break; done)
setup_platform . $current_platform

call_bitbake --parse-only
if [ $? -ne 0 ]
then 
	echo "ERROR: Bitbake failed to parse receipes."
fi

cd "$lastdir/releases/temp/yocto"
rm -rf build-$current_platform

# Remove svn information from source tree (like export)
echo "Strip the repository"
for i in $(find ./sources -iname ".svn" -type d)
do
	rm -rf $i
done
rm -rf .svn
cd "$lastdir"

#*****************************************************************************#
# Determine platforms to be built                                             #
#*****************************************************************************#
meta_guf_dir="./releases/temp/yocto/sources/meta-guf"
available_platforms=""
existing_platforms=""
non_existing_platforms=""

# Determine all available platforms
for platform in `find $meta_guf_dir/meta/conf/machine/*.conf $meta_guf_dir/meta-$configuration/conf/machine/*.conf -exec basename {} .conf \; 2>/dev/null`; do
	available_platforms="$available_platforms $platform"
done

if [ -z "$platforms" ]
then
	# If no platforms are given on the command line, select all
	existing_platforms="$available_platforms"
else
	# If platforms have been specified, check which of them exist
	for platform in $platforms
	do
		if [[ $available_platforms =~ $platform ]]
		then
			existing_platforms="$existing_platforms $platform"
		else
			non_existing_platforms="$non_existing_platforms $platform"
		fi
	done
fi

if [ -z "$existing_platforms" ]
then
	if [ -z "$platforms" -a -z "$existing_platforms" ]
	then
		echo "ERROR: There are no selected platforms existing for configuration $configuration in this revision."
	else
		echo -n "ERROR: The specified platforms do not exist in this revision:"
		echo "$non_existing_platforms" | tr "[:lower:]" "[:upper:]"
	fi
	exit 1
fi

#*****************************************************************************#
# Save SVN revision numbers for release                                       #
#*****************************************************************************#
echo $svnrev > ./releases/temp/yocto/.svnrev

#*****************************************************************************#
# Search for version information                                              #
#*****************************************************************************#
bitbake_project=$(grep "DISTRO_SHORT_NAME = " $meta_guf_dir/meta/conf/distro/guf.conf)
bitbake_project=${bitbake_project##DISTRO_SHORT_NAME = \"}
bitbake_project=${bitbake_project%%\"}
bitbake_project_version=$(grep "DISTRO_VERSION := " $meta_guf_dir/meta/conf/distro/guf.conf)
bitbake_project_version=${bitbake_project_version##DISTRO_VERSION := \"}
bitbake_project_version=${bitbake_project_version%%-*}
if [ ! -z $minor_version ]; then
	# Replace ".0" with given minor version number
	bsp_version=${bitbake_project_version/.0/.$minor_version}
	sed -i "s/DISTRO_VERSION := \"$bitbake_project_version/DISTRO_VERSION := \"$bsp_version/" $meta_guf_dir/meta/conf/distro/guf.conf
else
	bsp_version=$bitbake_project_version
fi

version=$bsp_version-r$svnrev-$configuration
project_and_version=$bitbake_project-$version

echo "Release version is: $project_and_version"
#*****************************************************************************#
# Check if version folder already exists                                      #
#*****************************************************************************#
if [ -d releases/$project_and_version ]
then
	if [ "$assume_yes" = "-y" ]
	then
		echo "Deleting releases/$project_and_version..."
		rm releases/$project_and_version -r -f
	else
		read -p "The directory releases/$project_and_version already exists. Overwrite? (y/n) " answer
		if [ "$answer" = "y" ]
		then
			echo "Deleting releases/$project_and_version..."
			rm releases/$project_and_version -r -f
		else
			echo "Release aborted"
			exit 1
		fi
	fi
fi

#*****************************************************************************#
# Rename temporary folder to version string                                   #
#*****************************************************************************#
echo "Saving release to folder releases/$project_and_version"
mv releases/temp releases/$project_and_version
builddir=$(cd ./releases/$project_and_version/yocto; pwd)


#*****************************************************************************#
# Display platforms to be built                                               #
#*****************************************************************************#
if [ ! -z "$non_existing_platforms" ]
then
	echo -n "WARNING: The following platforms do not exist:"
	echo "$non_existing_platforms" | tr "[:lower:]" "[:upper:]"
fi
echo -n "Platforms to be built:"
echo "$existing_platforms" | tr "[:lower:]" "[:upper:]"

#*****************************************************************************#
# Remove files and directories of other projects                              #
#*****************************************************************************#
echo "Removing files and directories of other projects..."

# Remove other configuration layers
for d in `find $builddir/sources/meta-guf/meta-* -maxdepth 0 ! -name "meta-$configuration" 2>/dev/null`
do
	rm -rf $d
done

# Remove this shell script
rm -f $builddir/release
# Remove the platform selection script
rm -f $builddir/setplatform
# Remove the version update script
rm -f $builddir/update_version 2>/dev/null
# Remove the external revision script
rm -f $builddir/external_revision 2>/dev/null
# Remove the parse packages script
rm -f $builddir/parse_packages.py
# Remove the jenkins helper script
rm -f $builddir/add_jenkins_sstate_cache.sh

#*****************************************************************************#
# Starting build
#*****************************************************************************#
echo -n "Build process started at "
date +%T

#*****************************************************************************#
# Build classified source code for all platforms                              #
#*****************************************************************************#
if [ "$skip_classified" = "-c" ]
then
	classified_platforms=$existing_platforms
else
	classified_platforms=$available_platforms
fi

echo -n "Building classified packages for platforms:"
echo "$classified_platforms" | tr "[:lower:]" "[:upper:]"

# Search all classified packages
classified_recipes=""
classified_recipe_files=""
for file in `find $builddir/sources/meta-guf/meta*/recipes* -name *.bb.classified 2>/dev/null`
do
	classified_recipe_files="$classified_recipe_files $file"
	recipe=$(recipe_from_classified $file)
	# add to list but drop duplicates due to several versions 
	if [[ ! $classified_recipes =~ $recipe ]]
	then
		classified_recipes="$classified_recipes $recipe"
	fi
done

echo "Available classified packages:$classified_recipes"

classified_recipes_built=''

for platform in $classified_platforms
do
	if ! setup_platform "$builddir" "$platform"; then continue; fi

	if [ ! -z $append_conf ]; then
		cat $append_conf >> conf/local.conf
	fi

	image=$(image_for_platform $platform)
	image_recipes=$(bitbake -g $image >> logfile_classified 2>&1  && cat pn-depends.dot | grep -v -e '-native' | grep -v digraph | grep -v -e '-image' | awk '{print $1}' | sort | uniq | sed 's/[^"]*"\([^"]*\)"/\1/')

	echo -n "Building classified packages for platform "
	echo "${platform}" | tr "[:lower:]" "[:upper:]"

	for recipe in $classified_recipes
	do
		#Check if image contains this package for this platform
		if $(for e in ${image_recipes[@]}; do [[ "$e" == "$recipe" ]] && exit 1; done);
		then
			echo "Skip $recipe for $platform" >> logfile_classified 2>&1
		else
			echo " * $recipe"
			classified_recipes_built="$classified_recipes_built $recipe"

			echo "************************************************">> logfile_classified 2>&1
			echo "  Building $recipe"  >> logfile_classified 2>&1
			echo "************************************************">> logfile_classified 2>&1

			echo call_bitbake $recipe  >> logfile_classified 2>&1

			call_bitbake $recipe  >> logfile_classified 2>&1
			if [ $? -ne 0 ]
			then 
				echo "ERROR: Bitbake $recipe failed for platform $platform."
			fi

		fi
	done

	for recipe in $classified_recipes
	do
		#Check if image contains this package for this platform
		if $(for e in ${image_recipes[@]}; do [[ "$e" == "$recipe" ]] && exit 1; done);
		then
			echo "Skip clean of $recipe for $platform" >> logfile_classified 2>&1
		else
			echo call_bitbake -c clean $recipe   >> logfile_classified 2>&1
			call_bitbake -c clean $recipe   >> logfile_classified 2>&1
			if [ $? -ne 0 ]
			then 
				echo "ERROR: Bitbake $recipe -c clean failed for platform $platform."
			fi
		fi
	done

done

#*****************************************************************************#
# Remove classified source code                                               #
#*****************************************************************************#

# Remove all *.classified directories
echo "Remove classified sources" >> logfile_classified 
echo "Classified receipes built: $classified_recipes_built" >> logfile_classified 

for dir in `find $builddir/sources/meta-guf/meta*/recipes* -type d -name *.classified 2>/dev/null`
do
	echo "rm -rf $dir" >> logfile_classified 
	rm -rf $dir
done

# Rename *.bb.classified files to *.bb
for file in $classified_recipe_files 
do
	file_without_classified_extension=${file%.classified}
		echo "mv $file $file_without_classified_extension" >> logfile_classified 
	mv $file $file_without_classified_extension

	recipe=$(recipe_from_classified $file)
	# delete the recipe if it was not built
	if [[ ! $classified_recipes_built =~ $recipe ]]
	then
		echo "rm -f $file_without_classified_extension" >> logfile_classified 
		rm -f $file_without_classified_extension
	fi
done


#*****************************************************************************#
# Build classified finished
#*****************************************************************************#
echo -n "Build classified completed at "
date +%T

#*****************************************************************************#
# Build platforms (and SDK and documentation)                                 #
#*****************************************************************************#
for platform in $existing_platforms
do
	echo -n "Building platform "
	echo "${platform}" | tr "[:lower:]" "[:upper:]"
	
	# check for available custom images for the current platform
	image=$(image_for_platform $platform)

	# Build BSP
	if ! setup_platform "$builddir" "$platform"; then continue; fi

	echo " * $image..."
	# Force kernel rebuild, hack as this should happen automatically due to the receipe changes, but ...
	bitbake virtual/kernel -C fetch &> logfile_bsp
	bitbake $image -c release &> logfile_bsp
	if [ $? -ne 0 ]
	then
		echo -n "    ERROR: There were errors building "
		echo -n "${platform}. " | tr "[:lower:]" "[:upper:]"
		if [ "$verbose" = "-v" ]; then
			echo "Excerpt from logfile_bsp:"
			grep "ERROR: Function failed:" logfile_bsp -A1 -B10 | awk '$0="    "$0'
		else
			echo "See logfile for details."
		fi
		if [ "$continue" = "-k" ]; then
			errors=$((errors + 1))
		else
			exit 1
		fi
	fi
	# Build SDK
	echo " * sdk..."
	bitbake $image -c release_sdk &> logfile_sdk
	if [ $? -ne 0 ]
	then
		echo -n "    ERROR: There were errors building the SDK for "
		echo -n "${platform}. " | tr "[:lower:]" "[:upper:]"
		if [ "$verbose" = "-v" ]; then
			echo "Excerpt from logfile_sdk:"
			grep "ERROR: Function failed:" logfile_sdk -A1 -B10 | awk '$0="    "$0'
		else
			echo "See logfile for details."
		fi
		if [ "$continue" = "-k" ]; then
			errors=$((errors + 1))
		else
			exit 1
		fi
	fi
	# Build documentation
	echo " * documentation..."
	bitbake documentation -c release &> logfile_doc
	if [ $? -ne 0 ]
	then
		echo -n "    ERROR: There were errors building the documentation for "
		echo -n "${platform}. " | tr "[:lower:]" "[:upper:]"
		if [ "$verbose" = "-v" ]; then
			echo "Excerpt from logfile_doc:"
			grep "ERROR: Function failed:" logfile_doc -A1 -B10 | awk '$0="    "$0'
		else
			echo "See logfile for details."
		fi
		if [ "$continue" = "-k" ]; then
			errors=$((errors + 1))
		else
			exit 1
		fi
	fi	
done

#*****************************************************************************#
# Build completed                                                             #
#*****************************************************************************#
echo -n "Build process completed at "
date +%T

#*****************************************************************************#
# Copy images to BSP folder                                                   #
#*****************************************************************************#
echo "Copying prebuilt images..."
for platform in $existing_platforms
do
	cp -r $builddir/build-$platform/tmp/deploy/release/* $builddir/.. 2>/dev/null
done

#*****************************************************************************#
# Remove documentation                                                        #
#*****************************************************************************#
rm -Rf $builddir/sources/meta-guf/meta/recipes-common/documentation



#*****************************************************************************#
# Pack build tree                                                             #
#*****************************************************************************#

cd $builddir/..
echo "Packing build tree..."
mv yocto $project_and_version
tar --exclude="$project_and_version/build*" --exclude="$project_and_version/downloads" \
		--exclude="$project_and_version/sstate-cache" \
		-cjvf $project_and_version.tar.bz2 $project_and_version > /dev/null

#*****************************************************************************#
# Clean up                                                                    #
#*****************************************************************************#
if [ "$dont_clean" != "-n" ] && [[ "$errors" -eq 0 ]]
then
	echo "Cleaning up..."
	rm -Rf $project_and_version
else
	echo "Skipping cleanup..."
fi

#*****************************************************************************#
# Generate MD5 sums                                                           #
#*****************************************************************************#
echo "Generating MD5 sums..."
find $PWD -type d ! \( -path $PWD/$project_and_version -prune \) | while read dir; do
	echo "cd $dir"
	cd $dir
	md5sum * > md5sums.txt 2> /dev/null
done

if [ "$continue" = "-k" ]; then
	printf "Release finished with %d errors\n" $errors
	exit $errors
else
	echo "Release finished"
fi
