mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 02:00:48 +02:00
377 lines
11 KiB
Bash
Executable File
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 "$@"
|