From f9bc92e7664916ce62ae5ae258b3de9c8df0769d Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 11 Mar 2026 04:12:50 -0400 Subject: [PATCH] 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-build-script-implementation.md | 507 ++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 docs/plans/2026-03-11-build-script-implementation.md diff --git a/docs/plans/2026-03-11-build-script-implementation.md b/docs/plans/2026-03-11-build-script-implementation.md new file mode 100644 index 0000000..fdade69 --- /dev/null +++ b/docs/plans/2026-03-11-build-script-implementation.md @@ -0,0 +1,507 @@ +# 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" +```