SPS · log · files · refs · tags · releases · people · languages · GitHub · file
bin/mkpkg
35840 bytes at 2909919 · GitHub
#!/bin/sh
# Build one SPS package from a recipe.
set -eu
prog=${0##*/}
keep_build=0
no_download=0
compression=
compression_set=0
output_dir=.
recipe=
build_dir=
stage_dir=
build_root=
artifact_file=
print_artifact=0
artifact_tmp=
download_tmp=
usage()
{
cat <<EOF
usage: $prog [options] [recipe]
Build the given recipe (default: ./recipe) into an SPS package archive.
options:
-o, --output DIR write the package to DIR
-C, --compression TYPE auto, none, gzip, xz, or zstd
-R, --build-root DIR run build phases with DIR/usr/bin and DIR/bin
first on PATH and expose it as \$SYSROOT
--no-download use only local sources and cached downloads
--keep-build retain build and staging directories
--artifact-file PATH write the package archive path to PATH
--print-artifact print the package archive path on stdout
-h, --help show this help
EOF
}
say()
{
printf '%s: %s\n' "$prog" "$*" >&2
}
die()
{
say "$*"
exit 1
}
die_status()
{
die_code=$1
shift
say "$*"
exit "$die_code"
}
cleanup()
{
cleanup_status=$?
trap - 0 HUP INT TERM
[ -z "$download_tmp" ] || rm -f "$download_tmp"
[ -z "$artifact_tmp" ] || rm -f "$artifact_tmp"
if [ "$keep_build" -eq 1 ]; then
[ -z "$build_dir" ] || say "build directory retained at $build_dir"
[ -z "$stage_dir" ] || say "stage directory retained at $stage_dir"
else
[ -z "$build_dir" ] || rm -rf "$build_dir"
[ -z "$stage_dir" ] || rm -rf "$stage_dir"
fi
exit "$cleanup_status"
}
trap cleanup 0
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
while [ "$#" -gt 0 ]; do
case $1 in
-o|--output)
[ "$#" -ge 2 ] || die_status 2 "$1 requires a directory"
output_dir=$2
shift 2
;;
--output=*)
output_dir=${1#*=}
[ -n "$output_dir" ] || die_status 2 "--output requires a directory"
shift
;;
-C|--compression)
[ "$#" -ge 2 ] || die_status 2 "$1 requires a type"
compression=$2
compression_set=1
shift 2
;;
--compression=*)
compression=${1#*=}
compression_set=1
[ -n "$compression" ] || die_status 2 "--compression requires a type"
shift
;;
--no-download)
no_download=1
shift
;;
-R|--build-root)
[ "$#" -ge 2 ] || die_status 2 "$1 requires a directory"
build_root=$2
shift 2
;;
--build-root=*)
build_root=${1#*=}
[ -n "$build_root" ] || die_status 2 "--build-root requires a directory"
shift
;;
--keep-build)
keep_build=1
shift
;;
--artifact-file)
[ "$#" -ge 2 ] || die_status 2 "$1 requires a path"
artifact_file=$2
[ -n "$artifact_file" ] || die_status 2 "--artifact-file requires a path"
shift 2
;;
--artifact-file=*)
artifact_file=${1#*=}
[ -n "$artifact_file" ] || die_status 2 "--artifact-file requires a path"
shift
;;
--print-artifact)
print_artifact=1
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
die_status 2 "unknown option: $1"
;;
*)
[ -z "$recipe" ] || die_status 2 "only one recipe may be specified"
recipe=$1
shift
;;
esac
done
if [ "$#" -gt 0 ]; then
[ -z "$recipe" ] || die_status 2 "only one recipe may be specified"
[ "$#" -eq 1 ] || die_status 2 "only one recipe may be specified"
recipe=$1
fi
[ -n "$recipe" ] || recipe=./recipe
if [ -d "$recipe" ]; then
recipe=$recipe/recipe
fi
[ -f "$recipe" ] || die_status 3 "recipe not found: $recipe"
[ -r "$recipe" ] || die_status 8 "recipe is not readable: $recipe"
if [ -n "$artifact_file" ]; then
case $artifact_file in
/*) ;;
*) artifact_file=$PWD/$artifact_file ;;
esac
artifact_parent=${artifact_file%/*}
[ -n "$artifact_parent" ] || artifact_parent=/
[ -d "$artifact_parent" ] ||
die_status 8 "--artifact-file directory does not exist: $artifact_parent"
if [ -e "$artifact_file" ] || [ -L "$artifact_file" ]; then
if [ -d "$artifact_file" ] && [ ! -L "$artifact_file" ]; then
die_status 8 "--artifact-file is a directory: $artifact_file"
fi
rm -f "$artifact_file" ||
die_status 8 "cannot replace artifact result file: $artifact_file"
fi
fi
script_dir=$(CDPATH= cd "$(dirname "$0")" 2>/dev/null && pwd) ||
die "cannot determine program directory"
common_file=
if [ -n "${SPS_LIBDIR:-}" ] && [ -r "$SPS_LIBDIR/common.sh" ]; then
common_file=$SPS_LIBDIR/common.sh
elif [ -r "$script_dir/../lib/common.sh" ]; then
# Running from the source tree.
common_file=$script_dir/../lib/common.sh
elif [ -r "$script_dir/../lib/sps/common.sh" ]; then
# Normal installed layout.
common_file=$script_dir/../lib/sps/common.sh
elif [ -r /usr/lib/sps/common.sh ]; then
common_file=/usr/lib/sps/common.sh
elif [ -r /usr/local/lib/sps/common.sh ]; then
common_file=/usr/local/lib/sps/common.sh
fi
[ -n "$common_file" ] || die "cannot locate lib/common.sh (set SPS_LIBDIR)"
# shellcheck source=../lib/common.sh
. "$common_file"
command -v sps_load_config >/dev/null 2>&1 ||
die "lib/common.sh does not provide sps_load_config"
sps_load_config
if [ "$compression_set" -eq 0 ]; then
compression=${SPS_COMPRESSION:-auto}
fi
case $compression in
zst) compression=zstd ;;
esac
case $compression in
auto|none|gzip|xz|zstd) ;;
*) die_status 2 "unsupported compression type '$compression'" ;;
esac
: "${SPS_ROOT:=/}"
: "${SPS_DB:=$SPS_ROOT/var/lib/sps}"
: "${SPS_CACHE:=$SPS_ROOT/var/cache/sps}"
: "${SPS_BUILD:=${TMPDIR:-/tmp}/sps}"
if [ -n "$build_root" ]; then
case $build_root in
/*) ;;
*) die_status 2 "--build-root must be an absolute path: $build_root" ;;
esac
build_root=${build_root%/}
[ -d "$build_root" ] || die_status 8 "build root does not exist: $build_root"
fi
makejobs=${MAKEJOBS:-${SPS_MAKEJOBS:-1}}
case $makejobs in
''|*[!0-9]*|0) die_status 2 "MAKEJOBS must be a positive integer" ;;
esac
command -v awk >/dev/null 2>&1 || die "awk is required"
command -v tar >/dev/null 2>&1 || die "tar is required"
command -v mktemp >/dev/null 2>&1 || die "mktemp is required"
recipe_dir=$(CDPATH= cd "$(dirname "$recipe")" 2>/dev/null && pwd) ||
die "cannot enter recipe directory"
recipe_file=$recipe_dir/$(basename "$recipe")
definition_sha256=$(sps_definition_sha256 "$recipe_file") ||
die_status 4 "invalid package definition support inputs"
default_arch=${SPS_ARCH:-}
if [ -z "$default_arch" ]; then
default_arch=$(uname -m 2>/dev/null) || die "cannot determine build architecture"
fi
mkdir -p "$SPS_BUILD/build" "$SPS_BUILD/stage" "$SPS_CACHE/sources" ||
die_status 8 "cannot create SPS build directories"
build_dir=$(mktemp -d "$SPS_BUILD/build/mkpkg.XXXXXX") ||
die_status 8 "cannot create a secure build directory"
stage_dir=$(mktemp -d "$SPS_BUILD/stage/mkpkg.XXXXXX") ||
die_status 8 "cannot create a secure stage directory"
records=$build_dir/recipe.records
parse_errors=$build_dir/recipe.errors
# Fold continued recipe lines into key/value records. Metadata stays data here;
# build commands are handled later.
if ! awk -v err="$parse_errors" '
function report(n, message) {
print FILENAME ":" n ": " message > err
bad = 1
}
function allowed(key) {
return key == "name" || key == "version" || key == "release" ||
key == "arch" || key == "description" || key == "source" ||
key == "hash" || key == "depend" || key == "builddep" ||
key == "optional" || key == "conflict" || key == "prepare" || key == "configure" ||
key == "build" || key == "install"
}
function emit(text, n, key, value, at) {
sub(/^[[:space:]]+/, "", text)
if (text == "" || text ~ /^#/) return
at = match(text, /[[:space:]]/)
if (!at) {
report(n, "expected a key followed by a value")
return
}
key = substr(text, 1, at - 1)
value = substr(text, at + 1)
sub(/^[[:space:]]+/, "", value)
sub(/[[:space:]]+$/, "", value)
if (!allowed(key)) {
report(n, "unknown recipe key \047" key "\047")
return
}
if (value == "") {
report(n, "empty value for \047" key "\047")
return
}
if (value ~ /\t/) {
report(n, "tab characters are not permitted in values")
return
}
print key "\t" value
}
{
line = $0
sub(/\r$/, "", line)
if (continued) {
sub(/^[[:space:]]+/, "", line)
logical = logical line
} else {
logical = line
start = NR
}
if (logical ~ /\\[[:space:]]*$/) {
sub(/\\[[:space:]]*$/, "", logical)
logical = logical " "
continued = 1
next
}
emit(logical, start)
logical = ""
continued = 0
}
END {
if (continued) report(start, "unterminated continuation")
if (bad) exit 1
}
' "$recipe_file" >"$records"; then
[ ! -s "$parse_errors" ] || cat "$parse_errors" >&2
die_status 4 "invalid recipe"
fi
# Read name/version/release/arch without sourcing the recipe.
expanded=$build_dir/recipe.expanded
validation_errors=$build_dir/validation.errors
if ! awk -F '\t' -v err="$validation_errors" -v default_arch="$default_arch" '
function report(message) {
print FILENAME ": " message > err
bad = 1
}
function replace(text, token, value, at, out) {
out = ""
while ((at = index(text, token)) != 0) {
out = out substr(text, 1, at - 1) value
text = substr(text, at + length(token))
}
return out text
}
function expand(value, pass, previous) {
for (pass = 1; pass <= 8; pass++) {
previous = value
value = replace(value, "${name}", value_for["name"])
value = replace(value, "${version}", value_for["version"])
value = replace(value, "${release}", value_for["release"])
value = replace(value, "${arch}", value_for["arch"])
if (value == previous)
break
}
return value
}
NR == FNR {
key = $1
value = substr($0, index($0, "\t") + 1)
if (key == "name" || key == "version" || key == "release" ||
key == "arch" || key == "description") {
count[key]++
if (count[key] > 1) report("duplicate \047" key "\047 record")
value_for[key] = value
}
if (key == "source") source_count++
if (key == "hash") hash_count++
if (key == "install") install_count++
next
}
{
if (!second_file) {
second_file = 1
if (!("arch" in value_for)) {
value_for["arch"] = default_arch
print "arch\t" default_arch
}
}
key = $1
value = substr($0, index($0, "\t") + 1)
value = expand(value)
print key "\t" value
}
END {
if (!("name" in value_for)) report("missing required \047name\047 record")
if (!("version" in value_for)) report("missing required \047version\047 record")
if (!("release" in value_for)) report("missing required \047release\047 record")
if (source_count != hash_count)
report("each source must have one corresponding hash")
if (!install_count) report("missing required \047install\047 command")
if (bad) exit 1
}
' "$records" "$records" >"$expanded"; then
[ ! -s "$validation_errors" ] || cat "$validation_errors" >&2
die_status 4 "invalid recipe"
fi
record_values()
{
awk -F '\t' -v wanted="$1" '
$1 == wanted { print substr($0, index($0, "\t") + 1) }
' "$expanded"
}
name=$(record_values name)
version=$(record_values version)
release=$(record_values release)
arch=$(record_values arch)
for field in name version release arch; do
case $field in
name) value=$name ;;
version) value=$version ;;
release) value=$release ;;
arch) value=$arch ;;
esac
if [ "$field" = name ]; then
case $value in
''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+-]*)
die_status 4 "invalid $field '$value'"
;;
esac
elif [ "$field" = arch ]; then
case $value in
''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+-]*)
die_status 4 "invalid $field '$value'"
;;
esac
else
case $value in
''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+~-]*)
die_status 4 "invalid $field '$value'"
;;
esac
fi
case $value in
[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*) ;;
*) die_status 4 "$field must begin with an alphanumeric character" ;;
esac
done
say "$name $version-$release"
sources=$build_dir/sources.list
hashes=$build_dir/source-hashes.list
record_values source >"$sources"
record_values hash >"$hashes"
if ! awk -v err="$validation_errors" '
{
value = $0
if (value !~ /^sha256:/) {
print "unsupported source hash \047" value "\047 (expected sha256:<hex>)" > err
bad = 1
next
}
hex = substr(value, 8)
if (length(hex) != 64 || hex !~ /^[0123456789abcdefABCDEF]+$/) {
print "invalid sha256 hash \047" value "\047" > err
bad = 1
}
}
END { if (bad) exit 1 }
' "$hashes"; then
cat "$validation_errors" >&2
die_status 4 "invalid recipe"
fi
sha256_file()
{
sps_sha256 "$1"
}
verify_source()
{
verify_file=$1
verify_record=$2
verify_expected=${verify_record#sha256:}
verify_expected=$(printf '%s\n' "$verify_expected" | tr 'ABCDEF' 'abcdef')
verify_actual=$(sha256_file "$verify_file") ||
die_status 10 "cannot hash source $verify_file"
verify_actual=$(printf '%s\n' "$verify_actual" | tr 'ABCDEF' 'abcdef')
if [ "$verify_actual" != "$verify_expected" ]; then
say "checksum mismatch for ${verify_file##*/}"
say "expected $verify_expected"
say "got $verify_actual"
exit 6
fi
say "checksum ok"
}
source_name()
{
source_value=$1
case $source_value in
http://*|https://*)
source_leaf=${source_value%%\?*}
source_leaf=${source_leaf%%\#*}
;;
file://*) source_leaf=${source_value#file://} ;;
*) source_leaf=$source_value ;;
esac
source_leaf=${source_leaf%/}
source_leaf=${source_leaf##*/}
case $source_leaf in
''|.|..|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+~-]*)
die_status 4 "source has an unsafe or unusable file name: $source_value"
;;
esac
printf '%s\n' "$source_leaf"
}
download_http()
{
download_url=$1
download_dest=$2
if command -v curl >/dev/null 2>&1; then
curl -fL --retry 3 --retry-delay 1 --progress-bar \
-o "$download_dest" "$download_url"
elif command -v wget >/dev/null 2>&1; then
wget -O "$download_dest" "$download_url"
else
die_status 9 "cannot download $download_url: curl or wget is required"
fi
}
source_dir=$build_dir/sources
unpack_dir=$build_dir/work
mkdir -p "$source_dir" "$unpack_dir"
source_pairs=$build_dir/source-pairs
awk 'NR == FNR { source[NR] = $0; next }
{ print source[FNR] "\t" $0 }' "$sources" "$hashes" >"$source_pairs"
acquired=$build_dir/acquired.list
: >"$acquired"
while IFS=' ' read -r source hash_record; do
[ -n "$source" ] || continue
leaf=$(source_name "$source")
destination=$source_dir/$leaf
if [ -e "$destination" ] || [ -L "$destination" ]; then
die_status 4 "duplicate source file name '$leaf'"
fi
case $source in
http://*|https://*)
expected=${hash_record#sha256:}
cache_file=$SPS_CACHE/sources/$expected-$leaf
if [ -f "$cache_file" ]; then
say "source cache hit $leaf"
verify_source "$cache_file" "$hash_record"
cp "$cache_file" "$destination" || die "cannot copy cached source $leaf"
else
[ "$no_download" -eq 0 ] ||
die_status 9 "source is not cached and downloads are disabled: $source"
say "fetching $leaf"
download_tmp=$(mktemp "$SPS_CACHE/sources/.download.XXXXXX") ||
die_status 8 "cannot create source download file"
if ! download_http "$source" "$download_tmp"; then
rm -f "$download_tmp"
download_tmp=
die_status 9 "download failed: $source"
fi
if ! verify_source "$download_tmp" "$hash_record"; then
rm -f "$download_tmp"
download_tmp=
exit 1
fi
chmod 644 "$download_tmp"
if ! mv "$download_tmp" "$cache_file"; then
rm -f "$download_tmp"
download_tmp=
die "cannot commit source to cache: $cache_file"
fi
download_tmp=
cp "$cache_file" "$destination" || die "cannot copy downloaded source $leaf"
fi
;;
file://*)
local_source=${source#file://}
[ -f "$local_source" ] || die_status 3 "local source not found: $source"
say "source $leaf"
cp "$local_source" "$destination" || die "cannot copy source $source"
verify_source "$destination" "$hash_record"
;;
*)
case $source in
/*) local_source=$source ;;
*) local_source=$recipe_dir/$source ;;
esac
[ -f "$local_source" ] || die_status 3 "local source not found: $source"
say "source $leaf"
cp "$local_source" "$destination" || die "cannot copy source $source"
verify_source "$destination" "$hash_record"
;;
esac
printf '%s\n' "$destination" >>"$acquired"
done <"$source_pairs"
for support in files patches; do
if [ -e "$recipe_dir/$support" ] || [ -L "$recipe_dir/$support" ]; then
[ -d "$recipe_dir/$support" ] && [ ! -L "$recipe_dir/$support" ] ||
die_status 4 "recipe $support must be a real directory"
if [ -e "$source_dir/$support" ] || [ -L "$source_dir/$support" ]; then
die_status 4 "source conflicts with recipe $support directory"
fi
cp -R "$recipe_dir/$support" "$source_dir/$support" ||
die "cannot copy recipe $support directory"
fi
done
archive_list()
{
archive=$1
case $archive in
*.tar) tar -tf "$archive" ;;
*.tar.gz|*.tgz) gzip -dc "$archive" | tar -tf - ;;
*.tar.xz|*.txz) xz -dc "$archive" | tar -tf - ;;
*.tar.zst|*.tzst) zstd -q -dc "$archive" | tar -tf - ;;
*.tar.bz2|*.tbz2) bzip2 -dc "$archive" | tar -tf - ;;
*) return 2 ;;
esac
}
archive_extract()
{
archive=$1
case $archive in
*.tar) tar -xf "$archive" -C "$unpack_dir" ;;
*.tar.gz|*.tgz) gzip -dc "$archive" | tar -xf - -C "$unpack_dir" ;;
*.tar.xz|*.txz) xz -dc "$archive" | tar -xf - -C "$unpack_dir" ;;
*.tar.zst|*.tzst) zstd -q -dc "$archive" | tar -xf - -C "$unpack_dir" ;;
*.tar.bz2|*.tbz2) bzip2 -dc "$archive" | tar -xf - -C "$unpack_dir" ;;
*) return 2 ;;
esac
}
while IFS= read -r archive; do
case $archive in
*.tar|*.tar.gz|*.tgz|*.tar.xz|*.txz|*.tar.zst|*.tzst|*.tar.bz2|*.tbz2)
members=$build_dir/archive.members
if ! archive_list "$archive" >"$members"; then
die "cannot list source archive ${archive##*/}"
fi
if ! awk '
{
path = $0
while (sub(/^\.\//, "", path)) { }
if (path == "" || path == ".") next
if (path ~ /^\// || path ~ /(^|\/)\.\.($|\/)/) {
print "unsafe archive member: " $0
bad = 1
}
}
END { if (bad) exit 1 }
' "$members" >"$build_dir/archive.errors"; then
cat "$build_dir/archive.errors" >&2
die_status 4 "refusing unsafe source archive ${archive##*/}"
fi
archive_extract "$archive" || die "cannot extract source archive ${archive##*/}"
;;
esac
done <"$acquired"
top_entries=$build_dir/top.entries
(CDPATH= cd "$unpack_dir" && find . ! -name . -prune -print) |
LC_ALL=C sort >"$top_entries"
top_count=$(awk 'END { print NR + 0 }' "$top_entries")
if [ "$top_count" -eq 1 ]; then
top_entry=$(sed -n '1p' "$top_entries")
top_entry=${top_entry#./}
if [ -d "$unpack_dir/$top_entry" ]; then
work_dir=$unpack_dir/$top_entry
else
work_dir=$unpack_dir
fi
else
work_dir=$unpack_dir
fi
# Write each build phase to a temporary script. These are the recipe fields we
# actually execute. The scripts live in a dedicated run directory rather than
# at the top of the build directory: names such as install.sh are exactly what
# autotools' auxiliary-directory heuristics look for, and a matching file in a
# parent directory would corrupt generated build systems.
mkdir -p "$build_dir/run"
for phase in prepare configure build install; do
record_values "$phase" >"$build_dir/run/$phase.sh"
done
PKG=$stage_dir
SRC=$source_dir
WORK=$work_dir
MAKEJOBS=$makejobs
export PKG SRC WORK MAKEJOBS
# Quote a path for a generated wrapper. SPS_ROOT is an absolute directory.
shell_quote()
{
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}
# Host tools must load the live disc's libraries, not SPS_ROOT. CPython's
# `make` imports just-built _sqlite3.so against the sqlite on the target;
# that needs LD_LIBRARY_PATH. gcc loading a half-installed libc from the
# disk would abort the compiler. Restore the caller's LD_LIBRARY_PATH.
write_host_ld_restore()
{
printf '%s\n' 'LD_LIBRARY_PATH=$SPS_HOST_LD_LIBRARY_PATH'
printf '%s\n' 'export LD_LIBRARY_PATH'
printf '%s\n' '[ -n "$LD_LIBRARY_PATH" ] || unset LD_LIBRARY_PATH'
}
# gcc on PATH must inject -L/-isystem itself. Autoconf often runs `gcc`
# without LDFLAGS, which is how alsa-utils printed "No linkable libasound
# was found" after alsa-lib was already in SPS_ROOT.
write_cc_wrapper()
{
wrapper_path=$1
real_cc=$2
[ -n "$real_cc" ] && [ -x "$real_cc" ] || return 0
{
printf '%s\n' '#!/bin/sh'
write_host_ld_restore
printf 'exec %s -isystem %s -L%s -L%s -L%s -L%s -Wl,-rpath-link,%s -Wl,-rpath-link,%s -Wl,-rpath-link,%s -Wl,-rpath-link,%s "$@"\n' \
"$(shell_quote "$real_cc")" \
"$(shell_quote "$target_inc")" \
"$(shell_quote "$target_root/usr/lib64")" \
"$(shell_quote "$target_root/usr/lib")" \
"$(shell_quote "$target_root/lib64")" \
"$(shell_quote "$target_root/lib")" \
"$(shell_quote "$target_root/usr/lib64")" \
"$(shell_quote "$target_root/usr/lib")" \
"$(shell_quote "$target_root/lib64")" \
"$(shell_quote "$target_root/lib")"
} >"$wrapper_path"
chmod 755 "$wrapper_path"
}
write_host_tool_wrapper()
{
wrapper_path=$1
real_tool=$2
[ -n "$real_tool" ] && [ -x "$real_tool" ] || return 0
{
printf '%s\n' '#!/bin/sh'
write_host_ld_restore
case ${wrapper_path##*/} in
python|python3)
printf '%s\n' 'unset PYTHONHOME PYTHONPATH'
;;
esac
printf 'exec %s "$@"\n' "$(shell_quote "$real_tool")"
} >"$wrapper_path"
chmod 755 "$wrapper_path"
}
# A controlled build root runs each phase with the root's /usr/bin and /bin
# ahead of the host PATH and exposes the root as $SYSROOT. Recipes building a
# staged toolchain use this to ensure their compiler, linker, and headers are
# taken from the target root rather than the host, mirroring an LFS sysroot.
wrapper_bin=
if [ -n "${SPS_ROOT:-}" ] && [ "$SPS_ROOT" != / ]; then
wrapper_bin=$build_dir/run
fi
if [ -n "$build_root" ]; then
if [ -n "$wrapper_bin" ]; then
run_env="PATH=$wrapper_bin:$build_root/usr/bin:$build_root/bin:$PATH SYSROOT=$build_root"
else
run_env="PATH=$build_root/usr/bin:$build_root/bin:$PATH SYSROOT=$build_root"
fi
say "using build root $build_root"
elif [ -n "$wrapper_bin" ]; then
run_env="PATH=$wrapper_bin:${SPS_ROOT%/}/usr/bin:$PATH"
else
run_env=
fi
# Live setup builds into SPS_ROOT (a mounted disk) with the live compiler.
# Autoconf's configure looks up GNU m4 on PATH. m4 is a runtime depend, so
# sget installs it into the target before autoconf builds; that bin dir
# must be on PATH. The live disc also seeds /usr/bin/m4.
# The live disc copies host /usr/include (so ALSA headers exist) but not
# host libasound. gcc must search packages pkin already unpacked into the
# target, not only live /usr. Exporting LDFLAGS is not enough: autoconf
# AC_CHECK_LIB often invokes `gcc conftest.c -lasound` with a cleared
# environment. Wrappers named gcc/g++ on PATH inject -L/-isystem instead.
# CPython then *runs* the new _sqlite3.so (check_extension_modules) and
# renames it to _sqlite3_failed if the loader picked the live disc's
# older libsqlite3. LD_LIBRARY_PATH must include SPS_ROOT for that
# import. Host gcc/python3/meson/git wrappers restore the previous
# LD_LIBRARY_PATH so they do not load a half-installed libc from the disk.
if [ -n "${SPS_ROOT:-}" ] && [ "$SPS_ROOT" != / ]; then
target_root=$SPS_ROOT
target_inc=$target_root/usr/include
target_pc=$target_root/usr/lib64/pkgconfig
target_pc=$target_pc:$target_root/usr/lib/pkgconfig
target_pc=$target_pc:$target_root/usr/share/pkgconfig
target_pc=$target_pc:$target_root/lib64/pkgconfig
target_pc=$target_pc:$target_root/lib/pkgconfig
target_libpath=$target_root/usr/lib64:$target_root/usr/lib
target_libpath=$target_libpath:$target_root/lib64:$target_root/lib
target_cflags="-I$target_inc"
target_ldflags="-L$target_root/usr/lib64 -L$target_root/usr/lib"
target_ldflags="$target_ldflags -L$target_root/lib64 -L$target_root/lib"
target_ldflags="$target_ldflags -Wl,-rpath-link,$target_root/usr/lib64"
target_ldflags="$target_ldflags -Wl,-rpath-link,$target_root/usr/lib"
PKG_CONFIG_SYSROOT_DIR=$target_root
PKG_CONFIG_LIBDIR=$target_pc
PKG_CONFIG_PATH=$target_pc${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}
PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
PKG_CONFIG_ALLOW_SYSTEM_LIBS=1
export PKG_CONFIG_SYSROOT_DIR PKG_CONFIG_LIBDIR PKG_CONFIG_PATH \
PKG_CONFIG_ALLOW_SYSTEM_CFLAGS PKG_CONFIG_ALLOW_SYSTEM_LIBS
CPATH=$target_inc${CPATH:+:$CPATH}
C_INCLUDE_PATH=$target_inc${C_INCLUDE_PATH:+:$C_INCLUDE_PATH}
CPLUS_INCLUDE_PATH=$target_inc${CPLUS_INCLUDE_PATH:+:$CPLUS_INCLUDE_PATH}
LIBRARY_PATH=$target_libpath${LIBRARY_PATH:+:$LIBRARY_PATH}
SPS_HOST_LD_LIBRARY_PATH=${LD_LIBRARY_PATH-}
SPS_TARGET_LIBRARY_PATH=$target_libpath
LD_LIBRARY_PATH=$target_libpath${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
export CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH LIBRARY_PATH \
SPS_HOST_LD_LIBRARY_PATH SPS_TARGET_LIBRARY_PATH LD_LIBRARY_PATH
# OpenSSL 3 looks up providers by a compiled-in MODULESDIR (/usr/lib64
# on the live disc). A just-built _ssl.so must load the target's
# modules, not the live ones, or CPython renames it *_failed.so.
if [ -d "$target_root/usr/lib64/ossl-modules" ]; then
OPENSSL_MODULES=$target_root/usr/lib64/ossl-modules
elif [ -d "$target_root/usr/lib/ossl-modules" ]; then
OPENSSL_MODULES=$target_root/usr/lib/ossl-modules
fi
if [ -n "${OPENSSL_MODULES:-}" ]; then
export OPENSSL_MODULES
fi
if [ -f "$target_root/etc/ssl/openssl.cnf" ]; then
OPENSSL_CONF=$target_root/etc/ssl/openssl.cnf
export OPENSSL_CONF
fi
CPPFLAGS="$target_cflags${CPPFLAGS:+ $CPPFLAGS}"
CFLAGS="$target_cflags${CFLAGS:+ $CFLAGS}"
CXXFLAGS="$target_cflags${CXXFLAGS:+ $CXXFLAGS}"
LDFLAGS="$target_ldflags${LDFLAGS:+ $LDFLAGS}"
export CPPFLAGS CFLAGS CXXFLAGS LDFLAGS
CMAKE_PREFIX_PATH=$target_root/usr${CMAKE_PREFIX_PATH:+:$CMAKE_PREFIX_PATH}
CMAKE_INCLUDE_PATH=$target_inc${CMAKE_INCLUDE_PATH:+:$CMAKE_INCLUDE_PATH}
CMAKE_LIBRARY_PATH=$target_libpath${CMAKE_LIBRARY_PATH:+:$CMAKE_LIBRARY_PATH}
export CMAKE_PREFIX_PATH CMAKE_INCLUDE_PATH CMAKE_LIBRARY_PATH
ACLOCAL_PATH=$target_root/usr/share/aclocal${ACLOCAL_PATH:+:$ACLOCAL_PATH}
export ACLOCAL_PATH
mkdir -p "$build_dir/run"
# Host gcc/python must be the live disc's, not a copy just unpacked
# into SPS_ROOT. Node's configure imports subprocess; a relocated
# target python3 without platlibdir cannot find _posixsubprocess.
host_path=
host_path_rest=$PATH
while [ -n "$host_path_rest" ]
do
host_dir=${host_path_rest%%:*}
if [ "$host_path_rest" = "$host_dir" ]; then
host_path_rest=
else
host_path_rest=${host_path_rest#*:}
fi
[ -n "$host_dir" ] || continue
case $host_dir in
"$target_root"|"$target_root"/*) continue ;;
esac
if [ -n "$host_path" ]; then
host_path=$host_path:$host_dir
else
host_path=$host_dir
fi
done
if [ -n "$build_root" ]; then
host_path=$build_root/usr/bin:$build_root/bin${host_path:+:$host_path}
fi
[ -n "$host_path" ] || host_path=/usr/bin:/bin
host_cc=$(PATH=$host_path command -v gcc 2>/dev/null) || host_cc=
host_cxx=$(PATH=$host_path command -v g++ 2>/dev/null) || host_cxx=
host_cc_alt=$(PATH=$host_path command -v cc 2>/dev/null) || host_cc_alt=
host_cxx_alt=$(PATH=$host_path command -v c++ 2>/dev/null) || host_cxx_alt=
write_cc_wrapper "$build_dir/run/gcc" "$host_cc"
write_cc_wrapper "$build_dir/run/g++" "$host_cxx"
if [ -z "$host_cc_alt" ] || [ "$host_cc_alt" = "$host_cc" ]; then
write_cc_wrapper "$build_dir/run/cc" "$host_cc"
else
write_cc_wrapper "$build_dir/run/cc" "$host_cc_alt"
fi
if [ -z "$host_cxx_alt" ] || [ "$host_cxx_alt" = "$host_cxx" ]; then
write_cc_wrapper "$build_dir/run/c++" "$host_cxx"
else
write_cc_wrapper "$build_dir/run/c++" "$host_cxx_alt"
fi
if [ -x "$build_dir/run/gcc" ]; then
CC=$build_dir/run/gcc
export CC
fi
if [ -x "$build_dir/run/g++" ]; then
CXX=$build_dir/run/g++
export CXX
fi
for host_tool in python3 python meson cmake git curl wget perl; do
host_tool_path=$(PATH=$host_path command -v "$host_tool" 2>/dev/null) ||
host_tool_path=
write_host_tool_wrapper "$build_dir/run/$host_tool" "$host_tool_path"
done
# Autoconf sources CONFIG_SITE before AC_CHECK_LIB. AM_PATH_ALSA
# (alsa-utils) only adds -L when --with-alsa-prefix is set; this
# covers packages that do not have that switch.
{
printf '%s\n' '# generated by mkpkg for SPS_ROOT'
printf ': "${CPPFLAGS=%s}"\n' "$target_cflags"
printf ': "${CFLAGS=%s}"\n' "$target_cflags"
printf ': "${CXXFLAGS=%s}"\n' "$target_cflags"
printf ': "${LDFLAGS=%s}"\n' "$target_ldflags"
} >"$build_dir/run/config.site"
CONFIG_SITE=$build_dir/run/config.site
export CONFIG_SITE
say "using $target_root for headers, libraries, and pkg-config"
fi
run_phase()
{
phase_name=$1
phase_file=$build_dir/run/$phase_name.sh
[ -s "$phase_file" ] || return 0
say "running $phase_name phase"
if ! (
CDPATH=
cd "$WORK" || exit 1
if [ -n "$build_root" ]; then
PATH=${wrapper_bin:+$wrapper_bin:}$build_root/usr/bin:$build_root/bin:$PATH
SYSROOT=$build_root
export PATH SYSROOT
elif [ -n "$wrapper_bin" ]; then
PATH=$wrapper_bin:${SPS_ROOT%/}/usr/bin:$PATH
export PATH
fi
sh -eu "$phase_file"
); then
die_status 10 "$phase_name phase failed"
fi
}
printf '\n' >&2
run_phase prepare
run_phase configure
run_phase build
run_phase install
# -lfoo needs libfoo.so. Some DESTDIR installs only ship libfoo.so.N, or
# leave a GNU ld script whose GROUP path is /usr/lib64 on the live ISO.
if [ -d "$stage_dir" ]; then
find "$stage_dir" \( -type f -o -type l \) -name 'lib*.so.[0-9]*' |
while IFS= read -r sofile || [ -n "$sofile" ]
do
[ -n "$sofile" ] || continue
soname=${sofile##*/}
so_dir=${sofile%/*}
base=${soname%%.so.*}.so
[ "$base" != "$soname" ] || continue
link=$so_dir/$base
if [ -f "$link" ] && [ ! -L "$link" ] &&
grep -qE 'GROUP|INPUT' "$link" 2>/dev/null
then
ln -sf "$soname" "$link"
continue
fi
if [ ! -e "$link" ] && [ ! -L "$link" ]; then
ln -sf "$soname" "$link"
fi
done
fi
if [ -e "$stage_dir/.SPS" ] || [ -L "$stage_dir/.SPS" ]; then
die_status 10 "install phase attempted to provide reserved .SPS metadata"
fi
# Hooks live beside the recipe. Only the four documented hook names are valid.
if [ -e "$recipe_dir/hooks" ] || [ -L "$recipe_dir/hooks" ]; then
[ -d "$recipe_dir/hooks" ] && [ ! -L "$recipe_dir/hooks" ] ||
die_status 4 "recipe hooks must be a real directory"
hook_list=$build_dir/hooks.list
(CDPATH= cd "$recipe_dir/hooks" && find . ! -name . -prune -print) >"$hook_list"
while IFS= read -r hook_entry || [ -n "$hook_entry" ]; do
hook_name=${hook_entry#./}
case $hook_name in
pre-install|post-install|pre-remove|post-remove) ;;
*) die_status 4 "unsupported recipe hook '$hook_name'" ;;
esac
[ -f "$recipe_dir/hooks/$hook_name" ] && [ ! -L "$recipe_dir/hooks/$hook_name" ] ||
die_status 4 "hook '$hook_name' must be a regular file"
done <"$hook_list"
fi
special_files=$build_dir/special-files
(CDPATH= cd "$stage_dir" &&
find . ! -type d ! -type f ! -type l -print) >"$special_files"
if [ -s "$special_files" ]; then
sed 's#^./##; s#^#unsupported staged file type: #' "$special_files" >&2
die_status 10 "package contains unsupported filesystem objects"
fi
payload_count=$(CDPATH= cd "$stage_dir" && find . ! -name . -prune -print |
awk 'END { print NR + 0 }')
[ "$payload_count" -gt 0 ] || die_status 10 "install phase produced an empty package"
mkdir "$stage_dir/.SPS" || die "cannot create package metadata directory"
if [ -d "$recipe_dir/hooks" ]; then
mkdir "$stage_dir/.SPS/hooks" || die "cannot create hook metadata directory"
for hook_name in pre-install post-install pre-remove post-remove; do
[ -f "$recipe_dir/hooks/$hook_name" ] || continue
cp "$recipe_dir/hooks/$hook_name" "$stage_dir/.SPS/hooks/$hook_name" ||
die "cannot copy hook '$hook_name'"
chmod 0644 "$stage_dir/.SPS/hooks/$hook_name" ||
die "cannot set hook mode '$hook_name'"
done
fi
meta=$stage_dir/.SPS/meta
files=$stage_dir/.SPS/files
file_hashes=$stage_dir/.SPS/hashes
awk -F '\t' -v definition_sha256="$definition_sha256" '
BEGIN {
print "format\t1"
print "definition_sha256\t" definition_sha256
}
$1 == "name" || $1 == "version" || $1 == "release" || $1 == "arch" ||
$1 == "description" {
scalar[$1] = $0
next
}
$1 == "source" || $1 == "hash" || $1 == "depend" ||
$1 == "builddep" || $1 == "optional" || $1 == "conflict" {
repeated[++count] = $0
}
END {
key[1] = "name"
key[2] = "version"
key[3] = "release"
key[4] = "arch"
key[5] = "description"
for (i = 1; i <= 5; i++)
if (key[i] in scalar) print scalar[key[i]]
for (i = 1; i <= count; i++) print repeated[i]
}
' "$expanded" >"$meta"
(
CDPATH= cd "$stage_dir"
find . -type d -print | awk '
$0 != "." && $0 !~ /^\.\/\.SPS($|\/)/ {
sub(/^\.\//, "")
print $0 "/"
}'
find . ! -type d -print | awk '
$0 !~ /^\.\/\.SPS($|\/)/ {
sub(/^\.\//, "")
print
}'
) | LC_ALL=C sort >"$files"
: >"$file_hashes"
while IFS= read -r payload; do
case $payload in
*/) continue ;;
esac
payload_file=$stage_dir/$payload
if [ -f "$payload_file" ] && [ ! -L "$payload_file" ]; then
payload_hash=$(sha256_file "$payload_file") ||
die_status 10 "cannot hash staged file $payload"
printf 'sha256\t%s\t%s\n' "$payload_hash" "$payload" >>"$file_hashes"
fi
done <"$files"
if [ "$compression" = auto ]; then
if command -v zstd >/dev/null 2>&1; then
compression=zstd
elif command -v xz >/dev/null 2>&1; then
compression=xz
elif command -v gzip >/dev/null 2>&1; then
compression=gzip
else
compression=none
fi
fi
case $compression in
zstd) command -v zstd >/dev/null 2>&1 || die_status 10 "zstd compression requested but zstd is unavailable" ;;
xz) command -v xz >/dev/null 2>&1 || die_status 10 "xz compression requested but xz is unavailable" ;;
gzip) command -v gzip >/dev/null 2>&1 || die_status 10 "gzip compression requested but gzip is unavailable" ;;
esac
mkdir -p "$output_dir" || die "cannot create output directory: $output_dir"
output_dir=$(CDPATH= cd "$output_dir" 2>/dev/null && pwd) ||
die "cannot enter output directory"
base=$name-$version-$release-$arch.pkg.tar
case $compression in
none) suffix= ;;
gzip) suffix=.gz ;;
xz) suffix=.xz ;;
zstd) suffix=.zst ;;
esac
artifact=$output_dir/$base$suffix
[ ! -e "$artifact" ] || die_status 7 "output package already exists: $artifact"
plain_archive=$build_dir/$base
if ! tar -cf "$plain_archive" -C "$stage_dir" .; then
die_status 10 "cannot create package archive"
fi
output_tmp=$(mktemp "$output_dir/.mkpkg.XXXXXX") ||
die_status 8 "cannot create temporary output file"
case $compression in
none)
if ! cp "$plain_archive" "$output_tmp"; then
rm -f "$output_tmp"
die_status 10 "cannot write package archive"
fi
;;
gzip)
if ! gzip -n -c "$plain_archive" >"$output_tmp"; then
rm -f "$output_tmp"
die_status 10 "gzip compression failed"
fi
;;
xz)
if ! xz -c "$plain_archive" >"$output_tmp"; then
rm -f "$output_tmp"
die_status 10 "xz compression failed"
fi
;;
zstd)
if ! zstd -q -c "$plain_archive" >"$output_tmp"; then
rm -f "$output_tmp"
die_status 10 "zstd compression failed"
fi
;;
esac
chmod 644 "$output_tmp"
mv "$output_tmp" "$artifact" || die "cannot commit package archive: $artifact"
# sget needs the package path without swallowing build output.
if [ -n "$artifact_file" ]; then
artifact_tmp=$(mktemp "$artifact_file.XXXXXX") ||
die_status 8 "cannot create artifact result file"
if ! printf '%s\n' "$artifact" >"$artifact_tmp"; then
rm -f "$artifact_tmp"
artifact_tmp=
die_status 8 "cannot write artifact result file"
fi
chmod 644 "$artifact_tmp"
mv "$artifact_tmp" "$artifact_file" ||
die "cannot commit artifact result file: $artifact_file"
artifact_tmp=
fi
say "built ${artifact##*/}"
if [ "$print_artifact" -eq 1 ]; then
printf '%s\n' "$artifact"
fi