#!/bin/sh -ef
#
# Run svace static analysis on source package in hasher chroot.
#
# Copyright (C) 2024-2026  Egor Ignatov <egori@altlinux.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#

# Determine selfdir for helper scripts.
selfdir="$(dirname "$(realpath "$0")")"

. hsh-sh-functions
. hsh-sh-rebuild-functions

print_version()
{
	cat <<EOF
$PROG version 1.8
Written by Egor Ignatov <egori@altlinux.org>

Copyright (C) 2024-2026  Egor Ignatov <egori@altlinux.org>
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
EOF
	exit
}

stage()
{
	[ -z "$quiet" ] || return 0
	message "$*"
}

show_help()
{
	cat <<EOF
hsh-svace - run svace static analysis on source package in hasher chroot.

Usage: $PROG [options] [<path-to-workdir>] <source-package>
  or:  $PROG [options] --workdir=DIR <source-package>
  or:  $PROG [options] --analyze-only [<path-to-workdir>] <results-tar>

<path-to-workdir> must be valid writable directory.

Options:
  --analyze-only              skip build, analyze from results tar;
  --apt-config=FILE           path to custom apt.conf file;
  --bind-svace                use svace from host via bind mount of /opt;
  --build-only                skip svace analysis, just build;
  --excludedocs               do not install documentation files;
  --hasp-serveraddr=ADDR      hasp server address for svace license;
  --hasher-priv-dir=DIR       hasher-priv directory;
  --install-svace[=VER]       install svace from rpm-build-svace package
                              (default; VER overrides version);
  --mountpoints=LIST          comma-separated list of known mount points;
  --nodeps                    ignore package dependencies (dangerous);
  --number=NUMBER             subconfig identifier;
  --outdir=DIR                directory for results (default .);
  --rebuild                   use existing hasher;
  --svace-config=ARG          svace config command (repeatable);
  --svace-config-file=FILE    svace config file;
  --svace-warning=ARG         svace warning command (repeatable);
  --svace-warning-all         enable all svace warnings (svace warning all true);
  --svace-warning-file=FILE   svace warnings config file;
  --target=ARCH               target architecture;
  --wait-lock                 wait for workdir and hasher-priv locks;
  --no-wait-lock              do not wait for workdir and hasher-priv locks;
  --without-stuff             do not generate apt index files;
  --with-stuff                generate apt index files;
  --workdir=DIR               path to workdir to use;
  -S ADDR                     short form for --hasp-serveraddr;
  -q, --quiet                 try to be more quiet;
  -v, --verbose               print a message for each action;
  -V, --version               print program version and exit;
  -h, --help                  show this text and exit.

Report bugs to https://bugzilla.altlinux.org/

EOF
	exit
}

TEMP=`getopt -n $PROG -o S:,$getopt_common_opts \
	-l analyze-only,apt-config:,bind-svace,build-only,excludedocs,hasp-serveraddr:,hasher-priv-dir:,install-svace::,mountpoints:,no-lock,nodeps,number:,outdir:,rebuild,svace-config:,svace-config-file:,svace-warning:,svace-warning-all,svace-warning-file:,target:,wait-lock,no-wait-lock,without-stuff,with-stuff,workdir:,$getopt_common_longopts \
	-- "$@"` ||
	show_usage
prepare_cgroup "$TEMP" "$0" "$@"
eval set -- "$TEMP"

build_only=
analyze_only=
install_svace=
bind_svace=
svace_version=
svace_config_file=
svace_config_args=
svace_warning_all=
svace_warning_file=
svace_warning_args=
nodeps=
rebuild=
workdir_from_opt=
hasp_serveraddr="${hasp_serveraddr:-}"
outdir="$saved_cwd"

