Splux Linux

source-based Linux using SPS

SPS · log · files · refs · tags · releases · people · languages · GitHub · file

lib/common.sh


Shell

25821 bytes at 2909919 · GitHub

#!/bin/sh
# Shared shell helpers used by the SPS commands.

SPS_EX_USAGE=2
SPS_EX_NOTFOUND=3
SPS_EX_PACKAGE=4
SPS_EX_DEPENDENCY=5
SPS_EX_CHECKSUM=6
SPS_EX_CONFLICT=7
SPS_EX_PERMISSION=8
SPS_EX_NETWORK=9
SPS_EX_BUILD=10
SPS_EX_DATABASE=11

sps_basename()
{
	case ${1-} in
		*/*) printf '%s\n' "${1##*/}" ;;
		*) printf '%s\n' "${1-}" ;;
	esac
}

sps_program=${sps_program:-${sps_prog:-$(sps_basename "$0")}}
[ -z "${SPS_LIBDIR+x}" ] || export SPS_LIBDIR

sps_die()
{
	case ${1-} in
		''|*[!0-9]*) sps_status=1 ;;
		*) sps_status=$1; shift ;;
	esac
	printf '%s: %s\n' "$sps_program" "$*" >&2
	exit "$sps_status"
}

sps_warn()
{
	printf '%s: %s\n' "$sps_program" "$*" >&2
}

sps_usage()
{
	printf 'usage: %s\n' "$*" >&2
	exit "$SPS_EX_USAGE"
}

sps_need_arg()
{
	[ "$#" -ge 2 ] && [ -n "${2-}" ] ||
		sps_die "$SPS_EX_USAGE" "option '$1' requires an argument"
}

sps_mkdir()
{
	[ -d "$1" ] || mkdir -p "$1" ||
		sps_die "$SPS_EX_PERMISSION" "cannot create directory: $1"
}

