Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/profile.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"fchdir",
"fchmod",
"fchmodat",
"fchmodat2",
Copy link
Copy Markdown
Member

@thunder-coding thunder-coding Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the default since moby/profiles@91990ca, so just remind me to properly do a diff of profile.json with the older copy in github.com/moby/moby and rebase it against the new location in github.com/moby/profiles once this PR is merged

"fchown",
"fchown32",
"fchownat",
Expand Down
214 changes: 167 additions & 47 deletions scripts/run-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@ TERMUX_SCRIPTDIR=$(cd "$(realpath "$(dirname "$0")")"; cd ..; pwd)
BUILDSCRIPT_NAME=build-package.sh
CONTAINER_HOME_DIR=/home/builder

# Detect container runtime.
# Explicit choice via TERMUX_CONTAINER_RUNTIME takes priority.
# Otherwise, auto-detect: prefer docker if available, fall back to podman.
_detect_runtime() {
if [ -n "${TERMUX_CONTAINER_RUNTIME:-}" ]; then
case "$TERMUX_CONTAINER_RUNTIME" in
docker|podman) echo "$TERMUX_CONTAINER_RUNTIME"; return;;
*) echo "Error: TERMUX_CONTAINER_RUNTIME must be 'docker' or 'podman'" >&2; exit 1;;
esac
fi
if command -v docker &>/dev/null; then
echo "docker"
elif command -v podman &>/dev/null; then
Comment thread
playday3008 marked this conversation as resolved.
echo "podman"
else
echo "Error: Neither docker nor podman found in PATH" >&2
exit 1
fi
}

# Detect superuser command.
# If running as root, no sudo is needed. Otherwise, check for sudo.
_detect_sudo() {
if [ "$(id -u)" = "0" ]; then
echo ""
elif command -v sudo &>/dev/null; then
echo "sudo"
elif command -v doas &>/dev/null; then
echo "doas"
elif command -v run0 &>/dev/null; then
echo "run0"
else
echo "Error: This script must be run as root or with sudo/doas available in PATH" >&2
exit 1
fi
}
Comment thread
playday3008 marked this conversation as resolved.

_show_usage() {
echo "Usage: $0 [OPTIONS] [COMMAND]"
echo ""
Expand All @@ -22,9 +59,11 @@ _show_usage() {
echo " -m, --mount-termux-dirs Mount /data and ~/.termux-build into the container."
echo " This is useful for building locally for development"
echo " with host IDE and editors."
echo ""
echo "Supported environment variables:"
echo " TERMUX_BUILDER_IMAGE_NAME The name of the Docker image to use"
echo " CONTAINER_NAME The name of the Docker container to create/use"
echo " TERMUX_CONTAINER_RUNTIME Set to 'docker' or 'podman' to override auto-detection"
echo " TERMUX_DOCKER_RUN_EXTRA_ARGS Extra arguments to pass to 'docker run' while"
echo " creating the container"
echo " TERMUX_DOCKER_EXEC_EXTRA_ARGS Extra arguments to pass to 'docker exec' while"
Expand All @@ -44,6 +83,10 @@ _show_usage() {
echo "- The dry-run option will only work if the first argument passed to this script"
echo " which runs docker contains '$BUILDSCRIPT_NAME', and it will run"
echo " 'build-package-dry-run-simulation.sh' with arguments passed to this script."
echo "- When using Podman (via TERMUX_CONTAINER_RUNTIME=podman),"
echo " the container runs rootless. No sudo is required, and host file"
echo " permissions are never altered. Container runs as root in the user namespace"
echo " (which maps to your unprivileged host user)."
exit 0
}

Expand All @@ -59,11 +102,13 @@ while (( $# != 0 )); do
TERMUX_DOCKER_RUN_EXTRA_ARGS="--volume /data:/data --volume $HOME/.termux-build:$CONTAINER_HOME_DIR/.termux-build $TERMUX_DOCKER_RUN_EXTRA_ARGS"
shift 1;;
--) shift 1; break;;
-*) echo "Error: Unknown option '$1'" 1>&2; shift 1; exit 1;;
-*) echo "Error: Unknown option '$1'" >&2; shift 1; exit 1;;
*) break;;
esac
done

RUNTIME=$(_detect_runtime) || exit 1

