#! /bin/bash

## 2004-07-01 H. Hallingstad

shopt -s nullglob dotglob
set -e

## Check input arguments

function usage {
	cat >&2 <<EOF
Usage: $(basename $0) [--name NAME|clean|rebuild]
Builds the library and executables of a C Project

See .Makefile.g.* for auto-generated files. Create Makefile.user to
alter the building.

See http://www.pvv.org/~hakonhal/main.cgi/c/building/ for detailed description.
EOF

	exit 1 
}

# Parse command line arguments

name=
if (($#==2)) && [ "$1" == "--name" ]
then
	name="$2"
elif (($#==1))
then
	case "$1" in
	rebuild|clean)
		[ -d src/ ] && find src/ -name '*.[do]' -exec 'rm' '{}' ';'
		[ -d psrc/ ] && find psrc/ -name '*.[do]' -exec 'rm' '{}' ';'
		[ -f .Makefile.g.cars ] && rm .Makefile.g.cars
		[ -f .Makefile.g.src ] && rm .Makefile.g.src
		[ -f .Makefile.g.psrc ] && rm .Makefile.g.psrc
		rm -f *.car
		rm -f *.car.gz
		rm -f *.car.bz2
		rm -rf cars/*/
		rm -rf lib/
		rm -rf bin/
		[ "$1" == "clean" ] && exit 0
		;;
	*)
		usage
		;;
	esac
elif (($#!=0))
then
	usage
fi

# NAME defaults to the common directory prefix of all source files 
# under src/, with slashes converted to underscores.

if [ ! -d src ]
then
	echo -e "The src/ directory does not exist!\n" >&2
	usage
fi

# Precondition:	Both arguments must begin with "./", and if any of the
# arguments denote a directory it must end in "/".
# Postcondition: Prints the common directory path (longest match) to
# stdout, starting with "./" and ending in "/".
function cmn_dirname {
	local one="${1%/*}/"
	local one_len="${#one}"
	local two="${2%/*}/"
	local two_len="${#two}"
	local index=2
	while ((index<one_len)) && ((index<two_len)) && \
		[ "${one:$index:1}" == "${two:$index:1}" ]
	do
		((index+=1))
	done
	local cmn_dir="${one:0:$index}"
	echo "${cmn_dir%/*}/"
}			

if [ -z "$name" ]
then
	cmn_dir=
	for path in $(find src/ -name '*.c')
	do
		file="./${path#src/}"
		dir="${file%/*}/"
		if [ -z "$cmn_dir" ]
		then
			cmn_dir="$dir"
		else
			cmn_dir="$(cmn_dirname $cmn_dir $file)"
		fi
	done
	
	cmn_dir="${cmn_dir#./}"
	cmn_dir="${cmn_dir%/}"

	if [ -z "$cmn_dir" ]
	then
		echo "Empty name! You should therefore specify a NAME" >&2
		usage
		exit 1
	fi

	if [ ! -d "src/$cmn_dir" ]
	then
		echo "Path under src/ containing '/'??"
		exit 1
	fi
	
	name="${cmn_dir//\//_}"
fi

# First .Makefile.g.cars is made, that if necessary unpacks the C archives 
# found in cars/. 

# $1 is the extension, and $2 is the tar command
function unpack {
	for path in cars/*$1
	do
		dir="${path%$1}"
		car_dirs="$car_dirs $dir"
		car_names="$car_names ${dir#cars/}"
		targets="${targets}$dir: $path"$'\n\t'
		targets="${targets}rm -rf \$@"$'\n\t'
		targets="${targets}mkdir \$@"$'\n\t'
		targets="${targets}cd \$@; $2 ../../\$<"$'\n\n'
	done
}

targets=
car_dirs=
unpack .car "tar xf"
unpack .car.bz2 "tar xjf"
unpack .car.gz "tar xzf"

if [ ! -z "$car_dirs" ]
then
	cat > .Makefile.g.cars <<EOF
all: $car_dirs

$targets
EOF

	make -f .Makefile.g.cars
fi

# 1. The source files in src/ are compiled and linked into a library lib/NAME.a
# When compiling, the header files in the C archives under cars/ are added to 
# the header search path.

CC="gcc -W -Wall -Wstrict-prototypes -Wmissing-prototypes -Wunused"
CFLAGS="-Isrc/"

for path in $car_dirs
do
	if [ ! -d $path/src ]
	then
		echo "Unpacked C archive at $path/ does not have the src/ directory!" >&2
		exit 1
	else
		CFLAGS="$CFLAGS -I$path/src"
	fi
done

dfiles=
ofiles=
for path in $(find src/ -name '*.c')
do
	prefix="${path%.c}"
	ofiles="$ofiles"$' \\\n\t'"$prefix.o"
	dfiles="$dfiles"$' \\\n\t'"$prefix.d"
done

if [ -z "$ofiles" ]
then
	echo "No C files in src/ to compile!" >&2
	exit 1
fi

mkdir -p lib/

# 2. The library lib/NAME.a and the header files under src/ are put in a tar 
# archive NAME.car. (This is the definition of a C archive.)

hfiles=
for path in $(find src/ -name '*.h')
do
	hfiles="$hfiles $path"
done

if which bzip2 > /dev/null
then
	carname=$name.car.bz2
	tar_cmd="tar cjf \$@ lib/lib$name.a $hfiles"
elif which gzip > /dev/null
then
	carname=$name.car.gz
	tar_cmd="tar czf \$@ lib/lib$name.a $hfiles"
else
	carname=$name.car
	tar_cmd="tar cf \$@ lib/lib$name.a $hfiles"
fi

cat > .Makefile.g.src <<EOF
CC=$CC
CFLAGS=$CFLAGS

-include Makefile.user

all: $carname

$carname: lib/lib$name.a($ofiles)
	$tar_cmd

-include $dfiles

%.d: target=\$(subst /,\\/,\$@)
%.d: %.c
	\$(CC) \$(CFLAGS) -MM -c \$< \\
	| sed "s/^.*\\\\.o:/\$(target:%.d=%.o):/" \\
	> \$@
EOF

make -f .Makefile.g.src

# Then Makefile.g.psrc is made, which for each source file in psrc/, say
# psrc/PROGRAM.c, makes an executable program bin/PROGRAM. When compiling, the 
# header files in all C archives are added to the header search path. The 
# linker links with all C archive libraries.

LDLIBS="-Llib/ -l$name"
for cname in $car_names
do
	if [ ! -d cars/$cname/lib ]
	then
		echo "Unpacked C archive at cars/$cname/ does not have the lib/ directory!" >&2
		exit 1
	elif [ ! -f cars/$cname/lib/lib$cname.a ]
	then
		echo "Unpacked C archive at cars/$cname/ does not have a library lib/lib$cname.a!" >&2
		exit 1
	fi
	LDLIBS="$LDLIBS -Lcars/$cname/lib/ -l$cname"
	libs="$libs"$' \\\n\t'"cars/$cname/lib/lib$cname.a"
done

LDFLAGS=
pnames=
for path in psrc/*.c
do
	prefix="${path%.c}"
	ppath="bin/${prefix#psrc/}"
	
	pdfiles="$pdfiles"$' \\\n\t'"$prefix.d"
	pnames="$pnames"$' \\\n\t'"$ppath"
	ptargets="${ptargets}$ppath: $prefix.o lib/lib$name.a $libs"$'\n\t'
	ptargets="${ptargets}\$(CC) \$(LDFLAGS) -o \$@ \$< \$(LDLIBS)"$'\n\n'
done

if [ ! -z "$pnames" ]
then
	mkdir -p bin/

	cat > .Makefile.g.psrc <<EOF
CC=$CC
CFLAGS=$CFLAGS
LDFLAGS=$LDFLAGS
LDLIBS=$LDLIBS

-include Makefile.user

all: $pnames

$ptargets

-include $pdfiles

%.d: target=\$(subst /,\\/,\$@)
%.d: %.c
	\$(CC) \$(CFLAGS) -MM -c \$< \\
	| sed "s/^.*\\\\.o:/\$(target:%.d=%.o):/" \\
	> \$@
EOF

	make -f .Makefile.g.psrc
fi


# Addendum: When source files are deleted 
#
# When the user deletes source files (.h/.c), those files that
# depended on those source files have their dependency stored in a .d
# file. When that dependency file is included in the .Makefile.g.src
# or .Makefile.g.psrc file, Make will fail since it cannot find a way
# to build that source file. (It should have taken the missing source
# file as a sign that the prerequisite had changed, but otherwise
# ignored it.)
#
# 1. We must be able to tell when a file is deleted. 
# 	Solution: Store the path of the source files when building.
#
# 2. We must delete .d and .o files corresponding to removed .c files.
#
# 3. We must be able to tell which files were dependent on that file.
#
# This is exactly what the dependency files contain. What about making
# a PHONY target of all .h and .c files that are deleted, whose
# command does nothing (or delete the .d and .o files mentioned in
# step 2). 
#
# This means there are two causes why commands of .o targets are
# executed. 1. Source files that the .o target is dependent on, has
# changed (normal case), and 2. source files that the .o target is
# dependent on, has been deleted.
#
# 4. We must delete the corresponding .d and .o files