sps_root_path()
{
	sps_path=$1
	case $sps_path in
		/*)
			if [ "${SPS_ROOT:-/}" = / ]; then
				printf '%s\n' "$sps_path"
			else
				printf '%s%s\n' "${SPS_ROOT%/}" "$sps_path"
			fi
			;;
		*) printf '%s/%s\n' "${SPS_ROOT:-/}" "$sps_path" ;;
	esac
}

sps_mktemp_dir()
{
	sps_tmp_parent=$1
	sps_tmp_tag=${2:-work}
	sps_mkdir "$sps_tmp_parent"
	sps_tmp=$(mktemp -d "$sps_tmp_parent/.sps-$sps_tmp_tag.XXXXXX") ||
		sps_die "$SPS_EX_PERMISSION" \
			"cannot create temporary directory in $sps_tmp_parent"
	printf '%s\n' "$sps_tmp"
}

sps_require_cmd()
{
	command -v "$1" >/dev/null 2>&1 ||
		sps_die "$SPS_EX_PACKAGE" "required command not found: $1"
}

# Resolve the hasher once per process. src update hashes hundreds of recipes.
sps_sha256_kind=

sps_sha256_detect()
{
	[ -n "$sps_sha256_kind" ] && return 0
	if command -v sha256sum >/dev/null 2>&1; then
		sps_sha256_kind=sha256sum
	elif command -v sha256 >/dev/null 2>&1; then
		sps_sha256_kind=sha256
	elif command -v shasum >/dev/null 2>&1; then
		sps_sha256_kind=shasum
	elif command -v openssl >/dev/null 2>&1; then
		sps_sha256_kind=openssl
	else
		sps_die "$SPS_EX_CHECKSUM" \
			"no SHA-256 utility found (tried sha256sum, sha256, shasum, openssl)"
	fi
}

# Print only the digest. Avoid an extra awk process here.
sps_sha256()
{
	sps_sha256_detect || return $?
	case $sps_sha256_kind in
		sha256sum)
			sps_hash_line=$(sha256sum "$1") || return $?
			printf '%s\n' "${sps_hash_line%% *}"
			;;
		sha256)
			sha256 -q "$1"
			;;
		shasum)
			sps_hash_line=$(shasum -a 256 "$1") || return $?
			printf '%s\n' "${sps_hash_line%% *}"
			;;
		openssl)
			sps_hash_line=$(openssl dgst -sha256 "$1") || return $?
			printf '%s\n' "${sps_hash_line##* }"
			;;
		*)
			sps_die "$SPS_EX_CHECKSUM" \
				"no SHA-256 utility found (tried sha256sum, sha256, shasum, openssl)"
			;;
	esac
}

# Same digest as sps_sha256, from stdin. Used for the recipe-only definition
# manifest so src update does not create a throwaway directory per package.
sps_sha256_stream()
{
	sps_sha256_detect || return $?
	case $sps_sha256_kind in
		sha256sum)
			sps_hash_line=$(sha256sum) || return $?
			printf '%s\n' "${sps_hash_line%% *}"
			;;
		sha256)
			sha256 -q
			;;
		shasum)
			sps_hash_line=$(shasum -a 256) || return $?
			printf '%s\n' "${sps_hash_line%% *}"
			;;
		openssl)
			sps_hash_line=$(openssl dgst -sha256) || return $?
			printf '%s\n' "${sps_hash_line##* }"
			;;
		*)
			sps_die "$SPS_EX_CHECKSUM" \
				"no SHA-256 utility found (tried sha256sum, sha256, shasum, openssl)"
			;;
	esac
}

# Lowercase a SHA-256 hex digest. GNU sha256sum is already lowercase; skip tr.
sps_hex_normalize()
{
	sps_hex_in=$1
	case $sps_hex_in in
		*[ABCDEF]*)
			sps_hex_in=$(printf '%s\n' "$sps_hex_in" |
				LC_ALL=C tr 'ABCDEF' 'abcdef') || return 1
			;;
	esac
	case $sps_hex_in in
		''|*[!0123456789abcdef]*) return 1 ;;
	esac
	[ "${#sps_hex_in}" -eq 64 ] || return 1
	printf '%s\n' "$sps_hex_in"
}

# Recipe-only definition digest from the recipe file's SHA-256. Same two-line
# manifest the support-tree path would write for a package with no files/,
# patches/, or hooks/.
sps_definition_from_recipe_file_hash()
{
	sps_rfh=$(sps_hex_normalize "$1") || return 1
	sps_rfh_out=$(printf 'sps-package-definition\t1\nfile\t-\t%s\trecipe\n' \
		"$sps_rfh" | sps_sha256_stream) || return 1
	sps_hex_normalize "$sps_rfh_out"
}

# Hash every input which makes up one package definition.  The manifest uses
# only fixed, package-relative names, so moving an unchanged package directory
# does not change its digest.  files/ and patches/ are copied recursively by
# mkpkg, making their directory and file modes observable build inputs too.
sps_definition_sha256()
(
	sps_definition_input=${1-}
	case $sps_definition_input in
		'') sps_warn 'package definition recipe is missing'; return 1 ;;
		*/*)
			sps_definition_parent=${sps_definition_input%/*}
			sps_definition_leaf=${sps_definition_input##*/}
			;;
		*)
			sps_definition_parent=.
			sps_definition_leaf=$sps_definition_input
			;;
	esac
	case $sps_definition_parent in
		-*) sps_definition_parent=./$sps_definition_parent ;;
	esac
	sps_definition_dir=$(CDPATH= cd -P "$sps_definition_parent" 2>/dev/null &&
		pwd -P) || {
		sps_warn 'cannot enter package definition directory'
		return 1
	}
	sps_definition_recipe=$sps_definition_dir/$sps_definition_leaf
	if [ ! -f "$sps_definition_recipe" ] ||
	   [ -L "$sps_definition_recipe" ] ||
	   [ ! -r "$sps_definition_recipe" ]; then
		sps_warn 'package definition recipe must be a readable regular file'
		return 1
	fi

	# Recipe-only packages produce a two-line manifest. Hash that without a
	# workspace directory; src update indexes hundreds of these.
	sps_definition_need_support=0
	for sps_definition_support in files patches hooks; do
		if [ -e "$sps_definition_dir/$sps_definition_support" ] ||
		   [ -L "$sps_definition_dir/$sps_definition_support" ]; then
			sps_definition_need_support=1
			break
		fi
	done
	if [ "$sps_definition_need_support" -eq 0 ]; then
		sps_definition_hash=$(sps_sha256 "$sps_definition_recipe") || {
			sps_warn 'cannot hash package definition recipe'
			return 1
		}
		sps_definition_out=$(sps_definition_from_recipe_file_hash \
			"$sps_definition_hash") || {
			sps_warn 'cannot hash package definition manifest'
			return 1
		}
		printf '%s\n' "$sps_definition_out"
		return 0
	fi

	sps_definition_tmp=$(mktemp -d \
		"${TMPDIR:-/tmp}/.sps-definition.XXXXXX" 2>/dev/null) || {
		sps_warn 'cannot create package definition hash workspace'
		return 1
	}
	sps_definition_cleanup()
	{
		rm -f "$sps_definition_tmp/manifest" \
			"$sps_definition_tmp/paths.raw" \
			"$sps_definition_tmp/paths" \
			"$sps_definition_tmp/unsafe-path" \
			"$sps_definition_tmp/unsafe-type"
		rmdir "$sps_definition_tmp" 2>/dev/null || :
	}
	trap 'sps_definition_cleanup' 0
	trap 'exit 1' 1 2 3 15

	sps_definition_hash_file()
	{
		sps_definition_value=$(sps_sha256 "$1") || return $?
		sps_definition_value=$(printf '%s\n' "$sps_definition_value" |
			LC_ALL=C tr 'ABCDEF' 'abcdef') || return $?
		case $sps_definition_value in
			''|*[!0123456789abcdef]*) return 1 ;;
		esac
		[ "${#sps_definition_value}" -eq 64 ] || return 1
		printf '%s\n' "$sps_definition_value"
	}

	sps_definition_manifest=$sps_definition_tmp/manifest
	printf 'sps-package-definition\t1\n' >"$sps_definition_manifest" ||
		return 1
	sps_definition_hash=$(sps_definition_hash_file \
		"$sps_definition_recipe") || {
		sps_warn 'cannot hash package definition recipe'
		return 1
	}
	printf 'file\t-\t%s\trecipe\n' "$sps_definition_hash" \
		>>"$sps_definition_manifest" || return 1

	cd "$sps_definition_dir" || return 1
	for sps_definition_support in files patches hooks; do
		if [ ! -e "$sps_definition_support" ] &&
		   [ ! -L "$sps_definition_support" ]; then
			continue
		fi
		if [ ! -d "$sps_definition_support" ] ||
		   [ -L "$sps_definition_support" ]; then
			sps_warn "recipe $sps_definition_support must be a real directory"
			return 1
		fi

		rm -f "$sps_definition_tmp/unsafe-path" \
			"$sps_definition_tmp/unsafe-type"
		if ! find "$sps_definition_support" -exec sh -c '
			marker=$1
			shift
			for path do
				case $path in
					*"	"*|*"
