The easiest way to install Kivy on Ubuntu
- Create a new file called
kivy-android-ubuntu.sh
.
- Copy the script into the file and save it.
- Open a terminal and navigate to the directory where you saved the script.
- Make the script executable:
chmod +x kivy-android-ubuntu.sh
- Run the script:
Script
#!/usr/bin/env bash
# Kivy Quickstart Install Script
#
# This script installs all required tools to build and run Kivy applications on Android with automatic hot reloading.
# It sets up the environment using Python 3.11 (via uv), installs Kivy, Buildozer, Kivy-Reloader, Cython, and all build dependencies, then downloads and configures scrcpy for easy Android screen mirroring.
# After preparing the environment, it creates a demo Kivy app, initializes Buildozer, and deploys the app to your connected Android device with hot reload enabled.
#
# Requirements:
# - Ubuntu 22.04+ or Linux Mint 21.x+ (tested on Linux Mint 22.1 "xia")
# - or any recent Ubuntu-derived distribution (untested)
#
# WARNING: Use at your own risk. Review the script before running.
set -euo pipefail
# ── colors ──────────────────────────────────────────────────────────────────────
RED=$(tput setaf 1) # errors
GRN=$(tput setaf 2) # info / success
BLU=$(tput setaf 4) # timestamp
RST=$(tput sgr0)
log() { printf '\n%s[%(%F %T)T]%s %s%s%s\n' "$BLU" -1 "$RST" "$GRN" "$1" "$RST"; }
die() { printf '\n%s[%(%F %T)T]%s %s%s%s\n' "$BLU" -1 "$RST" "$RED" "$1" "$RST"; exit 1; }
# ── uv ──────────────────────────────────────────────────────────────────────────
log "Checking for uv"
if ! command -v uv >/dev/null 2>&1; then
log "Installing uv"
curl -Ls https://astral.sh/uv/install.sh | sh || die "uv install failed"
else
log "uv already present"
fi
export PATH="$HOME/.local/bin:$PATH"
# ── buildozer deps ──────────────────────────────────────────────────────────────
log "Updating APT and installing buildozer dependencies"
sudo apt update -y
sudo apt install -y \
build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
libsqlite3-dev curl libncurses-dev xz-utils tk-dev libxml2-dev \
libxmlsec1-dev libffi-dev liblzma-dev git zip unzip openjdk-17-jdk \
autoconf libtool pkg-config cmake adb
# ensure scrcpy sees adb at its hard‑coded path
if [[ ! -e /usr/local/bin/adb ]]; then
log "Creating /usr/local/bin/adb → /usr/bin/adb symlink"
sudo ln -s /usr/bin/adb /usr/local/bin/adb
fi
# ── project structure ──────────────────────────────────────────────────────────
PROJECT_DIR=$(realpath kivyschool-hello)
log "Ensuring project directory: $PROJECT_DIR"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
log "Ensuring main.py exists, otherwise creating a demo app"
if [[ ! -f main.py ]]; then
cat > main.py <<'PY'
import trio
from beautifulapp import MainApp
app = MainApp()
trio.run(app.async_run, "trio")
PY
mkdir -p beautifulapp
mkdir -p beautifulapp/screens
cat > beautifulapp/__init__.py <<'PY'
from beautifulapp.screens.main_screen import MainScreen
from kivy_reloader.app import App
class MainApp(App):
def build(self):
return MainScreen()
PY
cat > beautifulapp/screens/main_screen.py <<'PY'
import os
from kivy.uix.screenmanager import Screen
from kivy_reloader.utils import load_kv_path
main_screen_kv = os.path.join("beautifulapp", "screens", "main_screen.kv")
load_kv_path(main_screen_kv)
class MainScreen(Screen):
pass
PY
cat > beautifulapp/screens/main_screen.kv <<'PY'
<MainScreen>:
BoxLayout:
orientation: 'vertical'
Button:
text: 'Welcome to Kivy Reloader!'
PY
cat > kivy-reloader.toml <<'PY'
[kivy_reloader]
PHONE_IPS = ["192.168.1.69"] # Replace with your phone IP
HOT_RELOAD_ON_PHONE = true
FULL_RELOAD_FILES = ["main.py", "beautifulapp/__init__.py"]
WATCHED_FOLDERS_RECURSIVELY = ["beautifulapp"]
PY
else
log "main.py already present"
fi
# ── uv environment ──────────────────────────────────────────────────────────────
log "Installing and pinning Python 3.11 with uv"
uv python install 3.11
uv python pin 3.11
log "Initializing project with uv"
if [[ ! -f pyproject.toml ]]; then
uv init
else
log "pyproject.toml already present"
fi
log "Adding Kivy‑Reloader, Cython, Buildozer to dependencies"
uv add kivy-reloader cython buildozer
# ── scrcpy 3.3.1 ────────────────────────────────────────────────────────────────
VERSION="3.3.1"
SCRCPY_BIN="/usr/local/bin/scrcpy"
SCRCPY_SERVER="/usr/local/share/scrcpy/scrcpy-server"
SCRCPY_LINK="/usr/local/bin/scrcpy-server"
INSTALLED_VERSION=$(scrcpy --version 2>/dev/null | head -n1 | cut -d' ' -f2 || echo "")
SERVER_PRESENT=false
[[ -e "$SCRCPY_SERVER" || -e "$SCRCPY_LINK" ]] && SERVER_PRESENT=true
if command -v scrcpy >/dev/null 2>&1 && [[ "$INSTALLED_VERSION" == "$VERSION" ]] && $SERVER_PRESENT; then
log "scrcpy $VERSION already installed, skipping download"
else
URL="https://github.com/Genymobile/scrcpy/releases/download/v$VERSION/scrcpy-linux-x86_64-v$VERSION.tar.gz"
ARCHIVE="scrcpy-linux-x86_64-v$VERSION.tar.gz"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
log "Downloading scrcpy $VERSION..."
cd "$TMPDIR"
curl -LO "$URL" || die "Failed to download scrcpy"
log "Extracting scrcpy archive..."
tar xf "$ARCHIVE"
log "Installing scrcpy to /usr/local"
cd "scrcpy-linux-x86_64-v$VERSION"
sudo install -Dm755 scrcpy "$SCRCPY_BIN"
sudo install -Dm644 scrcpy-server "$SCRCPY_SERVER"
log "Creating expected symlink: $SCRCPY_LINK"
sudo ln -sf "$SCRCPY_SERVER" "$SCRCPY_LINK"
log "scrcpy $VERSION installed successfully"
fi
# ── buildozer ───────────────────────────────────────────────────────────────────
cd "$PROJECT_DIR"
log "Creating buildozer.spec"
[[ -f buildozer.spec ]] || uv run kivy-reloader init
log "Building, deploying, running, and tailing logcat"
uv run kivy-reloader run build