Skip to content

The easiest way to install Kivy on Ubuntu

  1. Create a new file called kivy-android-ubuntu.sh.
  2. Copy the script into the file and save it.
  3. Open a terminal and navigate to the directory where you saved the script.
  4. Make the script executable:
    chmod +x kivy-android-ubuntu.sh
    
  5. Run the script:
    ./kivy-android-ubuntu.sh
    

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