fix: rebuild native fnOS package with fnpack
This commit is contained in:
+169
-58
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user