"*) : >"$marker"; exit 0 ;;
				esac
			done
		' sh "$sps_definition_tmp/unsafe-path" {} +; then
			sps_warn "cannot inspect package definition $sps_definition_support"
			return 1
		fi
		if [ -e "$sps_definition_tmp/unsafe-path" ]; then
			sps_warn "package definition $sps_definition_support contains a tab or newline in a path"
			return 1
		fi
		if ! find "$sps_definition_support" ! -type d ! -type f \
			-exec sh -c ': >"$1"' sh \
			"$sps_definition_tmp/unsafe-type" {} +; then
			sps_warn "cannot inspect package definition $sps_definition_support"
			return 1
		fi
		if [ -e "$sps_definition_tmp/unsafe-type" ]; then
			sps_warn "package definition $sps_definition_support contains a symlink or special file"
			return 1
		fi
		if ! find "$sps_definition_support" -print \
			>"$sps_definition_tmp/paths.raw"; then
			sps_warn "cannot list package definition $sps_definition_support"
			return 1
		fi
		if ! LC_ALL=C sort "$sps_definition_tmp/paths.raw" \
			>"$sps_definition_tmp/paths"; then
			sps_warn "cannot sort package definition $sps_definition_support"
			return 1
		fi

		while IFS= read -r sps_definition_path ||
		      [ -n "$sps_definition_path" ]; do
			[ -n "$sps_definition_path" ] || continue
			if [ -d "$sps_definition_path" ] &&
			   [ ! -L "$sps_definition_path" ]; then
				sps_definition_mode=$(sps_mode_of \
					"$sps_definition_path") || {
					sps_warn "cannot read a $sps_definition_support directory mode"
					return 1
				}
				printf 'directory\t%s\t-\t%s\n' \
					"$sps_definition_mode" "$sps_definition_path" \
					>>"$sps_definition_manifest" || return 1
			elif [ -f "$sps_definition_path" ] &&
			     [ ! -L "$sps_definition_path" ]; then
				sps_definition_mode=$(sps_mode_of \
					"$sps_definition_path") || {
					sps_warn "cannot read a $sps_definition_support file mode"
					return 1
				}
				sps_definition_hash=$(sps_definition_hash_file \
					"$sps_definition_path") || {
					sps_warn "cannot hash a $sps_definition_support file"
					return 1
				}
				printf 'file\t%s\t%s\t%s\n' "$sps_definition_mode" \
					"$sps_definition_hash" "$sps_definition_path" \
					>>"$sps_definition_manifest" || return 1
			else
				sps_warn "package definition $sps_definition_support changed while hashing"
				return 1
			fi
		done <"$sps_definition_tmp/paths"
	done

	sps_definition_hash_file "$sps_definition_manifest"
)

