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

508 lines
12 KiB
Markdown

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