#!/bin/sh
# shellcheck shell=ash
# alint APKBUILD - scan APKBUILD template for common mistakes
#
# Adapted from xlint from Void Linux's xtools to Alpine Linux
# https://github.com/leahneukirchen/xtools/
#
# Required packages (names are Alpine Linux pkgs):
# busybox - for grep, sed, tr, sort and other simple utiltiies

export LC_ALL=C

scan() {
	# shellcheck disable=2039
	# 2039: local operator is not posix but we can use it in ash/dash
	local rx="$1" msg="$2" tag="$3" severity="$4"
	grep -E -Hn -e "$rx" "$apkbuild" |
		sed "s~^\([^:]*:[^:]*:\)\(.*\)~$severity:[$tag]:\1$msg~"
}

variables=$(echo -n "#.*
_.*
startdir
srcdir
pkgdir
subpkgdir
builddir
arch
depends
depends_dev
depends_doc
depends_openrc
depends_libs
depends_static
checkdepends
giturl
install
.*.pre-install
.*.post-install
.*.pre-upgrade
.*.post-upgrade
.*.pre-deinstall
.*.post-deinstall
install_if
license
makedepends
makedepends_build
makedepends_host
md5sums
sha256sums
sha512sums
options
pkgdesc
pkggroups
pkgname
pkgrel
pkgusers
pkgver
provides
provider_priority
replaces
replaces_priority
source
subpackages
triggers
ldpath
linguas
sonameprefix
somask
url
langdir
patch_args
pcprefix
HOSTCC" | tr '\n' '|')

valid_options="
!archcheck
!check
checkroot
net
!strip
suid
!tracedeps
chmod-clean
!dbg
toolchain
!fhs
libtool
charset.alias
textrels
!spdx
ldpath-recursive
sover-namecheck
lib64
$(echo "$CUSTOM_VALID_OPTIONS" | tr ' ' '\n')
"

valid_arches="
x86_64
!x86_64
x86
!x86
armel
!armel
armhf
!armhf
armv7
!armv7
s390x
!s390x
ppc
!ppc
ppc64
!ppc64
ppc64le
!ppc64le
all
noarch
mips
!mips
mipsel
!mipsel
mips64
!mips64
mips64el
!mips64el
aarch64
!aarch64
$(echo "$CUSTOM_VALID_ARCHES" | tr ' ' '\n')
"

# This is a list of packages that need builddir= set because scripts/bootstrap.sh
# might want to build -bootstrap versionss
default_builddir_exception="
fortify-headers
linux-headers
musl
libc-dev
pkgconf
zlib
openssl
ca-certificates
libbsd
libtls-standalone
busybox
busybox-initscripts
binutils
make
apk-tools
file
gmp
mpfr4
mpc1
isl
cloog
gcc
openrc
alpine-conf
alpine-baselayout
alpine-keys
alpine-base
build-base
attr
libcap
patch
sudo
acl
fakeroot
tar
pax-utils
lzip
abuild
ncurses
libedit
openssh
libcap-ng
util-linux
libaio
lvm2
popt
xz
json-c
argon2
cryptsetup
kmod
lddtree
mkinitfs
community/go
libffi
community/ghc
linux-lts
linux-firmware
$(echo "$CUSTOM_BOOTSTRAP_PACKAGES" | tr ' ' '\n')
"

# This takes the format of $pkgname@$regex...
# you can have unlimited regexes and each one is checked
# they are checked with 'grep -E'
bad_versions="
wireshark@[0-9]\.[13579]*\..*[0-9]
nginx@[0-9]+.*[13579].+[0-9]
mesa@[0-9]+.*[0-9].+0
$(echo "$CUSTOM_BAD_VERSIONS" | tr ' ' '\n')
"

default_builddir_value() {
	[ "$SKIP_DEFAULT_BUILDDIR_VALUE" ] && return 0
	[ "$SKIP_AL1" ] && return 0

	# If the package is in our exceptions don't tell the user
	if printf "%s" "$default_builddir_exception" | grep -q -x -- "$pkgname"; then
		return 0
	fi

	if [ "$builddir" = "/$pkgname-$pkgver" ] || [ "$_builddir" = "/$pkgname-$pkgver" ]; then
		scan '^(_)?builddir=' "builddir can be removed as it is the default value" 'AL1' 'MC'
	fi
}

unnecessary_return_1() {
	[ "$SKIP_UNNECESSARY_RETURN_1" ] && return 0
	[ "$SKIP_AL2" ] && return 0
	scan '\|\| return 1' "|| return 1 is not required as set -e is used" 'AL2' 'MC'
}

pkgname_quoted() {
	[ "$SKIP_PKGNAME_QUOTED" ] && return 0
	[ "$SKIP_AL3" ] && return 0
	scan "^pkgname=[\"'][^$]+[\"']" "pkgname must not be quoted" 'AL3' 'TP'
}

pkgver_quoted() {
	[ "$SKIP_PKGVER_QUOTED" ] && return 0
	[ "$SKIP_AL4" ] && return 0
	scan "^pkgver=[\"'][^$]+[\"']" "pkgver must not be quoted" 'AL4' 'TP'
}

empty_variable() {
	[ "$SKIP_EMPTY_VARIABLE" ] && return 0
	[ "$SKIP_AL5" ] && return 0
	scan '^[A-Za-z0-9_]*=(""|''|)$' "variable set to empty string: \2" 'AL5' 'MC'
}

custom_variable() {
	[ "$SKIP_CUSTOM_VARIABLE" ] && return 0
	[ "$SKIP_AL6" ] && return 0
	grep -E -oHn -- '^[\sA-Za-z0-9_]*=' "$apkbuild" | \
		sed "s|:[ 	]*|:|g" | \
		grep -E -v -- ':('"$variables"')=' | \
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/IC:[AL6]:\1prefix custom variable with _: \2/"
}

indent_tabs() {
	[ "$SKIP_INDENT_TABS" ] && return 0
	[ "$SKIP_AL7" ] && return 0
	scan '^  ' "indent with tabs" 'AL7' 'IC'
}

trailing_whitespace() {
	[ "$SKIP_TRAILING_WHITESPACE" ] && return 0
	[ "$SKIP_AL8" ] && return 0
	scan '[	 ]$' "trailing whitespace" 'AL8' 'IC'
}

backticks_usage() {
	[ "$SKIP_BACKTICKS_USAGE" ] && return 0
	[ "$SKIP_AL25" ] && return 0
	scan '[^\\]`' "use \$() instead of backticks" 'AL25' 'SP'
}

function_keyword() {
	[ "$SKIP_FUNCTION_KEYWORD" ] && return 0
	[ "$SKIP_AL9" ] && return 0
	scan '^	*function\b' 'do not use the function keyword' 'AL9' 'SC'
}

space_before_function_parenthesis() {
	[ "$SKIP_SPACE_BEFORE_FUNCTION_PARENTHESIS" ] && return 0
	[ "$SKIP_AL10" ] && return 0
	scan '^	*[^ ]*  *\(\)' 'do not use space before function parenthesis' 'AL10' 'TC'
}

space_after_function_parenthesis() {
	[ "$SKIP_SPACE_AFTER_FUNCTION_PARENTHESIS" ] && return 0
	[ "$SKIP_AL11" ] && return 0
	scan '^	*[^ ]*\(\)(|   *)\{' 'use one space after function parenthesis' 'AL11' 'TC'
}

newline_opening_brace() {
	[ "$SKIP_NEWLINE_OPENING_BRACE" ] && return 0
	[ "$SKIP_AL12" ] && return 0
	scan '^	*[^ ]*\(\)$' 'do not use a newline before function opening brace' 'AL12' 'TC'
}

superfluous_cd_builddir() {
	[ "$SKIP_SUPERFLUOUS_CD_BUILDDIR" ] && return 0
	[ "$SKIP_AL13" ] && return 0
	# shellcheck disable=2039
	# 2039: local operator is not posix but we can use it in ash/dash
	local cds='' cdscount='' prevcd='' phase="$1"

	# All ocurrences of the 'cd' command being used
	# 1. Print file with line numbers.
	# 2. Print the function from the opening declaration up to the closing bracked
	# 3. grep for all ocurrences of the 'cd' command (ignore obviously invalid ones
	#	like matching 'cd' until the end of the line)
	cds="$(cat -n "$apkbuild" \
		   | sed -n "/^\s\+[0-9].*	$phase() {/,/[0-9].*	}/p" \
		   | grep '\bcd ')"

	# Number of ocurrences of the 'cd' command being used
	# Used to tell if we are in a phase() with a single cd statement
	# in that case we can be free to warn the user that their cd statement
	# is superfluous if it is to "$builddir", this avoids problems of previous
	# 'cd' statements to other places giving false positives
	cdscount="$(printf "%s\\n" "$cds" | wc -l)"

	# if the previous line had a 'cd "$builddir"' statement
	prevcd=0

	# If it is the first cd of the program
	firstcd=1

	# Use newline as our IFS delimiter, so we can iterate over lines with
	# the for construct, since the while loop will create a subshell that
	# prevents the value of the prevcd variable from being propagated
	# to future runs
	OLDIFS="$IFS"
	IFS="
"
	local line; for line in $(printf "%s\\n" "$cds"); do
		linenum="$(printf "%s\\n" "$line" | awk '{ print $1 }')"
		statement="$(printf "%s\\n" "$line" | awk '{ $1="" ; print $0 }')"
		[ -z "$statement" ] && continue
		if echo "$statement" | grep -E -q 'cd ["]?\$[{]?[_]?builddir["}]?+($| )' ; then
			if [ "$prevcd" -eq 1 ] || [ "$cdscount" -eq 1 ] || [ "$firstcd" -eq 1 ]; then
				printf "MP:[AL13]:%s:%s:cd \"\$builddir\" can be removed in phase '%s'\\n" \
					"$apkbuild" \
					"$linenum" \
					"$phase"
			fi
			prevcd=1
		else
			prevcd=0
		fi
		# Can be set to 0 in the first loop and the re-set it to 0 in any next loops
		firstcd=0
	done
	IFS="$OLDIFS"
}

pkgname_has_uppercase() {
	[ "$SKIP_PKGNAME_HAS_UPPERCASE" ] && return 0
	[ "$SKIP_AL14" ] && return 0
	scan '^pkgname=[a-z0-9\._\-]*[A-Z]' 'pkgname must not have uppercase characters' 'AL14' 'SC'
}

pkgver_has_pkgrel() {
	[ "$SKIP_PKGVER_HAS_PKGREL" ] && return 0
	[ "$SKIP_AL15" ] && return 0
	scan '^pkgver=.*(-r|_r[^c])' 'pkgver must not have -r or _r' 'AL15' 'SC'
}

_builddir_is_set() {
	[ "$SKIP__BUILDDIR_IS_SET" ] && return 0
	[ "$SKIP_AL26" ] && return 0
	if [ -z "$builddir" ] && [ -n "$_builddir" ]; then
		scan '^_builddir=' 'rename _builddir to builddir' 'AL26' 'SP'
	fi
}

literal_integer_is_quoted() {
	[ "$SKIP_LITERAL_INTEGER_IS_QUOTED" ] && return 0
	[ "$SKIP_AL28" ] && return 0
	scan '^[A-Za-z0-9_]*=('\''|")[0-9]+("|'\'')' 'literal integers must not be quoted' 'AL28' 'MC'
}