while :; do
	case "$1" in
		--analyze-only) analyze_only=1
			;;
		--apt-config) shift; apt_config="$1"
			;;
		--bind-svace) bind_svace=1
			;;
		--build-only) build_only=1
			;;
		--excludedocs) exclude_docs=--excludedocs
			;;
		-S|--hasp-serveraddr) shift; hasp_serveraddr="$1"
			;;
		--hasher-priv-dir)
			hasher_priv_dir="$(opt_check_dir "$1" "$2")"
			shift
			;;
		--install-svace) shift; install_svace=1; svace_version="$1"
			;;
		--mountpoints) shift; known_mountpoints="$1"
			;;
		--no-lock) no_lock=1
			;;
		--nodeps) nodeps=--nodeps
			;;
		--number)
			number="$(opt_check_number_ge_0 "$1" "$2")"
			shift
			;;
		--outdir) shift; outdir="$1"
			;;
		--rebuild) rebuild=1
			;;
		--svace-config) shift
			svace_config_args="$svace_config_args --config '$(printf %s "$1" | sed "s/'/'\\\\''/g")'"
			;;
		--svace-config-file) shift; svace_config_file="$1"
			;;
		--svace-warning) shift
			svace_warning_args="$svace_warning_args --warning '$(printf %s "$1" | sed "s/'/'\\\\''/g")'"
			;;
		--svace-warning-all) svace_warning_all=1
			;;
		--svace-warning-file) shift; svace_warning_file="$1"
			;;
		--target) shift; target="$1"
			[ -z "${target##[A-Za-z]*}" ] ||
				fatal "--target: $target: invalid architecture."
			;;
		--wait-lock) lock_nowait=
			;;
		--no-wait-lock) lock_nowait=1
			;;
		--without-stuff) no_stuff=1
			;;
		--with-stuff) no_stuff=
			;;
		--workdir) shift; workdir="$1"
			workdir_from_opt=1
			;;
		--) shift; break
			;;
		*) parse_common_option "$1"
			;;
	esac
	shift
done

# Set up fd 3 for hasher commands stderr:
# verbose mode — show on stderr, otherwise — suppress.
if [ -n "$verbose" ]; then
	exec 3>&2
else
	exec 3>/dev/null
fi

if [ -z "$workdir_from_opt" ]; then
	if [ -z "$workdir" ] || [ "$#" -ge 2 ]; then
		# One more argument for workdir.
		[ "$#" -ge 2 ] || show_usage 'Insufficient arguments.'
		workdir="$1"
		shift
	fi
fi

# Exactly one argument.
[ "$#" -ge 1 ] || show_usage 'Insufficient arguments.'
[ "$#" -le 1 ] || show_usage 'Too many arguments.'

[ -z "${outdir##/*}" ] || outdir="$saved_cwd/$outdir"
[ -d "$outdir" ] || fatal "$outdir: output directory does not exist."

if [ -n "$build_only" ] && [ -n "$analyze_only" ]; then
	fatal '--build-only and --analyze-only are mutually exclusive.'
fi

if [ -n "$install_svace" ] && [ -n "$bind_svace" ]; then
	fatal '--install-svace and --bind-svace are mutually exclusive.'
fi

if [ -n "$svace_warning_all" ] && [ -n "$svace_warning_file" ]; then
	fatal '--svace-warning-all and --svace-warning-file are mutually exclusive.'
fi

if [ -z "$build_only" ] && [ -z "$hasp_serveraddr" ]; then
	if [ -n "$analyze_only" ]; then
		show_usage 'Set hasp serveraddr with -S option or hasp_serveraddr env variable.'
	else
		show_usage 'Set hasp serveraddr with -S option or hasp_serveraddr env variable.' \
			'To disable analyze stage use --build-only option.'
	fi
fi

set_workdir

source="$1"
[ -z "${source##/*}" ] || source="$saved_cwd/$source"
sname="${source##*/}"
shift

if [ -n "$analyze_only" ]; then
	[ -f "$source" ] ||
		fatal "$sname: results tar not found."
fi

rc=0

lock_workdir

if [ -n "$rebuild" ]; then
	[ -d "$chroot" ] || fatal "$chroot: cannot find chroot."
	deduce_lock_hasher_priv
else
	stage "initializing chroot..."
	rm -rf aptbox
	if [ -d chroot ]; then
		deduce_lock_hasher_priv
		hsh-rmchroot --no-lock "$workdir" \
			${hasher_priv_dir:+--hasher-priv-dir="$hasher_priv_dir"} \
			${quiet:+$quiet} \
			${verbose:+$verbose} \
			${number:+--number=$number} \
			2>&3 #
	else
		allocate_lock_hasher_priv
	fi

	mkaptbox --no-lock "$workdir" \
		${target:+--target="$target"} \
		${apt_config:+--apt-config="$apt_config"} \
		${quiet:+$quiet} \
		${verbose:+$verbose} \
		${no_stuff:+--no-stuff} \
		2>&3 #
	hsh-mkchroot --no-lock "$workdir" \
		${hasher_priv_dir:+--hasher-priv-dir="$hasher_priv_dir"} \
		${quiet:+$quiet} \
		${verbose:+$verbose} \
		${number:+--number=$number} \
		2>&3 #
	hsh-initroot --no-lock "$workdir" \
		${hasher_priv_dir:+--hasher-priv-dir="$hasher_priv_dir"} \
		${quiet:+$quiet} \
		${verbose:+$verbose} \
		${exclude_docs:+--excludedocs} \
		${number:+--number=$number} \
		2>&3 #
fi

stage "installing svace..."
# Install svace into chroot.
if [ -z "$bind_svace" ]; then
	# Write desired version if specified.
	if [ -n "$svace_version" ]; then
		create_entry_header
		cat >>"$entry" <<__EOF__
printf '%s\n' "$(quote_shell "$svace_version")" \
	>/etc/rpm-build-svace-version
__EOF__

		requested_mountpoints= \
		wlimit_time_elapsed=$wlimit_time_short \
		wlimit_time_idle=$wlimit_time_short \
			chrootuid1 </dev/null ||
			fatal "Failed to write svace version."
	fi

	share_network=1 \
	hsh-install --no-lock "$workdir" \
		${hasher_priv_dir:+--hasher-priv-dir="$hasher_priv_dir"} \
		${quiet:+$quiet} \
		${verbose:+$verbose} \
		${number:+--number=$number} \
		-- rpm-build-svace 2>&3 >&2
fi

stage "verifying svace..."
# Verify svace is available in chroot.
create_entry_header
cat >>"$entry" <<'__EOF__'
test -x /opt/svace/bin/svace
__EOF__

requested_mountpoints="${bind_svace:+/opt}" \
wlimit_time_elapsed=$wlimit_time_short \
wlimit_time_idle=$wlimit_time_short \
	chrootuid1 </dev/null ||
	fatal "svace not found in chroot: /opt/svace/bin/svace is missing or not executable."

# Build stage — skip in analyze-only mode.
if [ -z "$analyze_only" ]; then
	# Install source package.
	stage "installing source package..."
	install_source_package

	case "$source_package_type" in
		src.rpm|pkg.tar) ;;
		*) fatal "$sname: unsupported source package type: $source_package_type" ;;
	esac

	# Install build dependencies.
	if [ -z "$nodeps" ]; then
		stage "installing build dependencies..."
		install_deps_pkg 2>&3 >&2
	fi

	# Install build helper script into chroot.
	stage "running svace build..."
	install -pm755 $verbose "$selfdir/hsh-svace-build.sh" \
		chroot/.host/svace-build >&2

	# Copy svace config file into chroot if specified.
	if [ -n "$svace_config_file" ]; then
		[ -f "$svace_config_file" ] ||
			fatal "$svace_config_file: svace config file not found."
		install -pm644 $verbose "$svace_config_file" \
			chroot/.host/svace-config.conf >&2
		svace_config_args="--config-file /.host/svace-config.conf${svace_config_args:+ $svace_config_args}"
	fi

	# Run svace build.
	purge_chroot_out

	create_entry_header
	cat >>"$entry" <<__EOF__
