7 tasks covering gitignore updates, script scaffold, helpers, keystore management, release/debug builds, clean, and e2e testing. Made-with: Cursor
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"