pkgname_used_in_source() {
    [ "$SKIP_PKGNAME_USED_IN_SOURCE" ] && return 0
    [ "$SKIP_AL29" ] && return 0
	local i; for i in $distilled_source; do
		if printf "%s\\n" "$i" | grep -E -q '(.*?::)?[a-z]+://[^"]+\$\{?pkgname\}?'; then
			i="$(printf "%s\\n" "$i" | sed -e 's|\$|\\$|g' -e 's|{|\\\{|g' -e 's|}|\\\}|g')"
			scan "$i" '$pkgname should not be used in the source url' 'AL29' 'MC'
		fi
	done
}

double_underscore_in_variable() {
	[ "$SKIP_DOUBLE_UNDERSCORE_IN_VARIABLE" ] && return 0
	[ "$SKIP_AL30" ] && return 0
	# Run this twice, once which will detect variables without the local keyword
	# which requires matching the = sign at the end. The second time will match
	# for variables declared with the local keyword which do not require the =
	# sign
	scan '(^|	)__[A-Za-z0-9_].*=' 'double underscore on variables are reserved' 'AL30' 'MC'
	scan '(^|	)local ([A-Za-z0-9_])?.*__[A-Za-z0-9_].*(=)?' 'double underscore on variables are reserved' 'AL30' 'MC'
}

