Files
live_recorder/scripts/build-fnos-package.sh
T

223 lines
8.0 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
VERSION=1.2.13
OUTPUT="${1:-$ROOT_DIR/artifacts/fnos/liverecorder-${VERSION}-x86_64.fpk}"
WORKSPACE_CACHE=$(CDPATH= cd -- "$ROOT_DIR/.." && pwd)
DOTNET_BIN="${DOTNET:-$WORKSPACE_CACHE/.dotnet8/dotnet}"
NUGET_FEED="${LIVERECORDER_NUGET_FEED:-$WORKSPACE_CACHE/.nuget-feed}"
NUGET_PACKAGES="${NUGET_PACKAGES:-$WORKSPACE_CACHE/.nuget-packages}"
DOTNET_CLI_HOME="${DOTNET_CLI_HOME:-$WORKSPACE_CACHE/.dotnet-cli-home}"
BUILD_TMP_ROOT="${LIVERECORDER_BUILD_TMPDIR:-$WORKSPACE_CACHE/.fnos-build-tmp}"
if [ -n "${FNPACK:-}" ]; then
FNPACK_BIN="$FNPACK"
elif [ -x "$ROOT_DIR/.tools/fnpack" ]; then
FNPACK_BIN="$ROOT_DIR/.tools/fnpack"
else
FNPACK_BIN=fnpack
fi
FNPACK_BIN=$(command -v "$FNPACK_BIN") || {
printf 'fnpack is required; install it from the fnOS developer portal or set FNPACK.\n' >&2
exit 1
}
for command_name in npm apt-get curl dpkg-deb readelf realpath find install sha256sum tar node; do
command -v "$command_name" >/dev/null 2>&1 || {
printf 'required build command is missing: %s\n' "$command_name" >&2
exit 1
}
done
test -x "$DOTNET_BIN" || { printf 'missing .NET SDK: %s\n' "$DOTNET_BIN" >&2; exit 1; }
test -d "$NUGET_FEED" || { printf 'missing offline NuGet feed: %s\n' "$NUGET_FEED" >&2; exit 1; }
mkdir -p "$BUILD_TMP_ROOT" "$(dirname -- "$OUTPUT")"
WORK_DIR=$(mktemp -d "${BUILD_TMP_ROOT%/}/liverecorder-fnos-build.XXXXXX")
trap 'rm -rf -- "$WORK_DIR"' EXIT
STAGE="$WORK_DIR/stage"
PACKED_ROOT="$WORK_DIR/packed"
FNPACK_TMP_ROOT="$WORK_DIR/fnpack-tmp"
EXTRACT_ROOT="$WORK_DIR/debian-root"
RUNTIME_ROOT="$STAGE/app/runtime"
mkdir -p "$STAGE/app/server" "$RUNTIME_ROOT/bin" "$RUNTIME_ROOT/lib" \
"$RUNTIME_ROOT/etc/ssl/certs" \
"$PACKED_ROOT" "$FNPACK_TMP_ROOT" "$EXTRACT_ROOT"
cp -a "$ROOT_DIR/fnos/." "$STAGE/"
printf 'Building frontend...\n'
npm run build --prefix "$ROOT_DIR/frontend"
printf 'Publishing self-contained .NET application...\n'
export NUGET_PACKAGES DOTNET_CLI_HOME
"$DOTNET_BIN" restore "$ROOT_DIR/src/LiveRecorder.WebApi/LiveRecorder.WebApi.csproj" \
-r linux-x64 \
--source "$NUGET_FEED" \
--source "https://api.nuget.org/v3/index.json" \
--disable-parallel
"$DOTNET_BIN" publish "$ROOT_DIR/src/LiveRecorder.WebApi/LiveRecorder.WebApi.csproj" \
-c Release \
-r linux-x64 \
--self-contained true \
--no-restore \
-p:DebugType=None \
-p:DebugSymbols=false \
-p:PublishSingleFile=false \
-p:PublishReadyToRun=false \
-o "$STAGE/app/server" \
/maxcpucount:1
rm -f "$STAGE/app/server/"*.pdb
mkdir -p "$STAGE/app/server/wwwroot"
cp -a "$ROOT_DIR/frontend/dist/." "$STAGE/app/server/wwwroot/"
printf 'Downloading pinned Debian Bookworm native runtime packages...\n'
APT_ROOT="$WORK_DIR/apt"
mkdir -p \
"$APT_ROOT/etc/apt" \
"$APT_ROOT/var/lib/apt/lists/partial" \
"$APT_ROOT/var/lib/dpkg" \
"$APT_ROOT/var/cache/apt/archives/partial"
cp "$ROOT_DIR/scripts/fnos-bookworm.sources.list" "$APT_ROOT/etc/apt/sources.list"
touch "$APT_ROOT/var/lib/dpkg/status"
APT_OPTIONS=(
-o "Dir::Etc::sourcelist=$APT_ROOT/etc/apt/sources.list"
-o "Dir::Etc::sourceparts=-"
-o "Dir::State::status=$APT_ROOT/var/lib/dpkg/status"
-o "Dir::State::lists=$APT_ROOT/var/lib/apt/lists"
-o "Dir::Cache=$APT_ROOT/var/cache/apt"
-o "Dir::Cache::archives=$APT_ROOT/var/cache/apt/archives"
-o "Debug::NoLocking=1"
-o "APT::Architecture=amd64"
-o "Acquire::Languages=none"
)
apt-get "${APT_OPTIONS[@]}" update
apt-get "${APT_OPTIONS[@]}" \
--download-only \
--no-install-recommends \
--yes \
install \
curl \
ca-certificates
shopt -s nullglob
packages=("$APT_ROOT"/var/cache/apt/archives/*.deb)
test "${#packages[@]}" -gt 0 || { printf 'APT did not download runtime packages\n' >&2; exit 1; }
for package_file in "${packages[@]}"; do
dpkg-deb -x "$package_file" "$EXTRACT_ROOT"
done
shopt -u nullglob
resolve_extracted_file() {
local current=$1 target normalized depth=0
while [ -L "$current" ]; do
depth=$((depth + 1))
[ "$depth" -le 32 ] || { printf 'too many symlink levels: %s\n' "$1" >&2; return 1; }
target=$(readlink -- "$current")
case "$target" in
/*) current="$EXTRACT_ROOT$target" ;;
*) current="$(dirname -- "$current")/$target" ;;
esac
normalized=$(realpath -m -- "$current")
case "$normalized" in
"$EXTRACT_ROOT"/*) current="$normalized" ;;
*) printf 'Debian package link escapes extraction root: %s\n' "$1" >&2; return 1 ;;
esac
done
[ -f "$current" ] || { printf 'missing extracted runtime file: %s\n' "$1" >&2; return 1; }
printf '%s\n' "$current"
}
copy_extracted_file() {
local source resolved destination=$2 mode=${3:-0644}
source=$1
resolved=$(resolve_extracted_file "$source")
mkdir -p "$(dirname -- "$destination")"
install -m "$mode" "$resolved" "$destination"
}
printf 'Assembling minimal relocatable runtime...\n'
copy_extracted_file "$EXTRACT_ROOT/usr/bin/curl" "$RUNTIME_ROOT/bin/curl" 0755
CA_CONFIG="$EXTRACT_ROOT/etc/ca-certificates.conf"
CA_SOURCE_ROOT="$EXTRACT_ROOT/usr/share/ca-certificates"
CA_BUNDLE="$RUNTIME_ROOT/etc/ssl/certs/ca-certificates.crt"
: >"$CA_BUNDLE"
if [ -s "$CA_CONFIG" ]; then
while IFS= read -r certificate; do
case "$certificate" in
''|'#'*|'!'*) continue ;;
esac
[ -f "$CA_SOURCE_ROOT/$certificate" ] || continue
sed -e '$a\' "$CA_SOURCE_ROOT/$certificate" >>"$CA_BUNDLE"
done <"$CA_CONFIG"
else
while IFS= read -r -d '' certificate; do
sed -e '$a\' "$certificate" >>"$CA_BUNDLE"
done < <(find "$CA_SOURCE_ROOT" -type f -name '*.crt' -print0 | sort -z)
fi
[ -s "$CA_BUNDLE" ] || { printf 'unable to assemble CA certificate bundle\n' >&2; exit 1; }
chmod 0644 "$CA_BUNDLE"
declare -A COPIED_LIBRARIES=()
ELF_QUEUE=()
while IFS= read -r -d '' elf_file; do
if readelf -h "$elf_file" >/dev/null 2>&1; then
ELF_QUEUE+=("$elf_file")
fi
done < <(find "$RUNTIME_ROOT/bin" -type f -print0)
is_system_glibc_library() {
case "$1" in
ld-linux-*.so.*|libc.so.*|libBrokenLocale.so.*|libanl.so.*|libdl.so.*|libm.so.*|libmvec.so.*|libnss_compat.so.*|libnss_dns.so.*|libnss_files.so.*|libnss_hesiod.so.*|libpthread.so.*|libresolv.so.*|librt.so.*|libthread_db.so.*|libutil.so.*)
return 0
;;
*) return 1 ;;
esac
}
queue_index=0
while [ "$queue_index" -lt "${#ELF_QUEUE[@]}" ]; do
elf_file=${ELF_QUEUE[$queue_index]}
queue_index=$((queue_index + 1))
while IFS= read -r needed; do
[ -n "$needed" ] || continue
is_system_glibc_library "$needed" && continue
[ -z "${COPIED_LIBRARIES[$needed]:-}" ] || continue
source=$(find "$EXTRACT_ROOT" \( -type f -o -type l \) -name "$needed" -print -quit)
[ -n "$source" ] || {
printf 'unable to resolve native dependency %s required by %s\n' "$needed" "$elf_file" >&2
exit 1
}
destination="$RUNTIME_ROOT/lib/$needed"
copy_extracted_file "$source" "$destination" 0755
COPIED_LIBRARIES[$needed]=1
ELF_QUEUE+=("$destination")
done < <(readelf -d "$elf_file" 2>/dev/null | sed -n 's/.*Shared library: \[\([^]]*\)\].*/\1/p')
done
for required_file in \
"$RUNTIME_ROOT/bin/curl"; do
test -x "$required_file" || { printf 'native runtime file is missing: %s\n' "$required_file" >&2; exit 1; }
done
node "$ROOT_DIR/scripts/generate-fnos-icons.mjs" "$STAGE" "$STAGE/app/ui/images"
chmod 0755 "$STAGE/cmd/"*
printf 'Packing with official fnOS fnpack...\n'
(
cd "$PACKED_ROOT"
TMPDIR="$FNPACK_TMP_ROOT" "$FNPACK_BIN" build --directory "$STAGE"
)
mv "$PACKED_ROOT/liverecorder.fpk" "$OUTPUT"
(
cd "$(dirname -- "$OUTPUT")"
sha256sum "$(basename -- "$OUTPUT")" >"$(basename -- "$OUTPUT").sha256"
)
LIVERECORDER_VERIFY_TMPDIR="${LIVERECORDER_VERIFY_TMPDIR:-$BUILD_TMP_ROOT}" \
"$ROOT_DIR/scripts/verify-fnos-package.sh" "$OUTPUT"
printf 'Built %s\n' "$OUTPUT"