#!/usr/bin/env bash # 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 RUNTIME_ROOT="" TARGET_LIBC="" SOURCE_ROOT="/" DRY_RUN=false STAGING="" COMMITTED=false declare -a PUBLISHED_PATHS=() WEBKITGTK6_EXEC_SOURCE="" WEBKITGTK6_BUNDLE_SOURCE="" WEBKITGTK6_NAMESPACE_DIR="" GSTREAMER_PLUGIN_SOURCE="" GSTREAMER_SCANNER_SOURCE="" GDK_PIXBUF_SOURCE="" GIO_MODULE_SOURCE="" SCHEMA_SOURCE="" MIME_SOURCE="" GLYCIN_BINARY_SOURCE="" GLYCIN_DATA_SOURCE="" SOURCE_LAYOUT="" GDK_PIXBUF_BACKEND="" Usage() { cat <<'EOF' Usage: scripts/package_standalone_webkit.sh --runtime-root DIR --libc ABI [OPTIONS] Copy the native WebKitGTK 6 process resources needed by a standalone Mocktail runtime. Shared-library closure and ELF path patching are handled by the parent packager. Required options: --runtime-root DIR Existing Mocktail runtime directory to populate. --libc ABI Native source ABI: glibc or musl. Optional: --source-root DIR Inspect a mounted native root instead of / (test/audit). --dry-run Stage and validate everything, but publish nothing. -h, --help Show this help. EOF } Log() { printf '[standalone-webkit] %s\n' "$*" >&2 } Die() { Log "error: $*" exit 1 } Cleanup() { local status=$? trap - EXIT if (( status != 0 )) && [[ "${COMMITTED}" == false ]]; then local path for path in "${PUBLISHED_PATHS[@]}"; do rm -rf -- "${path}" done fi [[ -z "${STAGING}" ]] || rm -rf -- "${STAGING}" exit "${status}" } ParseArguments() { while (( $# > 0 )); do case "$1" in --runtime-root) (( $# >= 2 )) || Die "--runtime-root requires a directory" RUNTIME_ROOT="$2" shift 2 ;; --libc) (( $# >= 2 )) || Die "--libc requires glibc or musl" TARGET_LIBC="$2" shift 2 ;; --source-root) (( $# >= 2 )) || Die "--source-root requires a directory" SOURCE_ROOT="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; -h|--help) Usage exit 0 ;; *) Die "unknown option: $1" ;; esac done [[ -n "${RUNTIME_ROOT}" ]] || Die "--runtime-root is required" [[ "${TARGET_LIBC}" == glibc || "${TARGET_LIBC}" == musl ]] || Die "--libc must be glibc or musl" [[ -d "${RUNTIME_ROOT}" && ! -L "${RUNTIME_ROOT}" ]] || Die "runtime root must be an existing non-symlink directory" [[ -d "${SOURCE_ROOT}" && ! -L "${SOURCE_ROOT}" ]] || Die "source root must be an existing non-symlink directory" RUNTIME_ROOT="$(cd -P -- "${RUNTIME_ROOT}" && pwd)" SOURCE_ROOT="$(cd -P -- "${SOURCE_ROOT}" && pwd)" } RequireCommands() { local command_name for command_name in awk cat cp dirname find grep install mkdir mktemp mv \ readelf readlink rm sed sort wc; do command -v "${command_name}" >/dev/null 2>&1 || Die "missing required command: ${command_name}" done } SourcePath() { local suffix="$1" if [[ "${SOURCE_ROOT}" == / ]]; then printf '/%s\n' "${suffix#/}" else printf '%s/%s\n' "${SOURCE_ROOT}" "${suffix#/}" fi } FindDirectoryContaining() { local required_name="$1" shift local suffix candidate for suffix in "$@"; do candidate="$(SourcePath "${suffix}")" if [[ -d "${candidate}" && ! -L "${candidate}" && -f "${candidate}/${required_name}" && ! -L "${candidate}/${required_name}" ]]; then printf '%s\n' "${candidate}" return 0 fi done return 1 } FindDirectory() { local suffix candidate for suffix in "$@"; do candidate="$(SourcePath "${suffix}")" if [[ -d "${candidate}" && ! -L "${candidate}" ]]; then printf '%s\n' "${candidate}" return 0 fi done return 1 } FindRegularFile() { local suffix candidate for suffix in "$@"; do candidate="$(SourcePath "${suffix}")" if [[ -f "${candidate}" && ! -L "${candidate}" ]]; then printf '%s\n' "${candidate}" return 0 fi done return 1 } ValidateNamespaceDirectory() { local namespace_directory="$1" local family="$2" case "${family}:${namespace_directory}" in webkitgtk6:/usr/lib/webkitgtk-6.0|\ webkitgtk6:/usr/lib/x86_64-linux-gnu/webkitgtk-6.0|\ webkitgtk6:/usr/lib64/webkitgtk-6.0|\ webkitgtk6:/usr/libexec/webkitgtk-6.0) ;; *) Die "unsupported ${family} namespace directory: ${namespace_directory}" ;; esac } NamespaceDirectoryForSource() { local source="$1" local family="$2" local namespace_directory if [[ "${SOURCE_ROOT}" == / ]]; then namespace_directory="${source}" else case "${source}" in "${SOURCE_ROOT}"/*) namespace_directory="${source#"${SOURCE_ROOT}"}" ;; *) Die "${family} process directory escaped the source root: ${source}" ;; esac fi ValidateNamespaceDirectory "${namespace_directory}" "${family}" printf '%s\n' "${namespace_directory}" } DiscoverSources() { WEBKITGTK6_EXEC_SOURCE="$(FindDirectoryContaining WebKitWebProcess \ usr/lib/webkitgtk-6.0 \ usr/lib/x86_64-linux-gnu/webkitgtk-6.0 \ usr/lib64/webkitgtk-6.0 \ usr/libexec/webkitgtk-6.0)" || Die "cannot find native WebKitGTK 6 process directory" WEBKITGTK6_NAMESPACE_DIR="$(NamespaceDirectoryForSource \ "${WEBKITGTK6_EXEC_SOURCE}" webkitgtk6)" WEBKITGTK6_BUNDLE_SOURCE="$(FindDirectoryContaining \ libwebkitgtkinjectedbundle.so usr/lib/webkitgtk-6.0/injected-bundle \ usr/lib/x86_64-linux-gnu/webkitgtk-6.0/injected-bundle \ usr/lib64/webkitgtk-6.0/injected-bundle)" || Die "cannot find WebKitGTK 6 injected bundle" GSTREAMER_PLUGIN_SOURCE="$(FindDirectory \ usr/lib/gstreamer-1.0 \ usr/lib/x86_64-linux-gnu/gstreamer-1.0 \ usr/lib64/gstreamer-1.0)" || Die "cannot find the native GStreamer plugin directory" GSTREAMER_SCANNER_SOURCE="$(FindRegularFile \ usr/lib/gstreamer-1.0/gst-plugin-scanner \ usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner \ usr/libexec/gstreamer-1.0/gst-plugin-scanner \ usr/lib64/gstreamer-1.0/gst-plugin-scanner)" || Die "cannot find the native GStreamer plugin scanner" GIO_MODULE_SOURCE="$(FindDirectory usr/lib/gio/modules \ usr/lib/x86_64-linux-gnu/gio/modules \ usr/lib64/gio/modules)" || Die "cannot find native GIO modules" SCHEMA_SOURCE="$(FindDirectory usr/share/glib-2.0/schemas)" || Die "cannot find GLib schemas" MIME_SOURCE="$(FindDirectory usr/share/mime)" || Die "cannot find the shared MIME database" GDK_PIXBUF_SOURCE="$(FindDirectory \ usr/lib/gdk-pixbuf-2.0/2.10.0 \ usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0 \ usr/lib64/gdk-pixbuf-2.0/2.10.0 || true)" GLYCIN_BINARY_SOURCE="$(FindDirectory \ usr/lib/glycin-loaders/2+ usr/lib64/glycin-loaders/2+ \ usr/libexec/glycin-loaders/2+ || true)" GLYCIN_DATA_SOURCE="$(FindDirectory \ usr/share/glycin-loaders/2+/conf.d || true)" if [[ -n "${GDK_PIXBUF_SOURCE}" && -d "${GDK_PIXBUF_SOURCE}/loaders" ]]; then GDK_PIXBUF_BACKEND=classic elif [[ -n "${GLYCIN_BINARY_SOURCE}" && -n "${GLYCIN_DATA_SOURCE}" ]]; then GDK_PIXBUF_BACKEND=glycin else Die "cannot find classic GdkPixbuf modules or glycin loaders" fi case "${WEBKITGTK6_EXEC_SOURCE}" in */usr/lib/x86_64-linux-gnu/webkitgtk-6.0) SOURCE_LAYOUT=debian-multiarch ;; */usr/lib/webkitgtk-6.0) SOURCE_LAYOUT=arch-lib ;; */usr/libexec/webkitgtk-6.0) SOURCE_LAYOUT=void-libexec ;; *) SOURCE_LAYOUT=custom ;; esac } 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 version_info interpreter="$(ReadElfInterpreter "${path}")" case "${interpreter}" in *ld-musl-*.so.1) printf musl; return 0 ;; *ld-linux*.so.*) printf glibc; return 0 ;; esac needed="$(LC_ALL=C readelf -d "${path}" 2>/dev/null | sed -n 's/.*Shared library: \[\([^]]*\)\].*/\1/p')" if grep -Fxq libc.so.6 <<<"${needed}"; then printf glibc return 0 fi if grep -Fxq libc.so <<<"${needed}"; then printf musl return 0 fi version_info="$(LC_ALL=C readelf --version-info "${path}" 2>/dev/null || true)" if grep -Eq 'GLIBC_[0-9]' <<<"${version_info}"; then printf glibc else printf unknown fi } ValidateElf() { local path="$1" local label="$2" local allow_unknown="${3:-false}" local machine detected [[ -f "${path}" && ! -L "${path}" ]] || Die "${label} is not a regular file: ${path}" machine="$(LC_ALL=C readelf -h "${path}" 2>/dev/null | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')" [[ "${machine}" == *X86-64* ]] || Die "${label} is not x86-64 ELF" detected="$(DetectElfLibc "${path}")" if [[ "${allow_unknown}" == true && "${detected}" == unknown ]]; then return 0 fi [[ "${detected}" == "${TARGET_LIBC}" ]] || Die "${label} targets ${detected}, expected ${TARGET_LIBC}: ${path}" } ValidateSourceTree() { local directory="$1" local label="$2" [[ -d "${directory}" && ! -L "${directory}" ]] || Die "${label} is not a regular directory" local canonical target link canonical="$(readlink -f -- "${directory}")" while IFS= read -r -d '' link; do target="$(readlink -f -- "${link}" 2>/dev/null || true)" [[ -n "${target}" && ( "${target}" == "${canonical}" || "${target}" == "${canonical}/"* ) ]] || Die "${label} contains an escaping or broken symlink: ${link}" done < <(find -P "${directory}" -type l -print0) if find -P "${directory}" -mindepth 1 \ ! -type d ! -type f ! -type l -print -quit | grep -q .; then Die "${label} contains a special filesystem entry" fi } ValidateSources() { local process_name for process_name in WebKitGPUProcess WebKitNetworkProcess WebKitWebProcess; do ValidateElf "${WEBKITGTK6_EXEC_SOURCE}/${process_name}" \ "WebKitGTK 6 ${process_name}" done ValidateElf "${WEBKITGTK6_BUNDLE_SOURCE}/libwebkitgtkinjectedbundle.so" \ "WebKitGTK 6 injected bundle" ValidateElf "${GSTREAMER_SCANNER_SOURCE}" "GStreamer plugin scanner" [[ -f "${SCHEMA_SOURCE}/gschemas.compiled" ]] || Die "GLib schema directory has no gschemas.compiled" [[ -f "${MIME_SOURCE}/mime.cache" ]] || Die "shared MIME database has no mime.cache" [[ -f "${GIO_MODULE_SOURCE}/libgiognutls.so" ]] || Die "GIO TLS module libgiognutls.so is unavailable" local plugin_count gio_count plugin_count="$(find -P "${GSTREAMER_PLUGIN_SOURCE}" -maxdepth 1 \ -type f -name '*.so' -printf . | wc -c)" gio_count="$(find -P "${GIO_MODULE_SOURCE}" -maxdepth 1 \ -type f -name '*.so' -printf . | wc -c)" (( plugin_count > 0 )) || Die "GStreamer plugin directory is empty" (( gio_count > 0 )) || Die "GIO module directory is empty" } CopyTree() { local source="$1" local destination="$2" local label="$3" ValidateSourceTree "${source}" "${label}" mkdir -p -- "${destination}" cp -a -- "${source}/." "${destination}/" } CopyFlatElfSet() { local source="$1" local destination="$2" local label="$3" mkdir -p -- "${destination}" local path count=0 while IFS= read -r -d '' path; do ValidateElf "${path}" "${label}" true install -m 0755 -- "${path}" "${destination}/${path##*/}" ((count += 1)) done < <(find -P "${source}" -maxdepth 1 -type f -name '*.so' \ -print0 | sort -z) (( count > 0 )) || Die "${label} set is empty" } CopyWebKitProcesses() { local source="$1" local destination="$2" local label="$3" mkdir -p -- "${destination}" local process_name for process_name in WebKitGPUProcess WebKitNetworkProcess WebKitWebProcess; do install -m 0755 -- "${source}/${process_name}" \ "${destination}/${process_name}" done Log "copied ${label} processes from ${source}" } WriteClassicPixbufCache() { local source_cache="${GDK_PIXBUF_SOURCE}/loaders.cache" local destination_cache= destination_cache="${STAGING}/lib/plugins/gdk-pixbuf-2.0/2.10.0/loaders.cache" [[ -f "${source_cache}" && ! -L "${source_cache}" ]] || Die "classic GdkPixbuf loader cache is unavailable" sed -E \ -e 's@^# LoaderDir = .*$@# LoaderDir = lib/plugins/gdk-pixbuf-2.0/2.10.0/loaders@' \ -e 's#^"[^"]*/([^/"]+\.so)"$#"lib/plugins/gdk-pixbuf-2.0/2.10.0/loaders/\1"#' \ "${source_cache}" >"${destination_cache}" # Module records contain only a quoted path. Image signatures such as XPM # can also start with '/' but have additional fields after the string. if grep -Eq '^"/[^"]*"$' \ "${destination_cache}"; then Die "GdkPixbuf cache still contains an absolute loader path" fi } CopyGdkPixbufResources() { local destination="${STAGING}/lib/plugins/gdk-pixbuf-2.0/2.10.0" mkdir -p -- "${destination}" if [[ "${GDK_PIXBUF_BACKEND}" == classic ]]; then CopyFlatElfSet "${GDK_PIXBUF_SOURCE}/loaders" \ "${destination}/loaders" "GdkPixbuf loader" WriteClassicPixbufCache return fi printf '%s\n' \ '# GdkPixbuf Image Loader Modules file' \ '# No classic modules: this native build uses glycin loaders.' \ >"${destination}/loaders.cache" mkdir -p -- "${STAGING}/lib/plugins/glycin-loaders/2+" \ "${STAGING}/share/glycin-loaders/2+/conf.d" local executable config while IFS= read -r -d '' executable; do # Arch ships the HEIF loader even when its optional libheif dependency is # absent. Mocktail's WebKit UI does not require HEIF/AVIF decoding. [[ "${executable##*/}" != glycin-heif ]] || continue ValidateElf "${executable}" "glycin image loader" install -m 0755 -- "${executable}" \ "${STAGING}/lib/plugins/glycin-loaders/2+/${executable##*/}" done < <(find -P "${GLYCIN_BINARY_SOURCE}" -maxdepth 1 -type f \ -perm -0100 -print0 | sort -z) while IFS= read -r -d '' config; do [[ "${config##*/}" != glycin-heif.conf ]] || continue sed -E \ 's#^([[:space:]]*Exec=).*/([^/[:space:]]+)[[:space:]]*$#\1lib/plugins/glycin-loaders/2+/\2#' \ "${config}" \ >"${STAGING}/share/glycin-loaders/2+/conf.d/${config##*/}" done < <(find -P "${GLYCIN_DATA_SOURCE}" -maxdepth 1 -type f \ -name '*.conf' -print0 | sort -z) if grep -REq '^[[:space:]]*Exec=/' \ "${STAGING}/share/glycin-loaders/2+/conf.d"; then Die "glycin configuration still contains an absolute executable path" fi } CopyGtkData() { CopyTree "${SCHEMA_SOURCE}" "${STAGING}/share/glib-2.0/schemas" \ "GLib schemas" CopyTree "${MIME_SOURCE}" "${STAGING}/share/mime" \ "shared MIME database" local suffix source destination for suffix in icons/Adwaita icons/AdwaitaLegacy themes/Default \ fonts/Adwaita; do source="$(SourcePath "usr/share/${suffix}")" [[ -d "${source}" && ! -L "${source}" ]] || continue destination="${STAGING}/share/${suffix}" CopyTree "${source}" "${destination}" "GTK data ${suffix}" done source="$(SourcePath usr/share/icons/hicolor/index.theme)" if [[ -f "${source}" && ! -L "${source}" ]]; then mkdir -p -- "${STAGING}/share/icons/hicolor" install -m 0644 -- "${source}" \ "${STAGING}/share/icons/hicolor/index.theme" fi if [[ -d "${STAGING}/share/fonts" ]]; then mkdir -p -- "${STAGING}/share/fontconfig" cat >"${STAGING}/share/fontconfig/fonts.conf" <<'EOF' ../fonts fontconfig EOF fi } WriteEnvironmentFile() { local glycin_rel="" fontconfig_rel="" if [[ "${GDK_PIXBUF_BACKEND}" == glycin ]]; then glycin_rel=share/glycin-loaders/2+ fi if [[ -f "${STAGING}/share/fontconfig/fonts.conf" ]]; then fontconfig_rel=share/fontconfig/fonts.conf fi cat >"${STAGING}/webkit.env" <