variable_capitalized() {
	[ "$SKIP_VARIABLE_CAPITALIZED" ] && return 0
	[ "$SKIP_AL31" ] && return 0
	scan '^[a-z0-9_]*[A-Z].*=' 'variables must not have capital letters' 'AL31' 'MC'
    scan '(^|   )local [a-z0-9_ ]*[A-Z].*(=)?' 'variables must not have capital letters' 'AL31' 'MC'
}

braced_variable() {
	[ "$SKIP_BRACED_VARIABLE" ] && return 0
	[ "$SKIP_AL32" ] && return 0
	# Match a Sigil ($) then a brace and any valid value until the end brace and then
	# match end-of-line or a character that can't be in the name of a variable
	grep -Eo -Hn -e '\$\{[A-Za-z0-9_]+\}($|["\./\(\)= -])' "$apkbuild" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/MP:[AL32]:\1unnecessary usage of braces: \2/" |
		sed 's|[^}]$||g' # This strips the last match in the grep
}

cpan_variable() {
	[ "$SKIP_CPAN_VARIABLE" ] && return 0
	[ "$SKIP_AL35" ] && return 0
	scan '^cpandepends=' 'merge the contents of cpandepends into depends and remove it' 'AL35' 'MC'
	scan '^cpanmakedepends=' 'merge the contents of cpanmakedepends into makedepends and remove it' 'AL35' 'MC'
	scan '^cpancheckdepends=' 'merge the contents of cpancheckdepends into checkdepends and remove it' 'AL35' 'MC'
}

