Skip to content

The easiest way to install Kivy Reloader on macOS

Prerequisites: This script requires Homebrew to be installed on your macOS system. If you don't have Homebrew installed, the script will guide you through the installation process.

  1. Create a new file called kivy-android-macos.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-macos.sh
    
  5. Run the script:
    ./kivy-android-macos.sh
    

Script

#!/usr/bin/env bash

# Kivy Quickstart Install Script (macOS version)
#
# 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: 
#   - macOS 13 Ventura or later (tested on macOS 15 Sequoia)
#   - Homebrew must be installed (https://brew.sh)
#   - Required dependencies must be installed via Homebrew:
#     brew install android-platform-tools openjdk@17 autoconf automake libtool pkg-config cmake openssl
#
# 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)

timestamp() { date +"%F %T"; }

log() { printf '\n%s[%s]%s %s%s%s\n' "$BLU" "$(timestamp)" "$RST" "$GRN" "$1" "$RST"; }
die() { printf '\n%s[%s]%s %s%s%s\n' "$BLU" "$(timestamp)" "$RST" "$RED" "$1" "$RST"; exit 1; }

# ── homebrew ──────────────────────────────────────────────────────────────────────
log "Checking for Homebrew"

if ! command -v brew >/dev/null 2>&1; then
    log "Homebrew is not installed."

    echo
    echo "👉 To install Homebrew, run the following command:"
    echo '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
    echo

    # Suggest correct prefix based on architecture
    if [[ "$(uname -m)" == "arm64" ]]; then
        HOMEBREW_PREFIX="/opt/homebrew"
    else
        HOMEBREW_PREFIX="/usr/local"
    fi

    echo "After installation, add Homebrew to your PATH (Zsh default):"
    echo "  echo 'eval \"\$(${HOMEBREW_PREFIX}/bin/brew shellenv)\"' >> ~/.zprofile"
    echo "  eval \"\$(${HOMEBREW_PREFIX}/bin/brew shellenv)\""
    echo

    die "Homebrew is required but was not found. Please install it and rerun this script."
else
    HOMEBREW_PREFIX="$(brew --prefix)"
    log "Homebrew already present at $HOMEBREW_PREFIX"
    log "Version: $(brew --version | head -n1)"

    # Ensure Homebrew is in PATH for current session
    eval "$(${HOMEBREW_PREFIX}/bin/brew shellenv)"

    # Ensure Homebrew is loaded in all future Zsh sessions (idempotent)
    if ! grep -q 'brew shellenv' "$HOME/.zprofile" 2>/dev/null; then
        echo "eval \"\$(${HOMEBREW_PREFIX}/bin/brew shellenv)\"" >> "$HOME/.zprofile"
        log "Added Homebrew to PATH in ~/.zprofile"
    fi
fi

# ── 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"

# ── project structure ──────────────────────────────────────────────────────────
PROJECT_DIR="$(pwd)/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 kivy_reloader.app import App

from beautifulapp.screens.main_screen import MainScreen


class MainApp(App):
    def build(self):
        return MainScreen()

PY

  cat > beautifulapp/screens/main_screen.py <<'PY'
from kivy.uix.screenmanager import Screen
from kivy_reloader.utils import load_kv_path

load_kv_path(__file__)


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]

HOT_RELOAD_ON_PHONE = true
FULL_RELOAD_FILES = ["main.py", "beautifulapp/__init__.py"]
WATCHED_FOLDERS_RECURSIVELY = ["."]
STREAM_USING = "USB"
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

# ── adb ────────────────────────────────────────────────────────────────
log "Checking for adb"
if ! command -v adb >/dev/null 2>&1; then
    die "adb not found. Please install android-platform-tools via Homebrew first."
else
    log "adb already present"
fi

# ── scrcpy ─────────────────────────────────────────────────────────────────────
VERSION="3.3.1"

# Detect architecture (Apple Silicon vs Intel)
if [[ "$(uname -m)" == "arm64" ]]; then
  ARCHIVE="scrcpy-macos-aarch64-v$VERSION.tar.gz"
  EXTRACT_DIR="scrcpy-macos-aarch64-v$VERSION"
elif [[ "$(uname -m)" == "x86_64" ]]; then
  ARCHIVE="scrcpy-macos-x86_64-v$VERSION.tar.gz"
  EXTRACT_DIR="scrcpy-macos-x86_64-v$VERSION"
else
  die "Unsupported architecture for scrcpy"
fi

# ── openjdk 17 (required for Buildozer/Bundletool) ─────────────────────────────
JAVA_VERSION_REQUIRED=17

log "Checking for Java (OpenJDK $JAVA_VERSION_REQUIRED)"

