nsnake/cbuild.sh

300 lines
9 KiB
Bash
Executable file

#!/bin/sh
# Written by Noah Swerhun
# https://noahsw.xyz
# Development: https://git.noahsw.xyz/cbuild.sh/log.html
# --- USER CONFIG ---
CC="gcc"
CFLAGS="-Wall -Wpedantic"
LDFLAGS=""
LDLIBS=""
TARGET="nsnake"
SRCDIR="src"
OBJDIR="obj"
MAKEFILE=".makefile"
PKG_CONFIG_LIBS="ncurses"
HEADER_DIR="include"
# -------------------
PROG_COMMAND="./cbuild.sh __progress__"
SRC="$(find ${SRCDIR} -name '*\.c' 2> /dev/null)"
OBJ="$(find ${SRCDIR} -name '*\.c' 2> /dev/null |
sed "s/${SRCDIR}\//${OBJDIR}\//" |
sed "s/\.c$/.o/" |
tr '\n' ' ')"
srcnum="$(find ${SRCDIR} -name '*\.c' -exec printf %c {} + 2> /dev/null | wc -c)"
CFLAGS="${CFLAGS} $(pkg-config --cflags "${PKG_CONFIG_LIBS}")"
LDLIBS="${CFLAGS} $(pkg-config --libs "${PKG_CONFIG_LIBS}")"
clear_formatting="\033[0m"
bold="\033[1m"
info_color="\033[34m" # default: "\033[34m" - blue
err_color="\033[31m" # default: "\033[31m" - red
build_progress_color="\033[35m" # default: "\033[35m" - magenta
build_info_color="\033[32m" # default: "\033[32m" - green
link_info_color="\033[36m" # default: "\033[36m" - cyan
usage() {
less <<EOF
cbuild.sh: A simple, customizable, automated, and portable build script for C
projects. This script works by automatically detecting .c source files in
SRCDIR, generating a makefile, compiling them into objects in OBJDIR, and
finally linking them to TARGET.
Customization:
Open cbuild.sh in your editor and navigate to the USER CONFIG section (at
the top of the file).
CC the compiler to use (default: gcc)
CFLAGS compilation flags (default: -Wall -Wpedantic)
LDFLAGS linker flags (default: none)
LDLIBS linker libraries (default: none)
TARGET final target to link (default: a.out)
SRCDIR the directory where the script will search for .c source files
(default: src)
OBJDIR the directory where compiled objects will be placed. (default: obj)
MAKEFILE the filename of the generated makefile (default: .makefile)
PKG_CONFIG_LIBS uses flags generated by pkg-config for the given library
name.
HEADER_DIR the directory in which header files will be placed by the 'new'
command. Note that this is relative to SRCDIR. Setting this to
'.' means headers will be placed in the same directory as
source files. Setting this to 'include' means headers will be
placed in SRCDIR/include. (defualt: include)
Usage:
./cbuild.sh [COMMAND]
COMMAND:
--help display this help message
build generate the makefile, compile objects and link target.
clean remove makefile, objects, and target.
buildcn clean, and then build.
generate ONLY generate the makefile.
run [ARGS] build, then execute TARGET with arguments ARGS.
init equivalent to 'new module main' (see below).
dryrun print all commands that will be executed during the build process
to stdout.
new header [NAME] [...]
generate a header file with name NAME in HEADER_DIR. Multiple
NAMEs may be provided. Including '.h' in NAME is optional.
new module [NAME] [...]
generate a new source file NAME.c in SRCDIR and a new header
NAME.h in HEADER_DIR. Multiple NAMEs may be provided.
EOF
}
export_vars() {
export CC
export CFLAGS
export LDFLAGS
export LDLIBS
export TARGET
export OBJ
export PROG_COMMAND
}
info() {
printf "${info_color}${bold}[*]${clear_formatting}\
${info_color} %s${clear_formatting} %s${info_color} %s${clear_formatting}\n" \
"${1}" "${2}" "${3}"
}
err() {
printf "${err_color}${bold}[!]${clear_formatting}\
${err_color} %s${clear_formatting} %s${err_color} %s${clear_formatting}\n" \
"${1}" "${2}" "${3}"
}
gen_makefile() {
info "Generating makefile..."
[ -f "${MAKEFILE}" ] && chmod +w "${MAKEFILE}"
cat > "${MAKEFILE}" <<'EOF'
##########################
# GENERATED BY cbuild.sh #
# DO NOT MODIFY BY HAND #
##########################
.POSIX:
$(TARGET): $(OBJ)
@$(PROG_COMMAND) link $(TARGET)
@$(CC) $(LDFLAGS) -o $(TARGET) $(OBJ) $(LDLIBS)
EOF
i=1
echo "${SRC}" | while read srcname; do
objname="$(echo "${srcname}" |
sed "s/${SRCDIR}\//${OBJDIR}\//" |
sed "s/\.c$/.o/")"
includes="$(grep '#include ".*"' ${srcname} |
awk -F\" '{ print $2 }')"
printf "%s: %s " "${objname}" "${srcname}" >> "${MAKEFILE}"
for line in $includes; do
printf "%s " "${srcname%$(basename ${srcname})}${line}" >> "${MAKEFILE}"
done
printf "\n" >> "${MAKEFILE}"
printf "\t@%s object %s\n" \
'$(PROG_COMMAND)' "${objname}" >> "${MAKEFILE}"
printf "\t@%s %s -o %s -c %s\n" '$(CC)' '$(CFLAGS)' \
"${objname}" "${srcname}" >> "${MAKEFILE}"
i=$((${i}+1))
done
chmod -w "${MAKEFILE}"
}
build() {
gen_makefile
[ ! -d "${OBJDIR}" ] && mkdir "${OBJDIR}"
export_vars
make -f "${MAKEFILE}" -e "${TARGET}" -n | grep -c "^${CC}" > .cbuild_prog.tmp
if [ "$(cat .cbuild_prog.tmp)" = 0 ]; then
info "Target" "${TARGET}" "up to date"
rm .cbuild_prog.tmp
return 0;
fi
info "Beginning build..."
make -f "${MAKEFILE}" -e "${TARGET}"
ret=$?
[ "${ret}" = 0 ] &&
info "Build SUCCESSFUL" ||
err "Build FAILURE"
rm .cbuild_prog.tmp
return "${ret}"
}
clean() {
[ -d "${OBJDIR}" ] &&
rm -rf "${OBJDIR}"
[ -f "${MAKEFILE}" ] &&
chmod +w "${MAKEFILE}"
[ -f "${TARGET}" ] && [ -f "${MAKEFILE}" ] &&
rm "${TARGET}" "${MAKEFILE}"
}
run() {
build || exit $?
args="$@"
info "Running" "./${TARGET} ${args}"
"./${TARGET}" $@ ||
err "Run FAILURE" "./${TARGET}" "returned $?"
}
dry_run() {
gen_makefile
export_vars
make -f "${MAKEFILE}" -e "${TARGET}" -n
}
new() {
case $1 in
header)
shift 1
for arg in $@; do
if [ "${arg}" != "__MODULE__" ]; then
path="${SRCDIR}/${HEADER_DIR}/${arg%.h}.h"
clean_path="$(echo ${path} | sed 's/\.\///g')"
macro="$(basename ${clean_path} .h | tr '[a-z]' '[A-Z]')"
[ ! -d "$(dirname ${clean_path})" ] &&
mkdir -p "$(dirname ${clean_path})"
if [ ! -f "${clean_path}" ]; then
cat > "${clean_path}" <<EOF
#ifndef ${macro}_H
#define ${macro}_H
#endif
EOF
else
[ "${2}" != "__MODULE__" ] &&
err "Header" "${clean_path}" "already exists"
fi
[ "${2}" != "__MODULE__" ] &&
info "Created header" "${clean_path}"
fi
done
;;
module)
shift 1
for arg in $@; do
./cbuild.sh new header "${arg}" '__MODULE__'
path="${SRCDIR}/${arg}.c"
if [ "${HEADER_DIR}" = "." ]; then
include="./$(basename ${arg}).h"
else
include="$(dirname ${path#${SRCDIR}/} |
sed -e 's/\/[A-z]*/\/../g' \
-e 's/[A-z]*\//..\//' \
-e 's/^[A-z]*$/../')/${HEADER_DIR}/${arg}.h"
fi
[ ! -d "$(dirname ${path})" ] &&
mkdir -p "$(dirname ${path})"
if [ ! -f "${path}" ]; then
cat > "${path}" <<EOF
#include "${include}"
EOF
[ "${arg}" != "main" ] && echo >> "${path}" ||
cat >> "${path}" <<EOF
int main(int argc, char **argv) {
return 0;
}
EOF
else
err "Module" "${path%.c}" "already exists"
return 1
fi
info "Created module" "${path%.c}"
done
;;
esac
}
__progress__() {
ntargets=$(cat .cbuild_prog.tmp)
export_vars
rtargets="$(make -f "${MAKEFILE}" -e "${TARGET}" -n | grep -c "^${CC}")"
targetno=$((${ntargets} - ${rtargets} + 1))
if [ "${1}" = "object" ]; then
printf "${build_progress_color}${bold}[%3d/%d]${clear_formatting} " \
"${targetno}" "${ntargets}"
printf "${build_info_color}Building${clear_formatting} "
elif [ "${1}" = "link" ]; then
printf "${link_info_color}${bold}[%3d/%d]${clear_formatting} " "${targetno}" "${ntargets}"
printf "${link_info_color}${bold}Linking${clear_formatting} "
fi
echo "${2}"
}
case $1 in
build) build;;
clean) clean;;
buildcn) clean ; build;;
generate) gen_makefile && info "Done";;
run) shift 1 && run $@;;
dryrun) dry_run;;
new) shift 1 && new $@;;
init) new module main;;
-h|--help|help) usage;;
__progress__) __progress__ "${2}" "${3}";;
*) echo "Invalid command. Try --help";;
esac