# If 'build-package-dry-run-simulation.sh' does not return 85 (EX_C__NOOP), or if
# $1 (the first argument passed to this script which runs docker) does not contain
# $BUILDSCRIPT_NAME, this condition will evaluate false and this script which
Expand All @@ -74,7 +119,7 @@ if [ "${dry_run}" = "true" ]; then
RETURN_VALUE=0
OUTPUT="$("$TERMUX_SCRIPTDIR/scripts/bin/build-package-dry-run-simulation.sh" "$@" 2>&1)" || RETURN_VALUE=$?
if [ $RETURN_VALUE -ne 0 ]; then
echo "$OUTPUT" 1>&2
echo "$OUTPUT" >&2
if [ $RETURN_VALUE -eq 85 ]; then # EX_C__NOOP
echo "$0: Exiting since '$BUILDSCRIPT_NAME' would not have built any packages"
exit 0
Expand All @@ -86,13 +131,33 @@ if [ "${dry_run}" = "true" ]; then
fi

UNAME=$(uname)
if [ "$RUNTIME" = "podman" ]; then
REPOROOT="$(dirname $(readlink -f $0))/../"
elif [ "$RUNTIME" = "docker" ]; then
if [ "$UNAME" = Darwin ]; then
# Workaround for mac readlink not supporting -f.
REPOROOT=$PWD
else
REPOROOT="$(dirname $(readlink -f $0))/../"
fi
else
echo "Error: Unsupported runtime '$RUNTIME'" >&2
exit 1
fi

# Custom seccomp profile (profile.json) is required: the default profiles of
# Docker and Podman do not unconditionally allow personality(), which breaks
# proot-based cross-compilation (qemu-arm). The profile also includes
# fchmodat2 for modern glibc compat.
# CAP_SYS_ADMIN and /dev/fuse are needed for fuse-overlayfs mounts used by
# the build system.
if [ "$UNAME" = Darwin ]; then
# Workaround for mac readlink not supporting -f.
REPOROOT=$PWD
SEC_OPT=""
else
REPOROOT="$(dirname $(readlink -f $0))/../"
SEC_OPT=" --security-opt seccomp=$REPOROOT/scripts/profile.json --security-opt apparmor=_custom-termux-package-builder-$CONTAINER_NAME --cap-add CAP_SYS_ADMIN --device /dev/fuse"
SEC_OPT=" --security-opt seccomp=$REPOROOT/scripts/profile.json --cap-add CAP_SYS_ADMIN --device /dev/fuse"
if [ "$RUNTIME" = "docker" ]; then
SEC_OPT+=" --security-opt apparmor=_custom-termux-package-builder-$CONTAINER_NAME"
fi
fi

if [ "${CI:-}" = "true" ]; then
Expand All @@ -111,14 +176,15 @@ else
fi

USER=builder
SUDO_CMD=$(_detect_sudo) || exit 1

if [ -n "${TERMUX_DOCKER_USE_SUDO-}" ]; then
SUDO="sudo"
SUDO=$SUDO_CMD
else
SUDO=""
fi

echo "Running container '$CONTAINER_NAME' from image '$TERMUX_BUILDER_IMAGE_NAME'..."
echo "Running container '$CONTAINER_NAME' from image '$TERMUX_BUILDER_IMAGE_NAME' (runtime: $RUNTIME)..."

# Check whether attached to tty and adjust docker flags accordingly.
if [ -t 1 ]; then
Expand All @@ -127,54 +193,57 @@ else
DOCKER_TTY=""
fi

APPARMOR_PARSER=""
if command -v apparmor_parser > /dev/null; then
APPARMOR_PARSER="apparmor_parser"
fi

if [ -z "$APPARMOR_PARSER" ] || ! $SUDO aa-status --enabled; then
echo "WARNING: apparmor_parser not found, AppArmor profiles will not be loaded!"
echo " This is not recommended, as it may cause security issues and unexpected behavior"
echo " Avoid executing untrusted code in the container"
# AppArmor (Docker only)
if [ "$RUNTIME" = "docker" ]; then
APPARMOR_PARSER=""
fi
if command -v apparmor_parser > /dev/null; then
APPARMOR_PARSER="apparmor_parser"
fi

load_apparmor_profile() {
local profile_path="$1"
local msg="${2:-}"
if [ -n "$APPARMOR_PARSER" ]; then
if [ -n "$msg" ]; then
echo "$msg..."
fi
cat "$profile_path" | sed -e "s/{{CONTAINER_NAME}}/$CONTAINER_NAME/g" | sudo "$APPARMOR_PARSER" -rK
if [ -z "$APPARMOR_PARSER" ] || ! $SUDO aa-status --enabled; then
echo "WARNING: apparmor_parser not found, AppArmor profiles will not be loaded!"
echo " This is not recommended, as it may cause security issues and unexpected behavior"
echo " Avoid executing untrusted code in the container"
APPARMOR_PARSER=""
fi
}

