#!/usr/bin/env zsh # TODO: # - Make more checks about whether the “things” built and installed are C. # (stuff *will* break if you add non-C things as targets) # - Clean some more (or a lot). I mean, this script could even be reused if # it were cleaner, more readable and somewhat more documented. # # WARNINGS and LIMITATIONS: # - Using a relative path in DESTDIR= *will* fail. # # Script output function info { echo "${fg_bold[green]}-- ${fg_bold[white]}$@${reset_color}" } function warning { echo "${fg_bold[yellow]}-- $@${reset_color}" >&2 } function error { echo "${fg_bold[red]}-- ERROR: $@${reset_color}" >&2 } # Makefiles’ output function write { echo "$@" >> $Makefile } function build_string { local type_indicator="$1" local operation="$2" local file_name="$3" local color="blue" case $operation in build) color=blue;; build-alt) color=cyan;; assemble) color=green;; generate) color=yellow;; install) color=red;; remove) color=white;; *) color=magenta;; esac printf "${fg_bold[$color]} %-6s ${fg_bold[white]}$file_name${reset_color}\n" \ "$type_indicator >" } function CC { build_string CC build "$@" } function LD { build_string LD assemble "$@" } function IN { build_string IN install "$@" } function LN { build_string LN other "$@" } function RM { build_string RM remove "$@" } function SED { build_string SED generate "$@" } function DIR { build_string DIR other "$@" } function TAR { build_string TAR generate "$@" } # Generic helpers function has { local elem="$1" shift 1 for i in "$@"; do [[ "$i" == "$elem" ]] && return 0 done return 1 } function has_array_key { local element="$1" shift 1 for key value ($@) { [[ "$key" == "$element" ]] && return 0 } return 1 } # Specialized helpers function get_distfiles { for file in "${dist[@]}" $(echo ${sources[@]}) $(echo ${depends[@]}); do if [[ -z "${nodist[$file]}" ]]; then echo "$file" fi done typeset -a src src=($(echo ${sources[@]})) for file in ${src[@]}; do typeset file="${file%.c}.h" if [[ -e "$file" ]]; then echo "$file" fi done } function exists { [[ "$(whence -w ${1})" != "${1}: none" ]] } function duplicated { local elem="$1" local count=0 shift 1 local i for i in $@; do if [[ "$elem" == "$i" ]]; then ((count++)) if (( $count > 1 )); then return 0 fi fi done return 1 } function dirdep { dirname="$(dirname "$1")" local sources=(${(@s/ /)2}) local depends=(${(@s/ /)3}) for dep in ${sources[@]} ${depends[@]}; do if [[ "$(dirname $dep)" == "$dirname" ]]; then return 0 fi done if [[ "$dirname" == "." ]]; then return 0 fi echo "$dirname" } for dir in "@SHAREDIR@/build.zsh" ./build; do [[ -d "$dir" ]] && { for i in "$dir"/*.zsh; do . "$i" done } done ## # And so it begins ## function main { # “Global”, per-project configuration. typeset -a prefixes directories # “Local”, per-target configuration. typeset -A sources type depends install auto filename nodist typeset -A chmod # Per-target configuration that is specific to certain target types. # FIXME: Those should be removed from this file and exported in # the .prelude of their backend, or similar. typeset -A ldflags cflags crflags prefixes=( PREFIX '/usr/local' BINDIR '$(PREFIX)/bin' LIBDIR '$(PREFIX)/lib' SHAREDIR '$(PREFIX)/share' INCLUDEDIR '$(PREFIX)/include' MANDIR '$(SHAREDIR)/man' ) if [[ -f project.zsh && -r project.zsh ]]; then . ./project.zsh else error "No “project.zsh” found in $(pwd)." exit 1 fi : > $Makefile for target in ${targets[@]}; do local _type=${type[$target]} if exists ${_type}.prelude; then ${_type}.prelude fi done if [[ -n "$package" && -n "$version" ]]; then export package version write "PACKAGE = '$package'" write "VERSION = '$version'" write else if $root; then error "No \$package or no \$version defined in your project.zsh!" error "Making tarballs and other stuff will be disabled." error "(please note that those variables should be defined only..." error " ... in the root directory of your project’s repository)" root=false fi fi # Got some issues of PATH being replaced by a value of $path… for prefix _path in ${prefixes[@]}; do write "$prefix := $_path" done for var val in ${variables[@]}; do write "$var := ${val}" done write write "Q := @" write if [[ -z "${all}" ]] || (( ${#all[@]} == 0 )); then all=(${targets[@]}) fi write -n "all: ${all[@]}" if $gnu; then write "\n" else write "\n\t@:\n" fi typeset -l -a exported_rules local target_index=1 while (($target_index <= ${#targets[@]})); do local target="${targets[$target_index]}" if has "${target}" "${exported_rules[@]}"; then ((target_index++)) continue else exported_rules+=("$target") fi typeset -a src src=($(echo ${sources[$target]})) local installdir="${install[$target]}" if exists "${type[$target]}.build"; then ${type[$target]}.build else error "No predefined rule for the following type: ${type[$target]}" error " (expect trouble, nothing’s gonna work!)" fi if [[ "${installdir}" == "-" ]]; then write "${target}.install:" write "" else if exists "${type[$target]}.install"; then ${type[$target]}.install else if [[ -z "${installdir}" ]]; then error "No install[${type[${target}]}] and no default installation directory." error "Your “install” rule will be broken!" else local F="${filename[$target]}" if [[ -z "$F" ]]; then F="${target}" fi write "${target}.install: \$(DESTDIR)${installdir}" write "\t@echo '$(IN ${installdir}/${filename})'" write "\t@mkdir -p '\$(DESTDIR)/${installdir}'" write "\t${Q}install -m755 $target \$(DESTDIR)${installdir}/$filename" write fi fi fi if exists "${type[$target]}.clean"; then ${type[$target]}.clean else write "${target}.clean:" write "\t@echo '$(RM ${target})'" write "\t${Q}rm -f ${target}" write fi if [[ "${installdir}" == "-" ]]; then write "${target}.uninstall:" write "" else if exists "${type[$target]}.uninstall"; then ${type[$target]}.uninstall else write "${target}.uninstall:" write "\t@echo '$(RM "${installdir}/${target}")'" write "\t${Q}rm -f '\$(DESTDIR)${installdir}/${target}'" write fi fi ((target_index++)) done typeset -l -a target_directories for target in "${targets[@]}"; do directory="${target%/*}" if [[ "$target" =~ .*/ ]] && ! has "${directory}" "${target_directories[@]}"; then target_directories+=(${directory}) fi done for dir in ${target_directories[@]}; do write "${dir}:" write "\t${Q}mkdir -p ${dir}" done for dir in ${directories[@]}; do write "\$(DESTDIR)${dir}:" write "\t@echo '$(DIR ${dir})'" write "\t${Q}mkdir -p \$(DESTDIR)${dir}" done for dir __ in ${prefixes[@]}; do write "\$(DESTDIR)\$(${dir}):" write "\t@echo '$(DIR "\$(${dir})")'" write "\t${Q}mkdir -p \$(DESTDIR)\$(${dir})" done write -n "install:" for target in ${targets[@]}; do write -n " ${target}.install" done write "\n\t@:\n" write -n "uninstall:" for target in ${targets[@]}; do write -n " ${target}.uninstall" done write "\n\t@:\n" write -n "clean:" (( ${#targets[@]} > 0 )) && { for target in ${targets[@]}; do write -n " ${target}.clean" #( # typeset -a src # src=($(echo ${sources[$target]})) # for file in ${src[@]}; do # write -n " ${file%.c}.o.clean" # done #) done } write write "distclean: clean" if $root; then write "dist: dist-gz dist-xz dist-bz2" write "\t"$Q'rm -- $(PACKAGE)-$(VERSION)' write write "distdir:" write "\t"$Q'rm -rf -- $(PACKAGE)-$(VERSION)' write "\t"$Q'ln -s -- . $(PACKAGE)-$(VERSION)' write typeset -a distfiles distfiles=($(get_distfiles)) local i for i in {1..${#distfiles[@]}}; do if duplicated "${distfiles[${i}]}" "${distfiles[@]}"; then distfiles[${i}]= fi done for i flag in gz z xz J bz2 j; do write "dist-${i}: \$(PACKAGE)-\$(VERSION).tar.$i" write "\$(PACKAGE)-\$(VERSION).tar.$i: distdir" write "\t@echo '$(TAR "\$(PACKAGE)-\$(VERSION).tar.$i")'" write "\t${Q}tar c${flag}f \$(PACKAGE)-\$(VERSION).tar.$i \\" for i in {1..${#distfiles}}; do if [[ -n "${distfiles[$i]}" ]]; then write -n "\t\t\$(PACKAGE)-\$(VERSION)/${distfiles[$i]}" if (( i != ${#distfiles} )); then write " \\" fi fi done write "\n" done fi write "help:" if [[ -n "$package" ]]; then write " @echo '${fg_bold[white]} :: $package-$version${reset_color}'" write " @echo ''" fi write " @echo '${fg_bold[white]}Generic targets:${reset_color}'" typeset -la help help=( help "Prints this help message." all "Builds all targets." dist "Creates tarballs of the files of the project." install "Installs the project." clean "Removes compiled files." uninstall "Deinstalls the project." ) for rule message in ${help[@]}; do printf " @echo '${reset_color} - ${fg_bold[green]}%-14s${fg[white]} $message${reset_color}'\n" \ "$rule" >> $Makefile done write " @echo ''" write " @echo '${fg_bold[white]}CLI-modifiable variables:${reset_color}'" for VAR __ in ${variables}; do printf " @echo ' - ${fg_bold[blue]}%-14s${fg[white]} \${$VAR}${reset_color}'\n" "$VAR" >> $Makefile done for VAR __ in ${prefixes}; do printf " @echo ' - ${fg_bold[blue]}%-14s${fg[white]} \${$VAR}${reset_color}'\n" "$VAR" >> $Makefile done write " @echo ''" write " @echo '${fg_bold[white]}Project targets: ${reset_color}'" # Computing how many characters should be reserved for the left part # of those lines. The name of the target will be displayed in it. local name_slot_size=14 for T in ${targets[@]}; do if [[ "${auto[$T]}" == true ]]; then continue fi if (( $#T > 22 )); then name_slot_size=30 break elif (( $#T > 14 )); then name_slot_size=22 fi done for T in ${targets[@]}; do if [[ "${auto[$T]}" != true ]]; then printf " @echo ' - ${fg_bold[yellow]}%-${name_slot_size}s${fg[white]} ${type[$T]}${reset_color}'\n" "$T" >> $Makefile fi done write " @echo ''" write " @echo '${fg_bold[white]}Makefile options:${reset_color}'" printf " @echo ' - %-14s $gnu'\n" "gnu:" >> $Makefile printf " @echo ' - %-14s $colors'\n" "colors:" >> $Makefile write " @echo ''" write " @echo '${fg_bold[white]}Rebuild the Makefile with:${reset_color}'" write " @echo ' zsh ./build.zsh$($colors && echo -n " -c")$($gnu && echo -n " -g")'" write ".PHONY: all clean distclean dist install uninstall help" write } export Makefile=Makefile export MAKE='$(MAKE)' export Q='$(Q)' export gnu=false export colors=false while (($# > 0)); do case "$1" in -c|--colors) export colors=true autoload -U colors colors ;; -g|--gnu) export gnu=true ;; -h|--help) echo "usage: $0 [OPTIONS]" echo echo "Options:" echo " -h, --help Print this help message." echo " -c, --colors Use colors in your Makefiles" echo " (relies on zsh/colors and your current \$TERM)" echo " -g, --gnu Sell your soul for gmake-dependent features." return 0 ;; *) error "unrecognised parameter: $1" return 1 ;; esac shift 1 done echo "Generating Makefiles..." root=true main