#!/bin/bash
#
# Scans a given codebase for architectural errors, and updates the list of known architectural errors if no new errors are found.
#
# This script generates the following output files;
#
# * `component-errors.tsv`, which contains any new component errors not previously identified.
# * `.silverthread/known-component-errors.tsv`, which contains the list of known component errors.
# * `.silverthread/rules.txt (if not already existing), which contains a set of rules to flag known errors.
#
# @variable JOB_NAME The name to give to the codebase.
# @variable BUILD_NUMBER The numeric identifier of the build, may be substituted for the unique ID of the build if the
# build system does not support build numbering.
# @variable VAULT_PATH The path to the vault storing scan data.
# @variable SUBDIR The path to a subdirectory containing a subset of the code to scan. Useful for testing script changes
# on large codebases where scans may take several hours.
# @arg $1 The path to the UDA file defining codebase architecture.
# @arg $2 The path to the folder containing source code to scan.
PROJECT_NAME="${JOB_NAME}"
SYSTEM_VERSION="build-${BUILD_NUMBER}"
MAX_CORE_SIZE=255
UDA_PATH="$1"
SOURCE_PATH="$2"
# Path separator. Change to \\ on Windows.
SEP="/"
# Tell CodeMRI where to find the Vault.
export CMRI_VAULT="${VAULT_PATH}"
# @description Runs a script using the CodeMRI CLI.
#
# @stdin The script to run via CodeMRI CLI.
function run_codemri() {
cat > temp.sc
yes | cmri batch run temp.sc
}
# @description Attempts to cretae a new project within the data vault.
#
# @arg $1 The name of the project to create.
function create_project() {
PROJECT_NAME=$1
run_codemri <<EOF
project add -n "${PROJECT_NAME}"
EOF
}
# @description Attempts to import code into the given system under the given project. For simplicity, the
# system name will be <project-name>-<system-version>.
#
# @arg $1 The name of the project.
# @arg $2 The version of the system.
function import_code() {
PROJECT_NAME=$1
SYSTEM_VERSION=$2
run_codemri <<EOF
system add -s "${PROJECT_NAME}" -n "${PROJECT_NAME}" -v "${SYSTEM_VERSION}" -o "${SOURCE_PATH}${SUBDIR}"
select "${PROJECT_NAME}/${PROJECT_NAME}-${SYSTEM_VERSION}"
system config set uda_file "${UDA_PATH}"
system config set scan_in_place True
# Only set the languages if we have set a value for them.
$([[ ! -z "$LANGUAGES" ]] && echo system config set languages ${LANGUAGES})
EOF
}
# @description Attempts to generate a file containing known issues to flag as warnings. This should be run when running the build for
# the first time.
#
# This generates two files:
#
# * `.silverthread/rules.txt`, which contains the rules to flag the known errors.
# * `.silverthread/known-component-errors.tsv`, which contains a list of known issues.
function generate_baseline() {
# Generate our baseline errors file if none exists.
if [[ "$BASELINE" = "1" ]]; then
echo "No baseline errors. Running baseline errors."
run_codemri <<EOF
select "${PROJECT_NAME}"/"${PROJECT_NAME}-${SYSTEM_VERSION}"
job run generate_query_data
system query component-relationships --severity error --out "${SOURCE_PATH}${SEP}.silverthread${SEP}known-component-errors.tsv" --output-format tsv
EOF
else
echo "Baseline errors already exist, skipping generation."
fi
}
# @description Scans the codebase, and generates files that contain any architectural violations. These files are named:
#
# * `component-errors.tsv`, which contains any architectural violations at the component level.
#
# If there are no architectural errors, then the list of known errors will be regenerated and placed into .silverthread/known-component-errors.tsv.
function run_analysis() {
run_codemri <<EOF
select "${PROJECT_NAME}"/"${PROJECT_NAME}-${SYSTEM_VERSION}"
job run generate_query_data
system query component-relationships --severity error --out "component-errors.tsv" --output-format tsv
system query entity-relationships --from-component @from_component --to-component @to_component --out "component-error-entities.tsv" --output-format tsv
EOF
COUNT_VIOLATIONS=$(expr `wc -l component-errors.tsv | awk '{print $1;}'` - 1)
if [[ ${COUNT_VIOLATIONS} -gt 0 ]]; then
echo "${COUNT_VIOLATIONS} architectural violation(s) detected. Failing build."
echo "Architectural Violations Follow:"
column -t < component-errors.tsv
echo "Entity Relationships in Errors:"
column -t < component-error-entities.tsv
exit 1
fi
echo 'No new architectural violations detected.'
echo 'Checking core size.'
run_codemri <<EOF
select "${PROJECT_NAME}"/"${PROJECT_NAME}-${SYSTEM_VERSION}"
system query files --out "files.tsv" --output-format tsv
EOF
FIELD_CYCLIC_GROUP_SIZE=$(cat files.tsv | head -n 1 | tr "\t" "\n" | cat -n | grep cyclic_group_size | cut -f1)
LARGEST_CORE_SIZE=$(cut -f$FIELD_CYCLIC_GROUP_SIZE files.tsv | sort -nr | uniq | grep -v 'cyclic_group_size' | head -n 1)
if [[ $LARGEST_CORE_SIZE -gt $MAX_CORE_SIZE ]]; then
echo "The core size of $LARGEST_CORE_SIZE exceeds the maximum allowed core size of ${MAX_CORE_SIZE}."
else
echo "The core size of $LARGEST_CORE_SIZE is within an acceptable range (below ${MAX_CORE_SIZE})."
fi
echo "Updating list of known errors..."
run_codemri <<EOF
select "${PROJECT_NAME}"/"${PROJECT_NAME}-${SYSTEM_VERSION}"
system query component-relationships --severity warning --out "${SOURCE_PATH}${SEP}.silverthread${SEP}known-component-errors.tsv" --output-format tsv
EOF
}
# And check to see if our vault exists, if not create it. We'll verify the vault exists by checking for the vault folder.
if [[ ! -e "${VAULT_PATH}" ]]; then
echo "No vault found. Please create a vault at \"${VAULT_PATH}\" by running cmri vault create --vault \"${VAULT_PATH}\"."
echo "After creating the vault, log in by running cmri account login --vault \"${VAULT_PATH}\""
exit 1
fi
# Check to see if our project0 already exists, if not, create it.
if [[ ! -e "${VAULT_PATH}${SEP}projects${SEP}${PROJECT_NAME}" ]]; then
create_project "${PROJECT_NAME}"
fi
import_code "${PROJECT_NAME}" "${SYSTEM_VERSION}"
# If we don't have a set of rules yet (either nothing checked in, or nothing previously generated), we'll need to generate a new set.
generate_baseline
run_analysis
exit $?