0
0
mirror of https://github.com/termux/termux-packages.git synced 2024-11-27 05:08:56 +00:00
termux-packages/scripts/bootstrap/termux-bootstrap-second-stage.sh
agnostic-apollo 7827140577
scripts(bootstrap/termux-bootstrap-second-stage.sh): redesign and fix termux bootstrap second stage as per package managers spec
- `apt`/`dpkg`:
  - `dpkg` allows any type of executable file like binaries or shell scripts as maintainer scripts instead of just a script to be run with `bash`, so we execute the file directly instead of passing to `bash`.
  - Set the execute permission for the file before execution, just like `dpkg`, as we are now executing directly instead of passing it to a shell.
  - Change current working directory to `/` before execution, that is consistent with behaviour of normal package installation with `dpkg`, even though it may cause problems.
  - Export the internal environment variables that `dpkg` exports while executing maintainer scripts.
  - Pass `configure` as the first argument to `postinst` script when executing it as per `dpkg` spec. This fixes bootstrap second stage for `apt`/`dpkg` Termux packages, like `nano`, which check if first argument is `configure` before running `postinst` logic. `nano` is currently the only package with a `postinst` script in the default bootstrap generated, and `postinst` logic wouldn't run for `apt` package manager and only for `pacman` before. The `termux-tools` package is the only package with a `preinst` script that deletes files for mirrors that were deleted/invalid, but that does not need to be executed for a working package configuration on a fresh install as only working mirror files would get extracted.
  Check 903902eaa2/packages/nano/build.sh (L28) for `postinst` example.
- `pacman`:
  - Unset `post_install` function if already defined in the env before sourcing package `install` file.
  - Make a more strict check for `post_install` function existence.
  - Change current working directory to `/` before execution, that is consistent with behaviour of normal package installation with `pacman`, even though it may cause problems.
  - Pass the package version as the first argument to `post_install` function when executing it as per `pacman` spec.
- Global changes:
  - The `termux-bootstrap-second-stage.sh` file will now be installed under the `$TERMUX_BOOTSTRAP_CONFIG_DIR_PATH` directory at `$TERMUX__PREFIX/etc/termux/bootstrap` instead of as a `profile.d` script.
  - The Termux app will add support to run the `termux-bootstrap-second-stage.sh` file after it extracts the bootstrap before any shell starts, unlike previously where it was executed as a `profile.d` script by the `login` shell, whose run order could be after other `profile.d` scripts. The second stage will now only be allowed to run once based on the `termux-bootstrap-second-stage.sh.lock` file as a symlink in the same directory as `termux-bootstrap-second-stage.sh` file and only the first instance of `termux-bootstrap-second-stage.sh` which was able to create the symlink as an atomic operation would be allowed to run the second stage in the entire lifetime of the rootfs, check its help for more info.
  - The `profile.d` script support is still left as a fallback with the new `01-termux-bootstrap-second-stage-fallback.sh` script with a potentially higher run order due to `01-` prefix, which calls the `termux-bootstrap-second-stage.sh` file  if the `termux-bootstrap-second-stage.sh.lock` file  does not exist and deletes itself afterwards so that it is not sourced again. This will be removed in future when support has been added in the Termux app to officially install pacman bootstraps, which are currently been extracted from a shell instead of by the app, requiring the need for a `profile.d` script to run the second stage. Check the file comments for more info.
  - If any maintainer script fails, second stage will exit with failure as that implies that bootstrap setup has failed and rootfs shouldn't be used. The Termux app will also wipe prefix directory if that happens so that a broken prefix isn't used, so full bootstrap installation must be checked before any new app releases. The fallback bootstrap second stage will not wipe the prefix though as that may wipe other changes done to prefix and users should wipe manually if needed.
2024-06-17 23:58:20 +05:00

378 lines
14 KiB
Bash
Executable File

