Skip to content

Instantly share code, notes, and snippets.

@gitawego
Last active June 3, 2026 17:29
Show Gist options
  • Select an option

  • Save gitawego/126079d23b8429a9cf8133737eb3c4de to your computer and use it in GitHub Desktop.

Select an option

Save gitawego/126079d23b8429a9cf8133737eb3c4de to your computer and use it in GitHub Desktop.
setup_ubuntu.sh
#!/usr/bin/env bash
# Exit immediately if a command exits with a non-zero status
set -e
echo "=== 1. Updating Termux repositories and installing proot-distro ==="
pkg update -y
pkg install proot-distro -y
echo "=== 2. Installing Ubuntu via proot-distro ==="
# Two checks for "already installed": (1) proot-distro's own
# `list` subcommand reports it, (2) the install rootfs directory
# exists. Both are needed because on some Termux versions the
# directory may exist without being registered, and vice versa.
# If either says installed, skip — `proot-distro install` itself
# errors non-zero when the distro is present, which would abort
# the script under `set -e`.
# Note: the directory name is 'containers' (plural), NOT
# 'installed-rootfs' as older docs sometimes say.
UBUNTU_INSTALLED=0
if proot-distro list 2>/dev/null | grep -qE '^ubuntu[[:space:]]'; then
UBUNTU_INSTALLED=1
echo " -- proot-distro list reports ubuntu installed"
fi
if [ -d "$PREFIX/var/lib/proot-distro/containers/ubuntu" ]; then
UBUNTU_INSTALLED=1
echo " -- rootfs directory exists at $PREFIX/var/lib/proot-distro/containers/ubuntu"
fi
if [ "$UBUNTU_INSTALLED" -eq 0 ]; then
proot-distro install ubuntu
else
echo "Ubuntu container is already installed. Skipping installation step."
fi
echo "=== 3. Provisioning 'dev' user inside Ubuntu ==="
# We used to pass a heredoc on stdin to 'proot-distro login
# ubuntu -- bash', but proot 5+ warns
# 'can't sanitize binding "/proc/self/fd/0"'
# and the heredoc content sometimes doesn't reach the inner
# bash — so the apt-get/adduser steps silently no-op, and
# step 4 fails with 'no user dev defined in /etc/passwd'.
# Fix: write the script to a temp file on the Termux side and
# pass it as an argument. proot binds the path reliably and
# the inner bash reads it from disk.
USER_SETUP_SCRIPT="$(mktemp -t acs-user-setup.XXXXXX.sh)"
trap 'rm -f "$USER_SETUP_SCRIPT"' EXIT
cat > "$USER_SETUP_SCRIPT" << 'SCRIPT'
set -e
export DEBIAN_FRONTEND=noninteractive
if id "dev" &>/dev/null; then
echo "User 'dev' already exists inside Ubuntu container."
echo "Skipping user creation (and passwd/adduser/sudo install)."
else
echo "Installing adduser, passwd, sudo (prereqs for useradd)..."
apt-get update -y
apt-get install -y --no-install-recommends \
adduser passwd sudo
echo "Creating user 'dev'..."
# Use /usr/sbin/adduser explicitly in case PATH is restricted
ADDUSER="$(command -v adduser || echo /usr/sbin/adduser)"
USERMOD="$(command -v usermod || echo /usr/sbin/usermod)"
"$ADDUSER" --disabled-password --gecos "" dev
"$USERMOD" -aG sudo dev
echo "Allowing 'dev' to use sudo without a password prompt..."
# Idempotent: replace the line if present, append otherwise
SUDOERS_LINE='dev ALL=(ALL) NOPASSWD:ALL'
grep -qFx "$SUDOERS_LINE" /etc/sudoers || echo "$SUDOERS_LINE" >> /etc/sudoers
fi
SCRIPT
proot-distro login ubuntu -- bash "$USER_SETUP_SCRIPT"
rm -f "$USER_SETUP_SCRIPT"
trap - EXIT
echo "=== 4. Installing base deps + Node.js + Claude Code inside Ubuntu ==="
# Base build tools (needed for npm packages with native deps, e.g.
# node-gyp, sharp). curl/git for fetching more things. Node 18+ for
# Claude Code / Codex / opencode. Claude Code itself installs via
# the official Anthropic install script.
# Same stdin issue as step 3 — we write the script to a temp file
# and pass it as an argument, not via heredoc on stdin.
DEV_SETUP_SCRIPT="$(mktemp -t acs-dev-setup.XXXXXX.sh)"
trap 'rm -f "$USER_SETUP_SCRIPT" "$DEV_SETUP_SCRIPT"' EXIT
cat > "$DEV_SETUP_SCRIPT" << 'SCRIPT'
set -e
# If we're already inside the proot, $HOME is the user's home in
# the container. Run everything as 'dev' (the user we just made).
cd "$HOME"
echo " -- apt update + base packages --"
sudo apt-get update -y
sudo apt-get install -y --no-install-recommends \
curl ca-certificates git build-essential pkg-config python3
echo " -- Node.js via fnm (Fast Node Manager) --"
# fnm installs fast and self-contained (single binary, no apt
# repository to add). It auto-uses the version pinned in the
# project's package.json `engines.node` field when run from the
# repo, so the proot ends up with whatever the project requires.
# Re-runs are idempotent — fnm is a no-op if the right version
# is already installed and on PATH.
#
# The order matters: the installer's `curl ... | bash` does
# NOT modify the current shell's PATH (it only writes to
# ~/.bashrc, which isn't sourced in a non-interactive shell).
# And on a RE-RUN, the new proot shell session doesn't
# inherit any PATH additions from the previous session, so
# even a previously-installed fnm may not be on PATH. We
# solve both by:
# 1. Prepending the standard fnm install dir to PATH
# UNCONDITIONALLY (so 'command -v fnm' finds an
# existing install, AND so the eval below can find fnm)
# 2. Checking by absolute path (test -x) for robustness
export PATH="$HOME/.local/share/fnm:$PATH"
FNM_BIN="$HOME/.local/share/fnm/fnm"
if [ ! -x "$FNM_BIN" ]; then
curl -fsSL https://fnm.vercel.app/install | bash
fi
# We do NOT pass --use-on-cd here: that flag emits a self-eval
# line intended for interactive shells (so fnm re-evaluates
# the dir on every prompt). Inside a non-interactive script
# the self-eval causes an infinite recursion that errors out
# with a partial command name (e.g. 'ady' from 'ready' in the
# recursion). Use --shell bash only — that's a one-shot
# export of PATH and FNM_* variables, no recursion.
# shellcheck disable=SC2046
eval "$(fnm env --shell bash)"
# Install the version pinned by the project's engines.node. If
# we're not inside a project with a pinned version, fall back
# to the latest LTS.
FNM_NODE_VERSION="${FNM_NODE_VERSION:-lts-latest}"
if ! fnm list --installed 2>/dev/null | grep -q "$FNM_NODE_VERSION"; then
fnm install --lts "$FNM_NODE_VERSION" 2>/dev/null || fnm install "$FNM_NODE_VERSION"
fi
fnm use --lts "$FNM_NODE_VERSION" 2>/dev/null || fnm use "$FNM_NODE_VERSION"
echo " node: $(node -v)"
echo " npm: $(npm -v)"
echo " -- pnpm (via official install script) --"
# pnpm 10+ supports the packageManager field. The install
# script honors PNPM_VERSION env var and respects the
# packageManager pin in the current directory.
#
# Same fresh-proot-shell-PATH problem as fnm: a previously
# installed pnpm at ~/.local/share/pnpm isn't on PATH in a
# new proot session, so 'command -v pnpm' returns not-found
# and the installer runs again. Fix: prepend the install
# dir to PATH unconditionally and check by absolute path.
export PATH="$HOME/.local/share/pnpm:$PATH"
PNPM_BIN="$HOME/.local/share/pnpm/pnpm"
if [ ! -x "$PNPM_BIN" ]; then
# The pnpm installer inspects the SHELL env var to decide
# which rc file to update. Inside the proot, SHELL is
# typically unset (proot doesn't forward it from the host),
# which makes pnpm error with 'Could not infer shell type'.
# Set it explicitly before the install.
export SHELL="${SHELL:-/bin/bash}"
curl -fsSL https://get.pnpm.io/install.sh | sh -
fi
echo " pnpm: $(pnpm --version 2>/dev/null || echo 'not found')"
echo " -- Claude Code (claude CLI) --"
# The official installer puts `claude` in ~/.local/bin (added to
# PATH by the installer's shell-init step). Re-run is a no-op if
# already installed.
if ! command -v claude >/dev/null 2>&1; then
curl -fsSL https://claude.ai/install.sh | bash
fi
echo " claude: $(claude --version 2>/dev/null || echo 'not found')"
echo " -- Homebrew (brew) --"
# Homebrew on Linux installs into ~/.linuxbrew (no sudo needed for
# the install itself; brew manages its own prefix). It compiles
# formulae from source, so the first 'brew install' of anything
# can be slow — but once installed, it's a convenient way to
# grab CLI tools without apt's package aging. Skip if already
# present (re-running is a no-op).
if ! command -v brew >/dev/null 2>&1; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
echo " brew: $(brew --version 2>/dev/null | head -1 || echo 'not found')"
# Add ~/.local/bin to PATH in the proot user's .bashrc so future
# interactive 'ubuntu' sessions have pnpm, claude, and any other
# user-local bins on PATH without having to `source` anything.
# Idempotent: only append the line if it isn't already there.
if [ -n "$HOME" ] && [ -d "$HOME" ]; then
PROOT_BASHRC="$HOME/.bashrc"
touch "$PROOT_BASHRC"
if ! grep -qF 'export PATH="$HOME/.local/bin:$PATH"' "$PROOT_BASHRC" 2>/dev/null; then
printf '\n# added by acs setup_ubuntu.sh: user-local bins on PATH\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$PROOT_BASHRC"
fi
# Convenience alias: 'cc' for Claude Code with auto-permission
# approval. Inside the proot there's no interactive prompt
# available, so --dangerously-skip-permissions is needed for
# unattended use. Idempotent.
if ! grep -qF "alias cc='claude --dangerously-skip-permissions'" "$PROOT_BASHRC" 2>/dev/null; then
printf "\n# added by acs setup_ubuntu.sh: cc alias for Claude Code\nalias cc='claude --dangerously-skip-permissions'\n" >> "$PROOT_BASHRC"
fi
fi
SCRIPT
proot-distro login ubuntu --user dev -- bash "$DEV_SETUP_SCRIPT"
rm -f "$DEV_SETUP_SCRIPT"
trap - EXIT
echo "=== 5. Setting up persistent alias in Termux ~/.bashrc ==="
BASHRC="$HOME/.bashrc"
ALIAS_LINE='alias ubuntu="proot-distro login ubuntu --user dev"'
# Create .bashrc if it doesn't exist
touch "$BASHRC"
# Append the alias line only if it isn't already present
if ! grep -Fxq "$ALIAS_LINE" "$BASHRC"; then
echo "" >> "$BASHRC"
echo "$ALIAS_LINE" >> "$BASHRC"
echo "Alias appended to $BASHRC"
else
echo "Alias already exists in $BASHRC"
fi
echo "=== 6. Environment Ready ==="
echo "The setup is complete! To apply the environment changes immediately,"
echo "execute the final step to source your profile:"
echo ""
echo " source ~/.bashrc"
echo ""
echo "After that, simply type 'ubuntu' to log straight into your container."
echo ""
echo "Inside the container, \`claude\` is on PATH. From there, ACS running"
echo "on Termux can detect the proot install and write configs into it."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment