diff options
| author | davidovski <david@davidovski.xyz> | 2026-10-01 10:34:33 +0100 |
|---|---|---|
| committer | davidovski <david@davidovski.xyz> | 2026-10-01 10:34:33 +0100 |
| commit | eca55e60c29440c4e2e007b54ea1a1983e965954 (patch) | |
| tree | a98173c09f69864cc25395bfdab67d682434854b /src/util/shmk | |
| parent | f20bd9d6369c2e1246dcdd4af26b085da99c18e0 (diff) | |
Diffstat (limited to 'src/util/shmk')
| -rw-r--r-- | src/util/shmk/shmk.README | 129 | ||||
| -rwxr-xr-x | src/util/shmk/shmk.sh | 283 |
2 files changed, 412 insertions, 0 deletions
diff --git a/src/util/shmk/shmk.README b/src/util/shmk/shmk.README new file mode 100644 index 0000000..43671ac --- /dev/null +++ b/src/util/shmk/shmk.README @@ -0,0 +1,129 @@ +# shmk + +shmk is a simple utility to build complicated shell projects + +## Usage + +### single file + +shmk can be invoked on a single sh file to "build" it: + +```sh +$ shmk myprog.sh file_output.sh +``` + +### in a wider project + +shmk can also serve as a "make" tool, building libraries and executables, running tests (checks) and installing your project to a path. + +see [shmk project](#shmk project) for more info + +## Directives + +Shmk will take each file and parse it before making an output. Most of the file will remain unchanged, however some directives will be parsed: + + +### `#include file` + +Will include a file 'in place'. Will not include a file more than once within the output + +The first file with the given name will be used. The search path by default follows this order: + +``` +./ +$DIST/ +/usr/lib/ +/usr/local/lib/ +/usr/share/shmk/ +``` + +Using the `-I` option will prepend a file to this list. If no `.sh` extension is given, both the `filenanme` and the `filename.sh` will be checked + +# shmk project + +A shmk project is typically built using a `build.shmk` file + +An example build shmk file could like this (chek this project's shmk file for more examples): + +```sh +#!/usr/bin/env shmk + +LIBS=" + src/lib/mylibrary.sh + src/lib/second_library.sh +" + +PROGS=" + src/myutil.sh +" + +CHECKS=" + tests/test_myutil.sh +" +``` + +By making this file executable, shmk will be invoked via its shebang and it will parse this file as a project fille. + +The difference between a lib and a prog (library and program) is functionally only in where the files get installed. Programs should be intsalled to a `bin/` directory and are inteded to be run directly. Libraries, even though they are functionally shell programs, are intended to be included by other shell programs and provide variables and functions that are to be used. + +## stages + +When shmk builds a project, it is able to execute the following steps: (by default in the given order) + +- **clean** + - cleans the project files + - removes outputted files, executables etc +- **build** + - builds all libraries and programs using shmk +- **check** + - run all the available tests +- **install** + - install binaries and libraries + - by default will install progs to `/usr/local/bin/` and libraries to `/usr/local/lib/` + - `/usr/local/` can be changed using the env var `$PREFIX` + - instead of installing to system, package maintainers may use `$DESTDIR` to choose where to install shmk +- **uninstall** + - this stage is not run by default + - opposite of install, will follow the same install path and remove any libraries or programs it would have installed + +## custom build stages + +If desired, a custom build stage can be created: + +```sh + +prog_mycprog () { + gcc -o ${DIST}/myprog src/myprog.c +} + +``` + +Any function prefixed with `prog_` or `lib_` will be treated as an extra program or library to be built, and said function will be run after handling all of the files mentioned in `LIBS` and `PROGS` variables. This will not override anything from these lists. +These + +As per the example, this can be useful for incorporating non-shmk build stages. As shmk is interpreted like a normal shell file, the syntax should be the same as a shell allowing for extensible configuration of build stages. + +The prefix `check_` can also be used for custom check stages. These will not override the exsiting checks + +### Checks and tests + +A test in shmk is called a `check` (since test is already a shell command). The testing framework is rather rudimentary, and currenty can only be used with custom `check_` prefixed fucntions in the shmk build. Alternatively binaries from the `$CHECKS` variable will also be run. These are not to be confused with other build stages where the programs are never executed themselves: checks are run as-is. + +Checks are run consecutively until either all checks reutrn 0 (success) or a single check returns 1. + +This feature is intended to be used with the `shtests` tool. + + +## Syntax and formatting + + +### shell features + +shmk is intended to be as portable as possible, following exclusively POSIX shell syntax. This means that unlike in bash or csh, there is no formal structure for lists, instead whitespace separated strings are used. New lines or spaces both work, however for the sake of simplicity, newlines are prefered. + +### env vars + +shmk exposes a few different environment variables in the build environment (ie to be used in custom build stages): + +- `${DIST}` - temporary "out" directory where libraries and binaries are placed. +- `${PREFIX}` - where things will be installed diff --git a/src/util/shmk/shmk.sh b/src/util/shmk/shmk.sh new file mode 100755 index 0000000..be5f00b --- /dev/null +++ b/src/util/shmk/shmk.sh @@ -0,0 +1,283 @@ +#!/bin/sh +# +# A tool to build a single executable from a collection of shell scripts + +#include colors +#include libhbar + +PREFIX=${PREFIX:-/usr} + +usage () { + cat << EOF +${BLUE}Available Options: + +EOF +} + +# find a file in the search tree +# +# _find_file filename +# +_find_file () { + for p in $search_path; do + for f in $p/$1.sh $p/$1; do + $verbose \ + && printf "${LIGHT_BLACK}checking: $f\n" 1>&2 + + [ -f "$f" ] && { + $verbose \ + && printf "${LIGHT_BLACK}found: $f for $1\n" 1>&2 + echo "$f" + return 0 + } + done + done + return 1 +} + +# parse the lines of a file +# +# _parse_lines < file +# +_parse_lines () { + + # keep a running log of which files we've already included + included="" + + # go through the lines + while IFS= read -r line; do + case "$line" in + "#include "*) + # find the file + file="$(_find_file ${line#\#include})" + + # if we have already included this file, do not parse it + case "$included" in + *"$file"*) + $verbose \ + && printf "${LIGHT_BLACK}already included $file, won't include twice\n" 1>&2 + ;; + *) + cat $file | _parse_lines + included="$included $file" + esac + ;; + + "#>"*) + eval ${line#'#>'} + ;; + "#!"*) + printf "%s\n" "$line" + ;; + "#"*);; + *) + printf "%s\n" "$line" + ;; + esac + done +} + +# 'build' a shell file too an output +# goes through the file without running it and interprets all shmk commands +# +# build_shmk file [output] +# +build_shmk () { + [ -f "$1" ] || return 1 + [ ! -z "$2" ] && output="$2" + [ -z "$output" ] && output=$(basename $1) + output=./${output%.*} + + $clean && { + rm -f $output + exit + } + + $verbose && echo "building $1 to $output" + + [ ! -d "${output%/*}" ] && mkdir -p "${output%/*}" + + cat $1 \ + | _parse_lines > ${output} && + [ ! -z ${entry_function} ] && echo "$entry_function" >> ${output} + chmod +x ${output} +} + +# take a .shmk file and build it +# +interpret_shmk () { + . $1 + local cmdlist="" + + ## TODO override if already set + DIST="./dist" + BUILDS="$(sed -rn "s/^build_(.*)\s*\(\)\s*\{/\1/p" $1) $BUILDS" + PROGS="$(sed -rn "s/^prog_(.*)\s*\(\)\s*\{/\1/p" $1) $PROGS " + LIBS="$(sed -rn "s/^lib_(.*)\s*\(\)\s*\{/\1/p" $1) $LIBS" + CHECKS="$(sed -rn "s/^check_(.*)\s*\(\)\s*\{/\1/p" $1) $CHECKS" + + shift + [ -z "$1" ] && set -- clean build check + while [ ! -z "$1" ]; do + case "$1" in + # install programs and libraries to system + # + install) + for prog in $PROGS; do + prog=$(basename $prog) + prog="${prog%.*}" + cmdlist="$cmdlist + install -Dm755 $DIST/$prog ${DESTDIR}/${PREFIX}/bin/$prog #Install program $prog" + done + for lib in $LIBS; do + lib=$(basename $lib) + lib="${lib%.*}" + cmdlist="$cmdlist + install -Dm755 $DIST/$lib ${DESTDIR}/${PREFIX}/lib/$lib.sh #Install library $lib" + done + ;; + + # install programs and libraries to system + # + uninstall) + for prog in $PROGS; do + prog=$(basename $prog) + prog="${prog%.*}" + cmdlist="$cmdlist + rm ${DESTDIR}/${PREFIX}/bin/$prog #Uninstall program $prog" + done + for lib in $LIBS; do + lib=$(basename $lib) + lib="${lib%.*}" + cmdlist="$cmdlist + rm $DIST/$lib ${DESTDIR}/${PREFIX}/lib/$lib.sh #Uninstall library $lib" + done + ;; + + clean) + [ -d "$DIST" ] && cmdlist="$cmdlist + rm -r $DIST #Clean" + ;; + + check) + for check in $CHECKS; do + command -v check_$check > /dev/null 2>&1 && cmdlist="$cmdlist + check_$check #Check $check" + done + ;; + + *) # build all programs + search_path="$DIST $search_path" + + for lib in $LIBS; do + name=$(basename $lib) + build_cmd=lib_$lib + command -v $build_cmd > /dev/null 2>&1 || { + build_cmd="build_shmk $lib $DIST/$name" + } + cmdlist="$cmdlist + $build_cmd #Build library $name" + done + + for prog in $PROGS; do + name=$(basename $prog) + build_cmd=prog_$prog + command -v $build_cmd > /dev/null 2>&1 || { + build_cmd="build_shmk $prog $DIST/$name" + } + cmdlist="$cmdlist + $build_cmd #Build program $name" + done + + # build all libs + ;; + esac + shift + done + + local out len i + $verbose && out=/dev/stdout || out=/dev/null + len=$(($(echo "$cmdlist" | wc -l)-1)) + i=-1 + echo "$cmdlist" | while read -r cmd; do + i=$((i+1)) + outstring="$(printf "${LIGHT_BLACK}[${LIGHT_BLUE}%s${LIGHT_BLACK}/${LIGHT_BLUE}%s${LIGHT_BLACK}] ${LIGHT_WHITE}%s" \ + "$i" "$len" "${cmd#*#}")" + + printf "\r%s\r" "${outstring}" #"$($verbose && printf "\n\n" || printf "\n")" + if ! $verbose && command -v hbar > /dev/null 2>&1; then + printf "\n" + hbar -l 1 -r "${RESET}" -c "${BG_BLUE}" -n "$i" "$len" + fi + + ${cmd%#*} 2>&1 > $out || { + printf "\n${RED}Error $?\n" + exit 1 + } + done + printf "\n" +} + +search_path=". +$DIST/ +/usr/lib/ +/usr/local/lib/ +/usr/share/shmk/ +" + +verbose=false +clean=false + +# test shmk was invoked on a file if so put the filename at the end because it will break getopts +# this is a sketchy workaround because the shebang will always put the +# filename of the project file as the first arg, so this way we can +# happily introduce more arguments +[ -f "$1" ] && { + set -- $* $1 +} + +while getopts ":e:I:o:chv" opt; do + case "${opt}" in + e) + entry_function="$OPTARG" + ;; + o) + output="$OPTARG" + ;; + c) + clean=true + ;; + I) + search_path=" + $OPTARG + $search_path + " + ;; + v) + verbose=true + ;; + h) + usage && exit 0 + ;; + esac +done + +shift $((OPTIND-1)) + +[ -z $1 ] && { + exit 1 +} + +echo "$0 $*" + +[ -f "$1" ] && shebang="$(head -1 $1)" + +case "$shebang" in + "#!"*"shmk"*) + interpret_shmk $@ + ;; + *) + build_shmk $@ + ;; +esac + |