# Load the relaxed AppArmor profile first as we might need to change permissions
load_apparmor_profile ./scripts/profile-relaxed.apparmor
load_apparmor_profile() {
local profile_path="$1"
local msg="${2:-}"
if [ -n "$APPARMOR_PARSER" ]; then
if [ -n "$msg" ]; then
echo "$msg..."
fi
cat "$profile_path" | sed -e "s/{{CONTAINER_NAME}}/$CONTAINER_NAME/g" | $SUDO_CMD "$APPARMOR_PARSER" -rK
fi
}

# Load the relaxed AppArmor profile first as we might need to change permissions
load_apparmor_profile ./scripts/profile-relaxed.apparmor
fi

__change_builder_uid_gid() {
if [ "$UNAME" != Darwin ]; then
if [ $(id -u) -ne 1001 -a $(id -u) -ne 0 ]; then
echo "Changed builder uid/gid... (this may take a while)"
$SUDO docker exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo chown -R $(id -u):$(id -g) $CONTAINER_HOME_DIR
$SUDO docker exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo chown -R $(id -u):$(id -g) /data
$SUDO docker exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo usermod -u $(id -u) builder
$SUDO docker exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo groupmod -g $(id -g) builder
$SUDO $RUNTIME exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo chown -R $(id -u):$(id -g) $CONTAINER_HOME_DIR
$SUDO $RUNTIME exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo chown -R $(id -u):$(id -g) /data
$SUDO $RUNTIME exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo usermod -u $(id -u) builder
$SUDO $RUNTIME exec $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME sudo groupmod -g $(id -g) builder
fi
fi
}

