Skip to content

The easiest way to install Kivy Reloader on Windows

This installation requires two steps that must be completed in order:

  1. Windows PowerShell Script
  2. WSL2 Script

Step 1: Windows PowerShell Script

Prerequisites: This script requires PowerShell to be available on your Windows system (which is included by default in Windows 10 and later).

  1. Create a new file called kivy-android-windows.ps1.
  2. Copy the script into the file and save it.
  3. Open PowerShell as Administrator and navigate to the directory where you saved the script.
  4. If needed, set the execution policy to allow the script to run:
    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
    
  5. Run the script:
    .\kivy-android-windows.ps1
    

Windows Script

param (
    [string]$Url = "https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip"
)

Write-Output "Starting download and extraction script..."
Write-Output "URL provided: $Url"

# Extract filename and folder name from URL
$FileName = [System.IO.Path]::GetFileName($Url)
$FolderName = [System.IO.Path]::GetFileNameWithoutExtension($FileName)
Write-Output "Extracted filename: $FileName"
Write-Output "Target folder name: $FolderName"

# Use proper installation directory
$InstallBase = "$env:LOCALAPPDATA\Programs"
if (!(Test-Path -Path $InstallBase)) {
    New-Item -ItemType Directory -Path $InstallBase -Force
    Write-Output "Created installation directory: $InstallBase"
}
Write-Output "Installation base directory: $InstallBase"

# Set paths
$TempDestination = Join-Path -Path $env:TEMP -ChildPath $FileName
$ExtractedFolder = Join-Path -Path $InstallBase -ChildPath $FolderName

# Check if extracted folder already exists
if (Test-Path -Path $ExtractedFolder) {
    Write-Output "Target folder already exists: $ExtractedFolder"
    Write-Output "Skipping download and extraction."

    # Clean up any leftover temp files from previous runs
    if (Test-Path -Path $TempDestination) {
        Write-Output "Cleaning up leftover temporary file..."
        Remove-Item -Path $TempDestination -Force
        Write-Output "Cleaned up temporary zip file."
    }
} else {
    Write-Output "Target folder not found. Proceeding with download and extraction..."

    # Clean up any existing temp file first
    if (Test-Path -Path $TempDestination) {
        Write-Output "Removing existing temporary file..."
        Remove-Item -Path $TempDestination -Force
    }

    # Download the file to temp directory
    Write-Output "Downloading zip file..."
    try {
        Invoke-WebRequest -Uri $Url -OutFile $TempDestination
        Write-Output "Download completed successfully."
    } catch {
        Write-Output "Error downloading file: $($_.Exception.Message)"
        # Clean up partial download if it exists
        if (Test-Path -Path $TempDestination) {
            Remove-Item -Path $TempDestination -Force
        }
        throw
    }

    # Extract the .zip to installation directory
    Write-Output "Extracting zip file..."
    try {
        Expand-Archive -Path $TempDestination -DestinationPath $InstallBase -Force
        Write-Output "Extraction completed successfully."
    } catch {
        Write-Output "Error extracting file: $($_.Exception.Message)"
        throw
    } finally {
        # Always clean up temp file, even if extraction fails
        if (Test-Path -Path $TempDestination) {
            Remove-Item -Path $TempDestination -Force
            Write-Output "Cleaned up temporary zip file."
        }
    }
}

# Add to PATH if not already present
$CurrentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($CurrentPath -notlike "*$ExtractedFolder*") {
    Write-Output "Adding scrcpy to user PATH..."
    $NewPath = $CurrentPath + ";" + $ExtractedFolder
    [Environment]::SetEnvironmentVariable("PATH", $NewPath, "User")
    Write-Output "Added $ExtractedFolder to user PATH."
    Write-Output "Note: You may need to restart your terminal or PowerShell session for PATH changes to take effect."
} else {
    Write-Output "scrcpy directory already in PATH."
}

# Configure WSLENV to share USERPROFILE with WSL2
Write-Output "Configuring WSLENV for WSL2 compatibility..."
$CurrentWSLENV = [Environment]::GetEnvironmentVariable("WSLENV", "User")
$RequiredWSLENV = "USERPROFILE/p"

# Split current WSLENV by colons to check individual entries
$CurrentEntries = if ([string]::IsNullOrEmpty($CurrentWSLENV)) { @() } else { $CurrentWSLENV -split ':' }
$RequiredEntryExists = $CurrentEntries -contains $RequiredWSLENV

if ([string]::IsNullOrEmpty($CurrentWSLENV)) {
    # WSLENV doesn't exist, create it
    [Environment]::SetEnvironmentVariable("WSLENV", $RequiredWSLENV, "User")
    Write-Output "Created WSLENV environment variable with USERPROFILE/p"
} elseif (-not $RequiredEntryExists) {
    # WSLENV exists but doesn't contain the exact USERPROFILE/p entry
    $NewWSLENV = $CurrentWSLENV + ":" + $RequiredWSLENV
    [Environment]::SetEnvironmentVariable("WSLENV", $NewWSLENV, "User")
    Write-Output "Added USERPROFILE/p to existing WSLENV: $NewWSLENV"
} else {
    Write-Output "WSLENV already configured to share USERPROFILE with WSL2"
}