exec /.host/svace-build \
	${quiet:+$quiet} \
	${verbose:+$verbose} \
	--target="$(quote_shell "${target:-$def_target}")" \
	$svace_config_args
__EOF__

	requested_mountpoints="$required_mountpoints,/proc${bind_svace:+,/opt}" \
	wlimit_time_elapsed=$wlimit_time_long \
	wlimit_time_idle=$wlimit_time_long \
		chrootuid2 </dev/null &&
		verbose "$sname: svace build complete." ||
		fatal "$sname: svace build failed."
fi

# Load existing results for analyze-only mode.
if [ -n "$analyze_only" ]; then
	stage "extracting results..."
	purge_chroot_out

	install -pm644 $verbose "$source" \
		chroot/.in/hsh-svace-results.tar >&2

	create_entry_header
	cat >>"$entry" <<__EOF__
tar -xf hsh-svace-results.tar \
	--transform 's,^hsh-svace-results,out,' \
	-C "\$HOME"
__EOF__

	requested_mountpoints= \
	wlimit_time_elapsed=$wlimit_time_short \
	wlimit_time_idle=$wlimit_time_short \
		chrootuid2 </dev/null ||
		fatal "$sname: failed to extract results in chroot."
fi

# Run svace analyze — skip in build-only mode.
if [ -z "$build_only" ]; then
	stage "running svace analyze..."
	install -pm755 $verbose "$selfdir/hsh-svace-analyze.sh" \
		chroot/.host/svace-analyze >&2

	# In analyze-only mode, config was not applied during build,
	# so pass config args to the analyze script.
	if [ -n "$analyze_only" ]; then
		if [ -n "$svace_config_file" ]; then
			[ -f "$svace_config_file" ] ||
				fatal "$svace_config_file: svace config file not found."
			install -pm644 $verbose "$svace_config_file" \
				chroot/.host/svace-config.conf >&2
			svace_config_args="--config-file /.host/svace-config.conf${svace_config_args:+ $svace_config_args}"
		fi
	fi

	if [ -n "$svace_warning_file" ]; then
		[ -f "$svace_warning_file" ] ||
			fatal "$svace_warning_file: svace warnings file not found."
		install -pm644 $verbose "$svace_warning_file" \
			chroot/.host/svace-warnings.conf >&2
		svace_warning_args="--warnings-file /.host/svace-warnings.conf${svace_warning_args:+ $svace_warning_args}"
	elif [ -n "$svace_warning_all" ]; then
		svace_warning_args="--warning-all${svace_warning_args:+ $svace_warning_args}"
	fi

	create_entry_header
	cat >>"$entry" <<__EOF__
exec /.host/svace-analyze \
	${quiet:+$quiet} \
	${verbose:+$verbose} \
	-S "$(quote_shell "$hasp_serveraddr")" \
	$svace_warning_args \
	${analyze_only:+$svace_config_args}
__EOF__

	share_network=1 \
	requested_mountpoints="/proc${bind_svace:+,/opt}" \
	wlimit_time_elapsed=$wlimit_time_long \
	wlimit_time_idle=$wlimit_time_long \
		chrootuid2 </dev/null &&
		verbose "$sname: svace analyze complete." || {
			rc=$?
			message "$sname: svace analyze failed."
			if [ -n "$analyze_only" ]; then
				exit "$rc"
			fi
			message "Saving 'svace build' results instead."
			message "To analyze them later, run: $PROG --analyze-only <results-tar>"
		}
fi

stage "copying results..."
# Copy results.
# After a failed analyze, only the build results tar is available.
if [ -z "$build_only" ] && [ "$rc" -eq 0 ]; then
	results_tar=hsh-svace-results-analyzed.tar
else
	results_tar=hsh-svace-results.tar
fi

cp -f --backup=numbered "chroot/.out/$results_tar" \
	"$outdir/$results_tar" &&
	verbose "Results saved to $outdir/$results_tar" ||
	fatal "Failed to copy results."

exit "$rc"
