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.
- Create a new file called
kivy-android-macos.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-macos.sh
- 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