Files
Android-247-Radio/docs/plans/2026-03-11-build-script-implementation.md
cottongin f9bc92e766 docs: add build script implementation plan
7 tasks covering gitignore updates, script scaffold, helpers,
keystore management, release/debug builds, clean, and e2e testing.

Made-with: Cursor
2026-03-11 04:12:50 -04:00

12 KiB

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:

# 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

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

#!/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

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()):

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:

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

grep 'versionName' app/build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/'

Expected: 1.0

Step 4: Commit

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

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

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

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

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

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

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

git add build.sh
git commit -m "feat: add debug build and clean commands"

Task 7: End-to-end verification

Step 1: Fresh clean

./build.sh  # pick 4 (Clean)
rm -rf keystore/

Step 2: Full release flow

./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

./build.sh  # pick 2 (Build Debug APK)
# Verify: dist/radio247-v1.0-debug.apk exists
ls -la dist/

Step 4: Keystore check

./build.sh  # pick 3 (Manage Keystore)
# Should show "Keystore exists" message

Step 5: Final commit if any changes needed

git add -A
git commit -m "feat: build script complete"