# True when $1 is a package-relative config path whose packaged default pkin
# wrote beside it during an upgrade (".sps-new*"). Kept in common.sh so the
# config review command and the post-upgrade reminder share one definition.
sps_config_review_pending()
{
	[ "${SPS_PRESERVE:-etc}" != none ] || return 1
	[ -d "$SPS_DB/installed" ] && [ ! -L "$SPS_DB/installed" ] || return 1
	sps_cr_tab=$(printf '\tX')
	sps_cr_tab=${sps_cr_tab%X}
	for sps_cr_record in "$SPS_DB/installed"/*; do
		[ -d "$sps_cr_record" ] && [ ! -L "$sps_cr_record" ] || continue
		[ -f "$sps_cr_record/hashes" ] && [ ! -L "$sps_cr_record/hashes" ] || continue
		while IFS="$sps_cr_tab" read -r sps_cr_algo sps_cr_expected sps_cr_path \
		      || [ -n "$sps_cr_path" ]; do
			[ -n "$sps_cr_path" ] || continue
			sps_is_preserved_path "$sps_cr_path" || continue
			if sps_config_new_defaults "$sps_cr_path" | grep -q .; then
				return 0
			fi
		done <"$sps_cr_record/hashes"
	done
	return 1
}

sps_validate_package_name()
{
	case ${1-} in
		''|[!A-Za-z0-9]*|*/*|*[!A-Za-z0-9_+.-]*) return 1 ;;
		*) return 0 ;;
	esac
}