#!@TERMUX_PREFIX@/bin/bash
# shellcheck shell=bash
export TERMUX_PREFIX="@TERMUX_PREFIX@"
export TERMUX_PACKAGE_MANAGER="@TERMUX_PACKAGE_MANAGER@"
export TERMUX_PACKAGE_ARCH="@TERMUX_PACKAGE_ARCH@"
function log() { echo "[*]" "$@"; }
function log_error() { echo "[*]" "$@" 1>&2; }
show_help() {
cat <<'HELP_EOF'
termux-bootstrap-second-stage.sh runs the second stage of Termux
bootstrap installation.
Usage:
termux-bootstrap-second-stage.sh
Available command_options:
[ -h | --help ] Display this help screen.
The Termux app runs the bootstrap installion first stage by extracting
the bootstrap packages manually to the Termux rootfs directory under
private app data directory `/data/data/<package_name>` without using
the package managers like (`apt`/`dpkg` or `pacman`) to install
packages, as they are also part of the bootstrap.
Due to manual extraction, package configuration may not be properly
done, like running of maintainers sciprts like `preinst` and
`postinst`. Therefore, `termux-bootstrap-second-stage.sh` is run after
extraction to finish package configuration. The output of second stage
will get logged to Android `logcat` by the app.
Currently, only `postinst` scripts are run.
Running `preinst` scripts is not possible without an actual rootfs,
and support for running special scripts after extraction would need to
be written to handle packages that do require it.
If maintainer scripts of all packages are executed successfully,
`termux-bootstrap-second-stage.sh` will exit with exit code `0`,
otherwise with the exit code returned by the last failed script or
that of any other failure.
The second stage can only be run once in the complete lifetime of the
rootfs and running it again may put the rootfs in an inconsistent
state, so it is not allowed by default. This is done by creating the
`termux-bootstrap-second-stage.sh.lock` file as a symlink in the
same directory as `termux-bootstrap-second-stage.sh` file as that is
an atomic operation and only the first instance of
`termux-bootstrap-second-stage.sh` that creates it will be able to run
the second stage and other instances will fail. The lock file is never
deleted under normall operation. If rootfs directory is ever wiped,
then lock file will be deleted along with it as it exists under it,
and when bootstrap is setup again, second stage will be able to run
again. The `$TMPDIR` is not used for the lock file as that is often
deleted in the lifetime of the rootfs. If for some reason, second
stage must be force run again (not recommended), like in case of
previous failure and it must be re-run again for testing, then delete
the lock file manually and run `termux-bootstrap-second-stage.sh`
again.
**See Also:**
- https://github.com/termux/termux-packages/wiki/For-maintainers#bootstraps
HELP_EOF
}
main() {
local return_value
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_help || return $?
return 0
else
run_bootstrap_second_stage "$@"
return_value=$?
if [ $return_value -eq 64 ]; then # EX__USAGE
echo ""
show_help
fi
return $return_value
fi
}
run_bootstrap_second_stage() {
local return_value
if ! ln -s "termux-bootstrap-second-stage.sh" \
"@TERMUX_BOOTSTRAP_CONFIG_DIR_PATH@/termux-bootstrap-second-stage.sh.lock" 2>/dev/null; then
log "The termux bootstrap second stage has already been run before and cannot be run again."
log "If you still want to force run it again (not recommended), \
like in case of previous failure and it must be re-run again for testing, \
then delete the '@TERMUX_BOOTSTRAP_CONFIG_DIR_PATH@/termux-bootstrap-second-stage.sh.lock' \
file manually and run 'termux-bootstrap-second-stage.sh' again."
return 0
fi
log "Running termux bootstrap second stage"
run_bootstrap_second_stage_inner
return_value=$?
if [ $return_value -ne 0 ]; then
log_error "Failed to run termux bootstrap second stage"
return $return_value
fi
log "The termux bootstrap second stage completed successfully"
return 0
}
run_bootstrap_second_stage_inner() {
local return_value
log "Running postinst maintainer scripts"
run_package_postinst_maintainer_scripts
return_value=$?
if [ $return_value -ne 0 ]; then
log_error "Failed to run postinst maintainer scripts"
return $return_value
fi
return 0
}
run_package_postinst_maintainer_scripts() {
local return_value
local package_name
local package_version
local package_dir
local package_dir_basename
local script_path
local script_basename
if [ "${TERMUX_PACKAGE_MANAGER}" = "apt" ]; then
# - https://www.debian.org/doc/debian-policy/ch-maintainerscripts
# - https://manpages.debian.org/testing/dpkg-dev/deb-postinst.5.en.html
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/main/script.c#L178-L206
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/main/script.c#L107
if [ -d "${TERMUX_PREFIX}/var/lib/dpkg/info" ]; then
local dpkg_version
dpkg_version=$(dpkg --version | head -n 1 | sed -E 's/.*version ([^ ]+) .*/\1/')
if [[ ! "$dpkg_version" =~ ^[0-9].*$ ]]; then
log_error "Failed to find the 'dpkg' version"
log_error "$dpkg_version"
return 1
fi
# Check `dpkg --force-help` for current defaults.
# If they ever change, this will need to be updated.
# Currently, we are not parsing command output.
# - https://manpages.debian.org/testing/dpkg/dpkg.1.en.html#force~2
local dpkg_force_things="security-mac,downgrade"
# - https://manpages.debian.org/testing/dpkg/dpkg.1.en.html#D
# - https://manpages.debian.org/unstable/dpkg/dpkg.1.en.html#DPKG_DEBUG
# - https://manpages.debian.org/testing/dpkg/dpkg.1.en.html#DPKG_MAINTSCRIPT_DEBUG
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/main/script.c#L189
# - https://github.com/guillemj/dpkg/blob/1.22.6/lib/dpkg/debug.c#L123
# - https://github.com/guillemj/dpkg/blob/1.22.6/lib/dpkg/debug.h#L43
local dbg_scripts=02
local maintscript_debug=0
if [[ "$DPKG_DEBUG" =~ ^0[0-7]{1,6}$ ]] && [[ "$(( DPKG_DEBUG & dbg_scripts ))" != "0" ]]; then
maintscript_debug=1
fi
for script_path in "${TERMUX_PREFIX}/var/lib/dpkg/info/"*.postinst; do
script_basename="${script_path##*/}"
package_name="${script_basename::-9}"
log "Running '$package_name' package postinst"
# Execute permissions do not exist for maintainer
# scripts in bootstrap zips and since files are
# extracted manually by termux-app, they need to be
# assigned here, like `dpkg` does.
chmod u+x "$script_path" || return $?
(
# As per `dpkg` `script.c`:
# >Switch to a known good directory to give the
# >maintainer script a saner environment.
# The current working directory is handled the
# following way:
# - By default rootfs `/` is used.
# - If `$DPKG_ROOT` is set to an alternate rootfs
# path:
# - If `--force-script-chrootless` flag is not
# passed, then `$DPKG_ROOT` is chrooted into
# and then the current working directory is
# changed to `/`.
# - If flag is passed, then chroot is not done
# and only the current working directory is
# changed to `$DPKG_ROOT`.
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/main/script.c#L99-L130
# - https://github.com/guillemj/dpkg/blob/1.22.6/lib/dpkg/fsys-dir.c#L86
# - https://github.com/guillemj/dpkg/blob/1.22.6/lib/dpkg/fsys-dir.c#L33
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/common/force.c#L146-L149
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/common/force.c#L348
# - https://manpages.debian.org/unstable/dpkg/dpkg.1.en.html#DPKG_FORCE
# - https://wiki.debian.org/Teams/Dpkg/Spec/InstallBootstrap#Detached_chroot_handling
# Termux by default does not set `$DPKG_ROOT` and
# does not pass the `--force-script-chrootless`
# flag, so only current working directory is
# changed to the Android rootfs `/`.
# Moreover, Android apps cannot run chroot
# without root access, so `$DPKG_ROOT` cannot be
# normally used without `--force-script-chrootless`
# flag.
# Note that Termux rootfs is under private app
# data directory `/data/data/<package_name>,`
# which may cause problems for packages which try
# to use Android rootfs paths instead of Termux
# rootfs paths.
cd / || exit $?
# Export internal environment variables that
# `dpkg` exports for maintainer scripts.
# - https://manpages.debian.org/testing/dpkg/dpkg.1.en.html#Internal_environment
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/main/main.c#L751-L759
# - https://github.com/guillemj/dpkg/blob/1.22.6/src/main/script.c#L191-L197
export DPKG_MAINTSCRIPT_PACKAGE="$package_name"
export DPKG_MAINTSCRIPT_PACKAGE_REFCOUNT="1"
export DPKG_MAINTSCRIPT_ARCH="$TERMUX_PACKAGE_ARCH"
export DPKG_MAINTSCRIPT_NAME="postinst"
export DPKG_MAINTSCRIPT_DEBUG="$maintscript_debug"
export DPKG_RUNNING_VERSION="$dpkg_version"
export DPKG_FORCE="$dpkg_force_things"
export DPKG_ADMINDIR="${TERMUX_PREFIX}/var/lib/dpkg"
export DPKG_ROOT=""
# > The maintainer scripts must be proper executable
# > files; if they are scripts (which is recommended),
# > they must start with the usual `#!` convention.
# Execute it directly instead of with a shell,
# and exit with failure if it fails as that
# implies that bootstrap setup failed.
# The first argument is `configure`.
# The package version is the second argument
# if package is being upgraded, but not for first
# installation, so don't pass it.
# Check `deb-postinst(5)` for more info.
"$script_path" configure
return_value=$?
if [ $return_value -ne 0 ]; then
log_error "Failed to run '$package_name' package postinst"
exit $return_value
fi
) || return $?
done
fi
elif [ ${TERMUX_PACKAGE_MANAGER} = "pacman" ]; then
# - https://wiki.archlinux.org/title/PKGBUILD#install
# - https://gitlab.archlinux.org/pacman/pacman/-/blob/v6.1.0/lib/libalpm/add.c#L638-L647
if [ -d "${TERMUX_PREFIX}/var/lib/pacman/local" ]; then
# Package install files exist at `/var/lib/pacman/local/package-version/install`
for script_path in "${TERMUX_PREFIX}/var/lib/pacman/local/"*/install; do
package_dir="${script_path::-8}"
package_dir_basename="${package_dir##*/}"
# Extract package `version` in the format `epoch:pkgver-pkgrel`
# from the package_dir_basename in the format `package-version`.
# Do not use external programs to parse as that would require
# adding it as a dependency for second-stage.
# - https://wiki.archlinux.org/title/PKGBUILD#Version
# Set to anything after last dash "-"
local package_version_pkgrel="${package_dir_basename##*-}"
# Set to anything before and including last dash "-"
local package_name_and_version_pkgver="${package_dir_basename%"$package_version_pkgrel"}"
# Trim trailing dash "-"
package_name_and_version_pkgver="${package_name_and_version_pkgver%?}"
# Set to anything after last dash "-"
local package_version_pkgver="${package_name_and_version_pkgver##*-}"
# Combine pkgver and pkgrel
package_version="$package_version_pkgver-$package_version_pkgrel"
if [[ ! "$package_version" =~ ^([0-9]+:)?[^-]+-[^-]+$ ]]; then
log_error "The package_version '$package_version' extracted from package_dir_basename '$package_dir_basename' is not valid"
return 1
fi
log "Running '$package_dir_basename' package post_install"
(
# As per `pacman` install docs:
# > Each function is run chrooted inside the pacman install directory. See this thread.
# The `RootDir` is chrooted into and then the
# current working directory is changed to `/`.
# - https://bbs.archlinux.org/viewtopic.php?pid=913891
# - https://man.archlinux.org/man/pacman.conf.5.en#OPTIONS
# - https://gitlab.archlinux.org/pacman/pacman/-/blob/v6.1.0/src/pacman/conf.c#L855
# - https://gitlab.archlinux.org/pacman/pacman/-/blob/v6.1.0/lib/libalpm/alpm.c#L47
# - https://gitlab.archlinux.org/pacman/pacman/-/blob/v6.1.0/lib/libalpm/alpm.h#L1663-L1676
# - https://gitlab.archlinux.org/pacman/pacman/-/blob/v6.1.0/lib/libalpm/util.c#L657-L668
# - https://man7.org/linux/man-pages/man2/chroot.2.html
# But since Android apps cannot run chroot
# without root access, chroot is disabled by
# Termux pacman package and only current working
# directory is changed to the Android rootfs `/`.
# Note that Termux rootfs is under private app
# data directory `/data/data/<package_name>,`
# which may cause problems for packages which try
# to use Android rootfs paths instead of Termux
# rootfs paths.
# - https://github.com/termux/termux-packages/blob/953b9f2aac0dc94f3b99b2df6af898e0a95d5460/packages/pacman/util.c.patch
cd "/" || exit $?
# Source the package `install` file and execute
# `post_install` function if defined.
# Unset function if already defined in the env
unset -f post_install || exit $?
# shellcheck disable=SC1090
source "$script_path"
return_value=$?
if [ $return_value -ne 0 ]; then
log_error "Failed to source '$package_dir_basename' package install install"
exit $return_value
fi
if [[ "$(type -t post_install 2>/dev/null)" == "function" ]]; then
# cd again in case install file sourced changed the directory.
cd "/" || exit $?
# Execute the post_install function and exit
# with failure if it fails as that implies
# that bootstrap setup failed.
# The package version is the first argument.
# Check `PKGBUILD#install` docs for more info.
post_install "$package_version"
return_value=$?
if [ $return_value -ne 0 ]; then
log_error "Failed to run '$package_dir_basename' package post_install"
exit $return_value
fi
fi
) || return $?
done
fi
fi
return 0
}
# If running in bash, run script logic, otherwise exit with usage error
if [ -n "${BASH_VERSION:-}" ]; then
# If script is sourced, return with error, otherwise call main function
# - https://stackoverflow.com/a/28776166/14686958
# - https://stackoverflow.com/a/29835459/14686958
if (return 0 2>/dev/null); then
echo "${0##*/} cannot be sourced as \"\$0\" is required." 1>&2
return 64 # EX__USAGE
else
main "$@"
exit $?
fi
else
(echo "${0##*/} must be run with the bash shell."; exit 64) # EX__USAGE
fi