# Change location to extracted folder
Set-Location $ExtractedFolder
Write-Output "Changed directory to: $ExtractedFolder"

# Run adb commands
Write-Output "Running 'adb kill-server'..."
.\adb.exe kill-server

Write-Output "Running 'adb start-server'..."
.\adb.exe start-server

Write-Output "Running 'adb devices'..."
.\adb.exe devices

Write-Output ""
Write-Output "Installation completed! scrcpy has been installed to: $ExtractedFolder"
Write-Output "The installation directory has been added to your PATH."
Write-Output ""
Write-Output "IMPORTANT: If you ran this script from VS Code's integrated terminal:"
Write-Output "  - You must close ALL VS Code windows and restart VS Code for PATH changes to take effect"
Write-Output ""
Write-Output "For regular PowerShell/CMD windows:"
Write-Output "  - Simply restart your terminal or open a new one"
Write-Output ""
Write-Output "You can then run 'scrcpy' from any location."

Step 2: WSL2 Script

After completing Step 1, you must run the WSL2 script to set up the development environment:

Prerequisites:

  • WSL2 must be installed and configured on your Windows system
  • Step 1 (Windows PowerShell script) must be completed first
  • Ubuntu or similar Linux distribution running in WSL2

Instructions

  1. Restart WSL2 after completing Step 1 to ensure environment variables are available
  2. Create a new file called kivy-android-wsl2.sh in your WSL2 environment
  3. Copy the WSL2 script into the file and save it
  4. Open your WSL2 terminal and navigate to the directory where you saved the script
  5. Make the script executable:
    chmod +x kivy-android-wsl2.sh
    
  6. Run the script:
    ./kivy-android-wsl2.sh
    

WSL2 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:
#   - WSL2 (tested on Ubuntu 24.04 LTS)
#
# 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 libmtdev1

# 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 "$HOME/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

# ── scrcpy 3.3.1 ────────────────────────────────────────────────────────────────
log "Setting up Windows adb and scrcpy in WSL2"

# With proper WSLENV, USERPROFILE should already be a Linux path
if [[ -n "${USERPROFILE:-}" && -d "$USERPROFILE" ]]; then
    WIN_HOME="$USERPROFILE"
    log "Using USERPROFILE from WSLENV: $WIN_HOME"
else
    die "USERPROFILE not available via WSLENV. Please restart WSL2 after running the PowerShell script."
fi

WINDOWS_SCRCPY="$WIN_HOME/AppData/Local/Programs/scrcpy-win64-v3.3.1/scrcpy.exe"
WINDOWS_ADB="$WIN_HOME/AppData/Local/Programs/scrcpy-win64-v3.3.1/adb.exe"

shopt -s expand_aliases   # make aliases work in this non‑interactive run

if [[ -x "$WINDOWS_ADB" && -x "$WINDOWS_SCRCPY" ]]; then
  log "Creating WSL2 symlinks for adb and scrcpy"
  sudo ln -sf "$WINDOWS_ADB" /usr/local/bin/adb
  sudo ln -sf "$WINDOWS_SCRCPY" /usr/local/bin/scrcpy

  SECTION_BEGIN="# >>> kivyschool-android <<<"
  SECTION_END="# <<< kivyschool-android >>>"
  # remove any previous block
  sed -i "/$SECTION_BEGIN/,/$SECTION_END/d" ~/.bashrc
  # write fresh, single copy
  cat >> ~/.bashrc <<EOF
$SECTION_BEGIN
export DISPLAY=\$(grep -m1 nameserver /etc/resolv.conf | awk '{print \$2}'):0.0
alias adb='$WINDOWS_ADB'
alias scrcpy='$WINDOWS_SCRCPY'
alias bundletool='java -jar ~/tools/bundletool/bundletool-all-1.18.1.jar'
$SECTION_END
EOF

  source ~/.bashrc
else
  die "Windows adb or scrcpy not found at expected path: $WINDOWS_ADB or $WINDOWS_SCRCPY"
fi

# ── bundletool 1.18.1 ─────────────────────────────────────────────────────────
log "Installing bundletool 1.18.1"
BUNDLETOOL_DIR="$HOME/tools/bundletool"
mkdir -p "$BUNDLETOOL_DIR"
cd "$BUNDLETOOL_DIR"
if [[ ! -f bundletool-all-1.18.1.jar ]]; then
  wget -q https://github.com/google/bundletool/releases/download/1.18.1/bundletool-all-1.18.1.jar -O bundletool-all-1.18.1.jar || die "bundletool download failed"
else
  log "bundletool jar already present"
fi

# Create a real executable so non-interactive shells (Python, scripts) can find it
mkdir -p "$HOME/.local/bin"
WRAPPER="$HOME/.local/bin/bundletool"
cat > "$WRAPPER" <<'SH'
#!/usr/bin/env bash
exec java -jar "$HOME/tools/bundletool/bundletool-all-1.18.1.jar" "$@"
SH
chmod +x "$WRAPPER"
log "Installed bundletool wrapper at $WRAPPER"

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