# Print the packaged config default that pkin wrote beside a kept local file.
# $1 is the package-relative path. The only legal derivations are the plain
# ".sps-new" sibling and the ".sps-new.VERSION-RELEASE" collision spelling.
# Anything else is left for the caller to report, never followed or removed.
sps_config_new_defaults()
{
	sps_default_base=$1
	sps_default_dir=${sps_default_base%/*}
	[ "$sps_default_dir" != "$sps_default_base" ] || sps_default_dir=
	[ -n "$sps_default_dir" ] ||
		sps_default_dir=.
	case $sps_default_base in
		*"*"*|*"?"*|*"["*|*"]"*) return 0 ;;
	esac
	LC_ALL=C find "$(sps_root_path /$sps_default_dir)" -maxdepth 1 \
		-name "$(basename "$sps_default_base")"'.sps-new*' \
		-type f ! -type l 2>/dev/null | LC_ALL=C sort
}

sps_tab_safe()
{
	case ${1-} in
		*"	"*|*"
"*) return 1 ;;
		*) return 0 ;;
	esac
}

# SPS_PRESERVE is a comma-separated list of package-relative prefixes. "etc"
# covers etc and everything below it; "none" turns the check off.
sps_is_preserved_path()
{
	sps_preserve_path=${1#/}
	[ "${SPS_PRESERVE:-etc}" != none ] || return 1
	sps_preserve_rest=${SPS_PRESERVE:-etc}
	while :; do
		case $sps_preserve_rest in
			*,*) sps_preserve_prefix=${sps_preserve_rest%%,*}; sps_preserve_rest=${sps_preserve_rest#*,} ;;
			*) sps_preserve_prefix=$sps_preserve_rest; sps_preserve_rest= ;;
		esac
		sps_preserve_prefix=${sps_preserve_prefix#/}
		sps_preserve_prefix=${sps_preserve_prefix%/}
		if [ -n "$sps_preserve_prefix" ]; then
			case $sps_preserve_path in
				"$sps_preserve_prefix"|"$sps_preserve_prefix"/*) return 0 ;;
			esac
		fi
		[ -n "$sps_preserve_rest" ] || break
	done
	return 1
}

# True when $1 is a conservative in-root directory symlink that packages
# may share: usr-merge names, plus filesystem compat links var/run and
# var/lock. Absolute links, unexpected targets, and any other name fail.
sps_is_shared_dir_link()
{
	sps_share_rel=${1#/}
	sps_share_rel=${sps_share_rel%/}
	sps_share_path=$(sps_root_path "/$sps_share_rel")
	[ -L "$sps_share_path" ] || return 1
	[ -d "$sps_share_path" ] || return 1
	sps_share_target=$(readlink "$sps_share_path") || return 1
	sps_share_target=${sps_share_target%/}
	case $sps_share_rel in
		bin) [ "$sps_share_target" = usr/bin ] || return 1 ;;
		sbin) [ "$sps_share_target" = usr/sbin ] || return 1 ;;
		lib) [ "$sps_share_target" = usr/lib ] || return 1 ;;
		lib64) [ "$sps_share_target" = usr/lib64 ] || return 1 ;;
		var/run) [ "$sps_share_target" = ../run ] || return 1 ;;
		var/lock) [ "$sps_share_target" = ../run/lock ] || return 1 ;;
		*) return 1 ;;
	esac
	return 0
}

# True when $1 is a top-level usr-merge directory symlink.
sps_is_usr_merge_dir()
{
	case ${1#/} in
		bin|sbin|lib|lib64) sps_is_shared_dir_link "$1" ;;
		*) return 1 ;;
	esac
}

# Live images write /etc/sps/live. Only the target root is consulted so a
# host live marker cannot change pkin policy for a chroot or test root.
sps_is_live_bootstrap()
{
	[ -f "$(sps_root_path /etc/sps/live)" ]
}

# Replacing these on a live image would break the running disc.
sps_is_live_critical_path()
{
	case ${1#/} in
		bin/sh|usr/bin/sh|sbin/init|usr/sbin/init|\
		bin/busybox|usr/bin/busybox|sbin/busybox|usr/sbin/busybox)
			return 0 ;;
	esac
	return 1
}

# Cache the uid check. pkin calls this for many payload entries.
sps_is_root()
{
	if [ "${sps_uid_is_root+x}" != x ]; then
		sps_uid=$(id -u 2>/dev/null || printf '%s\n' 1)
		case $sps_uid in
			0) sps_uid_is_root=1 ;;
			*) sps_uid_is_root=0 ;;
		esac
	fi
	[ "$sps_uid_is_root" -eq 1 ]
}

# Turn the POSIX ls permission string into an octal mode. Keep the parsing in
# shell so mode lookup needs only the ls process.
sps_mode_of()
{
	sps_mode_line=$(LC_ALL=C ls -ld "$1" 2>/dev/null) || return 1
	sps_mode_text=${sps_mode_line%% *}
	# Some ls implementations append an ACL/xattr marker; ignore it.
	sps_mode_tail=${sps_mode_text#??????????}
	sps_mode_text=${sps_mode_text%"$sps_mode_tail"}
	case $sps_mode_text in ??????????) ;; *) return 1 ;; esac
	sps_mode_bits=${sps_mode_text#?}
	sps_mode_user=${sps_mode_bits%??????}
	sps_mode_rest=${sps_mode_bits#???}
	sps_mode_group=${sps_mode_rest%???}
	sps_mode_other=${sps_mode_rest#???}

	sps_mode_u=0; sps_mode_g=0; sps_mode_o=0; sps_mode_s=0
	case $sps_mode_user in r??) sps_mode_u=$((sps_mode_u + 4)) ;; esac
	case $sps_mode_user in ?w?) sps_mode_u=$((sps_mode_u + 2)) ;; esac
	case $sps_mode_user in ??[xst]) sps_mode_u=$((sps_mode_u + 1)) ;; esac
	case $sps_mode_group in r??) sps_mode_g=$((sps_mode_g + 4)) ;; esac
	case $sps_mode_group in ?w?) sps_mode_g=$((sps_mode_g + 2)) ;; esac
	case $sps_mode_group in ??[xst]) sps_mode_g=$((sps_mode_g + 1)) ;; esac
	case $sps_mode_other in r??) sps_mode_o=$((sps_mode_o + 4)) ;; esac
	case $sps_mode_other in ?w?) sps_mode_o=$((sps_mode_o + 2)) ;; esac
	case $sps_mode_other in ??[xst]) sps_mode_o=$((sps_mode_o + 1)) ;; esac
	case $sps_mode_user in ??[sS]) sps_mode_s=$((sps_mode_s + 4)) ;; esac
	case $sps_mode_group in ??[sS]) sps_mode_s=$((sps_mode_s + 2)) ;; esac
	case $sps_mode_other in ??[tT]) sps_mode_s=$((sps_mode_s + 1)) ;; esac
	printf '%d%d%d%d\n' "$sps_mode_s" "$sps_mode_u" "$sps_mode_g" "$sps_mode_o"
}

# Root installs normalize new payload ownership to root:root. Do not copy the
# builder's numeric uid/gid into the target system.
sps_normalize_owner()
{
	sps_is_root || return 0
	chown 0:0 "$1"
}

# mkdir gives us the lock atomically. Only remove a stale lock when its recorded
# numeric PID can be checked safely.
sps_lock_acquire()
{
	sps_lock_base=$1
	sps_lock_name=${2:-.lock}
	sps_lock_label=${3:-package database}
	case $sps_lock_name in ''|*/*|.|..) return 1 ;; esac
	sps_lock=$sps_lock_base/$sps_lock_name
	sps_lock_owned=0
	if mkdir "$sps_lock" 2>/dev/null; then
		printf '%s\n' "$$" >"$sps_lock/pid" || {
			rmdir "$sps_lock" 2>/dev/null || :
			return 1
		}
		sps_lock_owned=1
		return 0
	fi

	[ -d "$sps_lock" ] && [ ! -L "$sps_lock" ] || return 1
	[ -f "$sps_lock/pid" ] && [ ! -L "$sps_lock/pid" ] || return 1
	sps_lock_pid=$(sed -n '1p' "$sps_lock/pid" 2>/dev/null) || return 1
	case $sps_lock_pid in ''|*[!0-9]*) return 1 ;; esac
	if kill -0 "$sps_lock_pid" 2>/dev/null; then
		return 1
	fi

	sps_warn "removing stale $sps_lock_label lock '$sps_lock' from pid $sps_lock_pid"
	rm -f "$sps_lock/pid" 2>/dev/null || return 1
	rmdir "$sps_lock" 2>/dev/null || return 1
	if mkdir "$sps_lock" 2>/dev/null; then
		printf '%s\n' "$$" >"$sps_lock/pid" || {
			rmdir "$sps_lock" 2>/dev/null || :
			return 1
		}
		sps_lock_owned=1
		return 0
	fi
	return 1
}

