#!/usr/bin/env bash # Modified by vii from komaruworld/mocktail. See README "About this fork". # Copyright 2026 Mocktail Project Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 set -Eeuo pipefail umask 022 SCRIPT_DIR="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd -P -- "${SCRIPT_DIR}/.." && pwd)" BUILD_DIR="${PROJECT_ROOT}/build" OUTPUT="" OUTPUT_EXPLICIT=false MODE=standalone TARGET_LIBC=glibc ARTIFACT_NAME="" TARGET_INTERPRETER="" PACKAGING_HOST_LIBC=unknown DRY_RUN=false APPIMAGE_OUTPUT="" ANDROID_BUILD_TOOLS_ROOT="${MOCKTAIL_ANDROID_BUILD_TOOLS_ROOT:-}" ANDROID_APKANALYZER_ROOT="${MOCKTAIL_ANDROID_APKANALYZER_ROOT:-}" STAGING="" APPDIR="" PACKAGING_LIBRARY_PATH="" ANDROID_TOOLS_ABI="" APPIMAGE_FORMAT="${MOCKTAIL_APPIMAGE_FORMAT:-classic}" readonly -a PROJECT_ARTIFACTS=( mocktail mocktail_updater mocktail_failure_dialog mocktail_webview_helper libmocktail_audio_sdl.so libc.so libdl.so libm.so libz.so libandroid.so liblog.so libmediandk.so libOpenSLES.so libOpenMAXAL.so libEGL.so libGLESv2.so libvulkan.so ) readonly FREEBSD_SOCKET_HELPER="mocktail_freebsd_socket_helper" readonly -a RUNTIME_SCRIPTS=( collect_support_bundle.sh ) readonly STANDALONE_SUPPORT_PACKAGER="${PROJECT_ROOT}/scripts/package_standalone_support.sh" readonly STANDALONE_WEBKIT_PACKAGER="${PROJECT_ROOT}/scripts/package_standalone_webkit.sh" declare -A COPIED_SOURCES=() declare -A HOST_REQUIREMENTS=() declare -a DEPENDENCY_QUEUE=() Usage() { cat <<'EOF' Usage: scripts/package_portable.sh [OPTIONS] Create a relocatable x86-64 Linux bundle with a single ./run.sh launcher. Options: --build-dir DIR Release build directory (default: build). --output DIR Bundle output directory (default includes libc ABI). --libc ABI Target libc ABI: glibc (default) or musl. --mode MODE standalone (default) or thin; aliases: full, static, dynamic, and legacy minimal. --dry-run Audit artifacts and dependency policy without writing. --appimage FILE Also build FILE. Set MOCKTAIL_APPIMAGE_FORMAT to classic (default) or anylinux. AnyLinux uses a native /usr install instead of producing a portable directory. -h, --help Show this help. standalone bundles the application userspace and support tools. thin omits copied dependency libraries and bootstraps host packages before launch. The physical GPU kernel driver and vendor ICD remain host-provided. Every Mocktail ELF build artifact must match --libc. Classic glibc bundles use compact native aapt plus the Java apksigner library. A musl standalone bundle uses the Java apkanalyzer backend instead of hiding a glibc runtime dependency. No Android SDK development tree is published. Public release artifacts are: Mocktail-x86_64--.AppImage. EOF } Log() { printf '[portable] %s\n' "$*" >&2 } Die() { Log "error: $*" exit 1 } Cleanup() { [[ -z "${STAGING}" ]] || rm -rf -- "${STAGING}" [[ -z "${APPDIR}" ]] || rm -rf -- "${APPDIR}" } ParseArguments() { while (( $# > 0 )); do case "$1" in --build-dir) (( $# >= 2 )) || Die "--build-dir requires a directory" BUILD_DIR="$2" shift 2 ;; --output) (( $# >= 2 )) || Die "--output requires a directory" OUTPUT="$2" OUTPUT_EXPLICIT=true shift 2 ;; --libc) (( $# >= 2 )) || Die "--libc requires glibc or musl" TARGET_LIBC="$2" shift 2 ;; --mode) (( $# >= 2 )) || Die "--mode requires standalone or thin" MODE="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --appimage) (( $# >= 2 )) || Die "--appimage requires an output file" APPIMAGE_OUTPUT="$2" shift 2 ;; -h|--help) Usage exit 0 ;; *) Die "unknown option: $1" ;; esac done case "${MODE}" in standalone|full|static) MODE=standalone ;; dynamic|minimal) MODE=thin ;; esac [[ "${MODE}" == standalone || "${MODE}" == thin ]] || Die "--mode must be standalone or thin" [[ "${TARGET_LIBC}" == glibc || "${TARGET_LIBC}" == musl ]] || Die "--libc must be glibc or musl" [[ "${APPIMAGE_FORMAT}" == classic || "${APPIMAGE_FORMAT}" == anylinux ]] || Die "MOCKTAIL_APPIMAGE_FORMAT must be classic or anylinux" if [[ "${APPIMAGE_FORMAT}" == anylinux && "${TARGET_LIBC}" != glibc ]]; then Die "AnyLinux packaging requires a glibc build" fi if [[ "${TARGET_LIBC}" == musl && "${MODE}" == standalone ]]; then ANDROID_TOOLS_ABI=java else ANDROID_TOOLS_ABI=glibc fi ARTIFACT_NAME="mocktail-linux-x86_64-${TARGET_LIBC}-${MODE}" if [[ "${OUTPUT_EXPLICIT}" == false ]]; then OUTPUT="${PROJECT_ROOT}/dist/${ARTIFACT_NAME}" fi } RequireCommand() { command -v "$1" >/dev/null 2>&1 || Die "missing required command: $1" } CanonicalDirectory() { local path="$1" [[ -d "${path}" ]] || Die "directory does not exist: ${path}" cd -P -- "${path}" && pwd } ReadElfInterpreter() { LC_ALL=C readelf -l "$1" 2>/dev/null | sed -n 's/.*Requesting program interpreter: \([^]]*\).*/\1/p' } DetectElfLibc() { local path="$1" local interpreter needed interpreter_libc="" needed_libc="" interpreter="$(ReadElfInterpreter "${path}")" case "${interpreter}" in *ld-musl-*.so.1) interpreter_libc=musl ;; *ld-linux*.so.*) interpreter_libc=glibc ;; esac needed="$(LC_ALL=C readelf -d "${path}" 2>/dev/null | sed -n 's/.*Shared library: \[\([^]]*\)\].*/\1/p')" if grep -Eq '^(libc\.so\.6|ld-linux[^[:space:]]*\.so(\.[0-9]+)*)$' \ <<<"${needed}"; then needed_libc=glibc fi if grep -Eq '^(libc\.so|ld-musl-[^[:space:]]*\.so\.1)$' \ <<<"${needed}"; then if [[ -n "${needed_libc}" && "${needed_libc}" != musl ]]; then printf mixed return 0 fi needed_libc=musl fi if [[ -n "${interpreter_libc}" && -n "${needed_libc}" && "${interpreter_libc}" != "${needed_libc}" ]]; then printf mixed elif [[ -n "${interpreter_libc}" ]]; then printf '%s' "${interpreter_libc}" elif [[ -n "${needed_libc}" ]]; then printf '%s' "${needed_libc}" else printf unknown fi } DetectHostLibc() { local version shell_interpreter version="$(LC_ALL=C getconf GNU_LIBC_VERSION 2>/dev/null || true)" if [[ "${version}" == glibc\ * ]]; then printf glibc return 0 fi shell_interpreter="$(ReadElfInterpreter /proc/$$/exe)" case "${shell_interpreter}" in *ld-musl-*.so.1) printf musl ;; *ld-linux*.so.*) printf glibc ;; *) printf unknown ;; esac } ValidateElfLibc() { local path="$1" local label="$2" local detected interpreter detected="$(DetectElfLibc "${path}")" interpreter="$(ReadElfInterpreter "${path}")" [[ "${detected}" == "${TARGET_LIBC}" ]] || Die "${label} libc ABI mismatch: expected ${TARGET_LIBC}, detected ${detected} (interpreter=${interpreter:-none})" } ValidateArtifacts() { local artifact path architecture interpreter for artifact in "${PROJECT_ARTIFACTS[@]}"; do path="${BUILD_DIR}/${artifact}" [[ -f "${path}" && ! -L "${path}" ]] || Die "missing regular build artifact: ${path}" architecture="$(LC_ALL=C readelf -h "${path}" 2>/dev/null | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')" [[ "${architecture}" == *"X86-64"* ]] || Die "build artifact is not x86-64 ELF: ${path}" ValidateElfLibc "${path}" "build artifact ${artifact}" done TARGET_INTERPRETER="$(ReadElfInterpreter "${BUILD_DIR}/mocktail")" [[ -n "${TARGET_INTERPRETER}" ]] || Die "mocktail must be a dynamically linked ${TARGET_LIBC} executable" local freebsd_helper="${BUILD_DIR}/${FREEBSD_SOCKET_HELPER}" [[ -f "${freebsd_helper}" && ! -L "${freebsd_helper}" && -x "${freebsd_helper}" ]] || Die "missing executable FreeBSD socket helper: ${freebsd_helper}" LC_ALL=C readelf -h "${freebsd_helper}" 2>/dev/null | grep -Eq 'OS/ABI:[[:space:]]+UNIX - FreeBSD' || Die "socket helper is not branded for FreeBSD" LC_ALL=C readelf -h "${freebsd_helper}" 2>/dev/null | grep -Eq 'Machine:[[:space:]]+.*X86-64' || Die "socket helper is not an x86-64 ELF" [[ -z "$(ReadElfInterpreter "${freebsd_helper}")" ]] || Die "FreeBSD socket helper must be statically linked" LC_ALL=C readelf -n "${freebsd_helper}" 2>/dev/null | grep -Fq FreeBSD || Die "socket helper has no FreeBSD ABI note" } ResolveAndroidBuildTools() { local apksigner_path analyzer_path discovered_root if [[ -n "${ANDROID_BUILD_TOOLS_ROOT}" ]]; then ANDROID_BUILD_TOOLS_ROOT="$(CanonicalDirectory \ "${ANDROID_BUILD_TOOLS_ROOT}")" else apksigner_path="$(command -v apksigner)" || Die "apksigner is required to package the standalone updater" apksigner_path="$(readlink -f -- "${apksigner_path}")" discovered_root="$(dirname -- "${apksigner_path}")" ANDROID_BUILD_TOOLS_ROOT="$(CanonicalDirectory "${discovered_root}")" fi local required for required in apksigner lib/apksigner.jar NOTICE.txt source.properties; do [[ -f "${ANDROID_BUILD_TOOLS_ROOT}/${required}" && ! -L "${ANDROID_BUILD_TOOLS_ROOT}/${required}" ]] || Die "Android build-tools artifact is unavailable: ${required}" done if [[ "${ANDROID_TOOLS_ABI}" == glibc ]]; then for required in aapt lib64/libc++.so; do [[ -f "${ANDROID_BUILD_TOOLS_ROOT}/${required}" && ! -L "${ANDROID_BUILD_TOOLS_ROOT}/${required}" ]] || Die "Android native metadata artifact is unavailable: ${required}" done return 0 fi if [[ -n "${ANDROID_APKANALYZER_ROOT}" ]]; then ANDROID_APKANALYZER_ROOT="$(CanonicalDirectory \ "${ANDROID_APKANALYZER_ROOT}")" else analyzer_path="$(command -v apkanalyzer)" || Die "apkanalyzer is required to package portable APK metadata support" analyzer_path="$(readlink -f -- "${analyzer_path}")" ANDROID_APKANALYZER_ROOT="$(CanonicalDirectory \ "$(dirname -- "$(dirname -- "${analyzer_path}")")")" fi [[ -f "${ANDROID_APKANALYZER_ROOT}/lib/apkanalyzer-classpath.jar" && ! -L "${ANDROID_APKANALYZER_ROOT}/lib/apkanalyzer-classpath.jar" ]] || Die "Android apkanalyzer classpath is unavailable" [[ -f "${ANDROID_APKANALYZER_ROOT}/NOTICE.txt" && -f "${ANDROID_APKANALYZER_ROOT}/source.properties" ]] || Die "Android apkanalyzer license or source metadata is unavailable" } ListElfDependencies() { local output status=0 output="$(LC_ALL=C LD_LIBRARY_PATH="${PACKAGING_LIBRARY_PATH}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" \ ldd "$1" 2>&1)" || status=$? if grep -Eq '=>[[:space:]]*not found|^Error loading shared library .*: No such file' \ <<<"${output}"; then printf '%s\n' "${output}" >&2 return 1 fi if (( status != 0 )) && ! grep -q '^Error relocating ' <<<"${output}"; then printf '%s\n' "${output}" >&2 return 1 fi awk ' /=>[[:space:]]*\// { for (field = 1; field <= NF; ++field) { if ($field == "=>") { print $(field + 1) break } } next } /^[[:space:]]*\// { print $1 } ' <<<"${output}" } ListElfNeededSonames() { LC_ALL=C readelf -d "$1" 2>/dev/null | sed -n 's/.*Shared library: \[\([^]]*\)\].*/\1/p' } HostDependencyReason() { local soname="$1" case "${soname}" in ld-musl-*.so.1) printf musl-runtime ;; libc.so) if [[ "${TARGET_LIBC}" == musl ]]; then printf musl-runtime else return 1 fi ;; ld-linux*.so*|libc.so.*|libm.so.*|libdl.so.*|libpthread.so.*|librt.so.*|\ libresolv.so.*|libutil.so.*|libanl.so.*|libnss_*.so.*) printf glibc ;; libGL.so.*|libGLX.so.*|libGLdispatch.so.*|libOpenGL.so.*|libEGL.so.*|\ libGLESv1_CM.so.*|libGLESv2.so.*|libgbm.so.*|libdrm.so.*|libdrm_*.so.*) printf gpu-driver-stack ;; *) return 1 ;; esac } RecordHostRequirement() { local soname="$1" local reason="$2" HOST_REQUIREMENTS["${soname}"]="${reason}" } IsProjectArtifact() { local candidate="$1" local artifact for artifact in "${PROJECT_ARTIFACTS[@]}"; do [[ "${candidate}" != "${artifact}" ]] || return 0 done return 1 } QueueDependencies() { local source="$1" local dependencies dependency soname canonical reason destination local existing_hash new_hash dependencies="$(ListElfDependencies "${source}")" || Die "cannot resolve dependency closure for ${source}" while IFS= read -r dependency; do [[ -n "${dependency}" ]] || continue soname="${dependency##*/}" if reason="$(HostDependencyReason "${soname}")"; then RecordHostRequirement "${soname}" "${reason}" continue fi if IsProjectArtifact "${soname}"; then continue fi canonical="$(readlink -f -- "${dependency}")" [[ -f "${canonical}" ]] || Die "unresolved dependency: ${dependency}" if [[ "${MODE}" == thin ]]; then RecordHostRequirement "${soname}" thin-mode continue fi destination="${STAGING}/mocktail/lib/${soname}" if [[ -n "${COPIED_SOURCES[${soname}]+present}" ]]; then existing_hash="$(sha256sum "${COPIED_SOURCES[${soname}]}" | awk '{print $1}')" new_hash="$(sha256sum "${canonical}" | awk '{print $1}')" [[ "${existing_hash}" == "${new_hash}" ]] || Die "dependency basename collision for ${soname}" continue fi COPIED_SOURCES["${soname}"]="${canonical}" DEPENDENCY_QUEUE+=("${canonical}") if [[ "${DRY_RUN}" == false ]]; then install -m 0755 -- "${canonical}" "${destination}" fi done <<<"${dependencies}" } AuditWebViewHostStack() { local dependencies dependency soname reason dependencies="$(ListElfDependencies \ "${BUILD_DIR}/mocktail_webview_helper")" || Die "cannot resolve host WebKit dependency closure" while IFS= read -r dependency; do [[ -n "${dependency}" ]] || continue soname="${dependency##*/}" reason="$(HostDependencyReason "${soname}" 2>/dev/null || printf webview-stack)" RecordHostRequirement "${soname}" "${reason}" done <<<"${dependencies}" } AuditThinDependencies() { local artifact soname reason for artifact in "${PROJECT_ARTIFACTS[@]}"; do while IFS= read -r soname; do [[ -n "${soname}" ]] || continue if IsProjectArtifact "${soname}"; then continue fi reason="$(HostDependencyReason "${soname}" 2>/dev/null || printf thin-mode)" RecordHostRequirement "${soname}" "${reason}" done < <(ListElfNeededSonames "${BUILD_DIR}/${artifact}") done } CopyRuntimeTree() { local runtime_root="${STAGING}/mocktail" mkdir -p -- "${runtime_root}/bin" "${runtime_root}/lib" \ "${runtime_root}/scripts" "${runtime_root}/metadata" \ "${runtime_root}/runtime" chmod 0755 -- "${STAGING}" local artifact mode for artifact in "${PROJECT_ARTIFACTS[@]}"; do mode=0755 install -m "${mode}" -- "${BUILD_DIR}/${artifact}" \ "${runtime_root}/bin/${artifact}" done install -m 0755 -- "${BUILD_DIR}/${FREEBSD_SOCKET_HELPER}" \ "${runtime_root}/bin/${FREEBSD_SOCKET_HELPER}" install -m 0755 -- "${PROJECT_ROOT}/packaging/run.sh" "${STAGING}/run.sh" install -m 0755 -- "${PROJECT_ROOT}/packaging/mocktail-launcher.sh" \ "${runtime_root}/scripts/portable_launcher.sh" install -m 0755 -- "${PROJECT_ROOT}/scripts/install_thin_dependencies.sh" \ "${runtime_root}/scripts/install_thin_dependencies.sh" install -m 0644 -- "${PROJECT_ROOT}/config/roblox_compatibility.json" \ "${PROJECT_ROOT}/config/roblox_bootstrap_sources.json" \ "${PROJECT_ROOT}/config/roblox_host_abi_reference.json" \ "${PROJECT_ROOT}/config/roblox_signing_certificates.json" \ "${runtime_root}/metadata/" local script for script in "${RUNTIME_SCRIPTS[@]}"; do install -m 0755 -- "${PROJECT_ROOT}/scripts/${script}" \ "${runtime_root}/scripts/${script}" done } CopyApkAnalyzerClasspath() { local source_root="${ANDROID_APKANALYZER_ROOT}/lib" local destination_root="$1" local manifest_jar="${source_root}/apkanalyzer-classpath.jar" local class_path relative source destination class_path="$(unzip -p "${manifest_jar}" META-INF/MANIFEST.MF | tr -d '\r' | awk ' /^Class-Path: / { collecting = 1 value = substr($0, 13) next } collecting && /^ / { value = value substr($0, 2) next } collecting { print value emitted = 1 exit } END { if (collecting && !emitted) print value } ')" [[ -n "${class_path}" ]] || Die "Android apkanalyzer classpath is empty" install -d -m 0755 -- "${destination_root}" install -m 0644 -- "${manifest_jar}" \ "${destination_root}/apkanalyzer-classpath.jar" for relative in ${class_path}; do [[ "${relative}" != /* && "${relative}" != .. && "${relative}" != ../* && "${relative}" != */../* && "${relative}" != */.. ]] || Die "unsafe Android analyzer classpath entry: ${relative}" source="${source_root}/${relative}" [[ -f "${source}" && ! -L "${source}" ]] || Die "Android analyzer classpath entry is unavailable: ${relative}" destination="${destination_root}/${relative}" install -d -m 0755 -- "$(dirname -- "${destination}")" install -m 0644 -- "${source}" "${destination}" done } PackageAndroidRuntime() { local android_root="${STAGING}/mocktail/runtime/android-tools" install -d -m 0755 -- "${android_root}/bin" "${android_root}/lib" \ "${android_root}/licenses" install -m 0755 -- "${ANDROID_BUILD_TOOLS_ROOT}/apksigner" \ "${android_root}/bin/apksigner" install -m 0644 -- "${ANDROID_BUILD_TOOLS_ROOT}/lib/apksigner.jar" \ "${android_root}/lib/apksigner.jar" ln -s -- ../lib "${android_root}/bin/lib" install -m 0644 -- "${ANDROID_BUILD_TOOLS_ROOT}/NOTICE.txt" \ "${android_root}/licenses/build-tools.txt" if [[ "${ANDROID_TOOLS_ABI}" == glibc ]]; then install -d -m 0755 -- "${android_root}/bin/lib64" install -m 0755 -- "${ANDROID_BUILD_TOOLS_ROOT}/aapt" \ "${android_root}/bin/aapt" install -m 0755 -- "${ANDROID_BUILD_TOOLS_ROOT}/lib64/libc++.so" \ "${android_root}/bin/lib64/libc++.so" return 0 fi install -d -m 0755 -- \ "${android_root}/sdk/cmdline-tools/latest/lib" \ "${android_root}/sdk/build-tools/37.0.0" install -m 0755 -- "${PROJECT_ROOT}/packaging/aapt-apkanalyzer.sh" \ "${android_root}/bin/aapt" install -m 0755 -- "${PROJECT_ROOT}/packaging/apkanalyzer.sh" \ "${android_root}/bin/apkanalyzer" CopyApkAnalyzerClasspath \ "${android_root}/sdk/cmdline-tools/latest/lib" install -m 0755 -- "${PROJECT_ROOT}/packaging/aapt-apkanalyzer.sh" \ "${android_root}/sdk/build-tools/37.0.0/aapt" install -m 0644 -- "${ANDROID_BUILD_TOOLS_ROOT}/source.properties" \ "${android_root}/sdk/build-tools/37.0.0/source.properties" install -m 0644 -- "${ANDROID_APKANALYZER_ROOT}/source.properties" \ "${android_root}/sdk/cmdline-tools/latest/source.properties" install -m 0644 -- "${ANDROID_APKANALYZER_ROOT}/NOTICE.txt" \ "${android_root}/licenses/cmdline-tools.txt" } PackageStandaloneRuntime() { [[ "${MODE}" == standalone ]] || return 0 rmdir -- "${STAGING}/mocktail/runtime" "${STANDALONE_SUPPORT_PACKAGER}" \ --runtime-root "${STAGING}/mocktail" --libc "${TARGET_LIBC}" "${STANDALONE_WEBKIT_PACKAGER}" \ --runtime-root "${STAGING}/mocktail" --libc "${TARGET_LIBC}" PACKAGING_LIBRARY_PATH="$(find "${STAGING}/mocktail" -type f \ \( -perm -0100 -o -name '*.so' -o -name '*.so.*' \) -print0 | while IFS= read -r -d '' path; do IsElfFile "${path}" || continue dirname -- "${path}" done | LC_ALL=C sort -u | paste -sd: -)" } BuildStandaloneNamespace() { [[ "${MODE}" == standalone ]] || return 0 local environment_file="${STAGING}/mocktail/webkit.env" local webkit6_namespace namespace_root webkit6_namespace="$(sed -n \ 's/^MOCKTAIL_WEBKITGTK6_NAMESPACE_DIR=//p' "${environment_file}")" [[ "${webkit6_namespace}" =~ ^/usr/(lib|lib64|libexec|lib/x86_64-linux-gnu)/webkitgtk-6\.0$ ]] || Die "unsafe WebKitGTK 6 namespace directory: ${webkit6_namespace}" namespace_root="${STAGING}/mocktail/namespace" mkdir -p -- "${namespace_root}/usr/bin" \ "${namespace_root}${webkit6_namespace}/injected-bundle" local process_name for process_name in WebKitGPUProcess WebKitNetworkProcess WebKitWebProcess; do ln -- "${STAGING}/mocktail/libexec/webkitgtk-6.0/${process_name}" \ "${namespace_root}${webkit6_namespace}/${process_name}" done ln -- "${STAGING}/mocktail/lib/webkitgtk-6.0/injected-bundle/libwebkitgtkinjectedbundle.so" \ "${namespace_root}${webkit6_namespace}/injected-bundle/libwebkitgtkinjectedbundle.so" ln -- "$(readlink -f -- "${STAGING}/mocktail/runtime/bin/bwrap")" \ "${namespace_root}/usr/bin/bwrap" ln -- "$(readlink -f -- "${STAGING}/mocktail/runtime/bin/xdg-dbus-proxy")" \ "${namespace_root}/usr/bin/xdg-dbus-proxy" } IsElfFile() { LC_ALL=C readelf -h "$1" 2>/dev/null | grep -Eq '^[[:space:]]*Type:[[:space:]]*(DYN|EXEC)' } QueuePackagedRuntimeDependencies() { local source while IFS= read -r -d '' source; do IsElfFile "${source}" || continue case "${source#${STAGING}/mocktail/}" in bin/${FREEBSD_SOCKET_HELPER}) continue ;; runtime/android-tools/*) continue ;; esac QueueDependencies "${source}" done < <(find "${STAGING}/mocktail/bin" \ "${STAGING}/mocktail/runtime" "${STAGING}/mocktail/libexec" \ "${STAGING}/mocktail/lib/plugins" \ "${STAGING}/mocktail/lib/webkitgtk-6.0" \ -type f \( -perm -0100 -o -name '*.so' -o -name '*.so.*' \) \ -print0 2>/dev/null) } CollectDependencies() { local artifact source queue_index=0 if [[ "${MODE}" == thin ]]; then AuditThinDependencies return 0 fi if [[ "${DRY_RUN}" == true ]]; then for artifact in "${PROJECT_ARTIFACTS[@]}"; do QueueDependencies "${BUILD_DIR}/${artifact}" done else QueuePackagedRuntimeDependencies fi while (( queue_index < ${#DEPENDENCY_QUEUE[@]} )); do source="${DEPENDENCY_QUEUE[queue_index]}" ((queue_index += 1)) QueueDependencies "${source}" done } WriteDependencyManifest() { { printf 'Mocktail portable dependency contract\n' printf 'mode=%s\n' "${MODE}" printf 'architecture=x86_64\n' printf 'libc=%s\n' "${TARGET_LIBC}" printf 'interpreter=%s\n' "${TARGET_INTERPRETER}" printf 'android_tools_libc=%s\n\n' "${ANDROID_TOOLS_ABI}" printf 'Bundled userspace libraries:\n' if (( ${#COPIED_SOURCES[@]} == 0 )); then if [[ "${MODE}" == thin ]]; then printf ' (none; thin mode)\n' else printf ' (none required by the standalone closure)\n' fi else local soname while IFS= read -r soname; do printf ' %s <- %s\n' "${soname}" "${COPIED_SOURCES[${soname}]}" done < <(printf '%s\n' "${!COPIED_SOURCES[@]}" | sort) fi printf '\nHost-provided libraries:\n' if (( ${#HOST_REQUIREMENTS[@]} == 0 )); then printf ' (none)\n' else local soname while IFS= read -r soname; do printf ' %s (%s)\n' "${soname}" "${HOST_REQUIREMENTS[${soname}]}" done < <(printf '%s\n' "${!HOST_REQUIREMENTS[@]}" | sort) fi printf '\nThe Linux kernel and x86-64 %s ABI remain host-provided.\n' \ "${TARGET_LIBC}" printf 'Only the physical GPU driver, Vulkan ICD, and GL/display driver stack are host-provided.\n' } >"${STAGING}/mocktail/metadata/DEPENDENCIES.txt" } WriteAbiManifest() { { printf 'schema=2\n' printf 'architecture=x86_64\n' printf 'libc=%s\n' "${TARGET_LIBC}" printf 'mode=%s\n' "${MODE}" printf 'interpreter=%s\n' "${TARGET_INTERPRETER}" printf 'android_tools_libc=%s\n' "${ANDROID_TOOLS_ABI}" } >"${STAGING}/mocktail/metadata/ABI.txt" } PatchRuntimePaths() { local artifact if [[ "${MODE}" == thin ]]; then patchelf --set-rpath '$ORIGIN' "${STAGING}/mocktail/bin/mocktail" patchelf --remove-rpath \ "${STAGING}/mocktail/bin/mocktail_webview_helper" for artifact in "${PROJECT_ARTIFACTS[@]}"; do case "${artifact}" in mocktail|mocktail_webview_helper) continue ;; esac patchelf --set-rpath '$ORIGIN' \ "${STAGING}/mocktail/bin/${artifact}" done return 0 fi local elf directory relative_to_lib central_path existing_path new_path while IFS= read -r -d '' elf; do IsElfFile "${elf}" || continue LC_ALL=C readelf -d "${elf}" 2>/dev/null | grep -q 'Dynamic section' || continue case "${elf#${STAGING}/mocktail/}" in runtime/android-tools/*) continue ;; esac directory="$(dirname -- "${elf}")" relative_to_lib="$(realpath --relative-to="${directory}" -- \ "${STAGING}/mocktail/lib")" if [[ "${relative_to_lib}" == . ]]; then central_path='$ORIGIN' else central_path="\$ORIGIN/${relative_to_lib}" fi existing_path="$(patchelf --print-rpath "${elf}" 2>/dev/null || true)" new_path="\$ORIGIN:${central_path}" if [[ -n "${existing_path}" ]]; then new_path+=":${existing_path}" fi patchelf --set-rpath "${new_path}" "${elf}" done < <(find "${STAGING}/mocktail" -type f \ \( -perm -0100 -o -name '*.so' -o -name '*.so.*' \) -print0) patchelf --set-rpath '$ORIGIN:$ORIGIN/../lib' \ "${STAGING}/mocktail/bin/mocktail" \ "${STAGING}/mocktail/bin/mocktail_failure_dialog" \ "${STAGING}/mocktail/bin/mocktail_webview_helper" } WriteBundleChecksums() { touch "${STAGING}/mocktail/metadata/.mocktail-portable-bundle" ( cd -- "${STAGING}" find . -type f \ ! -path './mocktail/metadata/SHA256SUMS.txt' -print0 | LC_ALL=C sort -z | while IFS= read -r -d '' path; do sha256sum -- "${path}" done ) >"${STAGING}/mocktail/metadata/SHA256SUMS.txt" } VerifyBundle() { local elf dynamic_path dependencies packaged_interpreter packaged_libc local public_entries runtime_entries local expected_dynamic_path='$ORIGIN' if [[ "${MODE}" == standalone ]]; then expected_dynamic_path='$ORIGIN:$ORIGIN/../lib' fi public_entries="$(find "${STAGING}" -mindepth 1 -maxdepth 1 \ -printf '%f\n' | LC_ALL=C sort)" [[ "${public_entries}" == $'mocktail\nrun.sh' ]] || Die "portable root must contain only run.sh and mocktail/" [[ -x "${STAGING}/run.sh" && ! -L "${STAGING}/run.sh" && -d "${STAGING}/mocktail" && ! -L "${STAGING}/mocktail" ]] || Die "portable public layout contains an invalid launcher or runtime root" runtime_entries="$(find "${STAGING}/mocktail" -mindepth 1 -maxdepth 1 \ -printf '%f\n' | LC_ALL=C sort)" if [[ "${MODE}" == standalone ]]; then [[ "${runtime_entries}" == \ $'bin\nlib\nlibexec\nmetadata\nnamespace\nruntime\nscripts\nshare\nwebkit.env' ]] || Die "standalone runtime root has an unexpected public entry: ${runtime_entries//$'\n'/,}" else [[ "${runtime_entries}" == \ $'bin\nlib\nmetadata\nruntime\nscripts' ]] || Die "thin runtime root has an unexpected public entry: ${runtime_entries//$'\n'/,}" fi while IFS= read -r -d '' elf; do if LC_ALL=C readelf -d "${elf}" 2>/dev/null | grep -F -- "${BUILD_DIR}" >/dev/null; then Die "absolute build path remains in packaged ELF: ${elf}" fi done < <(find "${STAGING}/mocktail" -type f \ \( -perm -0100 -o -name '*.so' -o -name '*.so.*' \) -print0) dynamic_path="$(LC_ALL=C readelf -d "${STAGING}/mocktail/bin/mocktail" | sed -n 's/.*\(RPATH\|RUNPATH\).*\[\(.*\)\].*/\2/p')" [[ "${dynamic_path}" == "${expected_dynamic_path}" ]] || Die "unexpected packaged mocktail RUNPATH: ${dynamic_path}" packaged_interpreter="$(ReadElfInterpreter \ "${STAGING}/mocktail/bin/mocktail")" [[ "${packaged_interpreter}" == "${TARGET_INTERPRETER}" ]] || Die "packaged mocktail interpreter changed unexpectedly" packaged_libc="$(DetectElfLibc "${STAGING}/mocktail/bin/mocktail")" [[ "${packaged_libc}" == "${TARGET_LIBC}" ]] || Die "packaged mocktail libc ABI changed unexpectedly" grep -Fxq 'schema=2' "${STAGING}/mocktail/metadata/ABI.txt" || Die "portable ABI manifest has an invalid schema" grep -Fxq 'architecture=x86_64' \ "${STAGING}/mocktail/metadata/ABI.txt" || Die "portable ABI manifest has an invalid architecture" grep -Fxq "libc=${TARGET_LIBC}" \ "${STAGING}/mocktail/metadata/ABI.txt" || Die "portable ABI manifest has an invalid libc" grep -Fxq "mode=${MODE}" "${STAGING}/mocktail/metadata/ABI.txt" || Die "portable ABI manifest has an invalid packaging mode" grep -Fxq "interpreter=${TARGET_INTERPRETER}" \ "${STAGING}/mocktail/metadata/ABI.txt" || Die "portable ABI manifest has an invalid interpreter" grep -Fxq "android_tools_libc=${ANDROID_TOOLS_ABI}" \ "${STAGING}/mocktail/metadata/ABI.txt" || Die "portable ABI manifest has an invalid Android tools ABI" grep -Fxq "mode=${MODE}" \ "${STAGING}/mocktail/metadata/DEPENDENCIES.txt" || Die "portable dependency manifest has an invalid packaging mode" if [[ "${MODE}" == thin ]] && find "${STAGING}/mocktail/lib" -mindepth 1 -maxdepth 1 \ -print -quit | grep -q .; then Die "thin bundle unexpectedly contains userspace dependency libraries" fi local artifact for artifact in "${PROJECT_ARTIFACTS[@]}"; do if [[ "${MODE}" == standalone ]]; then dependencies="$(ListElfDependencies \ "${STAGING}/mocktail/bin/${artifact}")" || Die "packaged artifact has unresolved dependencies: ${artifact}" else ListElfNeededSonames \ "${STAGING}/mocktail/bin/${artifact}" >/dev/null || Die "cannot inspect packaged artifact dependencies: ${artifact}" fi done if [[ "${MODE}" == standalone ]]; then local support_elf for support_elf in \ runtime/bin/bash runtime/jre/bin/java \ runtime/bin/bwrap runtime/bin/xdg-dbus-proxy \ libexec/webkitgtk-6.0/WebKitWebProcess \ libexec/gstreamer-1.0/gst-plugin-scanner; do ListElfDependencies "${STAGING}/mocktail/${support_elf}" >/dev/null || Die "standalone support ELF has unresolved dependencies: ${support_elf}" done fi local android_runtime="${STAGING}/mocktail/runtime/android-tools/bin" local verification_path="${android_runtime}:${PATH}" local verification_java_home="${JAVA_HOME:-}" if [[ "${MODE}" == standalone ]]; then verification_path="${STAGING}/mocktail/runtime/bin:${verification_path}" verification_java_home="${STAGING}/mocktail/runtime/jre" fi if [[ "${ANDROID_TOOLS_ABI}" == java ]]; then JAVA_HOME="${verification_java_home}" PATH="${verification_path}" \ "${android_runtime}/apkanalyzer" --help >/dev/null 2>&1 else "${android_runtime}/aapt" version >/dev/null fi JAVA_HOME="${verification_java_home}" PATH="${verification_path}" \ "${android_runtime}/apksigner" \ version >/dev/null (cd -- "${STAGING}" && sha256sum --quiet -c mocktail/metadata/SHA256SUMS.txt) || Die "packaged file checksum verification failed" if [[ "${PACKAGING_HOST_LIBC}" == "${TARGET_LIBC}" ]]; then MOCKTAIL_SKIP_HOST_CHECK=1 MOCKTAIL_SKIP_NAMESPACE_CHECK=1 \ "${STAGING}/run.sh" --help >/dev/null else Log "skipping cross-libc launcher execution during thin packaging" fi } PublishBundle() { if [[ -e "${OUTPUT}" ]]; then [[ -d "${OUTPUT}" && ( -f "${OUTPUT}/mocktail/metadata/.mocktail-portable-bundle" || -f "${OUTPUT}/.mocktail-portable-bundle" ) ]] || Die "refusing to replace unmarked output path: ${OUTPUT}" rm -rf -- "${OUTPUT}" fi mv -- "${STAGING}" "${OUTPUT}" STAGING="" Log "bundle ready: ${OUTPUT}/run.sh" } BuildAppImage() { local icon_path icon_relative_path [[ -n "${APPIMAGE_OUTPUT}" ]] || return 0 APPDIR="$(mktemp -d "${OUTPUT%/*}/.Mocktail.AppDir.XXXXXX")" mkdir -p "${APPDIR}/usr/share/mocktail-bundle" \ "${APPDIR}/usr/share/applications" \ "${APPDIR}/usr/share/icons/hicolor/scalable/apps" cp -a -- "${OUTPUT}/." "${APPDIR}/usr/share/mocktail-bundle/" ln -s "share/mocktail-bundle/run.sh" "${APPDIR}/usr/mocktail" install -m 0755 -- "${PROJECT_ROOT}/packaging/AppRun" "${APPDIR}/AppRun" install -m 0644 -- \ "${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.desktop" \ "${APPDIR}/io.github.CoderDayton.nightcap.desktop" install -m 0644 -- \ "${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.desktop" \ "${APPDIR}/usr/share/applications/io.github.CoderDayton.nightcap.desktop" install -m 0644 -- \ "${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.svg" \ "${APPDIR}/io.github.CoderDayton.nightcap.svg" install -m 0644 -- \ "${PROJECT_ROOT}/packaging/io.github.CoderDayton.nightcap.svg" \ "${APPDIR}/usr/share/icons/hicolor/scalable/apps/io.github.CoderDayton.nightcap.svg" for icon_path in \ "${PROJECT_ROOT}"/packaging/icons/hicolor/*x*/apps/io.github.CoderDayton.nightcap.png; do [[ -f "${icon_path}" ]] || Die "hicolor icon set is incomplete" icon_relative_path="${icon_path#"${PROJECT_ROOT}/packaging/icons/hicolor/"}" install -D -m 0644 -- "${icon_path}" \ "${APPDIR}/usr/share/icons/hicolor/${icon_relative_path}" done ln -s io.github.CoderDayton.nightcap.svg "${APPDIR}/.DirIcon" mkdir -p -- "$(dirname -- "${APPIMAGE_OUTPUT}")" ARCH=x86_64 appimagetool "${APPDIR}" "${APPIMAGE_OUTPUT}" rm -rf -- "${APPDIR}" APPDIR="" Log "AppImage ready: ${APPIMAGE_OUTPUT}" } DryRun() { STAGING="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-portable-dry-run.XXXXXX")" CollectDependencies printf 'mode=%s\n' "${MODE}" printf 'libc=%s\n' "${TARGET_LIBC}" printf 'artifact_name=%s\n' "${ARTIFACT_NAME}" printf 'interpreter=%s\n' "${TARGET_INTERPRETER}" printf 'android_tools_libc=%s\n' "${ANDROID_TOOLS_ABI}" printf 'project_artifacts=%d\n' "${#PROJECT_ARTIFACTS[@]}" printf 'bundled_dependency_count=%d\n' "${#COPIED_SOURCES[@]}" printf 'host_dependency_count=%d\n' "${#HOST_REQUIREMENTS[@]}" if (( ${#HOST_REQUIREMENTS[@]} > 0 )); then local soname while IFS= read -r soname; do printf 'host=%s reason=%s\n' "${soname}" "${HOST_REQUIREMENTS[${soname}]}" done < <(printf '%s\n' "${!HOST_REQUIREMENTS[@]}" | sort) fi } Main() { ParseArguments "$@" if [[ -n "${APPIMAGE_OUTPUT}" && "${APPIMAGE_FORMAT}" == anylinux ]]; then [[ "${MODE}" == standalone ]] || Die "AnyLinux is self-contained; use classic for a thin bundle" [[ "${DRY_RUN}" == false ]] || Die "AnyLinux packaging requires a real native install, not a portable dry-run" local -a anylinux_arguments=( --build-dir "${BUILD_DIR}" --appimage "${APPIMAGE_OUTPUT}" ) if [[ "${MOCKTAIL_ANYLINUX_SYSTEM_INSTALL:-0}" == 1 ]]; then anylinux_arguments+=(--system-install) fi exec "${PROJECT_ROOT}/scripts/package_anylinux.sh" "${anylinux_arguments[@]}" fi RequireCommand readelf RequireCommand patchelf RequireCommand sha256sum RequireCommand paste RequireCommand unzip if [[ -n "${APPIMAGE_OUTPUT}" ]]; then RequireCommand appimagetool fi BUILD_DIR="$(CanonicalDirectory "${BUILD_DIR}")" ValidateArtifacts PACKAGING_HOST_LIBC="$(DetectHostLibc)" [[ "${PACKAGING_HOST_LIBC}" != unknown ]] || Die "cannot determine packaging host libc" if [[ "${MODE}" == standalone ]]; then RequireCommand ldd [[ "${PACKAGING_HOST_LIBC}" == "${TARGET_LIBC}" ]] || Die "standalone mode requires a matching ${TARGET_LIBC} host; detected ${PACKAGING_HOST_LIBC}" fi ResolveAndroidBuildTools trap Cleanup EXIT if [[ "${DRY_RUN}" == true ]]; then DryRun return 0 fi [[ ! -L "${OUTPUT}" ]] || Die "refusing symlink output path: ${OUTPUT}" OUTPUT="$(readlink -m -- "${OUTPUT}")" [[ "${OUTPUT}" != / && "${OUTPUT}" != "${PROJECT_ROOT}" ]] || Die "unsafe output path: ${OUTPUT}" mkdir -p -- "$(dirname -- "${OUTPUT}")" STAGING="$(mktemp -d "${OUTPUT}.tmp.XXXXXX")" CopyRuntimeTree PackageStandaloneRuntime PackageAndroidRuntime CollectDependencies WriteDependencyManifest PatchRuntimePaths BuildStandaloneNamespace WriteAbiManifest WriteBundleChecksums VerifyBundle PublishBundle BuildAppImage } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then Main "$@" fi