overwrite_xflags() {
	[ "$SKIP_OVERWRITE_XFLAGS" ] && return 0
	[ "$SKIP_AL36" ] && return 0
	# shellcheck disable=SC2016 disable=SC2086
	# We need to match either the start of the line or a whitespace
	# otherwise we can end up matching variables wrongly, like a variable
	# can be called 'LUA_CFLAGS' and we end up matching it because we only check
	# 'CFLAGS'
	#
	# The sed call is to strip any leading whitespace that is created
	grep -E -oHn -- "(^| |	)$1=\".*\"" $apkbuild |
		sed "s|:[ 	]*$1|:$1|g" |
		grep -vF -- "\$$1" |
		sed "s/^\([^:]*:[^:]*:\)\(.*\)/SP:[AL36]:\1$1 should not be overwritten, add \$$1 to it/"
}

invalid_option() {
	[ "$SKIP_INVALID_OPTION" ] && return 0
	[ "$SKIP_AL49" ] && return 0
	local i; for i in $options; do
		if ! echo "$valid_options" | grep -q -x -- "$i"; then
			scan "options=.*$i" "invalid option '$i'" 'AL49' 'MC'
		fi
	done
}

missing_default_prepare() {
	[ "$SKIP_MISSING_DEFAULT_PREPARE" ] && return 0
	[ "$SKIP_AL54" ] && return 0

	# Check if we have prepare() defined
	# shellcheck disable=SC2086
	grep -q "^prepare() {" $apkbuild || return 0

	# shellcheck disable=SC2086
	# The '( | )' part of the sed call is not by mistake, the first component is a whitespace
	# the second component is a literal tab character, created by doing 'CTRL + V, <tab>'
	if ! sed -n '/^prepare() {/,/^}/p' $apkbuild | grep -q -E "^( |	)*default_prepare"; then
		scan "^prepare\(\) \{" "prepare() is missing call to 'default_prepare'" 'AL54' 'SC'
	fi
}

build_type_not_none() {
	[ "$SKIP_BUILD_TYPE_NOT_NONE" ] && return 0
	[ "$SKIP_AL55" ] && return 0

	# Check inside the build function if we have a call to cmake
	if sed -n '/^build() {/,/^}/p' "$apkbuild" | grep -q -F cmake; then
		# Check if we have -DCMAKE_BUILD_TYPE= set
		_call="$(sed -n '/^build() {/,/^}/p' "$apkbuild" \
				| grep -E -o -- '^.*-DCMAKE_BUILD_TYPE=[A-Za-z]*( |$)' \
				| sed 's| *$||g')"
		# Wrap this in a loop as we might get more than one occurrence of -DCMAKE_BUILD_TYPE=
		# this happens whenever we need to build more than once, some situations require different
		# build options and others require separate staticlibs and sharedlibs builds.
		#
		# Regardless of the reason we should support it fully
		local i; for i in $_call; do
			# Check if we are actually dealing with -DCMAKE_BUILD_TYPE
			echo "$i" | grep -F -q -- '-DCMAKE_BUILD_TYPE=' || continue
			if [ "$(echo "$i" | sed 's|.*-DCMAKE_BUILD_TYPE=||g' )" != None ]; then
				grep -Eo -Hn -e "$i" "$apkbuild" |
					sed "s/^\([^:]*:[^:]*:\)\(.*\)/SP:[AL55]:\1CMAKE_BUILD_TYPE must be None: \2/" |
						sed -r 's|(: )[	]*|\1|g' # Strip all space and tabs caught by the grep
			fi
		done
	fi
}

invalid_arch() {
	[ "$SKIP_INVALID_ARCH" ] && return 0
	[ "$SKIP_AL57" ] && return 0
	local i; for i in $arch; do
		if ! echo "$valid_arches" | grep -q -x -- "$i"; then
			scan "arch=.*$i" "invalid arch '$i'" 'AL57' 'SC'
		fi
	done
}