# Check if Java is actually working (not just the macOS stub)
if java -version >/dev/null 2>&1; then
    # Java is working, check version
    JAVA_VERSION_OUTPUT=$(java -version 2>&1 | head -n1)
    log "Java version output: $JAVA_VERSION_OUTPUT"

    # More robust version parsing
    if [[ $JAVA_VERSION_OUTPUT =~ \"1\.([0-9]+)\. ]]; then
        # Java 8 format: "1.8.0_XXX" -> use second number (8)
        CURRENT_JAVA_VERSION=${BASH_REMATCH[1]}
        log "Major version: $CURRENT_JAVA_VERSION"
    elif [[ $JAVA_VERSION_OUTPUT =~ \"([0-9]+)\. ]]; then
        # Java 9+ format: "17.0.X" -> use first number (17)
        CURRENT_JAVA_VERSION=${BASH_REMATCH[1]}
        log "Major version: $CURRENT_JAVA_VERSION"
    else
        log "Unable to parse Java version from: $JAVA_VERSION_OUTPUT"
        # Try a simpler extraction as fallback
        CURRENT_JAVA_VERSION=$(echo "$JAVA_VERSION_OUTPUT" | grep -oE '[0-9]+' | head -n1)
        log "Fallback extraction gave: $CURRENT_JAVA_VERSION"
    fi

    if [[ -n "$CURRENT_JAVA_VERSION" && "$CURRENT_JAVA_VERSION" != "$JAVA_VERSION_REQUIRED" ]]; then
        log "Java version $CURRENT_JAVA_VERSION detected, but OpenJDK $JAVA_VERSION_REQUIRED is required"
        die "Please install OpenJDK $JAVA_VERSION_REQUIRED via Homebrew first."
    elif [[ -n "$CURRENT_JAVA_VERSION" && "$CURRENT_JAVA_VERSION" == "$JAVA_VERSION_REQUIRED" ]]; then
        log "Java $CURRENT_JAVA_VERSION already installed and correct version"
    else
        log "Could not determine Java version, OpenJDK $JAVA_VERSION_REQUIRED is required"
        die "Please install OpenJDK $JAVA_VERSION_REQUIRED via Homebrew first."
    fi
else
    # Java not working, need to install
    log "Java not found. Installing OpenJDK $JAVA_VERSION_REQUIRED via Homebrew..."
    die "Please install OpenJDK $JAVA_VERSION_REQUIRED via Homebrew first."
fi

# Link JDK so macOS recognizes it system-wide
HOMEBREW_PREFIX="$(brew --prefix)"
JDK_PATH="$HOMEBREW_PREFIX/opt/openjdk@$JAVA_VERSION_REQUIRED/libexec/openjdk.jdk"
SYSTEM_JDK_PATH="/Library/Java/JavaVirtualMachines/openjdk-$JAVA_VERSION_REQUIRED.jdk"

if [[ ! -e "$SYSTEM_JDK_PATH" ]]; then
    log "Linking OpenJDK $JAVA_VERSION_REQUIRED into $SYSTEM_JDK_PATH"
    sudo ln -sfn "$JDK_PATH" "$SYSTEM_JDK_PATH"
else
    log "OpenJDK $JAVA_VERSION_REQUIRED already linked at $SYSTEM_JDK_PATH"
fi

# Ensure PATH contains correct Java bin
JAVA_BIN="$HOMEBREW_PREFIX/opt/openjdk@$JAVA_VERSION_REQUIRED/bin"
if ! echo "$PATH" | grep -q "$JAVA_BIN"; then
    echo "export PATH=\"$JAVA_BIN:\$PATH\"" >> "$HOME/.zprofile"
    export PATH="$JAVA_BIN:$PATH"
    log "Added OpenJDK $JAVA_VERSION_REQUIRED to PATH"
fi

log "Java setup complete: $(java -version 2>&1 | head -n 1 || echo 'Java still not found')"

# ── bundletool 1.18.1 ──────────────────────────────────────────────────────────
log "Setting up bundletool 1.18.1"
BUNDLETOOL_DIR="$HOME/tools/bundletool"
BUNDLETOOL_JAR="bundletool-all-1.18.1.jar"
BUNDLETOOL_URL="https://github.com/google/bundletool/releases/download/1.18.1/$BUNDLETOOL_JAR"

mkdir -p "$BUNDLETOOL_DIR"
cd "$BUNDLETOOL_DIR"
if [[ ! -f "$BUNDLETOOL_JAR" ]]; then
  log "Downloading $BUNDLETOOL_JAR"
  curl -LO "$BUNDLETOOL_URL"
else
  log "Bundletool already present at $BUNDLETOOL_DIR/$BUNDLETOOL_JAR"
fi

# Create alias in ~/.zprofile if missing (Zsh is default on macOS)
ALIAS_LINE="alias bundletool='java -jar ~/tools/bundletool/$BUNDLETOOL_JAR'"
if [[ -f "$HOME/.zprofile" ]] && grep -qxF "$ALIAS_LINE" "$HOME/.zprofile"; then
  log "bundletool alias already exists in ~/.zprofile"
else
  printf "\n# bundletool alias\n%s\n" "$ALIAS_LINE" >> "$HOME/.zprofile"
  log "Added bundletool alias to ~/.zprofile"
fi

# Also add to current session
if ! command -v bundletool >/dev/null 2>&1; then
  eval "$ALIAS_LINE"
  log "bundletool alias loaded for current session"
fi

# ── buildozer dependencies ─────────────────────────────────────────────────────
log "Ensuring Buildozer dependencies are installed via Homebrew"

DEPS=(autoconf automake libtool pkg-config cmake openssl)

for dep in "${DEPS[@]}"; do
    if brew list --versions "$dep" >/dev/null 2>&1; then
        log "$dep already installed"
    else
        log "$dep not found"
        die "Please install $dep via Homebrew first."
    fi
done

# ── 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