sps_lock_release()
{
	[ "${sps_lock_owned:-0}" -eq 1 ] || return 0
	rm -f "$sps_lock/pid" 2>/dev/null || :
	rmdir "$sps_lock" 2>/dev/null || :
	sps_lock_owned=0
}

# Print repository declarations without normalizing or truncating them. General
# settings in sps.conf are ignored here; repos.conf is repository-only, so an
# unknown declaration there is preserved for repository.awk to reject.
sps_repo_lines()
{
	sps_repo_previous=
	for sps_repo_file in "$SPS_CONFIG" "$SPS_REPOS_CONFIG"; do
		[ -r "$sps_repo_file" ] || continue
		[ "$sps_repo_file" != "$sps_repo_previous" ] || continue
		if [ "$sps_repo_file" = "$SPS_CONFIG" ]; then
			sps_repo_main=1
		else
			sps_repo_main=0
		fi
		awk -v main_config="$sps_repo_main" '
			{
				line = $0
				sub(/\r$/, "", line)
				probe = line
				sub(/^[ \t]+/, "", probe)
				if (probe == "" || probe ~ /^#/)
					next
				split(probe, field, /[ \t]+/)
				if (!main_config || field[1] == "git" ||
				    field[1] == "dir" || field[1] == "repo")
					print line
			}
		' "$sps_repo_file" || return $?
		sps_repo_previous=$sps_repo_file
	done
}

# Number of CPUs for makejobs auto. Never prints zero or a non-integer.
sps_nproc()
{
	sps_n=
	if command -v nproc >/dev/null 2>&1; then
		sps_n=$(nproc 2>/dev/null) || sps_n=
	fi
	if [ -z "$sps_n" ] && command -v getconf >/dev/null 2>&1; then
		sps_n=$(getconf _NPROCESSORS_ONLN 2>/dev/null) || sps_n=
	fi
	case $sps_n in
		''|*[!0-9]*|0) printf '%s\n' 1 ;;
		*) printf '%s\n' "$sps_n" ;;
	esac
}

# Default sps.conf written onto live images and into a fresh target before
# the first sget. Flags occupy the rest of the line; see sps.conf(5).
sps_print_default_conf()
{
	printf '%s\n' \
		'db /var/lib/sps' \
		'cache /var/cache/sps' \
		'build /var/tmp/sps' \
		'repo_root /usr/src/sps' \
		'makejobs auto' \
		'cflags -O2 -pipe' \
		'cxxflags -O2 -pipe' \
		'march native' \
		'compression auto' \
		'preserve etc'
}

# Comment-only. setup, mkiso, and install-config never write an active
# community line. Administrators add `git community` themselves.
sps_print_community_repos_hint()
{
	printf '%s\n' \
		'' \
		'# Community recipes are not enabled by default.' \
		'# git community https://github.com/RobertFlexx/sps-community.git 50'
}