remote_patch_from_live_source() {
	[ "$SKIP_REMOTE_PATCH_FROM_LIVE_SOURCE" ] && return 0
	[ "$SKIP_AL60" ] && return 0
	local i; for i in $distilled_source; do
		if printf "%s\\n" "$i" | grep -q 'github.*/pull/[0-9]*.patch'; then
			i="$(printf "%s\\n" "$i" | sed -e 's|\$|\\$|g' -e 's|{|\\\{|g' -e 's|}|\\\}|g')"
			scan "$i" "live source '$i'" 'AL60' 'IC'
		fi
		if printf "%s\\n" "$i" | grep -q 'gitlab.*/merge_requests/[0-9]*.patch'; then
			i="$(printf "%s\\n" "$i" | sed -e 's|\$|\\$|g' -e 's|{|\\\{|g' -e 's|}|\\\}|g')"
			scan "$i" "live source '$i'" 'AL60' 'IC'
		fi
	done
}

bad_version() {
	[ "$SKIP_BAD_VERSION" ] && return 0
	[ "$SKIP_AL61" ] && return 0
	local line
	line="$(printf "%s\\n" "$bad_versions" | grep -o -- "^$pkgname@.*")"
	[ -z "$line" ] && return 0

	printf "%s\\n" "$line" | sed "s|^$pkgname@||" | tr '@' '\n' | while read -r regex; do
		if printf "%s\\n" "$pkgver" | grep -E -q -- "$regex"; then
			scan "^pkgver=" "Version '$pkgver' matches a bad version regex" 'AL61' 'SC'
		fi
	done
}

# Remove variable and it's surrounding double-quotes
# this allows for iterating over all strings inside
# a variable, first written for source= which can have
# multiple values and we need to check each of them but
# we can't source the variable because we need to check
# for bad usage of $pkgname and other variables.
_distill() {
	local variable="$1" apkbuild="$2"
	# Check if we are dealing with an unquoted variable
	if ! grep -q -o -- "^$variable=\"" "$apkbuild"; then
		grep -o -- "^$variable=.*" "$apkbuild" | sed -e "s|$variable=||"
	fi
	if grep -q -o -- "^$variable=\".*\"" "$apkbuild"; then
		grep -o -- "^$variable=\".*\"" "$apkbuild" \
			| sed -e "s|$variable=\"||" -e 's|"||g'
	else
		sed -n -e "/$variable=\"/,/\"/p" "$apkbuild" \
			| sed -e "s|$variable=\"||" -e 's|"||g' -e 's|^\s*||'
	fi
}

ret=0
for apkbuild; do
	if [ -f "$apkbuild" ]; then

    case "$apkbuild" in
        /*);;
        *) apkbuild=./"$apkbuild"
    esac

	# Source apkbuild, we need some nice values
	srcdir="" . "$apkbuild" || {
		echo "Failed to source APKBUILD in '$apkbuild'" ;
		exit 1;
	}
	# Distill the source by grabbing everything inside it, removing
	# source="" and remove all whitespace, it should end with a list:
	# source1
	# source2
	# source3d
	distilled_source="$(_distill "source" "$apkbuild")"

	default_builddir_value &
	_builddir_is_set &

	pkgname_quoted &
	pkgver_quoted &
	unnecessary_return_1 &
	empty_variable &
	custom_variable &
	indent_tabs &
	trailing_whitespace &
	backticks_usage &
	function_keyword &
	pkgname_has_uppercase &
	pkgver_has_pkgrel &
	space_before_function_parenthesis &
	space_after_function_parenthesis &
	newline_opening_brace &
	literal_integer_is_quoted &
	pkgname_used_in_source &
	double_underscore_in_variable &
	variable_capitalized &
	braced_variable &
	cpan_variable &
	overwrite_xflags "CFLAGS" &
	overwrite_xflags "GOFLAGS" &
	overwrite_xflags "CPPFLAGS" &
	overwrite_xflags "CXXFLAGS" &
	overwrite_xflags "FFLAGS" &
	overwrite_xflags "LDFLAGS" &
	overwrite_xflags "GOFLAGS" &
	missing_default_prepare &
	build_type_not_none &
	bad_version &
	[ "$arch" ] && invalid_arch &
	[ "$options" ] && invalid_option &
	[ "$source" ] && remote_patch_from_live_source &

	for phase in prepare build check package; do
		superfluous_cd_builddir "$phase" &
	done
	wait
	else
	echo no such apkbuild "$apkbuild" 1>&2
	fi | sort -t: -V | grep . && ret=1
done
exit $ret
