504 lines
14 KiB
Bash
Executable File
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 "$@"
|