linac/linac

504 lines
14 KiB
Bash
Executable File

#!/bin/bash
# shellcheck disable=SC1090,SC2004
projectName='LINAC'
projectDescription='LINAC is not a compiler'
projectVersion=0.10.1
projectAuthor='Joe <joe@thisisjoes.site>'
projectLicense='GPLv3'
Configure() {
declare -Ag config
config=(
[config_path]="config"
[config_file]="linac.conf"
[verbosity]="error"
[log_level]="info"
[log_path]="/tmp"
[log_file]="linac.log"
[src_path]="src/"
[build_path]="build/"
[module_path]="modules"
)
}
ReadConfiguration() {
if [ -f "${config[config_path]}"/"${config[config_file]}" ]; then
while read -r line
do
if echo "$line" | grep -F = &>/dev/null
then
varname=$(echo "$line" | cut -d '=' -f 1)
config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
fi
done < "${config[config_path]}"/"${config[config_file]}"
fi
}
CreateFiles() {
if [ ! -d "${config[config_path]}" ]; then
mkdir "${config[config_path]}"
mkdir "${config[src_path]}"
mkdir "${config[module_path]}"
mkdir "${config[build_path]}"
touch "${config[config_path]}"/"${config[config_file]}"
fi
}
InitCheck() {
local count
count="$(find . -maxdepth 1 -type f -regextype egrep -regex '.*\.(info|build)' 2>/dev/null | wc -l)"
if [ "$count" != 0 ]; then
for file in *.info; do
. "$file"
done
else
echo 'Project info not found. Have you initialized a project?' && exit 0;
fi
}
Clean() {
local clean_string
local dirty_string="$1"
local dirty_chars=('!' '@' '#' '$' '%' '^' '&' '\*' '\?' '~' "\'" '\"' "\\" '\/' '(' ')' '[' ']' '{' '}' '<' '>')
for i in "${dirty_chars[@]}"; do
dirty_string="${dirty_string//$i/''}"
done
shopt -s extglob
dirty_string="${dirty_string//$'\n'/' '}"
dirty_string="${dirty_string//[[:space:]]/'_'}"
dirty_string="${dirty_string//+(_)/'_'}"
dirty_string="${dirty_string/#_/''}"
clean_string="${dirty_string/%_/''}"
echo "$clean_string"
}
Log() {
LogTerm "$1" "$2"
LogFile "$1" "$2"
}
LogTerm() {
local term_log_type="${1^^}"
local term_log_message="$2"
local verbosity="${config[verbosity]^^}"
DoLogTerm() {
echo "$term_log_type - $term_log_message"
}
case $term_log_type in
DEBUG ) if [[ "$verbosity" = @(DEBUG) ]]; then DoLogTerm; fi;;
INFO ) if [[ "$verbosity" = @(DEBUG|INFO) ]]; then DoLogTerm; fi;;
WARN ) if [[ "$verbosity" = @(DEBUG|INFO|WARN) ]]; then DoLogTerm; fi;;
ERROR ) if [[ "$verbosity" = @(DEBUG|INFO|WARN|ERROR) ]]; then DoLogTerm; fi;;
CONFIG ) if [[ "$verbosity" = @(DEBUG|INFO) ]]; then DoLogTerm; fi;;
* ) if [[ "$verbosity" = @(DEBUG) ]]; then DoLogTerm; Log debug "Invalid term log type '$term_log_type' in context '${FUNCNAME[2]}'"; fi;;
esac
}
LogFile() {
local file_log_type="${1^^}"
local file_log_message="$2"
local log_level="${config[log_level]^^}"
DoLogFile() {
echo "[$(date '+%d/%b/%Y:%H:%M:%S.%3N')] ${file_log_type} - ${FUNCNAME[3]}: $file_log_message" >> "${config[log_path]}/${config[log_file]}"
}
case $file_log_type in
DEBUG ) if [[ "$log_level" = @(DEBUG) ]]; then DoLogFile; fi;;
INFO ) if [[ "$log_level" = @(DEBUG|INFO) ]]; then DoLogFile; fi;;
WARN ) if [[ "$log_level" = @(DEBUG|INFO|WARN) ]]; then DoLogFile; fi;;
ERROR ) if [[ "$log_level" = @(DEBUG|INFO|WARN|ERROR) ]]; then DoLogFile; fi;;
CONFIG ) if [[ "$log_level" != @(NONE) ]]; then DoLogFile; fi;;
* ) if [[ "$log_level" = @(DEBUG) ]]; then DoLogFile; Log debug "Invalid file log type '$file_log_type' in context '${FUNCNAME[2]}'"; fi;;
esac
}
BuildModule() {
Log debug "Got args '$*'"
local module="$1"
local version="$2"
local module_path="${config[module_path]}"
local path="$module_path/${module}/${version}"
cd "$path" || {
Log error "Failed to enter module directory '$path' for module '$module'"
return 1
}
linac get && linac build -m "${module}.build"
}
RetrieveModule() {
local module="$1"
local version="$2"
local url="https://$3"
Log info "Retreiving module '$module' version '$version' from '$url'"
if [[ "$version" =~ ^[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}.*$ ]]; then
local clone_version="v${version}"
else
local clone_version="$version"
fi
git clone -b "$clone_version" --depth 1 "$url" "${config[module_path]}/$module/$version" -c advice.detachedHead=false --quiet >/dev/null || {
Log error "Failed to retrieve module '$module' version '$version'. Aborting."
exit 1
}
}
CheckModuleCache() {
local module="$1"
local version="$2"
local url="$3"
local module_path="${config[module_path]}"
if [[ ! -d "$module_path" ]]; then
mkdir "$module_path"
fi
if [[ -d "${module_path}/${module}/${version}" ]]; then
Log info "Module '$module', version '$version' found in local cache."
else
Log info "Module '$module', version '$version' not in local cache."
return 1
fi
}
GetModuleDependencies() {
local module_file="${projectName,,}.module"
local -A module_dependencies
if [[ -f "$module_file" ]]; then
. "$module_file" ||
{
Log error "Failed to load LINAC module file '$module_file'"
return 1
}
Log info "Loaded LINAC module file '$module_file'"
else
Log info "'${projectName}' has no module file, skipping"
return 0
fi
local module
for module in "${!module_dependencies[@]}"; do
Log debug "$module"
Log debug "${module_dependencies[$module]}"
declare -A "$module"'='"${module_dependencies[$module]}"
done
for module in "${!module_dependencies[@]}"; do
local -n ref
ref=${module}
local version
version="${ref[v]}"
local url
url="${ref[u]}"
CheckModuleCache "$module" "$version" || {
RetrieveModule "$module" "$version" "$url"
}
BuildModule "$module" "$version"
unset -n ref
done
}
GetModulePaths() {
local -n __GetModulePaths_paths="$1"
local module_file="${projectName,,}.module"
local -A module_dependencies
if [[ -f "$module_file" ]]; then
. "$module_file" ||
{
Log error "Failed to load LINAC module file '$module_file'"
return 1
}
else
Log info "'${projectName}' has no module file, skipping"
return 0
fi
local module
for module in "${!module_dependencies[@]}"; do
Log debug "$module"
Log debug "${module_dependencies[$module]}"
declare -Ag "$module"'='"${module_dependencies[$module]}"
done
for module in "${!module_dependencies[@]}"; do
local -n ref
ref=${module}
local version
version="${ref[v]}"
__GetModulePaths_paths+=("${config[module_path]}/${module}/${version}/build/")
unset -n ref
done
}
ProcessBuildFile() {
local files
mapfile -t files < <(grep -Ev '^\s*#' "$1";)
for ((i=0;i<"${#files[*]}";i++));do
echo "${config[src_path]}${files[i]}"
done
}
BuildProject() {
local files
local name="${projectName,,}"
Log info "Building project '$projectName' from sources in '${config[src_path]}'"
if [ ! -d "${config[build_path]}" ]; then
Log warn "Configured build path '${config[build_path]}' not found, creating it..."
if mkdir -p "${config[build_path]}"; then
Log info "Created missing build path '${config[build_path]}'"
else
Log error "Failed to create missing build path '${config[build_path]}'"
return 1
fi
fi
num_parameters="$#"
Log debug "Got '$num_parameters' parameters"
case "$num_parameters" in
0 ) Log error "No build file given. Use 'linac help' for usage info."
return 1
;;
1 ) local target="$1"
;;
2 ) local option="$1"
local target="${*:$#}"
;;
* ) Log error "Too many parameters passed!"
return 1
;;
esac
Log debug "Using target '$target'"
local -a paths
local -a module_paths
GetModulePaths module_paths
local -a local_paths
mapfile -t local_paths < <(ProcessBuildFile "$target")
paths+=("${module_paths[@]}")
paths+=("${local_paths[@]}")
Log debug "Paths are '${paths[*]}'"
local files
mapfile -t files < <(for i in "${paths[@]}"; do find "$i" -type f; done)
local file_count
file_count="$(printf "%02d" "${#files[*]}")"
Log debug "File count is '$file_count'"
option="${option#?}"
Log debug "Option is '$option'"
case "$option" in
k ) Log info "Got option '$option', keeping comments."
local strip_comments=false
;;
m ) Log info "Got option '$option', building as submodule without stub or entrypoint."
local is_submodule=true
local strip_comments=true
;;
'' ) Log debug "No option passed, stripping comments."
local strip_comments=true
;;
* ) Log error "Invalid option '$option' passed."
return 1
;;
esac
if [[ ! "$is_submodule" == true ]]; then
Log info "Creating stub..."
MakeStub
else
local name="${projectName,,}"
[[ -f "${config[build_path]}$name" ]] && rm "${config[build_path]}$name"
fi
if [[ "$strip_comments" == true ]]; then
local args='-Ehrv'
local pattern='^\s*#'
else
local args='-Ehr'
local pattern='.*'
fi
Log debug "Args are '$args'"
Log debug "Pattern is '$pattern'"
local file_counter
for ((i=0;i<"${#files[*]}";i++));do
file_counter="$(printf "%02d" "$(( $i + 1 ))")"
Log info "Building script from source file ($file_counter/$file_count) '${files[$i]}'"
grep "$args" "$pattern" -- "${files[$i]}" >> "${config[build_path]}""$name" ||
{
Log error "Encountered error while building script from source file ($file_counter/$file_count) '${files[$i]}'"
return 1
}
done
}
InitProject() {
available_licenses=('' 'GPLv3' 'AGPLv3')
GetInitInfo() {
while [[ -z "$init_name" ]]; do
read -r -p "$(echo -e '\nWhat is the project'\''s name? ')" init_name
done
read -r -p "$(echo -e '\nPlease enter a description for the project. ')" init_description
read -r -p "$(echo -e '\nWho is the project'\''s author? ')" init_author
echo -e '\nPlease select a license for the project:\n'
for ((i=1;i<"${#available_licenses[*]}";i++)); do
echo -e " [$i] ${available_licenses[$i]}"
done
read -r -p "" init_license_num
init_license=${available_licenses[$init_license_num]}
CreateFiles
WriteInitInfo
}
WriteInitInfo() {
local project_name
project_name="$(Clean "$init_name")"
local escaped_description
escaped_description="${init_description//\'/\'\\\'\'}"
echo -e "projectName='$project_name'" > "${project_name,,}.info"
{
echo -e "projectDescription='$escaped_description'"
echo -e "projectVersion=0.1.0"
echo -e "projectAuthor='$init_author'"
echo -e "projectLicense=$init_license"
} >> "${project_name,,}.info"
echo -e "# Add relative paths to source files to this file. One per line." > "${project_name,,}.build"
}
local count
count="$(find . -maxdepth 1 -type f -regextype egrep -regex '.*\.(info|build)' 2>/dev/null | wc -l)"
if [ "$count" != 0 ]; then
read -r -p "$(echo -e 'It looks like there is already another LINAC project in this directory! Are you sure you want to initialize a new project? (Y/n) ')" init_confirm
case "$init_confirm" in
[yY]|[Yy][Ee][Ss] ) GetInitInfo;;
[Nn]|[Nn][Oo] ) exit 0;;
* ) exit 0;;
esac
else
GetInitInfo
fi
}
BumpVersion() {
local expression='(.*=).?([0-9]{1,})\.([0-9]{1,})\.([0-9]{1,}).?'
local part="$1"
local file="${projectName,,}.info"
case "$part" in
major ) sed -E -i "s/$expression/echo \1\$((\2+1)).0.0/ge" "$file";;
minor ) sed -E -i "s/$expression/echo \1\2.\$((\3+1)).0/ge" "$file";;
patch ) sed -E -i "s/$expression/echo \1\2.\3.\$((\4+1))/ge" "$file";;
esac
}
MakeStub() {
local name="${projectName,,}"
echo '#!/bin/bash' > "${config[build_path]}$name"
if [ -n "${config[shellcheck_ignore]}" ]; then
echo "# shellcheck disable=${config[shellcheck_ignore]}" >> "${config[build_path]}$name"
fi
echo "$(<"$name"'.info')" >> "${config[build_path]}$name"
}
PrintInfo() {
echo -e "${projectName} ${projectVersion}"' - '"${projectDescription}"
echo -e 'Licensed '"${projectLicense}"' by '"${projectAuthor}\n"
}
SubInit() {
InitProject
}
SubBump() {
InitCheck
BumpVersion "$@"
}
SubBuild() {
InitCheck
BuildProject "$@"
}
SubGet() {
InitCheck
GetModuleDependencies "$@"
}
SubHelp() {
case "$@" in
"build" )
echo " Builds a project from sources listed in the given build file. The file must contain a list of"
echo " paths to source files relative to your source directory. (Source directory may be configured"
echo " by defining 'src_path' in 'linac.conf')"
;;
"bump" )
echo " Bumps the project version in the project info file. Takes a version number name as an option:"
echo " One of either 'major','minor', or 'patch'. (Version must follow SemVer format to be bumped, e.g. '2.3.1'.)"
;;
"init" )
echo " Initializes a new project in the current directory. Starts a wizard which asks for basic information"
echo " about your project, then creates the necessary files and directories."
;;
"help" )
echo " Shows a help message with basic usage information. Takes another command as an option and provides"
echo " additional information for that command. Did you really need help with 'help'?"
;;
* )
echo "Error: '$*' is not a known command."
echo " Run 'linac help' for a list of known commands."
exit 1
;;
esac
}
SubCommand() {
subcommand="$1"
case "$subcommand" in
"" | "-h" | "--help" | "help" )
shift
local arg_count
arg_count=$(echo -n "$@" | wc -w)
if [ "$arg_count" != 0 ]; then
SubHelp "$@"
else
echo 'Usage: linac <command> [options]'
echo " Commands:"
echo " help [command] Shows this help. Takes another command as an option."
echo " build [build file] Builds project using the given build file."
echo " bump [version] Bumps the project version. Accepts 'major','minor','patch'."
echo " init Initializes a new project in the current directory."
fi
;;
* )
Log debug "Args are $*"
shift
Log debug "Shifted args"
Log debug "Args are $*"
if ! command -v Sub"${subcommand^}" > /dev/null; then
echo "Error: '$subcommand' is not a known command."
echo " Run 'linac help' for a list of known commands."
exit 1
else
Sub"${subcommand^}" "$@"
fi
;;
esac
}
Main() {
PrintInfo
Configure
ReadConfiguration
SubCommand "$@"
}
set -o pipefail
Main "$@"