fix: rebuild native fnOS package with fnpack

This commit is contained in:
2026-08-02 17:18:02 +08:00
parent e8772a39b5
commit de9f5ae110
8 changed files with 333 additions and 105 deletions
+169 -58
View File
@@ -2,7 +2,9 @@
set -euo pipefail
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
VERSION=1.0.0
VERSION=1.0.1
NODE_VERSION=22.18.0
NODE_ARCHIVE_SHA256=c1bfeecf1d7404fa74728f9db72e697decbd8119ccc6f5a294d795756dfcfca7
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}"
@@ -10,13 +12,21 @@ 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}"
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$ROOT_DIR" show -s --format=%ct HEAD)}"
mkdir -p "$BUILD_TMP_ROOT" "$(dirname -- "$OUTPUT")"
WORK_DIR=$(mktemp -d "${BUILD_TMP_ROOT%/}/liverecorder-fnos-build.XXXXXX")
trap 'rm -rf -- "$WORK_DIR"' EXIT
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
for command_name in npm apt-get dpkg-deb tar md5sum sha256sum node; do
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 xz node; do
command -v "$command_name" >/dev/null 2>&1 || {
printf 'required build command is missing: %s\n' "$command_name" >&2
exit 1
@@ -25,6 +35,21 @@ 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/usr/lib/postgresql/15/bin" "$RUNTIME_ROOT/usr/lib/postgresql/15/lib" \
"$RUNTIME_ROOT/usr/share/postgresql/15" "$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"
@@ -44,11 +69,11 @@ export NUGET_PACKAGES DOTNET_CLI_HOME
-p:DebugSymbols=false \
-p:PublishSingleFile=false \
-p:PublishReadyToRun=false \
-o "$WORK_DIR/payload/server" \
-o "$STAGE/app/server" \
/maxcpucount:1
rm -f "$WORK_DIR/payload/server/"*.pdb
mkdir -p "$WORK_DIR/payload/server/wwwroot"
cp -a "$ROOT_DIR/frontend/dist/." "$WORK_DIR/payload/server/wwwroot/"
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"
@@ -65,6 +90,7 @@ APT_OPTIONS=(
-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"
@@ -78,75 +104,160 @@ apt-get "${APT_OPTIONS[@]}" \
install \
postgresql-15 \
postgresql-client-15 \
nodejs \
ffmpeg \
curl \
ca-certificates
RUNTIME_ROOT="$WORK_DIR/payload/runtime"
mkdir -p "$RUNTIME_ROOT"
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
case "$(basename -- "$package_file")" in
libc6_*|libc-bin_*)
# Native programs must use the fnOS glibc/loader as one matched
# pair. Bundling Debian's libc while an executable still starts
# through the host loader can crash before main() on newer fnOS
# releases. All other runtime libraries remain private to the app.
continue
;;
esac
dpkg-deb -x "$package_file" "$RUNTIME_ROOT"
dpkg-deb -x "$package_file" "$EXTRACT_ROOT"
done
shopt -u nullglob
for forbidden_glibc_file in \
"$RUNTIME_ROOT/lib/x86_64-linux-gnu/libc.so.6" \
"$RUNTIME_ROOT/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"; do
test ! -e "$forbidden_glibc_file" || {
printf 'host glibc must not be shadowed: %s\n' "$forbidden_glibc_file" >&2
exit 1
}
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"
}
copy_dereferenced_tree() {
local source_root=$1 destination_root=$2 relative source mode
[ -d "$source_root" ] || { printf 'missing extracted runtime directory: %s\n' "$source_root" >&2; return 1; }
while IFS= read -r -d '' relative; do
mkdir -p "$destination_root/${relative#./}"
done < <(cd "$source_root" && find . -type d -print0)
while IFS= read -r -d '' relative; do
source="$source_root/${relative#./}"
mode=0644
[ -x "$source" ] && mode=0755
copy_extracted_file "$source" "$destination_root/${relative#./}" "$mode"
done < <(cd "$source_root" && find . \( -type f -o -type l \) -print0)
}
printf 'Assembling minimal relocatable runtime...\n'
for source in "$EXTRACT_ROOT/usr/lib/postgresql/15/bin/"*; do
[ -f "$source" ] || [ -L "$source" ] || continue
copy_extracted_file "$source" "$RUNTIME_ROOT/usr/lib/postgresql/15/bin/$(basename -- "$source")" 0755
done
copy_dereferenced_tree \
"$EXTRACT_ROOT/usr/share/postgresql/15" \
"$RUNTIME_ROOT/usr/share/postgresql/15"
for source in "$EXTRACT_ROOT/usr/lib/postgresql/15/lib/"*.so*; do
[ -f "$source" ] || [ -L "$source" ] || continue
copy_extracted_file "$source" "$RUNTIME_ROOT/usr/lib/postgresql/15/lib/$(basename -- "$source")" 0755
done
rm -rf \
"$RUNTIME_ROOT/usr/share/doc" \
"$RUNTIME_ROOT/usr/share/man" \
"$RUNTIME_ROOT/usr/share/lintian" \
"$RUNTIME_ROOT/usr/share/locale"
if [ ! -e "$RUNTIME_ROOT/usr/bin/node" ] && [ -x "$RUNTIME_ROOT/usr/bin/nodejs" ]; then
ln -s nodejs "$RUNTIME_ROOT/usr/bin/node"
NODE_ARCHIVE="$WORK_DIR/node-v${NODE_VERSION}-linux-x64.tar.xz"
NODE_DIST_ROOT="$WORK_DIR/node-dist"
printf 'Downloading pinned official Node.js %s runtime...\n' "$NODE_VERSION"
curl --fail --location --retry 3 \
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
--output "$NODE_ARCHIVE"
printf '%s %s\n' "$NODE_ARCHIVE_SHA256" "$NODE_ARCHIVE" | sha256sum --check --status
mkdir -p "$NODE_DIST_ROOT"
tar -xJf "$NODE_ARCHIVE" -C "$NODE_DIST_ROOT" --strip-components=1
install -m 0755 "$NODE_DIST_ROOT/bin/node" "$RUNTIME_ROOT/bin/node"
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" "$RUNTIME_ROOT/usr/lib/postgresql/15/bin" "$RUNTIME_ROOT/usr/lib/postgresql/15/lib" -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/usr/lib/postgresql/15/bin/postgres" \
"$RUNTIME_ROOT/usr/lib/postgresql/15/bin/initdb" \
"$RUNTIME_ROOT/usr/bin/node" \
"$RUNTIME_ROOT/usr/bin/ffmpeg" \
"$RUNTIME_ROOT/usr/bin/curl"; do
"$RUNTIME_ROOT/usr/lib/postgresql/15/bin/pg_ctl" \
"$RUNTIME_ROOT/bin/node" \
"$RUNTIME_ROOT/bin/curl"; do
test -x "$required_file" || { printf 'native runtime file is missing: %s\n' "$required_file" >&2; exit 1; }
done
mkdir -p "$WORK_DIR/payload/ui" "$WORK_DIR/package"
cp "$ROOT_DIR/fnos/ui/config" "$WORK_DIR/payload/ui/config"
node "$ROOT_DIR/scripts/generate-fnos-icons.mjs" "$WORK_DIR/package" "$WORK_DIR/payload/ui/images"
node "$ROOT_DIR/scripts/generate-fnos-icons.mjs" "$STAGE" "$STAGE/app/ui/images"
chmod 0755 "$STAGE/cmd/"*
printf 'Packing fnOS payload...\n'
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner \
-czf "$WORK_DIR/package/app.tgz" \
-C "$WORK_DIR/payload" \
server runtime ui
cp -a "$ROOT_DIR/fnos/cmd" "$ROOT_DIR/fnos/config" "$ROOT_DIR/fnos/wizard" "$WORK_DIR/package/"
chmod 0755 "$WORK_DIR/package/cmd/"*
checksum=$(md5sum "$WORK_DIR/package/app.tgz" | cut -d' ' -f1)
sed "s/@CHECKSUM@/$checksum/" "$ROOT_DIR/fnos/manifest" >"$WORK_DIR/package/manifest"
tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owner \
-czf "$OUTPUT" \
-C "$WORK_DIR/package" \
app.tgz cmd config wizard ICON.PNG ICON_256.PNG manifest
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"
+43 -23
View File
@@ -6,20 +6,25 @@ SMOKE_TMP_ROOT="${2:-${LIVERECORDER_SMOKE_TMPDIR:-${TMPDIR:-/tmp}}}"
mkdir -p "$SMOKE_TMP_ROOT"
SMOKE_TMP_ROOT=$(CDPATH= cd -- "$SMOKE_TMP_ROOT" && pwd)
WORK_DIR=$(mktemp -d "${SMOKE_TMP_ROOT%/}/liverecorder-fnos-smoke.XXXXXX")
PACKAGE_ROOT="$WORK_DIR/package"
APP_ROOT="$WORK_DIR/app"
DATA_ROOT="$WORK_DIR/var"
VOLUME_ROOT="$WORK_DIR/volume"
PORT=${LIVERECORDER_SMOKE_PORT:-19180}
CONTROL="$PACKAGE_ROOT/cmd/main"
cleanup() {
status=$?
if [ -x "$WORK_DIR/cmd/main" ]; then
TRIM_APPDEST="$WORK_DIR" \
TRIM_PKGVAR="$WORK_DIR/var" \
TRIM_APPDEST_VOL="$WORK_DIR/volume" \
if [ -x "$CONTROL" ]; then
TRIM_APPDEST="$APP_ROOT" \
TRIM_PKGVAR="$DATA_ROOT" \
TRIM_APPDEST_VOL="$VOLUME_ROOT" \
TRIM_SERVICE_PORT="$PORT" \
"$WORK_DIR/cmd/main" stop >/dev/null 2>&1 || true
"$CONTROL" stop >/dev/null 2>&1 || true
fi
if [ "$status" -ne 0 ]; then
printf '%s\n' 'fnOS smoke test failed; application logs follow:' >&2
for log_file in "$WORK_DIR/var/log/postgresql.log" "$WORK_DIR/var/log/liverecorder.log"; do
for log_file in "$DATA_ROOT/log/postgresql.log" "$DATA_ROOT/log/liverecorder.log"; do
if [ -f "$log_file" ]; then
printf '%s\n' "--- $log_file ---" >&2
tail -n 120 "$log_file" >&2 || true
@@ -31,21 +36,24 @@ cleanup() {
}
trap cleanup EXIT HUP INT TERM
tar -xzf "$PACKAGE" -C "$WORK_DIR"
tar -xzf "$WORK_DIR/app.tgz" -C "$WORK_DIR"
mkdir -p "$WORK_DIR/volume"
command -v ffmpeg >/dev/null 2>&1 || { printf 'system ffmpeg is required for the fnOS smoke test\n' >&2; exit 1; }
command -v ffprobe >/dev/null 2>&1 || { printf 'system ffprobe is required for the fnOS smoke test\n' >&2; exit 1; }
export TRIM_APPDEST="$WORK_DIR"
export TRIM_PKGVAR="$WORK_DIR/var"
export TRIM_APPDEST_VOL="$WORK_DIR/volume"
mkdir -p "$PACKAGE_ROOT" "$APP_ROOT" "$VOLUME_ROOT"
tar -xzf "$PACKAGE" -C "$PACKAGE_ROOT"
tar -xzf "$PACKAGE_ROOT/app.tgz" -C "$APP_ROOT"
export TRIM_APPDEST="$APP_ROOT"
export TRIM_PKGVAR="$DATA_ROOT"
export TRIM_APPDEST_VOL="$VOLUME_ROOT"
export TRIM_SERVICE_PORT="$PORT"
SMOKE_PASSWORD='LiveRecorder-Smoke-2026!'
wizard_admin_password="$SMOKE_PASSWORD" \
wizard_admin_password_confirm="$SMOKE_PASSWORD" \
"$WORK_DIR/cmd/install_callback"
"$WORK_DIR/cmd/main" start
"$WORK_DIR/cmd/main" status
"$PACKAGE_ROOT/cmd/install_callback"
"$CONTROL" start
"$CONTROL" status
BASE_URL="http://127.0.0.1:$PORT"
curl -fsS "$BASE_URL/" >"$WORK_DIR/index.html"
@@ -60,16 +68,28 @@ token=$(printf '%s' "$login_response" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
test -n "$token"
settings_response=$(curl -fsS -H "Authorization: Bearer $token" "$BASE_URL/api/settings")
expected_record_root="$WORK_DIR/volume/@appshare/liverecorder/records"
expected_record_root="$VOLUME_ROOT/@appshare/liverecorder/records"
printf '%s' "$settings_response" | grep -Fq "\"outputRoot\":\"$expected_record_root\""
runtime_libs="$WORK_DIR/runtime/lib/x86_64-linux-gnu:$WORK_DIR/runtime/usr/lib/x86_64-linux-gnu:$WORK_DIR/runtime/usr/lib/x86_64-linux-gnu/pulseaudio:$WORK_DIR/runtime/usr/lib/x86_64-linux-gnu/blas:$WORK_DIR/runtime/usr/lib/x86_64-linux-gnu/lapack:$WORK_DIR/runtime/usr/lib/postgresql/15/lib"
LD_LIBRARY_PATH="$runtime_libs" "$WORK_DIR/runtime/usr/bin/node" --version >/dev/null
LD_LIBRARY_PATH="$runtime_libs" "$WORK_DIR/runtime/usr/bin/ffmpeg" -version >/dev/null 2>&1
LD_LIBRARY_PATH="$runtime_libs" "$WORK_DIR/runtime/usr/bin/curl" -fsS "$BASE_URL/health/ready" >/dev/null
runtime_libs="$APP_ROOT/runtime/lib:$APP_ROOT/runtime/usr/lib/postgresql/15/lib"
ca_bundle="$APP_ROOT/runtime/etc/ssl/certs/ca-certificates.crt"
node_bin="$APP_ROOT/runtime/bin/node"
curl_bin="$APP_ROOT/runtime/bin/curl"
signer="$APP_ROOT/server/Platforms/Douyin/Signing/sign-xbogus.js"
"$WORK_DIR/cmd/main" stop
"$WORK_DIR/cmd/main" start
LD_LIBRARY_PATH="$runtime_libs" "$node_bin" --version | grep -q '^v22\.18\.0$'
signature=$(LD_LIBRARY_PATH="$runtime_libs" "$node_bin" "$signer" \
'aid=6383&device_platform=web&room_id=1' \
'Mozilla/5.0 LiveRecorder fnOS package smoke test')
test -n "$signature"
LD_LIBRARY_PATH="$runtime_libs" SSL_CERT_FILE="$ca_bundle" CURL_CA_BUNDLE="$ca_bundle" \
"$curl_bin" -fsS "$BASE_URL/health/ready" >/dev/null
ffmpeg -version >/dev/null 2>&1
ffprobe -version >/dev/null 2>&1
"$CONTROL" stop
"$CONTROL" start
"$CONTROL" status
curl -fsS "$BASE_URL/health/ready" | grep -q '"status":"ready"'
printf 'fnOS native smoke test passed: frontend, API, PostgreSQL, Node.js and FFmpeg are ready\n'
printf 'fnOS native smoke test passed: frontend, API, PostgreSQL, packaged Node/curl and system FFmpeg/FFprobe are ready\n'
+75 -10
View File
@@ -3,14 +3,19 @@ set -euo pipefail
PACKAGE=${1:?usage: verify-fnos-package.sh package.fpk}
VERIFY_TMP_ROOT="${LIVERECORDER_VERIFY_TMPDIR:-${TMPDIR:-/tmp}}"
MAX_APP_UNCOMPRESSED_BYTES=$((512 * 1024 * 1024))
mkdir -p "$VERIFY_TMP_ROOT"
WORK_DIR=$(mktemp -d "${VERIFY_TMP_ROOT%/}/liverecorder-fnos-verify.XXXXXX")
trap 'rm -rf -- "$WORK_DIR"' EXIT
manifest_value() {
sed -n "s/^$1[[:space:]]*=[[:space:]]*//p" "$WORK_DIR/manifest" | head -n 1 | tr -d '\r'
}
tar -xzf "$PACKAGE" -C "$WORK_DIR"
grep -q '^appname=liverecorder$' "$WORK_DIR/manifest"
grep -q '^version=1.0.0$' "$WORK_DIR/manifest"
grep -q '^platform=x86$' "$WORK_DIR/manifest"
test "$(manifest_value appname)" = "liverecorder"
test "$(manifest_value version)" = "1.0.1"
test "$(manifest_value platform)" = "x86"
test -x "$WORK_DIR/cmd/main"
test -x "$WORK_DIR/cmd/install_callback"
test -s "$WORK_DIR/wizard/install"
@@ -18,23 +23,69 @@ test -s "$WORK_DIR/wizard/upgrade"
test -s "$WORK_DIR/ICON.PNG"
test -s "$WORK_DIR/ICON_256.PNG"
expected=$(sed -n 's/^checksum=//p' "$WORK_DIR/manifest")
expected=$(manifest_value checksum)
actual=$(md5sum "$WORK_DIR/app.tgz" | cut -d' ' -f1)
test -n "$expected"
test "$expected" = "$actual"
gzip -t "$WORK_DIR/app.tgz"
gzip -dc "$WORK_DIR/app.tgz" >"$WORK_DIR/app.tar"
uncompressed_bytes=$(wc -c <"$WORK_DIR/app.tar")
if [ "$uncompressed_bytes" -gt "$MAX_APP_UNCOMPRESSED_BYTES" ]; then
printf 'app.tgz expands to %s bytes; limit is %s bytes\n' \
"$uncompressed_bytes" "$MAX_APP_UNCOMPRESSED_BYTES" >&2
exit 1
fi
tar -tf "$WORK_DIR/app.tar" >"$WORK_DIR/app-files.txt"
if awk '/^\// || /(^|\/)\.\.($|\/)/ { unsafe = 1; exit } END { exit unsafe ? 0 : 1 }' "$WORK_DIR/app-files.txt"; then
printf 'app.tgz contains an unsafe member path\n' >&2
exit 1
fi
tar -tvf "$WORK_DIR/app.tar" >"$WORK_DIR/app-metadata.txt"
if awk '
/^l/ {
marker = " -> "
offset = index($0, marker)
if (offset > 0) {
target = substr($0, offset + length(marker))
if (target ~ /^\// || target ~ /(^|\/)\.\.($|\/)/) { unsafe = 1; exit }
}
}
/^h/ {
marker = " link to "
offset = index($0, marker)
if (offset > 0) {
target = substr($0, offset + length(marker))
if (target ~ /^\// || target ~ /(^|\/)\.\.($|\/)/) { unsafe = 1; exit }
}
}
END { exit unsafe ? 0 : 1 }
' "$WORK_DIR/app-metadata.txt"; then
printf 'app.tgz contains an unsafe symbolic or hard link\n' >&2
exit 1
fi
tar -tzf "$WORK_DIR/app.tgz" >"$WORK_DIR/app-files.txt"
grep -q '^server/LiveRecorder.WebApi$' "$WORK_DIR/app-files.txt"
grep -q '^server/wwwroot/index.html$' "$WORK_DIR/app-files.txt"
grep -q '^server/Platforms/Douyin/Signing/sign-xbogus.js$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/lib/postgresql/15/bin/postgres$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/lib/postgresql/15/bin/initdb$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/bin/ffmpeg$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/bin/node$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/bin/curl$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/lib/postgresql/15/bin/pg_ctl$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/usr/share/postgresql/15/postgresql.conf.sample$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/bin/node$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/bin/curl$' "$WORK_DIR/app-files.txt"
grep -q '^runtime/etc/ssl/certs/ca-certificates.crt$' "$WORK_DIR/app-files.txt"
grep -q '^ui/config$' "$WORK_DIR/app-files.txt"
grep -q '^ui/images/icon_64.png$' "$WORK_DIR/app-files.txt"
if grep -Eq '^runtime/(usr/)?lib/x86_64-linux-gnu/(libc\.so\.6|ld-linux-x86-64\.so\.2)$' "$WORK_DIR/app-files.txt"; then
if grep -Eq '^runtime/.*/(ffmpeg|ffprobe)$' "$WORK_DIR/app-files.txt"; then
printf 'ffmpeg and ffprobe must come from the fnOS system environment\n' >&2
exit 1
fi
if grep -Eq '^runtime/lib/(ld-linux-.*|libc\.so\..*|libBrokenLocale\.so\..*|libanl\.so\..*|libdl\.so\..*|libm(vec)?\.so\..*|libnss_(compat|dns|files|hesiod)\.so\..*|libpthread\.so\..*|libresolv\.so\..*|librt\.so\..*|libthread_db\.so\..*|libutil\.so\..*)$' "$WORK_DIR/app-files.txt"; then
printf 'the FPK must use the fnOS glibc and matching system loader\n' >&2
exit 1
fi
@@ -44,4 +95,18 @@ if grep -Eq '^(data|records|postgres|log|var)/' "$WORK_DIR/app-files.txt"; then
exit 1
fi
printf 'fnOS package verified: %s\n' "$PACKAGE"
mkdir -p "$WORK_DIR/app"
tar -xf "$WORK_DIR/app.tar" -C "$WORK_DIR/app"
while IFS= read -r -d '' link_path; do
target=$(readlink -- "$link_path")
case "$target" in
/*) printf 'absolute symbolic link in app.tgz: %s -> %s\n' "$link_path" "$target" >&2; exit 1 ;;
esac
resolved=$(realpath -m -- "$(dirname -- "$link_path")/$target")
case "$resolved" in
"$WORK_DIR/app"/*) ;;
*) printf 'escaping symbolic link in app.tgz: %s -> %s\n' "$link_path" "$target" >&2; exit 1 ;;
esac
done < <(find "$WORK_DIR/app" -type l -print0)
printf 'fnOS package verified: %s (%s bytes uncompressed)\n' "$PACKAGE" "$uncompressed_bytes"