sps_load_config()
{
	[ "${SPS_ROOT+x}" = x ] && sps_env_root=1 || sps_env_root=0
	[ "${SPS_DB+x}" = x ] && sps_env_db=1 || sps_env_db=0
	[ "${SPS_CACHE+x}" = x ] && sps_env_cache=1 || sps_env_cache=0
	[ "${SPS_BUILD+x}" = x ] && sps_env_build=1 || sps_env_build=0
	[ "${SPS_REPO_ROOT+x}" = x ] && sps_env_repo_root=1 || sps_env_repo_root=0
	[ "${SPS_MAKEJOBS+x}" = x ] && sps_env_makejobs=1 || sps_env_makejobs=0
	[ "${SPS_COMPRESSION+x}" = x ] && sps_env_compression=1 || sps_env_compression=0
	[ "${SPS_ARCH+x}" = x ] && sps_env_arch=1 || sps_env_arch=0
	[ "${SPS_PRESERVE+x}" = x ] && sps_env_preserve=1 || sps_env_preserve=0
	[ "${SPS_CFLAGS+x}" = x ] && sps_env_cflags=1 || sps_env_cflags=0
	[ "${SPS_CXXFLAGS+x}" = x ] && sps_env_cxxflags=1 || sps_env_cxxflags=0
	[ "${SPS_CPPFLAGS+x}" = x ] && sps_env_cppflags=1 || sps_env_cppflags=0
	[ "${SPS_LDFLAGS+x}" = x ] && sps_env_ldflags=1 || sps_env_ldflags=0
	[ "${SPS_MARCH+x}" = x ] && sps_env_march=1 || sps_env_march=0

	SPS_ROOT=${SPS_ROOT:-/}
	case $SPS_ROOT in
		/*) ;;
		*) sps_die "$SPS_EX_USAGE" "SPS_ROOT must be an absolute path: $SPS_ROOT" ;;
	esac
	[ "$SPS_ROOT" = / ] || SPS_ROOT=${SPS_ROOT%/}

	if [ "${SPS_CONFIG+x}" != x ]; then
		SPS_CONFIG=$(sps_root_path /etc/sps/sps.conf)
	fi
	if [ "${SPS_REPOS_CONFIG+x}" != x ]; then
		SPS_REPOS_CONFIG=$(sps_root_path /etc/sps/repos.conf)
	fi

	sps_cfg_root=$SPS_ROOT
	sps_cfg_db=/var/lib/sps
	sps_cfg_cache=/var/cache/sps
	sps_cfg_build=/var/tmp/sps
	sps_cfg_repo_root=/usr/src/sps
	sps_cfg_makejobs=auto
	sps_cfg_compression=auto
	sps_cfg_arch=$(uname -m 2>/dev/null || printf '%s' unknown)
	sps_cfg_preserve=etc
	sps_cfg_cflags='-O2 -pipe'
	sps_cfg_cxxflags='-O2 -pipe'
	sps_cfg_cppflags=
	sps_cfg_ldflags=
	sps_cfg_march=native

	if [ -r "$SPS_CONFIG" ]; then
		while IFS=' 	' read -r sps_key sps_value; do
			case $sps_key in
				''|'#'*) continue ;;
				root) [ "$sps_env_root" -eq 1 ] || sps_cfg_root=$sps_value ;;
				db) sps_cfg_db=$sps_value ;;
				cache) sps_cfg_cache=$sps_value ;;
				build) sps_cfg_build=$sps_value ;;
				repo_root) sps_cfg_repo_root=$sps_value ;;
				makejobs) sps_cfg_makejobs=$sps_value ;;
				compression) sps_cfg_compression=$sps_value ;;
				arch) sps_cfg_arch=$sps_value ;;
				preserve) sps_cfg_preserve=$sps_value ;;
				cflags) sps_cfg_cflags=$sps_value ;;
				cxxflags) sps_cfg_cxxflags=$sps_value ;;
				cppflags) sps_cfg_cppflags=$sps_value ;;
				ldflags) sps_cfg_ldflags=$sps_value ;;
				march) sps_cfg_march=$sps_value ;;
				repo|git|dir) : ;;
				*) sps_warn "ignoring unknown configuration key '$sps_key' in $SPS_CONFIG" ;;
			esac
		done < "$SPS_CONFIG"
	fi

	if [ "$sps_env_root" -eq 0 ]; then
		SPS_ROOT=$sps_cfg_root
		case $SPS_ROOT in /*) ;; *) sps_die "$SPS_EX_USAGE" "configured root must be absolute" ;; esac
		[ "$SPS_ROOT" = / ] || SPS_ROOT=${SPS_ROOT%/}
	fi
	[ "$sps_env_db" -eq 1 ] || SPS_DB=$(sps_root_path "$sps_cfg_db")
	[ "$sps_env_cache" -eq 1 ] || SPS_CACHE=$(sps_root_path "$sps_cfg_cache")
	[ "$sps_env_build" -eq 1 ] || SPS_BUILD=$(sps_root_path "$sps_cfg_build")
	[ "$sps_env_repo_root" -eq 1 ] || SPS_REPO_ROOT=$(sps_root_path "$sps_cfg_repo_root")
	[ "$sps_env_makejobs" -eq 1 ] || SPS_MAKEJOBS=$sps_cfg_makejobs
	[ "$sps_env_compression" -eq 1 ] || SPS_COMPRESSION=$sps_cfg_compression
	[ "$sps_env_arch" -eq 1 ] || SPS_ARCH=$sps_cfg_arch
	[ "$sps_env_preserve" -eq 1 ] || SPS_PRESERVE=$sps_cfg_preserve
	[ "$SPS_COMPRESSION" = zst ] && SPS_COMPRESSION=zstd

	if [ "$SPS_MAKEJOBS" = auto ]; then
		SPS_MAKEJOBS=$(sps_nproc)
	fi
	case $SPS_MAKEJOBS in ''|*[!0-9]*|0) sps_die "$SPS_EX_USAGE" "makejobs must be a positive integer" ;; esac
	case $SPS_COMPRESSION in
		auto|none|gzip|xz|zstd) ;;
		*) sps_die "$SPS_EX_USAGE" "unsupported compression '$SPS_COMPRESSION'" ;;
	esac
	case $SPS_ARCH in
		''|*[!A-Za-z0-9._+-]*|[!A-Za-z0-9]*)
			sps_die "$SPS_EX_USAGE" "invalid architecture '$SPS_ARCH'" ;;
	esac
	case $SPS_REPO_ROOT in
		/*) ;;
		*) sps_die "$SPS_EX_USAGE" "SPS_REPO_ROOT must be an absolute path: $SPS_REPO_ROOT" ;;
	esac
	[ "$SPS_REPO_ROOT" = / ] || SPS_REPO_ROOT=${SPS_REPO_ROOT%/}
	case $SPS_PRESERVE in
		'') sps_die "$SPS_EX_USAGE" "preserve may not be empty" ;;
		*" "*|*"	"*|*"
"*) sps_die "$SPS_EX_USAGE" "preserve must be a comma-separated path-prefix list" ;;
	esac

	export SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_REPO_ROOT SPS_CONFIG SPS_REPOS_CONFIG
	export SPS_MAKEJOBS SPS_COMPRESSION SPS_ARCH SPS_PRESERVE

	if [ "$sps_env_cflags" -eq 1 ]; then
		CFLAGS=$SPS_CFLAGS
	else
		CFLAGS=$sps_cfg_cflags
	fi
	if [ "$sps_env_cxxflags" -eq 1 ]; then
		CXXFLAGS=$SPS_CXXFLAGS
	else
		CXXFLAGS=$sps_cfg_cxxflags
	fi
	if [ "$sps_env_cppflags" -eq 1 ]; then
		CPPFLAGS=$SPS_CPPFLAGS
	else
		CPPFLAGS=$sps_cfg_cppflags
	fi
	if [ "$sps_env_ldflags" -eq 1 ]; then
		LDFLAGS=$SPS_LDFLAGS
	else
		LDFLAGS=$sps_cfg_ldflags
	fi
	if [ "$sps_env_march" -eq 1 ]; then
		SPS_MARCH=$SPS_MARCH
	else
		SPS_MARCH=$sps_cfg_march
	fi
	case $SPS_MARCH in
		none|'') ;;
		*)
			case $CFLAGS in
				*-march=*) ;;
				*) CFLAGS="${CFLAGS:+$CFLAGS }-march=$SPS_MARCH" ;;
			esac
			case $CXXFLAGS in
				*-march=*) ;;
				*) CXXFLAGS="${CXXFLAGS:+$CXXFLAGS }-march=$SPS_MARCH" ;;
			esac
			;;
	esac
	SPS_CFLAGS=$CFLAGS
	SPS_CXXFLAGS=$CXXFLAGS
	SPS_CPPFLAGS=$CPPFLAGS
	SPS_LDFLAGS=$LDFLAGS
	export CFLAGS CXXFLAGS SPS_CFLAGS SPS_CXXFLAGS SPS_MARCH
	[ -n "${CPPFLAGS-}" ] && export CPPFLAGS SPS_CPPFLAGS
	[ -n "${LDFLAGS-}" ] && export LDFLAGS SPS_LDFLAGS
	return 0
}