bat-extras/build.sh
Rex Ackermann 03549d651a build: Add termux support
sets the OPT_PREFIX to /data/data/com.termux/files/usr if termux is detected.
2023-06-15 16:56:16 -07:00

737 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# bat-extras | Copyright (C) 2019-2023 eth-p | MIT License
#
# Repository: https://github.com/eth-p/bat-extras
# Issues: https://github.com/eth-p/bat-extras/issues
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Build-as-a-Library Functions:
# -----------------------------------------------------------------------------
# Redefines a function to print a constant string whenever called.
# This is used for lazy-loading of some getter functions.
#
# Arguments:
# 1 -- The function name.
# 2 -- The constant string to print.
#
# Output:
# The constant string.
batextras:lazy_done() {
eval "$(printf "%s() { printf \"%%s\n\" %q; }" "$1" "$2")"
"$1"
}
# Checks to see if a function is defined.
# Arguments:
# 1 -- The function name.
# If prefixed with "::", it will use "batextras:" as a namespace.
batextras:is_function_defined() {
local name="$1"
if [[ "${name:0:2}" = "::" ]]; then name="batextras:${name:2}"; fi
[[ "$(type -t "$name" || echo 'undefined')" = "function" ]]
return $?
}
# Prints the path to the project directory.
if ! batextras:is_function_defined ::get_project_directory; then
batextras:get_project_directory() {
batextras:lazy_done "${FUNCNAME[0]}" \
"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
}
fi
# Prints the path to the project source directory.
if ! batextras:is_function_defined ::get_source_directory; then
batextras:get_source_directory() {
batextras:lazy_done "${FUNCNAME[0]}" \
"$(batextras:get_project_directory)/src"
}
fi
# Prints the path to the project output directory for executables.
if ! batextras:is_function_defined ::get_output_bin_directory; then
batextras:get_output_bin_directory() {
batextras:lazy_done "${FUNCNAME[0]}" \
"$(batextras:get_project_directory)/bin"
}
fi
# Prints the path to the project output directory for manuals.
if ! batextras:is_function_defined ::get_output_man_directory; then
batextras:get_output_man_directory() {
batextras:lazy_done "${FUNCNAME[0]}" \
"$(batextras:get_project_directory)/man"
}
fi
# Prints the path to the project directory for documentation.
if ! batextras:is_function_defined ::get_docs_directory; then
batextras:get_docs_directory() {
batextras:lazy_done "${FUNCNAME[0]}" \
"$(batextras:get_project_directory)/doc"
}
fi
# Prints the declared version (in version.txt).
if ! batextras:is_function_defined ::get_version; then
batextras:get_version() {
batextras:lazy_done "${FUNCNAME[0]}" \
"$(cat "$(batextras:get_project_directory)/version.txt")"
}
fi
# Prints the paths for all source scripts in this project.
#
# Output:
# One line for each script with the full path to the script.
if ! batextras:is_function_defined ::get_source_paths; then
batextras:get_source_paths() {
for file in "$(batextras:get_source_directory)"/*.sh; do
printf "%s\n" "$file"
file_bin="$(basename -- "$file" ".sh")"
done
}
fi
# -----------------------------------------------------------------------------
# Main:
# Only run everything past this point if the script is not sourced.
# -----------------------------------------------------------------------------
(return 0 2>/dev/null) && return 0
HERE="$(batextras:get_project_directory)"
SRC="$(batextras:get_source_directory)"
BIN="$(batextras:get_output_bin_directory)"
MAN="$(batextras:get_output_man_directory)"
MAN_SRC="$(batextras:get_docs_directory)"
LIB="$HERE/lib"
source "${LIB}/print.sh"
source "${LIB}/opt.sh"
source "${LIB}/constants.sh"
source "${HERE}/mdroff.sh"
# -----------------------------------------------------------------------------
set -eo pipefail
exec 3>&1
# Runs the next build step.
#
# Arguments:
# 1 -- The build step function name.
# @ -- The function arguments.
#
# Input:
# The unprocessed file data.
#
# Output:
# The processed file data.
next() {
"$@"
return $?
}
# Prints a build step message.
smsg() {
case "$2" in
"SKIP") printc_msg " %{YELLOW} %{DIM}%s [skipped]%{CLEAR}\n" "$1" ;;
*) printc_msg " %{YELLOW} %s...%{CLEAR}\n" "$1" ;;
esac
}
# Prints a message to STDOUT (via FD 3).
# Works the same as printc.
printc_msg() {
printc "$@" 1>&3
}
# Prints a message to STDERR.
# Works the same as printc.
printc_err() {
printc "$@" 1>&2
}
# Escapes a sed pattern.
# Arguments:
# 1 -- The pattern.
#
# Output:
# The escaped string.
sed_escape() {
sed 's/\([][\\\/\^\$\*\.\-]\)/\\\1/g' <<< "$1"
}
# Checks if the output scripts will be minified.
# Arguments:
# 1 "all" -- All scripts will be minified.
# "any" -- Any scripts will be minified.
# "lib" -- Library scripts will be minified.
# "unsafe" -- Unsafe minifications will be applied.
will_minify() {
case "$1" in
all)
[[ "$OPT_MINIFY" =~ ^all($|\+.*) ]]
return $? ;;
unsafe)
[[ "$OPT_MINIFY" =~ ^.*+unsafe(\+.*)*$ ]]
return $? ;;
lib)
[[ "$OPT_MINIFY" =~ ^lib($|\+.*) ]]
return $? ;;
any|"")
[[ "$OPT_MINIFY" != "none" ]]
return $? ;;
none)
! will_minify any
return $? ;;
esac
return 1
}
# Generates the banner for the output files.
#
# Output:
# The contents of banner.txt
generate_banner() {
local step="$1"
if ! "$OPT_BANNER"; then
return 0
fi
# Don't run it unless the comments are removed or hidden.
if ! { will_minify all || "$OPT_COMPRESS"; }; then
return 0
fi
# Only run it in the compression step if both minifying and compressing.
if will_minify all && "$OPT_COMPRESS" && [[ "$step" != "step_compress" ]]; then
return 0
fi
# Write the banner.
bat "${HERE}/banner.txt"
}
# Build step: read
# Reads the file from its source.
#
# Arguments:
# 1 -- The source file.
# 2 -- A description of what is being read.
#
# Output:
# The file contents.
step_read() {
local what=""
if [[ -n "${2:-}" ]]; then
what=" $2"
fi
cat "$1"
smsg "Reading${what}"
}
# Build step: preprocess
# Preprocesses the script.
#
# This will embed library scripts and inline constants.
#
# Input:
# The original file contents.
#
# Output:
# The processed file contents.
step_preprocess() {
local line
local docvar
pp_consolidate | while IFS='' read -r line; do
# Skip certain lines.
[[ "$line" =~ ^LIB=.*$ ]] && continue
[[ "$line" =~ ^[[:space:]]*source[[:space:]]+[\"\']\$\{?LIB\}/(constants\.sh)[\"\'] ]] && continue
# Forward data.
echo "$line"
done | pp_inline_constants
smsg "Preprocessing"
}
# Build step: minify
# Minifies the output script.
#
# Input:
# The original file contents.
#
# Output:
# The minified file contents.
step_minify() {
if ! will_minify all; then
cat
smsg "Minifying" "SKIP"
return 0
fi
printf "#!/usr/bin/env bash\n"
generate_banner step_minify
pp_minify | pp_minify_unsafe
smsg "Minifying"
}
# Build step: compress
# Compresses the input into a gzipped self-executable script.
#
# Input:
# The original file contents.
#
# Output:
# The compressed self-executable script.
step_compress() {
if ! "$OPT_COMPRESS"; then
cat
smsg "Compressing" "SKIP"
return 0
fi
local wrapper
wrapper="$({
printf '#!/usr/bin/env bash\n'
generate_banner step_compress
printf "(exec -a \"\$0\" bash -c 'eval \"\$(cat <&3)\"' \"\$0\" \"\$@\" 3< <(dd bs=1 if=\"\$0\" skip=::: 2>/dev/null | gunzip)); exit \$?;\n"
})"
echo "${wrapper/:::/$(wc -c <<<"$wrapper" | sed 's/^[[:space:]]*//')}"
gzip
smsg "Compressing"
}
# Build step: write
# Writes the output script to a file.
#
# Arguments:
# 1 -- The file to write to.
#
# Input:
# The file contents.
#
# Output:
# The file contents.
step_write() {
tee "$1"
chmod +x "$1"
smsg "Building"
}
# Build step: write_install
# Optionally writes the output script to a file.
#
# Arguments:
# 1 -- The file to write to.
#
# Input:
# The file contents.
#
# Output:
# The file contents.
step_write_install() {
if [[ "$OPT_INSTALL" != true ]]; then
cat
smsg "Installing" "SKIP"
return 0
fi
tee "$1"
chmod +x "$1"
smsg "Installing"
}
# Build step: manpage_install
# Optionally writes the manpage to a gzipped file.
#
# Arguments:
# 1 -- The file to write to.
#
# Input:
# The file contents.
#
# Output:
# The file contents.
step_manpage_install() {
if [[ "$OPT_INSTALL" != true ]]; then
cat
smsg "Installing manual" "SKIP"
return 0
fi
gzip | tee "$1"
smsg "Installing manual"
}
# Build step: manpage_generate
# Generates a manpage document from a markdown document.
#
# Input:
# The markdown doc contents.
#
# Output:
# The roff manpage contents.
step_manpage_generate() {
if [[ "$OPT_MANUALS" != true ]]; then
cat
smsg "Generating manual" "SKIP"
return 0
fi
(mdroff)
smsg "Generating manual"
}
# -----------------------------------------------------------------------------
# Preprocessor:
# -----------------------------------------------------------------------------
# Consolidates all scripts into a single file.
# This follows all `source "${LIB}/..."` files and embeds them into the script.
pp_consolidate() {
PP_CONSOLIDATE_PROCESSED=()
pp_consolidate__do 0
}
pp_consolidate__do() {
local depth="$1"
local indent="$(printf "%-${depth}s" | tr ' ' $'\t')"
local line
while IFS='' read -r line; do
# Embed library scripts.
if [[ "$line" =~ ^[[:space:]]*source[[:space:]]+[\"\']\$\{?LIB\}/([a-z_-]+\.sh)[\"\'] ]]; then
local script_name="${BASH_REMATCH[1]}"
local script="$LIB/$script_name"
# Skip if it's the constants library.
[[ "$script_name" = "constants.sh" ]] && continue
# Skip if it's already embedded.
local other
for other in "${PP_CONSOLIDATE_PROCESSED[@]}"; do
[[ "$script" = "$other" ]] && continue 2
done
PP_CONSOLIDATE_PROCESSED+=("$script")
# Embed the script.
echo "${indent}# --- BEGIN LIBRARY FILE: ${BASH_REMATCH[1]} ---"
{
if will_minify lib; then
pp_strip_comments | pp_minify | pp_minify_unsafe
else
pp_strip_copyright | pp_strip_separators
fi
} < <(pp_consolidate__do "$((depth + 1))" < "$script") | sed "s/^/${indent}/"
echo "${indent}# --- END LIBRARY FILE ---"
continue
fi
# Forward data.
echo "$line"
done
}
# Inlines constants:
# EXECUTABLE_*
# PROGRAM_*
pp_inline_constants() {
local constants=("PROGRAM")
# Determine the PROGRAM_ constants.
local nf_constants="$( ( set -o posix ; set) | grep '^\(PROGRAM_\|EXECUTABLE_\)' | cut -d'=' -f1)"
local line
while read -r line; do
constants+=("$line")
done <<< "$nf_constants"
# Generate a sed replace for the constants.
local constants_pattern=''
local constant_name
local constant_value
for constant_name in "${constants[@]}"; do
constant_value="$(sed_escape "${!constant_name}")"
constant_name="$(sed_escape "$constant_name")"
constant_sed="s/\\\$${constant_name}\([^A-Za-z0-9_]\)/${constant_value}\1/; s/\\\${${constant_name}}/${constant_value}/g;"
constants_pattern="${constants_pattern}${constant_sed}"
done
sed "${constants_pattern}"
}
# Strips comments from a Bash source file.
pp_strip_comments() {
sed '/^[[:space:]]*#.*$/d'
}
# Strips copyright comments from the start of a Bash source file.
pp_strip_copyright() {
awk '/^#/ {if(!p){ next }} { p=1; print $0 }'
}
# Strips separator comments from the start of a Bash source file.
pp_strip_separators() {
awk '/^#\s*-{5,}/ { next; } {print $0}'
}
# Minify a Bash source file.
# https://github.com/mvdan/sh
pp_minify() {
if will_minify none; then
cat
return
fi
shfmt -mn
return $?
}
# Minifies the output script (unsafely).
# Right now, this doesn't do anything.
# This should be applied after shfmt minification.
pp_minify_unsafe() {
if ! will_minify unsafe; then
cat
return 0
fi
cat
}
# -----------------------------------------------------------------------------
# Options:
# -----------------------------------------------------------------------------
OPT_INSTALL=false
OPT_COMPRESS=false
OPT_VERIFY=true
OPT_BANNER=true
OPT_MANUALS=true
OPT_INLINE=true
OPT_MINIFY="lib"
OPT_PREFIX="/usr/local"
EXECUTABLE_BAT="$(basename -- "$EXECUTABLE_BAT")"
ALT_EXECS=()
BUILD_FILTER=()
DOCS_URL="https://github.com/eth-p/bat-extras/blob/master/doc"
DOCS_MAINTAINER="eth-p <eth-p@hidden.email>"
# -----------------------------------------------------------------------------
# Use a different default prefix when running on Termux.
if [[ "$(uname -o)" = "Android" ]] && [[ -n "${TERMUX_VERSION:-}" ]]; then
OPT_PREFIX="/data/data/com.termux/files/usr/"
else
OPT_PREFIX="/usr/local"
fi
# -----------------------------------------------------------------------------
# Parse arguments.
while shiftopt; do
# shellcheck disable=SC2034
case "$OPT" in
--install) OPT_INSTALL=true ;;
--compress) OPT_COMPRESS=true ;;
--manuals) OPT_MANUALS="${OPT_VAL:-true}" ;;
--no-manuals) OPT_MANUALS=false ;;
--verify) OPT_VERIFY=true ;;
--no-verify) OPT_VERIFY=false ;;
--banner) OPT_BANNER=true ;;
--no-banner) OPT_BANNER=false ;;
--inline) OPT_INLINE=true ;;
--no-inline) OPT_INLINE=false ;;
--prefix) shiftval; OPT_PREFIX="$OPT_VAL" ;;
--alternate-executable) shiftval; ALT_EXECS+=("bat"); EXECUTABLE_BAT="$OPT_VAL" ;;
--alternate-executable:bat) shiftval; ALT_EXECS+=("bat"); EXECUTABLE_BAT="$OPT_VAL" ;;
--alternate-executable:ripgrep) shiftval; ALT_EXECS+=("ripgrep"); EXECUTABLE_RIPGREP="$OPT_VAL" ;;
--alternate-executable:delta) shiftval; ALT_EXECS+=("delta"); EXECUTABLE_DELTA="$OPT_VAL" ;;
--alternate-executable:fzf) shiftval; ALT_EXECS+=("fzf"); EXECUTABLE_FZF="$OPT_VAL" ;;
--alternate-executable:git) shiftval; ALT_EXECS+=("git"); EXECUTABLE_GIT="$OPT_VAL" ;;
--minify) shiftval; OPT_MINIFY="$OPT_VAL" ;;
# Print scripts.
--show:source-paths) get_source_paths; exit 0 ;;
# Unknown options.
*)
if ! [[ -f "${SRC}/${OPT}.sh" ]]; then
printc_err "%{RED}%s: unknown option '%s'%{CLEAR}" "$PROGRAM" "$OPT"
exit 1
fi
BUILD_FILTER+=("$OPT")
;;
esac
done
if [[ "${#ALT_EXECS[@]}" -gt 0 ]]; then
printc_msg "%{YELLOW}Building executable scripts with alternate executables for:%{CLEAR}\n"
printc_msg "%{YELLOW} - %{CLEAR}%s\n" "${ALT_EXECS[@]}"
printc_msg "\n"
if ! command -v "$EXECUTABLE_BAT" &>/dev/null; then
printc_err "%{YELLOW}WARNING: Bash cannot execute bat's executable file.\n"
printc_err "%{YELLOW} The finished scripts may not run properly.%{CLEAR}\n"
fi
# shellcheck disable=SC2034
printc_msg "\n"
fi
if [[ "$OPT_INSTALL" = true ]]; then
printc_msg "%{YELLOW}Installing to %{MAGENTA}%s%{YELLOW}.%{CLEAR}\n" "$OPT_PREFIX"
else
printc_msg "%{YELLOW}This will not install the script.%{CLEAR}\n"
printc_msg "%{YELLOW}Use %{BLUE}--install%{YELLOW} for a global install.%{CLEAR}\n\n"
fi
if [[ "$OPT_INLINE" = false ]]; then
# Prevent full executable paths from being inlined.
while read -r exec; do
declare "$exec=$(basename "${!exec}")"
done < <(set | grep '^EXECUTABLE' | cut -d'=' -f1)
fi
# -----------------------------------------------------------------------------
# Check for resources.
if ! will_minify none && ! command -v shfmt &>/dev/null; then
printc_err "%{RED}Warning: cannot find shfmt. Unable to minify scripts.%{CLEAR}\n"
OPT_MINIFY=none
fi
# -----------------------------------------------------------------------------
# Check target directories exist.
[[ -d "$BIN" ]] || mkdir -p "$BIN"
if "$OPT_INSTALL"; then
[[ -d "${OPT_PREFIX}/bin" ]] || mkdir -p "${OPT_PREFIX}/bin"
[[ "$OPT_MANUALS" = "true" && ! -d "${OPT_PREFIX}/share/man/man1" ]] \
&& mkdir -p "${OPT_PREFIX}/share/man/man1"
fi
if [[ "$OPT_MANUALS" = "true" ]]; then
[[ -d "$MAN" ]] || mkdir -p "$MAN"
fi
# -----------------------------------------------------------------------------
# Find files.
SOURCES=()
printc_msg "%{YELLOW}Preparing scripts...%{CLEAR}\n"
while read -r file; do
file_bin="$(basename -- "$file" ".sh")"
buildable=false
if ! "$buildable" && [[ "${#BUILD_FILTER[@]}" -eq 0 ]]; then
buildable=true
elif ! "$buildable"; then
for buildable_file in "${BUILD_FILTER[@]}"; do
if [[ "$buildable_file" = "$file_bin" ]]; then
buildable=true
break
fi
done
fi
# If that one is allowed to build, add it to the sources list.
if "$buildable"; then
SOURCES+=("$file")
else
printc_msg " %{YELLOW}Skipping %{MAGENTA}%s%{CLEAR}\n" "$(basename "$file_bin")"
fi
done < <(batextras:get_source_paths)
if [[ "${#BUILD_FILTER[@]}" -gt 0 ]]; then
printf "\n"
fi
# -----------------------------------------------------------------------------
# Build files.
printc_msg "%{YELLOW}Building scripts...%{CLEAR}\n"
file_i=0
file_n="${#SOURCES[@]}"
for file in "${SOURCES[@]}"; do
((file_i++)) || true
filename="$(basename "$file" .sh)"
PROGRAM="$filename"
PROGRAM_VERSION="$(<"${HERE}/version.txt")"
printc_msg " %{YELLOW}[%s/%s] %{MAGENTA}%s%{CLEAR}\n" "$file_i" "$file_n" "$file"
step_read "$file" |
next step_preprocess |
next step_minify |
next step_compress |
next step_write "${BIN}/${filename}" |
next step_write_install "${OPT_PREFIX}/bin/${filename}" |
cat >/dev/null
# Build manuals.
if [[ -f "${HERE}/doc/${filename}.md" && "$OPT_MANUALS" = "true" ]]; then
step_read "${HERE}/doc/${filename}.md" "manual" |
next step_manpage_generate |
next step_write "${MAN}/${filename}.1" |
next step_manpage_install "${OPT_PREFIX}/share/man/man1/${filename}.1.gz" |
cat >/dev/null
fi
done
# -----------------------------------------------------------------------------
# Verify files by running the tests.
if "$OPT_VERIFY"; then
printc_msg "\n%{YELLOW}Verifying scripts...%{CLEAR}\n"
# Run the tests.
FAIL=0
SKIP=0
while read -r action data1 data2 splat; do
[[ "$action" == "result" ]] || continue
case "$data2" in
fail)
printc_err "\x1B[G\x1B[K%s failed.\n" "$data1"
((FAIL++)) || true
;;
skip)
printc_msg "\x1B[G\x1B[K%s skipped.\n" "$data1"
((SKIP++)) || true
;;
*)
printc_msg "\x1B[G\x1B[K%s" "$data1"
;;
esac
done < <("${HERE}/test.sh" --compiled --porcelain --jobs=8)
# Print the overall result.
printc_msg "\x1B[G\x1B[K"
if [[ "$FAIL" -ne 0 ]]; then
printc_err "%{RED}%s\n" "One or more tests failed."
printc_msg "\x1B[A\x1B[G\x1B[K%{RED}%s\n" "One or more tests failed."
printc_err "%{RED}%s%{CLEAR}\n" "Run './test.sh --failed' for more detailed information."
exit 1
fi
if [[ "$SKIP" -gt 0 ]]; then
printc_err "%{CYAN}One or more tests were skipped.\n"
printc_err "Run ./test.sh for more detailed information.%{CLEAR}\n"
fi
printc_msg "%{YELLOW}Verified successfully.%{CLEAR}\n"
fi