__change_container_pid_max() {
if [ "$UNAME" != Darwin ]; then
echo "Changing /proc/sys/kernel/pid_max to 65535 for packages that need to run native executables using proot (for 32-bit architectures)"
if [[ "$($SUDO docker exec $CONTAINER_NAME cat /proc/sys/kernel/pid_max)" -le 65535 ]]; then
echo "No need to change /proc/sys/kernel/pid_max, current value is $($SUDO docker exec $DOCKER_TTY $CONTAINER_NAME cat /proc/sys/kernel/pid_max)"
if [[ "$($SUDO $RUNTIME exec $CONTAINER_NAME cat /proc/sys/kernel/pid_max)" -le 65535 ]]; then
echo "No need to change /proc/sys/kernel/pid_max, current value is $($SUDO $RUNTIME exec $DOCKER_TTY $CONTAINER_NAME cat /proc/sys/kernel/pid_max)"
else
# On kernel versions >= 6.14, the pid_max value is pid namespaced, so we need to set it in the container namespace instead of host.
# But some distributions may backport the pid namespacing to older kernels, so we check whether it's effective by checking the value in the container after setting it.
$SUDO docker run --privileged --pid="container:$CONTAINER_NAME" --rm "$TERMUX_BUILDER_IMAGE_NAME" sh -c "echo 65535 | sudo tee /proc/sys/kernel/pid_max > /dev/null" || :
if [[ "$($SUDO docker exec $CONTAINER_NAME cat /proc/sys/kernel/pid_max)" -eq 65535 ]]; then
$SUDO $RUNTIME run --privileged --pid="container:$CONTAINER_NAME" --rm "$TERMUX_BUILDER_IMAGE_NAME" sh -c "echo 65535 | sudo tee /proc/sys/kernel/pid_max > /dev/null" || :
if [[ "$($SUDO $RUNTIME exec $CONTAINER_NAME cat /proc/sys/kernel/pid_max)" -eq 65535 ]]; then
echo "Successfully changed /proc/sys/kernel/pid_max for container namespace"
else
echo "Failed to change /proc/sys/kernel/pid_max for container, falling back to setting it on host..."
Expand All @@ -188,28 +257,57 @@ __change_container_pid_max() {
fi
}


if ! $SUDO docker container inspect $CONTAINER_NAME > /dev/null 2>&1; then
if ! $SUDO $RUNTIME container inspect $CONTAINER_NAME &>/dev/null; then
echo "Creating new container..."
$SUDO docker run \

RUNTIME_RUN_ARGS=" \
--detach \
--init \
--name $CONTAINER_NAME \
--volume $VOLUME \
$SEC_OPT \
--tty \
$TERMUX_DOCKER_RUN_EXTRA_ARGS \
$TERMUX_BUILDER_IMAGE_NAME
__change_builder_uid_gid
$TERMUX_DOCKER_RUN_EXTRA_ARGS"

if [ "$RUNTIME" = "podman" ]; then
# In rootless Podman the default user-namespace mapping is:
# container root (0) → host user ($UID)
# container 1-N → host subuid range
#
# We run as root *inside* the container so that:
# • chmod/chown (used by tar, dpkg, etc.) work within the userns
# • the image's builder-owned toolchain files are accessible (root can read all)
# • files written to the bind-mounted repo volume land as the host user on disk
#
# This is safe — "root" in a rootless container is the unprivileged host user;
# the user namespace already provides the sandbox. No host permissions are altered.
$SUDO $RUNTIME run \
$RUNTIME_RUN_ARGS \
--pids-limit=-1 \
--user root \
--env HOME=$CONTAINER_HOME_DIR \
$TERMUX_BUILDER_IMAGE_NAME
elif [ "$RUNTIME" = "docker" ]; then
$SUDO $RUNTIME run \
$RUNTIME_RUN_ARGS \
$TERMUX_BUILDER_IMAGE_NAME
__change_builder_uid_gid
else
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared arguments could be written together in a similar way to my other suggestion here also, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remaining concern I have is about the arguments here. The arguments $SUDO, $RUNTIME, run, $RUNTIME_RUN_ARGS, and $TERMUX_BUILDER_IMAGE_NAME are all the same and could be shared, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll check it, after testing

echo "Error: Unsupported runtime '$RUNTIME'" >&2
exit 1
fi
Comment thread
playday3008 marked this conversation as resolved.
__change_container_pid_max
fi

if [[ "$($SUDO docker container inspect -f '{{ .State.Running }}' $CONTAINER_NAME)" == "false" ]]; then
$SUDO docker start $CONTAINER_NAME >/dev/null 2>&1
if [[ "$($SUDO $RUNTIME container inspect -f '{{ .State.Running }}' $CONTAINER_NAME)" == "false" ]]; then
$SUDO $RUNTIME start $CONTAINER_NAME &>/dev/null
__change_container_pid_max
fi

load_apparmor_profile ./scripts/profile-restricted.apparmor "Loading restricted AppArmor profile"
# Load restricted AppArmor profile (Docker only)
if [ "$RUNTIME" = "docker" ]; then
load_apparmor_profile ./scripts/profile-restricted.apparmor "Loading restricted AppArmor profile"
fi

# Set traps to ensure that the process started with docker exec and all its children are killed.
. "$TERMUX_SCRIPTDIR/scripts/utils/docker/docker.sh"; docker__setup_docker_exec_traps
Expand All @@ -218,4 +316,26 @@ if [ "$#" -eq "0" ]; then
set -- bash
fi

$SUDO docker exec $CI_OPT --env "DOCKER_EXEC_PID_FILE_PATH=$DOCKER_EXEC_PID_FILE_PATH" --interactive $DOCKER_TTY $TERMUX_DOCKER_EXEC_EXTRA_ARGS $CONTAINER_NAME "$@"
# Execute command in container
RUNTIME_EXEC_ARGS=" \
$CI_OPT \
--env \"DOCKER_EXEC_PID_FILE_PATH=$DOCKER_EXEC_PID_FILE_PATH\" \
--interactive $DOCKER_TTY \
$TERMUX_DOCKER_EXEC_EXTRA_ARGS"

if [ "$RUNTIME" = "podman" ]; then
$SUDO $RUNTIME exec \
$RUNTIME_EXEC_ARGS \
--user root \
--env HOME=$CONTAINER_HOME_DIR \
$CONTAINER_NAME \
"$@"
elif [ "$RUNTIME" = "docker" ]; then
$SUDO $RUNTIME exec \
$RUNTIME_EXEC_ARGS \
$CONTAINER_NAME \
"$@"
Comment thread
robertkirkman marked this conversation as resolved.
Outdated
else
echo "Error: Unsupported runtime '$RUNTIME'" >&2
exit 1
fi
5 changes: 4 additions & 1 deletion scripts/utils/docker/docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
# or if output of command is redirected to file instead of terminal,
# like `docker exec cmd &> cmd.log </dev/null`.
# .
# Requires RUNTIME ("docker" or "podman"), SUDO (may be empty), and
# CONTAINER_NAME to be set by the caller.
# .
# See Also:
# - https://github.com/docker/cli/issues/2607
# - https://github.com/moby/moby/issues/9098
Expand Down Expand Up @@ -55,7 +58,7 @@ echo "Docker trap killing DOCKER_PROCESS" && \
docker_killtree "'"$signal"'" "$DOCKER_PID" || :
'
# Exec docker_exec_trap_command inside docker context
$SUDO docker exec "$CONTAINER_NAME" bash -c "$docker_exec_trap_command"
$SUDO ${RUNTIME:-docker} exec "$CONTAINER_NAME" bash -c "$docker_exec_trap_command"

fi

Expand Down