# Build Script Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Create an interactive bash build script that lets the user build signed release APKs without Android Studio. **Architecture:** A single `build.sh` at the project root. Organized as functions called from a main menu loop. Keystore management, signing, and APK output are self-contained flows. No modifications to `build.gradle.kts` — signing is injected via Gradle CLI properties. **Tech Stack:** Bash, `keytool` (JDK), Gradle wrapper (`./gradlew`) --- ### Task 1: Update .gitignore **Files:** - Modify: `.gitignore` **Step 1: Add dist/ and keystore/ entries** Add these lines to `.gitignore` after the existing keystore section: ```gitignore # Build script output dist/ keystore/ ``` Note: `*.jks` and `*.keystore` are already gitignored, but `keystore/` covers the directory and any non-key files in it. `dist/` prevents built APKs from being committed (backup to `*.apk` already being ignored). **Step 2: Verify** Run: `echo "test" > dist/test && git status` Expected: `dist/` does not appear in untracked files. **Step 3: Clean up and commit** ```bash rm -rf dist/ git add .gitignore git commit -m "chore: gitignore dist/ and keystore/ directories" ``` --- ### Task 2: Create build.sh scaffold **Files:** - Create: `build.sh` **Step 1: Create the script with shebang, color constants, and banner function** ```bash #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # No Color # --- Config --- KEYSTORE_DIR="keystore" KEYSTORE_FILE="$KEYSTORE_DIR/radio247-release.jks" KEY_ALIAS="radio247" DIST_DIR="dist" GRADLE_BUILD_FILE="app/build.gradle.kts" banner() { echo "" echo -e "${CYAN}╔══════════════════════════════════╗${NC}" echo -e "${CYAN}║${BOLD} Radio 247 Build Tool ${NC}${CYAN}║${NC}" echo -e "${CYAN}╚══════════════════════════════════╝${NC}" echo "" } info() { echo -e "${BLUE}▸${NC} $1"; } success() { echo -e "${GREEN}✓${NC} $1"; } warn() { echo -e "${YELLOW}⚠${NC} $1"; } error() { echo -e "${RED}✗${NC} $1"; } main_menu() { echo -e "${BOLD}What would you like to do?${NC}" echo "" echo -e " ${BOLD}1)${NC} Build Release APK ${DIM}(signed, shareable)${NC}" echo -e " ${BOLD}2)${NC} Build Debug APK ${DIM}(quick, for testing)${NC}" echo -e " ${BOLD}3)${NC} Manage Keystore ${DIM}(create or check signing key)${NC}" echo -e " ${BOLD}4)${NC} Clean ${DIM}(remove build artifacts)${NC}" echo -e " ${BOLD}5)${NC} Exit" echo "" local choice read -rp "> " choice case $choice in 1) build_release ;; 2) build_debug ;; 3) manage_keystore ;; 4) clean_build ;; 5) echo ""; exit 0 ;; *) warn "Invalid choice. Pick 1-5." ;; esac } # Placeholder functions — implemented in subsequent tasks build_release() { warn "Not yet implemented"; } build_debug() { warn "Not yet implemented"; } manage_keystore() { warn "Not yet implemented"; } clean_build() { warn "Not yet implemented"; } # --- Main --- banner while true; do main_menu echo "" done ``` **Step 2: Make executable and test** Run: `chmod +x build.sh && ./build.sh` Expected: Banner displays, menu shows 5 options, typing 1-4 shows "Not yet implemented", typing 5 exits. **Step 3: Commit** ```bash git add build.sh git commit -m "feat: add build.sh scaffold with menu" ``` --- ### Task 3: Implement helper functions **Files:** - Modify: `build.sh` **Step 1: Add version extraction function** Replace the placeholder section with these helpers (place them after the config block, before `banner()`): ```bash get_version_name() { grep 'versionName' "$GRADLE_BUILD_FILE" | head -1 | sed 's/.*"\(.*\)".*/\1/' } preflight_check() { local ok=true if ! command -v java &>/dev/null && [[ -z "${JAVA_HOME:-}" ]]; then error "Java not found. Install JDK 17+ or set JAVA_HOME." ok=false fi if [[ ! -x "./gradlew" ]]; then error "Gradle wrapper not found or not executable." ok=false fi if [[ "$ok" != true ]]; then exit 1 fi } ensure_dist_dir() { mkdir -p "$DIST_DIR" } ``` **Step 2: Add preflight call to main** Update the main section at the bottom to call `preflight_check` before the loop: ```bash # --- Main --- preflight_check banner while true; do main_menu echo "" done ``` **Step 3: Verify version extraction** Run: `source build.sh` is not ideal for testing functions, so test inline: ```bash grep 'versionName' app/build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/' ``` Expected: `1.0` **Step 4: Commit** ```bash git add build.sh git commit -m "feat: add preflight checks and version extraction" ``` --- ### Task 4: Implement keystore creation **Files:** - Modify: `build.sh` **Step 1: Replace the `manage_keystore` placeholder** ```bash manage_keystore() { if [[ -f "$KEYSTORE_FILE" ]]; then success "Keystore exists at ${BOLD}$KEYSTORE_FILE${NC}" echo "" info "Keystore details:" keytool -list -keystore "$KEYSTORE_FILE" -alias "$KEY_ALIAS" -storepass "$(read_keystore_password)" 2>/dev/null || warn "Could not read keystore (wrong password?)" return fi create_keystore } read_keystore_password() { local password read -rsp "Keystore password: " password echo >&2 echo "$password" } create_keystore() { echo -e "${BOLD}No keystore found. Let's create one.${NC}" echo "" local cn org password password_confirm read -rp "Your name (for the certificate): " cn [[ -z "$cn" ]] && { error "Name is required."; return 1; } read -rp "Organization (optional, Enter to skip): " org while true; do read -rsp "Password (min 6 chars): " password echo if [[ ${#password} -lt 6 ]]; then warn "Password must be at least 6 characters." continue fi read -rsp "Confirm password: " password_confirm echo if [[ "$password" != "$password_confirm" ]]; then warn "Passwords don't match. Try again." continue fi break done mkdir -p "$KEYSTORE_DIR" local dname="CN=$cn" [[ -n "$org" ]] && dname="$dname, O=$org" info "Creating keystore..." keytool -genkeypair \ -alias "$KEY_ALIAS" \ -keyalg RSA \ -keysize 2048 \ -validity 10000 \ -keystore "$KEYSTORE_FILE" \ -storepass "$password" \ -keypass "$password" \ -dname "$dname" \ 2>/dev/null if [[ -f "$KEYSTORE_FILE" ]]; then echo "" success "Keystore created at ${BOLD}$KEYSTORE_FILE${NC}" echo "" warn "IMPORTANT: Back up this file and remember your password." warn "If you lose either, you cannot update the app on devices" warn "that already have this version installed." else error "Keystore creation failed." return 1 fi } ``` **Step 2: Test keystore creation** Run: `./build.sh` → pick option 3 Expected: Prompts for name, org, password. Creates `keystore/radio247-release.jks`. **Step 3: Test existing keystore detection** Run: `./build.sh` → pick option 3 again Expected: Shows "Keystore exists" message. **Step 4: Clean up test keystore and commit** ```bash rm -rf keystore/ git add build.sh git commit -m "feat: add keystore creation and management" ``` --- ### Task 5: Implement release build **Files:** - Modify: `build.sh` **Step 1: Replace the `build_release` placeholder** ```bash build_release() { info "Preparing release build..." echo "" if [[ ! -f "$KEYSTORE_FILE" ]]; then warn "No keystore found. You need one to sign a release APK." echo "" local yn read -rp "Create one now? (y/n) " yn case $yn in [Yy]*) create_keystore || return 1 ;; *) return 0 ;; esac echo "" fi local password read -rsp "Keystore password: " password echo "" echo "" info "Building release APK... (this may take a minute)" echo "" local store_file store_file="$(cd "$(dirname "$KEYSTORE_FILE")" && pwd)/$(basename "$KEYSTORE_FILE")" if ./gradlew assembleRelease \ -Pandroid.injected.signing.store.file="$store_file" \ -Pandroid.injected.signing.store.password="$password" \ -Pandroid.injected.signing.key.alias="$KEY_ALIAS" \ -Pandroid.injected.signing.key.password="$password"; then copy_apk "release" else echo "" error "Build failed. Check the output above for details." return 1 fi } copy_apk() { local build_type="$1" local version version="$(get_version_name)" local source_apk="app/build/outputs/apk/$build_type/app-$build_type.apk" if [[ ! -f "$source_apk" ]]; then error "APK not found at $source_apk" return 1 fi ensure_dist_dir local dest_name="radio247-v${version}-${build_type}.apk" local dest_path="$DIST_DIR/$dest_name" cp "$source_apk" "$dest_path" local size size="$(du -h "$dest_path" | cut -f1 | xargs)" echo "" success "Build complete!" echo "" echo -e " ${BOLD}APK:${NC} $dest_path" echo -e " ${BOLD}Size:${NC} $size" echo "" echo -e " ${DIM}Share this file with anyone — they can install it${NC}" echo -e " ${DIM}on Android 9+ by opening the APK on their device.${NC}" } ``` **Step 2: Test (requires keystore)** Run: `./build.sh` → pick option 1 Expected: If no keystore, offers to create one. Then builds, copies APK to `dist/radio247-v1.0-release.apk`. **Step 3: Commit** ```bash git add build.sh git commit -m "feat: add release build with signing" ``` --- ### Task 6: Implement debug build and clean **Files:** - Modify: `build.sh` **Step 1: Replace the `build_debug` placeholder** ```bash build_debug() { info "Building debug APK..." echo "" if ./gradlew assembleDebug; then copy_apk "debug" else echo "" error "Build failed. Check the output above for details." return 1 fi } ``` **Step 2: Replace the `clean_build` placeholder** ```bash clean_build() { info "Cleaning build artifacts..." ./gradlew clean 2>/dev/null if [[ -d "$DIST_DIR" ]]; then rm -rf "$DIST_DIR" success "Removed $DIST_DIR/" fi success "Clean complete." } ``` **Step 3: Test debug build** Run: `./build.sh` → pick option 2 Expected: Builds debug APK, copies to `dist/radio247-v1.0-debug.apk`. **Step 4: Test clean** Run: `./build.sh` → pick option 4 Expected: Runs `gradle clean`, removes `dist/`, prints "Clean complete." **Step 5: Commit** ```bash git add build.sh git commit -m "feat: add debug build and clean commands" ``` --- ### Task 7: End-to-end verification **Step 1: Fresh clean** ```bash ./build.sh # pick 4 (Clean) rm -rf keystore/ ``` **Step 2: Full release flow** ```bash ./build.sh # pick 1 (Build Release APK) # Should prompt for keystore creation, then build # Verify: dist/radio247-v1.0-release.apk exists ls -la dist/ ``` **Step 3: Debug build** ```bash ./build.sh # pick 2 (Build Debug APK) # Verify: dist/radio247-v1.0-debug.apk exists ls -la dist/ ``` **Step 4: Keystore check** ```bash ./build.sh # pick 3 (Manage Keystore) # Should show "Keystore exists" message ``` **Step 5: Final commit if any changes needed** ```bash git add -A git commit -m "feat: build script complete" ```