Files
InstanceBrick--nightcap/scripts/package_standalone_support.sh
T
2026-08-28 17:54:52 +03:00

377 lines
11 KiB
Bash
Executable File

#!/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
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -Eeuo pipefail
umask 022
RUNTIME_ROOT=""
TARGET_LIBC=""
STAGING_ROOT=""
SUPPORT_RUNTIME=""
readonly -a REQUIRED_COMMANDS=(
bash
bwrap
xdg-dbus-proxy
jq
unzip
file
flock
timeout
readelf
sha256sum
curl
tar
gzip
find
sed
grep
awk
sort
comm
stat
id
mktemp
dirname
uname
)
Usage() {
cat <<'EOF'
Usage: scripts/package_standalone_support.sh --runtime-root DIR --libc ABI
Populate DIR/runtime with the native command, Java, and CA support needed by
the standalone Mocktail launcher. ABI must be glibc or musl and must match the
build host. ELF dependency closure is intentionally left to the parent
packager.
EOF
}
Log() {
printf '[standalone-support] %s\n' "$*" >&2
}
Die() {
Log "error: $*"
exit 1
}
Cleanup() {
if [[ -n "${STAGING_ROOT}" && -d "${STAGING_ROOT}" &&
! -L "${STAGING_ROOT}" ]]; then
rm -rf -- "${STAGING_ROOT}"
fi
}
trap Cleanup EXIT
ParseArguments() {
while (( $# > 0 )); do
case "$1" in
--runtime-root)
(( $# >= 2 )) || Die "--runtime-root requires a directory"
RUNTIME_ROOT="$2"
shift 2
;;
--runtime-root=*)
RUNTIME_ROOT="${1#*=}"
shift
;;
--libc)
(( $# >= 2 )) || Die "--libc requires glibc or musl"
TARGET_LIBC="$2"
shift 2
;;
--libc=*)
TARGET_LIBC="${1#*=}"
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"
}
RequireBuildCommands() {
local command_name
for command_name in cp cmp install java jlink ldd ln mkdir mv \
readlink realpath rm rmdir; do
command -v "${command_name}" >/dev/null 2>&1 ||
Die "required build command is unavailable: ${command_name}"
done
}
RejectUnsafeText() {
local -r value="$1"
local -r description="$2"
[[ -n "${value}" && "${value}" != *$'\n'* && "${value}" != *$'\r'* &&
"${value}" != *$'\t'* ]] || Die "${description} is empty or unsafe"
}
ValidateRuntimeRoot() {
RejectUnsafeText "${RUNTIME_ROOT}" "runtime root"
[[ "${RUNTIME_ROOT}" == /* ]] || Die "runtime root must be absolute"
[[ -d "${RUNTIME_ROOT}" && ! -L "${RUNTIME_ROOT}" ]] ||
Die "runtime root must be an existing non-symlink directory"
local canonical_root
canonical_root="$(realpath -e -- "${RUNTIME_ROOT}")" ||
Die "cannot canonicalize runtime root"
canonical_root="${canonical_root%/}"
[[ -n "${canonical_root}" && "${canonical_root}" != / ]] ||
Die "refusing unsafe runtime root"
[[ "${RUNTIME_ROOT%/}" == "${canonical_root}" ]] ||
Die "runtime root must be canonical and contain no symlink components"
RUNTIME_ROOT="${canonical_root}"
local -r destination="${RUNTIME_ROOT}/runtime"
if [[ -e "${destination}" || -L "${destination}" ]]; then
[[ -d "${destination}" && ! -L "${destination}" ]] ||
Die "runtime destination must not be a symlink or non-directory"
if find "${destination}" -mindepth 1 -maxdepth 1 -print -quit |
grep -q .; then
Die "runtime destination is not empty: ${destination}"
fi
fi
}
DetectHostLibc() {
local output=""
output="$(getconf GNU_LIBC_VERSION 2>/dev/null || true)"
if [[ "${output}" == glibc\ * ]]; then
printf 'glibc\n'
return
fi
output="$(ldd --version 2>&1 || true)"
if grep -qi musl <<<"${output}"; then
printf 'musl\n'
return
fi
local interpreter=""
interpreter="$(LC_ALL=C readelf -l /proc/$$/exe 2>/dev/null |
sed -n 's/.*Requesting program interpreter: \([^]]*\).*/\1/p')"
case "${interpreter}" in
*ld-linux*.so.*) printf 'glibc\n' ;;
*ld-musl-*.so.1) printf 'musl\n' ;;
*) printf 'unknown\n' ;;
esac
}
ValidateNativeHost() {
local host_libc
host_libc="$(DetectHostLibc)"
[[ "${host_libc}" != unknown ]] || Die "cannot determine host libc"
[[ "${host_libc}" == "${TARGET_LIBC}" ]] ||
Die "requested ${TARGET_LIBC} support on a ${host_libc} host; cross-packaging is unsupported"
}
PathIsInside() {
local -r path="$1"
local -r root="$2"
[[ "${path}" == "${root}" || "${path}" == "${root}/"* ]]
}
InstallRegularFile() {
local -r source="$1"
local -r destination="$2"
[[ -f "${source}" && ! -L "${source}" ]] ||
Die "source is not a regular file: ${source}"
mkdir -p -- "$(dirname -- "${destination}")"
if [[ -e "${destination}" || -L "${destination}" ]]; then
[[ -f "${destination}" && ! -L "${destination}" ]] ||
Die "support file collision: ${destination}"
cmp -s -- "${source}" "${destination}" ||
Die "different support files collide at ${destination}"
return
fi
cp -p -- "${source}" "${destination}"
}
CopyCommand() {
local -r name="$1"
local source resolved target_name
source="$(type -P -- "${name}" || true)"
[[ -n "${source}" ]] || Die "required runtime command is unavailable: ${name}"
RejectUnsafeText "${source}" "command path"
[[ "${source}" == /* ]] || Die "command path is not absolute: ${source}"
resolved="$(realpath -e -- "${source}")" ||
Die "cannot resolve runtime command: ${source}"
[[ -f "${resolved}" && -x "${resolved}" ]] ||
Die "runtime command is not an executable regular file: ${resolved}"
target_name="$(basename -- "${resolved}")"
[[ "${target_name}" =~ ^[A-Za-z0-9._+-]+$ ]] ||
Die "runtime command has an unsafe target name: ${resolved}"
InstallRegularFile "${resolved}" "${SUPPORT_RUNTIME}/bin/${target_name}"
if [[ "${name}" != "${target_name}" ]]; then
if [[ -e "${SUPPORT_RUNTIME}/bin/${name}" ||
-L "${SUPPORT_RUNTIME}/bin/${name}" ]]; then
Die "runtime command name collision: ${name}"
fi
ln -s -- "${target_name}" "${SUPPORT_RUNTIME}/bin/${name}"
fi
}
CopyRuntimeCommands() {
local command_name
for command_name in "${REQUIRED_COMMANDS[@]}"; do
CopyCommand "${command_name}"
done
if type -P -- rg >/dev/null 2>&1; then
CopyCommand rg
fi
}
CopyCaBundle() {
local -a candidates=()
if [[ -n "${SSL_CERT_FILE:-}" ]]; then
candidates+=("${SSL_CERT_FILE}")
fi
candidates+=(
/etc/ssl/certs/ca-certificates.crt
/etc/pki/tls/certs/ca-bundle.crt
/etc/ssl/ca-bundle.pem
/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
)
local candidate resolved=""
for candidate in "${candidates[@]}"; do
[[ "${candidate}" == /* && -r "${candidate}" ]] || continue
resolved="$(realpath -e -- "${candidate}" 2>/dev/null || true)"
if [[ -n "${resolved}" && -f "${resolved}" && -s "${resolved}" ]]; then
break
fi
resolved=""
done
[[ -n "${resolved}" ]] || Die "cannot find a readable host CA bundle"
mkdir -p -- "${SUPPORT_RUNTIME}/share/ca-certificates"
install -m 0644 -- "${resolved}" \
"${SUPPORT_RUNTIME}/share/ca-certificates/ca-bundle.crt"
}
BuildJlinkRuntime() {
local jlink_executable jlink_resolved java_home module_path
jlink_executable="$(type -P -- jlink || true)"
[[ -n "${jlink_executable}" ]] || Die "jlink is required to build support"
jlink_resolved="$(realpath -e -- "${jlink_executable}")" ||
Die "cannot resolve jlink"
java_home="$(dirname -- "$(dirname -- "${jlink_resolved}")")"
module_path="${java_home}/jmods"
[[ -f "${module_path}/java.base.jmod" &&
-f "${module_path}/java.logging.jmod" &&
-f "${module_path}/java.desktop.jmod" &&
-f "${module_path}/jdk.zipfs.jmod" ]] ||
Die "jlink JMOD files are unavailable under ${java_home}"
local compression=2
if "${jlink_resolved}" --help 2>&1 | grep -q 'zip-{0-9}'; then
compression=zip-6
fi
"${jlink_resolved}" --module-path "${module_path}" \
--add-modules java.base,java.logging,java.desktop,jdk.zipfs \
--strip-debug --no-man-pages \
--no-header-files --compress="${compression}" \
--output "${SUPPORT_RUNTIME}/jre"
# apkanalyzer needs java.desktop data structures, but neither it nor
# apksigner uses Java Sound. Some OpenJDK builds link this optional native
# library against ALSA, which is intentionally absent from the AnyLinux
# build image and must not become a standalone runtime dependency.
local java_sound_library
for java_sound_library in \
"${SUPPORT_RUNTIME}/jre/lib/libjsound.so" \
"${SUPPORT_RUNTIME}/jre/lib/libjsoundalsa.so"; do
if [[ -e "${java_sound_library}" || -L "${java_sound_library}" ]]; then
[[ ! -d "${java_sound_library}" ]] ||
Die "Java Sound library path is unexpectedly a directory"
rm -f -- "${java_sound_library}"
fi
done
[[ -x "${SUPPORT_RUNTIME}/jre/bin/java" ]] ||
Die "jlink did not produce a Java launcher"
ln -s -- "../jre/bin/java" "${SUPPORT_RUNTIME}/bin/java"
local modules
modules="$("${SUPPORT_RUNTIME}/jre/bin/java" --list-modules |
sed 's/@.*//' | LC_ALL=C sort)"
[[ "${modules}" == \
$'java.base\njava.datatransfer\njava.desktop\njava.logging\njava.prefs\njava.xml\njdk.zipfs' ]] ||
Die "jlink image contains unexpected modules"
}
ValidateCopiedSymlinks() {
local -r root="$1"
local link resolved
while IFS= read -r -d '' link; do
resolved="$(realpath -e -- "${link}")" ||
Die "packaged support contains a dangling symlink: ${link}"
PathIsInside "${resolved}" "${root}" ||
Die "packaged support symlink escapes its root: ${link}"
done < <(find -P "${root}" -type l -print0)
}
VerifySupportRuntime() {
local -r ca_bundle="${SUPPORT_RUNTIME}/share/ca-certificates/ca-bundle.crt"
[[ -s "${ca_bundle}" ]] || Die "packaged CA support is incomplete"
ValidateCopiedSymlinks "${SUPPORT_RUNTIME}"
}
PublishRuntime() {
local -r destination="${RUNTIME_ROOT}/runtime"
if [[ -d "${destination}" && ! -L "${destination}" ]]; then
rmdir -- "${destination}" || Die "runtime destination changed during build"
elif [[ -e "${destination}" || -L "${destination}" ]]; then
Die "runtime destination changed during build"
fi
mv -T -- "${SUPPORT_RUNTIME}" "${destination}"
rmdir -- "${STAGING_ROOT}" ||
Die "standalone support staging root is not empty after publish"
STAGING_ROOT=""
Log "native ${TARGET_LIBC} support populated at ${destination}"
}
Main() {
ParseArguments "$@"
RequireBuildCommands
ValidateRuntimeRoot
ValidateNativeHost
STAGING_ROOT="$(mktemp -d -- \
"${RUNTIME_ROOT}/.mocktail-standalone-support.XXXXXX")" ||
Die "cannot create support staging directory"
SUPPORT_RUNTIME="${STAGING_ROOT}/runtime"
mkdir -p -- "${SUPPORT_RUNTIME}/bin" "${SUPPORT_RUNTIME}/jre" \
"${SUPPORT_RUNTIME}/share"
rmdir -- "${SUPPORT_RUNTIME}/jre"
CopyRuntimeCommands
CopyCaBundle
BuildJlinkRuntime
VerifySupportRuntime
PublishRuntime
}
Main "$@"